mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
feat: local provider
This commit is contained in:
@@ -31,19 +31,18 @@ export const useEnsureWorkspace = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If user is not login and input a custom workspaceId, jump to 404 page
|
// If user is not login and input a custom workspaceId, jump to 404 page
|
||||||
if (
|
// if (
|
||||||
!user &&
|
// !user &&
|
||||||
router.query.workspaceId &&
|
// router.query.workspaceId &&
|
||||||
router.query.workspaceId !== defaultOutLineWorkspaceId
|
// router.query.workspaceId !== defaultOutLineWorkspaceId
|
||||||
) {
|
// ) {
|
||||||
debugger;
|
// router.push('/404');
|
||||||
router.push('/404');
|
// return;
|
||||||
return;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceId = user
|
const workspaceId = user
|
||||||
? (router.query.workspaceId as string) || workspacesMeta?.[0]?.id
|
? (router.query.workspaceId as string) || workspacesMeta[0]?.id
|
||||||
: defaultOutLineWorkspaceId;
|
: (router.query.workspaceId as string) || defaultOutLineWorkspaceId;
|
||||||
|
|
||||||
loadWorkspace(workspaceId).finally(() => {
|
loadWorkspace(workspaceId).finally(() => {
|
||||||
setWorkspaceLoaded(true);
|
setWorkspaceLoaded(true);
|
||||||
|
|||||||
@@ -21,12 +21,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.29.1",
|
"@playwright/test": "^1.29.1",
|
||||||
|
"@types/debug": "^4.1.7",
|
||||||
"fake-indexeddb": "4.0.1",
|
"fake-indexeddb": "4.0.1",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blocksuite/blocks": "0.3.0-20221228162706-9576a3a",
|
"@blocksuite/blocks": "0.3.0-20221228162706-9576a3a",
|
||||||
"@blocksuite/store": "0.3.0-20221228162706-9576a3a",
|
"@blocksuite/store": "0.3.0-20221228162706-9576a3a",
|
||||||
|
"debug": "^4.3.4",
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
"firebase": "^9.15.0",
|
"firebase": "^9.15.0",
|
||||||
"idb-keyval": "^6.2.0",
|
"idb-keyval": "^6.2.0",
|
||||||
|
|||||||
@@ -2,32 +2,52 @@ import assert from 'assert';
|
|||||||
import { BlockSchema } from '@blocksuite/blocks/models';
|
import { BlockSchema } from '@blocksuite/blocks/models';
|
||||||
import { Workspace } from '@blocksuite/store';
|
import { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import { getLogger } from './index.js';
|
||||||
import { AffineProvider, BaseProvider } from './provider/index.js';
|
import { AffineProvider, BaseProvider } from './provider/index.js';
|
||||||
import { MemoryProvider } from './provider/index.js';
|
import { LocalProvider } from './provider/index.js';
|
||||||
import { getKVConfigure } from './store.js';
|
import { getKVConfigure } from './store.js';
|
||||||
|
|
||||||
export class DataCenter {
|
export class DataCenter {
|
||||||
private readonly _providers = new Map<string, typeof BaseProvider>();
|
private readonly _providers = new Map<string, typeof BaseProvider>();
|
||||||
private readonly _workspaces = new Map<string, Promise<BaseProvider>>();
|
private readonly _workspaces = new Map<string, Promise<BaseProvider>>();
|
||||||
private readonly _config;
|
private readonly _config;
|
||||||
|
private readonly _logger;
|
||||||
|
|
||||||
static async init(): Promise<DataCenter> {
|
static async init(): Promise<DataCenter> {
|
||||||
const dc = new DataCenter();
|
const dc = new DataCenter();
|
||||||
dc.addProvider(AffineProvider);
|
dc.addProvider(AffineProvider);
|
||||||
dc.addProvider(MemoryProvider);
|
dc.addProvider(LocalProvider);
|
||||||
|
|
||||||
return dc;
|
return dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this._config = getKVConfigure('sys');
|
this._config = getKVConfigure('sys');
|
||||||
|
this._logger = getLogger('dc');
|
||||||
|
this._logger.enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
addProvider(provider: typeof BaseProvider) {
|
private addProvider(provider: typeof BaseProvider) {
|
||||||
this._providers.set(provider.id, provider);
|
this._providers.set(provider.id, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _initWithProvider(id: string, providerId: string) {
|
private async _getProvider(id: string, providerId: string): Promise<string> {
|
||||||
|
const providerKey = `workspace:${id}:provider`;
|
||||||
|
if (this._providers.has(providerId)) {
|
||||||
|
await this._config.set(providerKey, providerId);
|
||||||
|
return providerId;
|
||||||
|
} else {
|
||||||
|
const providerValue = await this._config.get(providerKey);
|
||||||
|
if (providerValue) return providerValue;
|
||||||
|
}
|
||||||
|
throw Error(`Provider ${providerId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initWorkspace(id: string, pid: string): Promise<BaseProvider> {
|
||||||
|
this._logger(`Init workspace ${id} with ${pid}`);
|
||||||
|
|
||||||
|
const providerId = await this._getProvider(id, pid);
|
||||||
|
|
||||||
// init workspace & register block schema
|
// init workspace & register block schema
|
||||||
const workspace = new Workspace({ room: id }).register(BlockSchema);
|
const workspace = new Workspace({ room: id }).register(BlockSchema);
|
||||||
|
|
||||||
@@ -35,38 +55,22 @@ export class DataCenter {
|
|||||||
assert(Provider);
|
assert(Provider);
|
||||||
const provider = new Provider();
|
const provider = new Provider();
|
||||||
|
|
||||||
console.log(`Loading workspace ${id} with provider ${Provider.id}`);
|
await provider.init({
|
||||||
await provider.init(getKVConfigure(id), workspace);
|
config: getKVConfigure(id),
|
||||||
|
logger: this._logger.extend(`${Provider.id}:${id}`),
|
||||||
|
workspace,
|
||||||
|
});
|
||||||
await provider.initData();
|
await provider.initData();
|
||||||
console.log(`Workspace ${id} loaded`);
|
this._logger(`Workspace ${id} loaded`);
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _initWorkspace(
|
|
||||||
id: string,
|
|
||||||
providerId: string
|
|
||||||
): Promise<BaseProvider> {
|
|
||||||
const providerKey = `workspace:${id}:provider`;
|
|
||||||
const providerValue = await this._config.get(providerKey);
|
|
||||||
|
|
||||||
if (this._providers.has(providerValue || providerId)) {
|
|
||||||
if (!providerValue) {
|
|
||||||
await this._config.set(providerKey, providerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._initWithProvider(id, await this._config.get(providerKey));
|
|
||||||
} else {
|
|
||||||
throw Error(`provider ${providerId} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initWorkspace(
|
async initWorkspace(
|
||||||
id: string,
|
id: string,
|
||||||
provider = 'memory'
|
provider = 'local'
|
||||||
): Promise<Workspace | null> {
|
): Promise<Workspace | null> {
|
||||||
if (id) {
|
if (id) {
|
||||||
console.log('initWorkspace', id);
|
|
||||||
if (!this._workspaces.has(id)) {
|
if (!this._workspaces.has(id)) {
|
||||||
this._workspaces.set(id, this._initWorkspace(id, provider));
|
this._workspaces.set(id, this._initWorkspace(id, provider));
|
||||||
}
|
}
|
||||||
@@ -81,4 +85,11 @@ export class DataCenter {
|
|||||||
const config = getKVConfigure(workspace);
|
const config = getKVConfigure(workspace);
|
||||||
return config.set(key, value);
|
return config.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWorkspaceList() {
|
||||||
|
const keys = await this._config.keys();
|
||||||
|
return keys
|
||||||
|
.filter(k => k.startsWith('workspace:'))
|
||||||
|
.map(k => k.split(':')[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import debug from 'debug';
|
||||||
import { DataCenter } from './datacenter.js';
|
import { DataCenter } from './datacenter.js';
|
||||||
|
|
||||||
const _initializeDataCenter = () => {
|
const _initializeDataCenter = () => {
|
||||||
@@ -13,3 +14,9 @@ const _initializeDataCenter = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getDataCenter = _initializeDataCenter();
|
export const getDataCenter = _initializeDataCenter();
|
||||||
|
|
||||||
|
export function getLogger(namespace: string) {
|
||||||
|
const logger = debug(namespace);
|
||||||
|
logger.log = console.log.bind(console);
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,79 @@
|
|||||||
export { AffineProvider } from './provider.js';
|
import assert from 'assert';
|
||||||
|
import { applyUpdate } from 'yjs';
|
||||||
|
|
||||||
|
import type { InitialParams } from '../index.js';
|
||||||
|
import { LocalProvider } from '../local/index.js';
|
||||||
|
|
||||||
|
import { downloadWorkspace } from './apis.js';
|
||||||
|
import { token, Callback } from './token.js';
|
||||||
|
|
||||||
|
export class AffineProvider extends LocalProvider {
|
||||||
|
static id = 'affine';
|
||||||
|
private _onTokenRefresh?: Callback = undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(params: InitialParams) {
|
||||||
|
super.init(params);
|
||||||
|
|
||||||
|
this._onTokenRefresh = () => {
|
||||||
|
if (token.refresh) {
|
||||||
|
this._config.set('token', token.refresh);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert(this._onTokenRefresh);
|
||||||
|
|
||||||
|
token.onChange(this._onTokenRefresh);
|
||||||
|
|
||||||
|
// initial login token
|
||||||
|
if (token.isExpired) {
|
||||||
|
try {
|
||||||
|
const refreshToken = await this._config.get('token');
|
||||||
|
await token.refreshToken(refreshToken);
|
||||||
|
|
||||||
|
if (token.refresh) {
|
||||||
|
this._config.set('token', token.refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(token.isLogin);
|
||||||
|
} catch (_) {
|
||||||
|
this._logger('Authorization failed, fallback to local mode');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._config.set('token', token.refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
if (this._onTokenRefresh) {
|
||||||
|
token.offChange(this._onTokenRefresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initData() {
|
||||||
|
await super.initData();
|
||||||
|
|
||||||
|
const workspace = this._workspace;
|
||||||
|
const doc = workspace.doc;
|
||||||
|
|
||||||
|
this._logger(`Login: ${token.isLogin}`);
|
||||||
|
|
||||||
|
if (workspace.room && token.isLogin) {
|
||||||
|
try {
|
||||||
|
const updates = await downloadWorkspace(workspace.room);
|
||||||
|
if (updates) {
|
||||||
|
applyUpdate(doc, new Uint8Array(updates));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this._logger('Failed to init cloud workspace', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if after update, the space:meta is empty
|
||||||
|
// then we need to get map with doc
|
||||||
|
// just a workaround for yjs
|
||||||
|
doc.getMap('space:meta');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import assert from 'assert';
|
|
||||||
import { Workspace } from '@blocksuite/store';
|
|
||||||
import { BaseProvider, ConfigStore } from '../index.js';
|
|
||||||
import { downloadWorkspace } from './apis.js';
|
|
||||||
import { token, Callback } from './token.js';
|
|
||||||
|
|
||||||
export class AffineProvider extends BaseProvider {
|
|
||||||
static id = 'affine';
|
|
||||||
private _onTokenRefresh?: Callback = undefined;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(config: ConfigStore, workspace: Workspace) {
|
|
||||||
super.init(config, workspace);
|
|
||||||
|
|
||||||
this._onTokenRefresh = () => {
|
|
||||||
if (token.refresh) {
|
|
||||||
this._config.set('token', token.refresh);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
assert(this._onTokenRefresh);
|
|
||||||
|
|
||||||
token.onChange(this._onTokenRefresh);
|
|
||||||
|
|
||||||
// initial login token
|
|
||||||
if (token.isExpired) {
|
|
||||||
try {
|
|
||||||
const refreshToken = await this._config.get('token');
|
|
||||||
await token.refreshToken(refreshToken);
|
|
||||||
|
|
||||||
if (token.refresh) {
|
|
||||||
this._config.set('token', token.refresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(token.isLogin);
|
|
||||||
} catch (_) {
|
|
||||||
console.warn('authorization failed, fallback to local mode');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._config.set('token', token.refresh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy() {
|
|
||||||
if (this._onTokenRefresh) {
|
|
||||||
token.offChange(this._onTokenRefresh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initData() {
|
|
||||||
const workspace = this._workspace;
|
|
||||||
const doc = workspace.doc;
|
|
||||||
|
|
||||||
console.log(workspace.room, token.isLogin);
|
|
||||||
|
|
||||||
if (workspace.room && token.isLogin) {
|
|
||||||
try {
|
|
||||||
const updates = await downloadWorkspace(workspace.room);
|
|
||||||
if (updates) {
|
|
||||||
Workspace.Y.applyUpdate(doc, new Uint8Array(updates));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to init cloud workspace', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if after update, the space:meta is empty
|
|
||||||
// then we need to get map with doc
|
|
||||||
// just a workaround for yjs
|
|
||||||
doc.getMap('space:meta');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getLogger } from '../../index.js';
|
||||||
import { bareClient } from './request.js';
|
import { bareClient } from './request.js';
|
||||||
|
|
||||||
export interface AccessTokenMessage {
|
export interface AccessTokenMessage {
|
||||||
@@ -27,6 +28,7 @@ const login = (params: LoginParams): Promise<LoginResponse> =>
|
|||||||
bareClient.post('api/user/token', { json: params }).json();
|
bareClient.post('api/user/token', { json: params }).json();
|
||||||
|
|
||||||
class Token {
|
class Token {
|
||||||
|
private readonly _logger;
|
||||||
private _accessToken!: string;
|
private _accessToken!: string;
|
||||||
private _refreshToken!: string;
|
private _refreshToken!: string;
|
||||||
|
|
||||||
@@ -34,17 +36,22 @@ class Token {
|
|||||||
private _padding?: Promise<LoginResponse>;
|
private _padding?: Promise<LoginResponse>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this._logger = getLogger('token');
|
||||||
|
this._logger.enabled = true;
|
||||||
|
|
||||||
this._setToken(); // fill with default value
|
this._setToken(); // fill with default value
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setToken(login?: LoginResponse) {
|
private _setToken(login?: LoginResponse) {
|
||||||
console.log('set login', login);
|
|
||||||
this._accessToken = login?.token || '';
|
this._accessToken = login?.token || '';
|
||||||
this._refreshToken = login?.refresh || '';
|
this._refreshToken = login?.refresh || '';
|
||||||
|
|
||||||
this._user = Token.parse(this._accessToken);
|
this._user = Token.parse(this._accessToken);
|
||||||
if (login) {
|
if (login) {
|
||||||
|
this._logger('set login', login);
|
||||||
this.triggerChange(this._user);
|
this.triggerChange(this._user);
|
||||||
|
} else {
|
||||||
|
this._logger('empty login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { Workspace } from '@blocksuite/store';
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { ConfigStore } from '../store.js';
|
import type { Logger, InitialParams, ConfigStore } from './index';
|
||||||
|
|
||||||
export class BaseProvider {
|
export class BaseProvider {
|
||||||
static id = 'memory';
|
static id = 'base';
|
||||||
protected _config!: ConfigStore;
|
protected _config!: ConfigStore;
|
||||||
|
protected _logger!: Logger;
|
||||||
protected _workspace!: Workspace;
|
protected _workspace!: Workspace;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Nothing to do here
|
// Nothing to do here
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(config: ConfigStore, workspace: Workspace) {
|
async init(params: InitialParams) {
|
||||||
this._config = config;
|
this._config = params.config;
|
||||||
this._workspace = workspace;
|
this._logger = params.logger;
|
||||||
|
this._workspace = params.workspace;
|
||||||
|
this._logger.enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
@@ -24,6 +28,16 @@ export class BaseProvider {
|
|||||||
throw Error('Not implemented: initData');
|
throw Error('Not implemented: initData');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// should return a blob url
|
||||||
|
async getBlob(_id: string): Promise<string | null> {
|
||||||
|
throw Error('Not implemented: getBlob');
|
||||||
|
}
|
||||||
|
|
||||||
|
// should return a blob unique id
|
||||||
|
async setBlob(_blob: Blob): Promise<string> {
|
||||||
|
throw Error('Not implemented: setBlob');
|
||||||
|
}
|
||||||
|
|
||||||
get workspace() {
|
get workspace() {
|
||||||
return this._workspace;
|
return this._workspace;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
export type { ConfigStore } from '../store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
export { BaseProvider } from './base.js';
|
import type { getLogger } from '../index';
|
||||||
|
import type { ConfigStore } from '../store';
|
||||||
|
|
||||||
|
export type Logger = ReturnType<typeof getLogger>;
|
||||||
|
|
||||||
|
export type InitialParams = {
|
||||||
|
config: ConfigStore;
|
||||||
|
logger: Logger;
|
||||||
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { ConfigStore, Workspace };
|
||||||
|
export type { BaseProvider } from './base.js';
|
||||||
export { AffineProvider } from './affine/index.js';
|
export { AffineProvider } from './affine/index.js';
|
||||||
export { MemoryProvider } from './memory.js';
|
export { LocalProvider } from './local/index.js';
|
||||||
|
|||||||
50
packages/data-center/src/datacenter/provider/local/index.ts
Normal file
50
packages/data-center/src/datacenter/provider/local/index.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { BlobStorage } from '@blocksuite/store';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import type { InitialParams } from '../index.js';
|
||||||
|
import { BaseProvider } from '../base.js';
|
||||||
|
import { IndexedDBProvider } from './indexeddb.js';
|
||||||
|
|
||||||
|
export class LocalProvider extends BaseProvider {
|
||||||
|
static id = 'local';
|
||||||
|
private _blobs!: BlobStorage;
|
||||||
|
private _idb?: IndexedDBProvider = undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
async init(params: InitialParams) {
|
||||||
|
super.init(params);
|
||||||
|
|
||||||
|
const blobs = await this._workspace.blobs;
|
||||||
|
assert(blobs);
|
||||||
|
this._blobs = blobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initData() {
|
||||||
|
assert(this._workspace.room);
|
||||||
|
this._logger('Loading local data');
|
||||||
|
this._idb = new IndexedDBProvider(
|
||||||
|
this._workspace.room,
|
||||||
|
this._workspace.doc
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._idb.whenSynced;
|
||||||
|
this._logger('Local data loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
super.destroy();
|
||||||
|
if (this._idb) {
|
||||||
|
await this._idb.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlob(id: string): Promise<string | null> {
|
||||||
|
return this._blobs.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBlob(blob: Blob): Promise<string> {
|
||||||
|
return this._blobs.set(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
199
packages/data-center/src/datacenter/provider/local/indexeddb.ts
Normal file
199
packages/data-center/src/datacenter/provider/local/indexeddb.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import * as idb from 'lib0/indexeddb.js';
|
||||||
|
import { Observable } from 'lib0/observable.js';
|
||||||
|
import type { Doc } from 'yjs';
|
||||||
|
import { applyUpdate, encodeStateAsUpdate, transact } from 'yjs';
|
||||||
|
|
||||||
|
const customStoreName = 'custom';
|
||||||
|
const updatesStoreName = 'updates';
|
||||||
|
|
||||||
|
const PREFERRED_TRIM_SIZE = 500;
|
||||||
|
|
||||||
|
const fetchUpdates = async (provider: IndexedDBProvider) => {
|
||||||
|
const [updatesStore] = idb.transact(provider.db as IDBDatabase, [
|
||||||
|
updatesStoreName,
|
||||||
|
]); // , 'readonly')
|
||||||
|
if (updatesStore) {
|
||||||
|
const updates = await idb.getAll(
|
||||||
|
updatesStore,
|
||||||
|
idb.createIDBKeyRangeLowerBound(provider._dbref, false)
|
||||||
|
);
|
||||||
|
transact(
|
||||||
|
provider.doc,
|
||||||
|
() => {
|
||||||
|
updates.forEach(val => applyUpdate(provider.doc, val));
|
||||||
|
},
|
||||||
|
provider,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const lastKey = await idb.getLastKey(updatesStore);
|
||||||
|
provider._dbref = lastKey + 1;
|
||||||
|
const cnt = await idb.count(updatesStore);
|
||||||
|
provider._dbsize = cnt;
|
||||||
|
}
|
||||||
|
return updatesStore;
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeState = (provider: IndexedDBProvider, forceStore = true) =>
|
||||||
|
fetchUpdates(provider).then(updatesStore => {
|
||||||
|
if (
|
||||||
|
updatesStore &&
|
||||||
|
(forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE)
|
||||||
|
) {
|
||||||
|
idb
|
||||||
|
.addAutoKey(updatesStore, encodeStateAsUpdate(provider.doc))
|
||||||
|
.then(() =>
|
||||||
|
idb.del(
|
||||||
|
updatesStore,
|
||||||
|
idb.createIDBKeyRangeUpperBound(provider._dbref, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(() =>
|
||||||
|
idb.count(updatesStore).then(cnt => {
|
||||||
|
provider._dbsize = cnt;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export class IndexedDBProvider extends Observable<string> {
|
||||||
|
doc: Doc;
|
||||||
|
name: string;
|
||||||
|
_dbref: number;
|
||||||
|
_dbsize: number;
|
||||||
|
private _destroyed: boolean;
|
||||||
|
whenSynced: Promise<IndexedDBProvider>;
|
||||||
|
db: IDBDatabase | null;
|
||||||
|
private _db: Promise<IDBDatabase>;
|
||||||
|
private _storeTimeout: number;
|
||||||
|
private _storeTimeoutId: NodeJS.Timeout | null;
|
||||||
|
private _storeUpdate: (update: Uint8Array, origin: any) => void;
|
||||||
|
|
||||||
|
constructor(name: string, doc: Doc) {
|
||||||
|
super();
|
||||||
|
this.doc = doc;
|
||||||
|
this.name = name;
|
||||||
|
this._dbref = 0;
|
||||||
|
this._dbsize = 0;
|
||||||
|
this._destroyed = false;
|
||||||
|
this.db = null;
|
||||||
|
this._db = idb.openDB(name, db =>
|
||||||
|
idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.whenSynced = this._db.then(async db => {
|
||||||
|
this.db = db;
|
||||||
|
const currState = encodeStateAsUpdate(doc);
|
||||||
|
const updatesStore = await fetchUpdates(this);
|
||||||
|
if (updatesStore) {
|
||||||
|
await idb.addAutoKey(updatesStore, currState);
|
||||||
|
}
|
||||||
|
if (this._destroyed) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.emit('synced', [this]);
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout in ms untill data is merged and persisted in idb.
|
||||||
|
this._storeTimeout = 1000;
|
||||||
|
|
||||||
|
this._storeTimeoutId = null;
|
||||||
|
|
||||||
|
this._storeUpdate = (update: Uint8Array, origin: any) => {
|
||||||
|
if (this.db && origin !== this) {
|
||||||
|
const [updatesStore] = idb.transact(
|
||||||
|
/** @type {IDBDatabase} */ this.db,
|
||||||
|
[updatesStoreName]
|
||||||
|
);
|
||||||
|
if (updatesStore) {
|
||||||
|
idb.addAutoKey(updatesStore, update);
|
||||||
|
}
|
||||||
|
if (++this._dbsize >= PREFERRED_TRIM_SIZE) {
|
||||||
|
// debounce store call
|
||||||
|
if (this._storeTimeoutId !== null) {
|
||||||
|
clearTimeout(this._storeTimeoutId);
|
||||||
|
}
|
||||||
|
this._storeTimeoutId = setTimeout(() => {
|
||||||
|
storeState(this, false);
|
||||||
|
this._storeTimeoutId = null;
|
||||||
|
}, this._storeTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
doc.on('update', this._storeUpdate);
|
||||||
|
this.destroy = this.destroy.bind(this);
|
||||||
|
doc.on('destroy', this.destroy);
|
||||||
|
}
|
||||||
|
|
||||||
|
override destroy() {
|
||||||
|
if (this._storeTimeoutId) {
|
||||||
|
clearTimeout(this._storeTimeoutId);
|
||||||
|
}
|
||||||
|
this.doc.off('update', this._storeUpdate);
|
||||||
|
this.doc.off('destroy', this.destroy);
|
||||||
|
this._destroyed = true;
|
||||||
|
return this._db.then(db => {
|
||||||
|
db.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys this instance and removes all data from indexeddb.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async clearData(): Promise<void> {
|
||||||
|
return this.destroy().then(() => {
|
||||||
|
idb.deleteDB(this.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String | number | ArrayBuffer | Date} key
|
||||||
|
* @return {Promise<String | number | ArrayBuffer | Date | any>}
|
||||||
|
*/
|
||||||
|
async get(
|
||||||
|
key: string | number | ArrayBuffer | Date
|
||||||
|
): Promise<string | number | ArrayBuffer | Date | any> {
|
||||||
|
return this._db.then(db => {
|
||||||
|
const [custom] = idb.transact(db, [customStoreName], 'readonly');
|
||||||
|
if (custom) {
|
||||||
|
return idb.get(custom, key);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String | number | ArrayBuffer | Date} key
|
||||||
|
* @param {String | number | ArrayBuffer | Date} value
|
||||||
|
* @return {Promise<String | number | ArrayBuffer | Date>}
|
||||||
|
*/
|
||||||
|
async set(
|
||||||
|
key: string | number | ArrayBuffer | Date,
|
||||||
|
value: string | number | ArrayBuffer | Date
|
||||||
|
): Promise<string | number | ArrayBuffer | Date> {
|
||||||
|
return this._db.then(db => {
|
||||||
|
const [custom] = idb.transact(db, [customStoreName]);
|
||||||
|
if (custom) {
|
||||||
|
return idb.put(custom, value, key);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String | number | ArrayBuffer | Date} key
|
||||||
|
* @return {Promise<undefined>}
|
||||||
|
*/
|
||||||
|
async del(key: string | number | ArrayBuffer | Date): Promise<undefined> {
|
||||||
|
return this._db.then(db => {
|
||||||
|
const [custom] = idb.transact(db, [customStoreName]);
|
||||||
|
if (custom) {
|
||||||
|
return idb.del(custom, key);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { BaseProvider } from './base.js';
|
|
||||||
|
|
||||||
export class MemoryProvider extends BaseProvider {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initData() {
|
|
||||||
console.log('Skip data reload in memory provider');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import { getDataCenter } from './utils.js';
|
|||||||
|
|
||||||
import 'fake-indexeddb/auto';
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
test('can init data center', async () => {
|
test('init data center', async () => {
|
||||||
const dataCenter = await getDataCenter();
|
const dataCenter = await getDataCenter();
|
||||||
expect(dataCenter).toBeTruthy();
|
expect(dataCenter).toBeTruthy();
|
||||||
|
|
||||||
@@ -12,24 +12,38 @@ test('can init data center', async () => {
|
|||||||
expect(workspace).toBeTruthy();
|
expect(workspace).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should init error', async () => {
|
test('should init error with unknown provider', async () => {
|
||||||
const dataCenter = await getDataCenter();
|
const dataCenter = await getDataCenter();
|
||||||
|
|
||||||
test.fail();
|
test.fail();
|
||||||
await dataCenter.initWorkspace('test', 'not exist provider');
|
await dataCenter.initWorkspace('test', 'not exist provider');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can init affine provider', async () => {
|
test.skip('init affine provider', async () => {
|
||||||
const dataCenter = await getDataCenter();
|
const dataCenter = await getDataCenter();
|
||||||
|
|
||||||
// TODO: set constant token for testing
|
// TODO: set constant token for testing
|
||||||
await dataCenter.setWorkspaceConfig(
|
await dataCenter.setWorkspaceConfig('6', 'token', 'YOUR_TOKEN');
|
||||||
'6',
|
|
||||||
'token',
|
|
||||||
'Zzq338Py_3veZwD4XTa0nyoDGsLqhd9nFeaT1p_SK43TAOCSkcV63Tn3kDUWfBI4JHKqX7mhED4IFejm_62KUpGXRWZv11c5BGay7Nhvb_br'
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspace = await dataCenter.initWorkspace('6', 'affine');
|
const workspace = await dataCenter.initWorkspace('6', 'affine');
|
||||||
|
|
||||||
expect(workspace).toBeTruthy();
|
expect(workspace).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('list workspaces', async () => {
|
||||||
|
const dataCenter = await getDataCenter();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
dataCenter.initWorkspace('test1'),
|
||||||
|
dataCenter.initWorkspace('test2'),
|
||||||
|
dataCenter.initWorkspace('test3'),
|
||||||
|
dataCenter.initWorkspace('test4'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(await dataCenter.getWorkspaceList()).toStrictEqual([
|
||||||
|
'test1',
|
||||||
|
'test2',
|
||||||
|
'test3',
|
||||||
|
'test4',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -134,6 +134,8 @@ importers:
|
|||||||
'@blocksuite/blocks': 0.3.0-20221228162706-9576a3a
|
'@blocksuite/blocks': 0.3.0-20221228162706-9576a3a
|
||||||
'@blocksuite/store': 0.3.0-20221228162706-9576a3a
|
'@blocksuite/store': 0.3.0-20221228162706-9576a3a
|
||||||
'@playwright/test': ^1.29.1
|
'@playwright/test': ^1.29.1
|
||||||
|
'@types/debug': ^4.1.7
|
||||||
|
debug: ^4.3.4
|
||||||
encoding: ^0.1.13
|
encoding: ^0.1.13
|
||||||
fake-indexeddb: 4.0.1
|
fake-indexeddb: 4.0.1
|
||||||
firebase: ^9.15.0
|
firebase: ^9.15.0
|
||||||
@@ -148,6 +150,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@blocksuite/blocks': 0.3.0-20221228162706-9576a3a_yjs@13.5.44
|
'@blocksuite/blocks': 0.3.0-20221228162706-9576a3a_yjs@13.5.44
|
||||||
'@blocksuite/store': 0.3.0-20221228162706-9576a3a_yjs@13.5.44
|
'@blocksuite/store': 0.3.0-20221228162706-9576a3a_yjs@13.5.44
|
||||||
|
debug: 4.3.4
|
||||||
encoding: 0.1.13
|
encoding: 0.1.13
|
||||||
firebase: 9.15.0_encoding@0.1.13
|
firebase: 9.15.0_encoding@0.1.13
|
||||||
idb-keyval: 6.2.0
|
idb-keyval: 6.2.0
|
||||||
@@ -159,6 +162,7 @@ importers:
|
|||||||
yjs: 13.5.44
|
yjs: 13.5.44
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@playwright/test': 1.29.1
|
'@playwright/test': 1.29.1
|
||||||
|
'@types/debug': 4.1.7
|
||||||
fake-indexeddb: 4.0.1
|
fake-indexeddb: 4.0.1
|
||||||
typescript: 4.9.3
|
typescript: 4.9.3
|
||||||
|
|
||||||
@@ -3623,6 +3627,12 @@ packages:
|
|||||||
'@babel/types': 7.20.5
|
'@babel/types': 7.20.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/debug/4.1.7:
|
||||||
|
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 0.7.31
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/eslint/8.4.10:
|
/@types/eslint/8.4.10:
|
||||||
resolution: {integrity: sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==}
|
resolution: {integrity: sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3697,6 +3707,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/ms/0.7.31:
|
||||||
|
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node/12.20.55:
|
/@types/node/12.20.55:
|
||||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Reference in New Issue
Block a user