mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-19 02:51:47 +08:00
chore: remove unused y-indexeddb (#6728)
This commit is contained in:
@@ -52,7 +52,6 @@ const allPackages = [
|
|||||||
'packages/common/env',
|
'packages/common/env',
|
||||||
'packages/common/infra',
|
'packages/common/infra',
|
||||||
'packages/common/theme',
|
'packages/common/theme',
|
||||||
'packages/common/y-indexeddb',
|
|
||||||
'tools/cli',
|
'tools/cli',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,6 @@ rust:
|
|||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
- '**/rustfmt.toml'
|
- '**/rustfmt.toml'
|
||||||
|
|
||||||
package:y-indexeddb:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- 'packages/common/y-indexeddb/**/*'
|
|
||||||
|
|
||||||
app:core:
|
app:core:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
|||||||
@@ -110,11 +110,10 @@ If you have questions, you are welcome to contact us. One of the best places to
|
|||||||
|
|
||||||
## Ecosystem
|
## Ecosystem
|
||||||
|
|
||||||
| Name | | |
|
| Name | | |
|
||||||
| -------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [@affine/component](packages/frontend/component) | AFFiNE Component Resources |  |
|
| [@affine/component](packages/frontend/component) | AFFiNE Component Resources |  |
|
||||||
| [@toeverything/y-indexeddb](packages/common/y-indexeddb) | IndexedDB database adapter for Yjs | [](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
|
| [@toeverything/theme](packages/common/theme) | AFFiNE theme | [](https://www.npmjs.com/package/@toeverything/theme) |
|
||||||
| [@toeverything/theme](packages/common/theme) | AFFiNE theme | [](https://www.npmjs.com/package/@toeverything/theme) |
|
|
||||||
|
|
||||||
## Upstreams
|
## Upstreams
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
lib
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# @toeverything/y-indexeddb
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- persistence data in indexeddb
|
|
||||||
- sub-documents support
|
|
||||||
- fully TypeScript
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { createIndexedDBProvider, downloadBinary } from '@toeverything/y-indexeddb';
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
|
|
||||||
const yDoc = new Y.Doc({
|
|
||||||
// we use `guid` as unique key
|
|
||||||
guid: 'my-doc',
|
|
||||||
});
|
|
||||||
|
|
||||||
// sync yDoc with indexedDB
|
|
||||||
const provider = createIndexedDBProvider(yDoc);
|
|
||||||
provider.connect();
|
|
||||||
await provider.whenSynced.then(() => {
|
|
||||||
console.log('synced');
|
|
||||||
provider.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
// dowload binary data from indexedDB for once
|
|
||||||
downloadBinary(yDoc.guid).then(blob => {
|
|
||||||
if (blob !== false) {
|
|
||||||
Y.applyUpdate(yDoc, blob);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## LICENSE
|
|
||||||
|
|
||||||
[MIT](https://github.com/toeverything/AFFiNE/blob/canary/LICENSE-MIT)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@toeverything/y-indexeddb",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.14.0",
|
|
||||||
"description": "IndexedDB database adapter for Yjs",
|
|
||||||
"repository": "toeverything/AFFiNE",
|
|
||||||
"author": "toeverything",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"indexeddb",
|
|
||||||
"yjs",
|
|
||||||
"yjs-adapter"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"import": "./dist/index.js",
|
|
||||||
"require": "./dist/index.cjs",
|
|
||||||
"default": "./dist/index.umd.cjs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@blocksuite/global": "0.14.0-canary-202404280529-c8e5f89",
|
|
||||||
"idb": "^8.0.0",
|
|
||||||
"nanoid": "^5.0.7",
|
|
||||||
"y-provider": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@blocksuite/blocks": "0.14.0-canary-202404280529-c8e5f89",
|
|
||||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
|
||||||
"fake-indexeddb": "^5.0.2",
|
|
||||||
"vite": "^5.2.8",
|
|
||||||
"vite-plugin-dts": "3.8.1",
|
|
||||||
"vitest": "1.4.0",
|
|
||||||
"y-indexeddb": "^9.0.12",
|
|
||||||
"yjs": "^13.6.14"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"yjs": "^13"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "y-indexeddb",
|
|
||||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"projectType": "library",
|
|
||||||
"sourceRoot": "packages/common/y-indexeddb/src",
|
|
||||||
"targets": {
|
|
||||||
"build": {
|
|
||||||
"executor": "@nx/vite:build",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "packages/common/y-indexeddb/dist"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"executor": "@nx/vite:build",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "packages/common/y-indexeddb/dist",
|
|
||||||
"watch": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,495 +0,0 @@
|
|||||||
/**
|
|
||||||
* @vitest-environment happy-dom
|
|
||||||
*/
|
|
||||||
import 'fake-indexeddb/auto';
|
|
||||||
|
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
|
|
||||||
import { AffineSchemas } from '@blocksuite/blocks/schemas';
|
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import type { Doc } from '@blocksuite/store';
|
|
||||||
import { DocCollection, Schema } from '@blocksuite/store';
|
|
||||||
import { openDB } from 'idb';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
||||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
import type { WorkspacePersist } from '../index';
|
|
||||||
import {
|
|
||||||
createIndexedDBProvider,
|
|
||||||
dbVersion,
|
|
||||||
DEFAULT_DB_NAME,
|
|
||||||
downloadBinary,
|
|
||||||
getMilestones,
|
|
||||||
markMilestone,
|
|
||||||
overwriteBinary,
|
|
||||||
revertUpdate,
|
|
||||||
setMergeCount,
|
|
||||||
} from '../index';
|
|
||||||
|
|
||||||
function initEmptyPage(page: Doc) {
|
|
||||||
const pageBlockId = page.addBlock(
|
|
||||||
'affine:page' as keyof BlockSuite.BlockModels,
|
|
||||||
{
|
|
||||||
title: new page.Text(''),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const surfaceBlockId = page.addBlock(
|
|
||||||
'affine:surface' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
pageBlockId
|
|
||||||
);
|
|
||||||
const frameBlockId = page.addBlock(
|
|
||||||
'affine:note' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
pageBlockId
|
|
||||||
);
|
|
||||||
const paragraphBlockId = page.addBlock(
|
|
||||||
'affine:paragraph' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
frameBlockId
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
pageBlockId,
|
|
||||||
surfaceBlockId,
|
|
||||||
frameBlockId,
|
|
||||||
paragraphBlockId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUpdates(id: string): Promise<Uint8Array[]> {
|
|
||||||
const db = await openDB(rootDBName, dbVersion);
|
|
||||||
const store = db
|
|
||||||
.transaction('workspace', 'readonly')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const data = (await store.get(id)) as WorkspacePersist | undefined;
|
|
||||||
assertExists(data, 'data should not be undefined');
|
|
||||||
expect(data.id).toBe(id);
|
|
||||||
return data.updates.map(({ update }) => update);
|
|
||||||
}
|
|
||||||
|
|
||||||
let id: string;
|
|
||||||
let docCollection: DocCollection;
|
|
||||||
const rootDBName = DEFAULT_DB_NAME;
|
|
||||||
|
|
||||||
const schema = new Schema();
|
|
||||||
|
|
||||||
schema.register(AffineSchemas);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
id = nanoid();
|
|
||||||
docCollection = new DocCollection({
|
|
||||||
id,
|
|
||||||
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
indexedDB.deleteDatabase('affine-local');
|
|
||||||
localStorage.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('indexeddb provider', () => {
|
|
||||||
test('connect', async () => {
|
|
||||||
const provider = createIndexedDBProvider(docCollection.doc);
|
|
||||||
provider.connect();
|
|
||||||
|
|
||||||
// todo: has a better way to know when data is synced
|
|
||||||
await setTimeout(200);
|
|
||||||
|
|
||||||
const db = await openDB(rootDBName, dbVersion);
|
|
||||||
{
|
|
||||||
const store = db
|
|
||||||
.transaction('workspace', 'readonly')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const data = await store.get(id);
|
|
||||||
expect(data).toEqual({
|
|
||||||
id,
|
|
||||||
updates: [
|
|
||||||
{
|
|
||||||
timestamp: expect.any(Number),
|
|
||||||
update: encodeStateAsUpdate(docCollection.doc),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const page = docCollection.createDoc({ id: 'page0' });
|
|
||||||
page.load();
|
|
||||||
const pageBlockId = page.addBlock(
|
|
||||||
'affine:page' as keyof BlockSuite.BlockModels,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const frameId = page.addBlock(
|
|
||||||
'affine:note' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
pageBlockId
|
|
||||||
);
|
|
||||||
page.addBlock(
|
|
||||||
'affine:paragraph' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
frameId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await setTimeout(200);
|
|
||||||
{
|
|
||||||
const store = db
|
|
||||||
.transaction('workspace', 'readonly')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const data = (await store.get(id)) as WorkspacePersist | undefined;
|
|
||||||
assertExists(data);
|
|
||||||
expect(data.id).toBe(id);
|
|
||||||
const testWorkspace = new DocCollection({
|
|
||||||
id: 'test',
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
// data should only contain updates for the root doc
|
|
||||||
data.updates.forEach(({ update }) => {
|
|
||||||
DocCollection.Y.applyUpdate(testWorkspace.doc, update);
|
|
||||||
});
|
|
||||||
const subPage = testWorkspace.doc.spaces.get('page0');
|
|
||||||
{
|
|
||||||
assertExists(subPage);
|
|
||||||
await store.get(subPage.guid);
|
|
||||||
const data = (await store.get(subPage.guid)) as
|
|
||||||
| WorkspacePersist
|
|
||||||
| undefined;
|
|
||||||
assertExists(data);
|
|
||||||
testWorkspace.getDoc('page0')?.load();
|
|
||||||
data.updates.forEach(({ update }) => {
|
|
||||||
DocCollection.Y.applyUpdate(subPage, update);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
expect(docCollection.doc.toJSON()).toEqual(testWorkspace.doc.toJSON());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('connect and disconnect', async () => {
|
|
||||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
|
||||||
provider.connect();
|
|
||||||
expect(provider.connected).toBe(true);
|
|
||||||
await setTimeout(200);
|
|
||||||
const snapshot = encodeStateAsUpdate(docCollection.doc);
|
|
||||||
provider.disconnect();
|
|
||||||
expect(provider.connected).toBe(false);
|
|
||||||
{
|
|
||||||
const page = docCollection.createDoc({ id: 'page0' });
|
|
||||||
page.load();
|
|
||||||
const pageBlockId = page.addBlock(
|
|
||||||
'affine:page' as keyof BlockSuite.BlockModels
|
|
||||||
);
|
|
||||||
const frameId = page.addBlock(
|
|
||||||
'affine:note' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
pageBlockId
|
|
||||||
);
|
|
||||||
page.addBlock(
|
|
||||||
'affine:paragraph' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
frameId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const updates = await getUpdates(docCollection.id);
|
|
||||||
expect(updates.length).toBe(1);
|
|
||||||
expect(updates[0]).toEqual(snapshot);
|
|
||||||
}
|
|
||||||
expect(provider.connected).toBe(false);
|
|
||||||
provider.connect();
|
|
||||||
expect(provider.connected).toBe(true);
|
|
||||||
await setTimeout(200);
|
|
||||||
{
|
|
||||||
const updates = await getUpdates(docCollection.id);
|
|
||||||
expect(updates).not.toEqual([]);
|
|
||||||
}
|
|
||||||
expect(provider.connected).toBe(true);
|
|
||||||
provider.disconnect();
|
|
||||||
expect(provider.connected).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cleanup', async () => {
|
|
||||||
const provider = createIndexedDBProvider(docCollection.doc);
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout(200);
|
|
||||||
const db = await openDB(rootDBName, dbVersion);
|
|
||||||
|
|
||||||
{
|
|
||||||
const store = db
|
|
||||||
.transaction('workspace', 'readonly')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const keys = await store.getAllKeys();
|
|
||||||
expect(keys).contain(docCollection.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await provider.cleanup();
|
|
||||||
provider.disconnect();
|
|
||||||
|
|
||||||
{
|
|
||||||
const store = db
|
|
||||||
.transaction('workspace', 'readonly')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const keys = await store.getAllKeys();
|
|
||||||
expect(keys).not.contain(docCollection.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('merge', async () => {
|
|
||||||
setMergeCount(5);
|
|
||||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
|
||||||
provider.connect();
|
|
||||||
{
|
|
||||||
const page = docCollection.createDoc({ id: 'page0' });
|
|
||||||
page.load();
|
|
||||||
const pageBlockId = page.addBlock(
|
|
||||||
'affine:page' as keyof BlockSuite.BlockModels
|
|
||||||
);
|
|
||||||
const frameId = page.addBlock(
|
|
||||||
'affine:note' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
pageBlockId
|
|
||||||
);
|
|
||||||
for (let i = 0; i < 99; i++) {
|
|
||||||
page.addBlock(
|
|
||||||
'affine:paragraph' as keyof BlockSuite.BlockModels,
|
|
||||||
{},
|
|
||||||
frameId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await setTimeout(200);
|
|
||||||
{
|
|
||||||
const updates = await getUpdates(id);
|
|
||||||
expect(updates.length).lessThanOrEqual(5);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("data won't be lost", async () => {
|
|
||||||
const doc = new DocCollection.Y.Doc();
|
|
||||||
const map = doc.getMap('map');
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
map.set(`${i}`, i);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const provider = createIndexedDBProvider(doc, rootDBName);
|
|
||||||
provider.connect();
|
|
||||||
provider.disconnect();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const newDoc = new DocCollection.Y.Doc();
|
|
||||||
const provider = createIndexedDBProvider(newDoc, rootDBName);
|
|
||||||
provider.connect();
|
|
||||||
provider.disconnect();
|
|
||||||
newDoc.getMap('map').forEach((value, key) => {
|
|
||||||
expect(value).toBe(parseInt(key));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('beforeunload', async () => {
|
|
||||||
const oldAddEventListener = window.addEventListener;
|
|
||||||
window.addEventListener = vi.fn((event: string, fn, options) => {
|
|
||||||
expect(event).toBe('beforeunload');
|
|
||||||
return oldAddEventListener(event, fn, options);
|
|
||||||
});
|
|
||||||
const oldRemoveEventListener = window.removeEventListener;
|
|
||||||
window.removeEventListener = vi.fn((event: string, fn, options) => {
|
|
||||||
expect(event).toBe('beforeunload');
|
|
||||||
return oldRemoveEventListener(event, fn, options);
|
|
||||||
});
|
|
||||||
const doc = new YDoc({
|
|
||||||
guid: '1',
|
|
||||||
});
|
|
||||||
const provider = createIndexedDBProvider(doc);
|
|
||||||
const map = doc.getMap('map');
|
|
||||||
map.set('1', 1);
|
|
||||||
provider.connect();
|
|
||||||
|
|
||||||
await setTimeout(200);
|
|
||||||
|
|
||||||
expect(window.addEventListener).toBeCalledTimes(1);
|
|
||||||
expect(window.removeEventListener).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
window.addEventListener = oldAddEventListener;
|
|
||||||
window.removeEventListener = oldRemoveEventListener;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('milestone', () => {
|
|
||||||
test('milestone', async () => {
|
|
||||||
const doc = new YDoc();
|
|
||||||
const map = doc.getMap('map');
|
|
||||||
const array = doc.getArray('array');
|
|
||||||
map.set('1', 1);
|
|
||||||
array.push([1]);
|
|
||||||
await markMilestone('1', doc, 'test1');
|
|
||||||
const milestones = await getMilestones('1');
|
|
||||||
assertExists(milestones);
|
|
||||||
expect(milestones).toBeDefined();
|
|
||||||
expect(Object.keys(milestones).length).toBe(1);
|
|
||||||
expect(milestones.test1).toBeInstanceOf(Uint8Array);
|
|
||||||
const snapshot = new YDoc();
|
|
||||||
applyUpdate(snapshot, milestones.test1);
|
|
||||||
{
|
|
||||||
const map = snapshot.getMap('map');
|
|
||||||
expect(map.get('1')).toBe(1);
|
|
||||||
}
|
|
||||||
map.set('1', 2);
|
|
||||||
{
|
|
||||||
const map = snapshot.getMap('map');
|
|
||||||
expect(map.get('1')).toBe(1);
|
|
||||||
}
|
|
||||||
revertUpdate(doc, milestones.test1, key =>
|
|
||||||
key === 'map' ? 'Map' : 'Array'
|
|
||||||
);
|
|
||||||
{
|
|
||||||
const map = doc.getMap('map');
|
|
||||||
expect(map.get('1')).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn = vi.fn(() => true);
|
|
||||||
doc.gcFilter = fn;
|
|
||||||
expect(fn).toBeCalledTimes(0);
|
|
||||||
|
|
||||||
for (let i = 0; i < 1e5; i++) {
|
|
||||||
map.set(`${i}`, i + 1);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 1e5; i++) {
|
|
||||||
map.delete(`${i}`);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 1e5; i++) {
|
|
||||||
map.set(`${i}`, i - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(fn).toBeCalled();
|
|
||||||
|
|
||||||
const doc2 = new YDoc();
|
|
||||||
applyUpdate(doc2, encodeStateAsUpdate(doc));
|
|
||||||
|
|
||||||
revertUpdate(doc2, milestones.test1, key =>
|
|
||||||
key === 'map' ? 'Map' : 'Array'
|
|
||||||
);
|
|
||||||
{
|
|
||||||
const map = doc2.getMap('map');
|
|
||||||
expect(map.get('1')).toBe(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('subDoc', () => {
|
|
||||||
test('basic', async () => {
|
|
||||||
let json1: any, json2: any;
|
|
||||||
{
|
|
||||||
const doc = new YDoc({
|
|
||||||
guid: 'test',
|
|
||||||
});
|
|
||||||
const map = doc.getMap();
|
|
||||||
const subDoc = new YDoc();
|
|
||||||
subDoc.load();
|
|
||||||
map.set('1', subDoc);
|
|
||||||
map.set('2', 'test');
|
|
||||||
const provider = createIndexedDBProvider(doc);
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout(200);
|
|
||||||
provider.disconnect();
|
|
||||||
json1 = doc.toJSON();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const doc = new YDoc({
|
|
||||||
guid: 'test',
|
|
||||||
});
|
|
||||||
const provider = createIndexedDBProvider(doc);
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout(200);
|
|
||||||
const map = doc.getMap();
|
|
||||||
const subDoc = map.get('1') as YDoc;
|
|
||||||
subDoc.load();
|
|
||||||
provider.disconnect();
|
|
||||||
json2 = doc.toJSON();
|
|
||||||
}
|
|
||||||
// the following line compares {} with {}
|
|
||||||
expect(json1['']['1'].toJSON()).toEqual(json2['']['1'].toJSON());
|
|
||||||
expect(json1['']['2']).toEqual(json2['']['2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('blocksuite', async () => {
|
|
||||||
const page0 = docCollection.createDoc({
|
|
||||||
id: 'page0',
|
|
||||||
});
|
|
||||||
page0.load();
|
|
||||||
const { paragraphBlockId: paragraphBlockIdPage1 } = initEmptyPage(page0);
|
|
||||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
|
||||||
provider.connect();
|
|
||||||
const page1 = docCollection.createDoc({
|
|
||||||
id: 'page1',
|
|
||||||
});
|
|
||||||
page1.load();
|
|
||||||
const { paragraphBlockId: paragraphBlockIdPage2 } = initEmptyPage(page1);
|
|
||||||
await setTimeout(200);
|
|
||||||
provider.disconnect();
|
|
||||||
{
|
|
||||||
const docCollection = new DocCollection({
|
|
||||||
id,
|
|
||||||
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout(200);
|
|
||||||
const page0 = docCollection.getDoc('page0') as Doc;
|
|
||||||
page0.load();
|
|
||||||
await setTimeout(200);
|
|
||||||
{
|
|
||||||
const block = page0.getBlockById(paragraphBlockIdPage1);
|
|
||||||
assertExists(block);
|
|
||||||
}
|
|
||||||
const page1 = docCollection.getDoc('page1') as Doc;
|
|
||||||
page1.load();
|
|
||||||
await setTimeout(200);
|
|
||||||
{
|
|
||||||
const block = page1.getBlockById(paragraphBlockIdPage2);
|
|
||||||
assertExists(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('utils', () => {
|
|
||||||
test('download binary', async () => {
|
|
||||||
const page = docCollection.createDoc({ id: 'page0' });
|
|
||||||
page.load();
|
|
||||||
initEmptyPage(page);
|
|
||||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout(200);
|
|
||||||
provider.disconnect();
|
|
||||||
const update = (await downloadBinary(
|
|
||||||
docCollection.id,
|
|
||||||
rootDBName
|
|
||||||
)) as Uint8Array;
|
|
||||||
expect(update).toBeInstanceOf(Uint8Array);
|
|
||||||
const newDocCollection = new DocCollection({
|
|
||||||
id,
|
|
||||||
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
applyUpdate(newDocCollection.doc, update);
|
|
||||||
await setTimeout();
|
|
||||||
expect(docCollection.doc.toJSON()['meta']).toEqual(
|
|
||||||
newDocCollection.doc.toJSON()['meta']
|
|
||||||
);
|
|
||||||
expect(Object.keys(docCollection.doc.toJSON()['spaces'])).toEqual(
|
|
||||||
Object.keys(newDocCollection.doc.toJSON()['spaces'])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('overwrite binary', async () => {
|
|
||||||
const doc = new YDoc();
|
|
||||||
const map = doc.getMap();
|
|
||||||
map.set('1', 1);
|
|
||||||
await overwriteBinary('test', new Uint8Array(encodeStateAsUpdate(doc)));
|
|
||||||
{
|
|
||||||
const binary = await downloadBinary('test');
|
|
||||||
expect(binary).toEqual(new Uint8Array(encodeStateAsUpdate(doc)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import { openDB } from 'idb';
|
|
||||||
import {
|
|
||||||
applyUpdate,
|
|
||||||
Doc,
|
|
||||||
encodeStateAsUpdate,
|
|
||||||
encodeStateVector,
|
|
||||||
UndoManager,
|
|
||||||
} from 'yjs';
|
|
||||||
|
|
||||||
import type { BlockSuiteBinaryDB, WorkspaceMilestone } from './shared';
|
|
||||||
import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared';
|
|
||||||
|
|
||||||
const snapshotOrigin = 'snapshot-origin';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
const saveAlert = (event: BeforeUnloadEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
return (event.returnValue =
|
|
||||||
'Data is not saved. Are you sure you want to leave?');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const writeOperation = async (op: Promise<unknown>) => {
|
|
||||||
window.addEventListener('beforeunload', saveAlert, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
await op;
|
|
||||||
window.removeEventListener('beforeunload', saveAlert, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function revertUpdate(
|
|
||||||
doc: Doc,
|
|
||||||
snapshotUpdate: Uint8Array,
|
|
||||||
getMetadata: (key: string) => 'Text' | 'Map' | 'Array'
|
|
||||||
) {
|
|
||||||
const snapshotDoc = new Doc();
|
|
||||||
applyUpdate(snapshotDoc, snapshotUpdate, snapshotOrigin);
|
|
||||||
|
|
||||||
const currentStateVector = encodeStateVector(doc);
|
|
||||||
const snapshotStateVector = encodeStateVector(snapshotDoc);
|
|
||||||
|
|
||||||
const changesSinceSnapshotUpdate = encodeStateAsUpdate(
|
|
||||||
doc,
|
|
||||||
snapshotStateVector
|
|
||||||
);
|
|
||||||
const undoManager = new UndoManager(
|
|
||||||
[...snapshotDoc.share.keys()].map(key => {
|
|
||||||
const type = getMetadata(key);
|
|
||||||
if (type === 'Text') {
|
|
||||||
return snapshotDoc.getText(key);
|
|
||||||
} else if (type === 'Map') {
|
|
||||||
return snapshotDoc.getMap(key);
|
|
||||||
} else if (type === 'Array') {
|
|
||||||
return snapshotDoc.getArray(key);
|
|
||||||
}
|
|
||||||
throw new Error('Unknown type');
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
trackedOrigins: new Set([snapshotOrigin]),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
applyUpdate(snapshotDoc, changesSinceSnapshotUpdate, snapshotOrigin);
|
|
||||||
undoManager.undo();
|
|
||||||
const revertChangesSinceSnapshotUpdate = encodeStateAsUpdate(
|
|
||||||
snapshotDoc,
|
|
||||||
currentStateVector
|
|
||||||
);
|
|
||||||
applyUpdate(doc, revertChangesSinceSnapshotUpdate, snapshotOrigin);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EarlyDisconnectError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super('Early disconnect');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CleanupWhenConnectingError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super('Cleanup when connecting');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const markMilestone = async (
|
|
||||||
id: string,
|
|
||||||
doc: Doc,
|
|
||||||
name: string,
|
|
||||||
dbName = DEFAULT_DB_NAME
|
|
||||||
): Promise<void> => {
|
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
|
||||||
upgrade: upgradeDB,
|
|
||||||
});
|
|
||||||
const db = await dbPromise;
|
|
||||||
const store = db
|
|
||||||
.transaction('milestone', 'readwrite')
|
|
||||||
.objectStore('milestone');
|
|
||||||
const milestone = await store.get('id');
|
|
||||||
const binary = encodeStateAsUpdate(doc);
|
|
||||||
if (!milestone) {
|
|
||||||
await store.put({
|
|
||||||
id,
|
|
||||||
milestone: {
|
|
||||||
[name]: binary,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
milestone.milestone[name] = binary;
|
|
||||||
await store.put(milestone);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMilestones = async (
|
|
||||||
id: string,
|
|
||||||
dbName: string = DEFAULT_DB_NAME
|
|
||||||
): Promise<null | WorkspaceMilestone['milestone']> => {
|
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
|
||||||
upgrade: upgradeDB,
|
|
||||||
});
|
|
||||||
const db = await dbPromise;
|
|
||||||
const store = db
|
|
||||||
.transaction('milestone', 'readonly')
|
|
||||||
.objectStore('milestone');
|
|
||||||
const milestone = await store.get(id);
|
|
||||||
if (!milestone) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return milestone.milestone;
|
|
||||||
};
|
|
||||||
|
|
||||||
export * from './provider';
|
|
||||||
export * from './shared';
|
|
||||||
export * from './utils';
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import type { IDBPDatabase } from 'idb';
|
|
||||||
import { openDB } from 'idb';
|
|
||||||
import type { DocDataSource } from 'y-provider';
|
|
||||||
import { createLazyProvider, writeOperation } from 'y-provider';
|
|
||||||
import type { Doc } from 'yjs';
|
|
||||||
import { diffUpdate, encodeStateVectorFromUpdate } from 'yjs';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
BlockSuiteBinaryDB,
|
|
||||||
IndexedDBProvider,
|
|
||||||
UpdateMessage,
|
|
||||||
} from './shared';
|
|
||||||
import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared';
|
|
||||||
import { mergeUpdates } from './utils';
|
|
||||||
|
|
||||||
let mergeCount = 500;
|
|
||||||
|
|
||||||
export function setMergeCount(count: number) {
|
|
||||||
mergeCount = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createIndexedDBDatasource = ({
|
|
||||||
dbName = DEFAULT_DB_NAME,
|
|
||||||
mergeCount,
|
|
||||||
}: {
|
|
||||||
dbName?: string;
|
|
||||||
mergeCount?: number;
|
|
||||||
}) => {
|
|
||||||
let dbPromise: Promise<IDBPDatabase<BlockSuiteBinaryDB>> | null = null;
|
|
||||||
const getDb = async () => {
|
|
||||||
if (dbPromise === null) {
|
|
||||||
dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
|
||||||
upgrade: upgradeDB,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return dbPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
const adapter = {
|
|
||||||
queryDocState: async (guid, options) => {
|
|
||||||
try {
|
|
||||||
const db = await getDb();
|
|
||||||
const store = db
|
|
||||||
.transaction('workspace', 'readonly')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const data = await store.get(guid);
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { updates } = data;
|
|
||||||
const update = mergeUpdates(updates.map(({ update }) => update));
|
|
||||||
|
|
||||||
const missing = options?.stateVector
|
|
||||||
? diffUpdate(update, options?.stateVector)
|
|
||||||
: update;
|
|
||||||
|
|
||||||
return { missing, state: encodeStateVectorFromUpdate(update) };
|
|
||||||
} catch (err: any) {
|
|
||||||
if (!err.message?.includes('The database connection is closing.')) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendDocUpdate: async (guid, update) => {
|
|
||||||
try {
|
|
||||||
const db = await getDb();
|
|
||||||
const store = db
|
|
||||||
.transaction('workspace', 'readwrite')
|
|
||||||
.objectStore('workspace');
|
|
||||||
|
|
||||||
// TODO: maybe we do not need to get data every time
|
|
||||||
const { updates } = (await store.get(guid)) ?? { updates: [] };
|
|
||||||
let rows: UpdateMessage[] = [
|
|
||||||
...updates,
|
|
||||||
{ timestamp: Date.now(), update },
|
|
||||||
];
|
|
||||||
if (mergeCount && rows.length >= mergeCount) {
|
|
||||||
const merged = mergeUpdates(rows.map(({ update }) => update));
|
|
||||||
rows = [{ timestamp: Date.now(), update: merged }];
|
|
||||||
}
|
|
||||||
await writeOperation(
|
|
||||||
store.put({
|
|
||||||
id: guid,
|
|
||||||
updates: rows,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
if (!err.message?.includes('The database connection is closing.')) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} satisfies DocDataSource;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...adapter,
|
|
||||||
disconnect: () => {
|
|
||||||
getDb()
|
|
||||||
.then(db => db.close())
|
|
||||||
.then(() => {
|
|
||||||
dbPromise = null;
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
},
|
|
||||||
cleanup: async () => {
|
|
||||||
const db = await getDb();
|
|
||||||
await db.clear('workspace');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We use `doc.guid` as the unique key, please make sure it not changes.
|
|
||||||
*/
|
|
||||||
export const createIndexedDBProvider = (
|
|
||||||
doc: Doc,
|
|
||||||
dbName: string = DEFAULT_DB_NAME
|
|
||||||
): IndexedDBProvider => {
|
|
||||||
const datasource = createIndexedDBDatasource({ dbName, mergeCount });
|
|
||||||
let provider: ReturnType<typeof createLazyProvider> | null = null;
|
|
||||||
|
|
||||||
const apis = {
|
|
||||||
get status() {
|
|
||||||
assertExists(provider);
|
|
||||||
return provider.status;
|
|
||||||
},
|
|
||||||
subscribeStatusChange(onStatusChange) {
|
|
||||||
assertExists(provider);
|
|
||||||
return provider.subscribeStatusChange(onStatusChange);
|
|
||||||
},
|
|
||||||
connect: () => {
|
|
||||||
if (apis.connected) {
|
|
||||||
apis.disconnect();
|
|
||||||
}
|
|
||||||
provider = createLazyProvider(doc, datasource, { origin: 'idb' });
|
|
||||||
provider.connect();
|
|
||||||
},
|
|
||||||
disconnect: () => {
|
|
||||||
datasource?.disconnect();
|
|
||||||
provider?.disconnect();
|
|
||||||
provider = null;
|
|
||||||
},
|
|
||||||
cleanup: async () => {
|
|
||||||
await datasource?.cleanup();
|
|
||||||
},
|
|
||||||
get connected() {
|
|
||||||
return provider?.connected || false;
|
|
||||||
},
|
|
||||||
datasource,
|
|
||||||
} satisfies IndexedDBProvider;
|
|
||||||
|
|
||||||
return apis;
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import type { DBSchema, IDBPDatabase } from 'idb';
|
|
||||||
import type { DataSourceAdapter } from 'y-provider';
|
|
||||||
|
|
||||||
export const dbVersion = 1;
|
|
||||||
export const DEFAULT_DB_NAME = 'affine-local';
|
|
||||||
|
|
||||||
export function upgradeDB(db: IDBPDatabase<BlockSuiteBinaryDB>) {
|
|
||||||
db.createObjectStore('workspace', { keyPath: 'id' });
|
|
||||||
db.createObjectStore('milestone', { keyPath: 'id' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IndexedDBProvider extends DataSourceAdapter {
|
|
||||||
connect: () => void;
|
|
||||||
disconnect: () => void;
|
|
||||||
cleanup: () => Promise<void>;
|
|
||||||
readonly connected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UpdateMessage = {
|
|
||||||
timestamp: number;
|
|
||||||
update: Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspacePersist = {
|
|
||||||
id: string;
|
|
||||||
updates: UpdateMessage[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceMilestone = {
|
|
||||||
id: string;
|
|
||||||
milestone: Record<string, Uint8Array>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface BlockSuiteBinaryDB extends DBSchema {
|
|
||||||
workspace: {
|
|
||||||
key: string;
|
|
||||||
value: WorkspacePersist;
|
|
||||||
};
|
|
||||||
milestone: {
|
|
||||||
key: string;
|
|
||||||
value: WorkspaceMilestone;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OldYjsDB extends DBSchema {
|
|
||||||
updates: {
|
|
||||||
key: number;
|
|
||||||
value: Uint8Array;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import type { IDBPDatabase } from 'idb';
|
|
||||||
import { openDB } from 'idb';
|
|
||||||
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
import type { BlockSuiteBinaryDB, OldYjsDB, UpdateMessage } from './shared';
|
|
||||||
import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared';
|
|
||||||
|
|
||||||
let allDb: IDBDatabaseInfo[];
|
|
||||||
|
|
||||||
export function mergeUpdates(updates: Uint8Array[]) {
|
|
||||||
const doc = new Doc();
|
|
||||||
updates.forEach(update => {
|
|
||||||
applyUpdate(doc, update);
|
|
||||||
});
|
|
||||||
return encodeStateAsUpdate(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function databaseExists(name: string): Promise<boolean> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const req = indexedDB.open(name);
|
|
||||||
let existed = true;
|
|
||||||
req.onsuccess = function () {
|
|
||||||
req.result.close();
|
|
||||||
if (!existed) {
|
|
||||||
indexedDB.deleteDatabase(name);
|
|
||||||
}
|
|
||||||
resolve(existed);
|
|
||||||
};
|
|
||||||
req.onupgradeneeded = function () {
|
|
||||||
existed = false;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* try to migrate the old database to the new database
|
|
||||||
* this function will be removed in the future
|
|
||||||
* since we don't need to support the old database
|
|
||||||
*/
|
|
||||||
export async function tryMigrate(
|
|
||||||
db: IDBPDatabase<BlockSuiteBinaryDB>,
|
|
||||||
id: string,
|
|
||||||
dbName = DEFAULT_DB_NAME
|
|
||||||
) {
|
|
||||||
do {
|
|
||||||
if (!allDb || localStorage.getItem(`${dbName}-migration`) !== 'true') {
|
|
||||||
try {
|
|
||||||
allDb = await indexedDB.databases();
|
|
||||||
} catch {
|
|
||||||
// in firefox, `indexedDB.databases` is not existed
|
|
||||||
if (await databaseExists(id)) {
|
|
||||||
await openDB<IDBPDatabase<OldYjsDB>>(id, 1).then(async oldDB => {
|
|
||||||
if (!oldDB.objectStoreNames.contains('updates')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const t = oldDB
|
|
||||||
.transaction('updates', 'readonly')
|
|
||||||
.objectStore('updates');
|
|
||||||
const updates = await t.getAll();
|
|
||||||
if (
|
|
||||||
!Array.isArray(updates) ||
|
|
||||||
!updates.every(update => update instanceof Uint8Array)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const update = mergeUpdates(updates);
|
|
||||||
const workspaceTransaction = db
|
|
||||||
.transaction('workspace', 'readwrite')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const data = await workspaceTransaction.get(id);
|
|
||||||
if (!data) {
|
|
||||||
console.log('upgrading the database');
|
|
||||||
await workspaceTransaction.put({
|
|
||||||
id,
|
|
||||||
updates: [
|
|
||||||
{
|
|
||||||
timestamp: Date.now(),
|
|
||||||
update,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// run the migration
|
|
||||||
await Promise.all(
|
|
||||||
allDb &&
|
|
||||||
allDb.map(meta => {
|
|
||||||
if (meta.name && meta.version === 1) {
|
|
||||||
const name = meta.name;
|
|
||||||
const version = meta.version;
|
|
||||||
return openDB<IDBPDatabase<OldYjsDB>>(name, version).then(
|
|
||||||
async oldDB => {
|
|
||||||
if (!oldDB.objectStoreNames.contains('updates')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const t = oldDB
|
|
||||||
.transaction('updates', 'readonly')
|
|
||||||
.objectStore('updates');
|
|
||||||
const updates = await t.getAll();
|
|
||||||
if (
|
|
||||||
!Array.isArray(updates) ||
|
|
||||||
!updates.every(update => update instanceof Uint8Array)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const update = mergeUpdates(updates);
|
|
||||||
const workspaceTransaction = db
|
|
||||||
.transaction('workspace', 'readwrite')
|
|
||||||
.objectStore('workspace');
|
|
||||||
const data = await workspaceTransaction.get(name);
|
|
||||||
if (!data) {
|
|
||||||
console.log('upgrading the database');
|
|
||||||
await workspaceTransaction.put({
|
|
||||||
id: name,
|
|
||||||
updates: [
|
|
||||||
{
|
|
||||||
timestamp: Date.now(),
|
|
||||||
update,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
localStorage.setItem(`${dbName}-migration`, 'true');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
} while (false);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadBinary(
|
|
||||||
guid: string,
|
|
||||||
dbName = DEFAULT_DB_NAME
|
|
||||||
): Promise<UpdateMessage['update'] | false> {
|
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
|
||||||
upgrade: upgradeDB,
|
|
||||||
});
|
|
||||||
const db = await dbPromise;
|
|
||||||
const t = db.transaction('workspace', 'readonly').objectStore('workspace');
|
|
||||||
const doc = await t.get(guid);
|
|
||||||
if (!doc) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return mergeUpdates(doc.updates.map(({ update }) => update));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function overwriteBinary(
|
|
||||||
guid: string,
|
|
||||||
update: UpdateMessage['update'],
|
|
||||||
dbName = DEFAULT_DB_NAME
|
|
||||||
) {
|
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
|
||||||
upgrade: upgradeDB,
|
|
||||||
});
|
|
||||||
const db = await dbPromise;
|
|
||||||
const t = db.transaction('workspace', 'readwrite').objectStore('workspace');
|
|
||||||
await t.put({
|
|
||||||
id: guid,
|
|
||||||
updates: [
|
|
||||||
{
|
|
||||||
timestamp: Date.now(),
|
|
||||||
update,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function pushBinary(
|
|
||||||
guid: string,
|
|
||||||
update: UpdateMessage['update'],
|
|
||||||
dbName = DEFAULT_DB_NAME
|
|
||||||
) {
|
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
|
||||||
upgrade: upgradeDB,
|
|
||||||
});
|
|
||||||
const db = await dbPromise;
|
|
||||||
const t = db.transaction('workspace', 'readwrite').objectStore('workspace');
|
|
||||||
const doc = await t.get(guid);
|
|
||||||
if (!doc) {
|
|
||||||
await t.put({
|
|
||||||
id: guid,
|
|
||||||
updates: [
|
|
||||||
{
|
|
||||||
timestamp: Date.now(),
|
|
||||||
update,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
doc.updates.push({
|
|
||||||
timestamp: Date.now(),
|
|
||||||
update,
|
|
||||||
});
|
|
||||||
await t.put(doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"include": ["./src"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"noEmit": false,
|
|
||||||
"outDir": "lib"
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.node.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../y-provider"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"outDir": "lib"
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { resolve } from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import dts from 'vite-plugin-dts';
|
|
||||||
|
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
build: {
|
|
||||||
minify: 'esbuild',
|
|
||||||
sourcemap: true,
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'src/index.ts'),
|
|
||||||
fileName: 'index',
|
|
||||||
name: 'ToEverythingIndexedDBProvider',
|
|
||||||
formats: ['es', 'cjs', 'umd'],
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
globals: {
|
|
||||||
idb: 'idb',
|
|
||||||
yjs: 'yjs',
|
|
||||||
'y-provider': 'yProvider',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
external: ['idb', 'yjs', 'y-provider'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
dts({
|
|
||||||
entryRoot: resolve(__dirname, 'src'),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# A set of provider utilities for Yjs
|
|
||||||
|
|
||||||
## createLazyProvider
|
|
||||||
|
|
||||||
A factory function to create a lazy provider. It will not download the document from the provider until the first time a document is loaded at the parent doc.
|
|
||||||
|
|
||||||
To use it, first define a `DatasourceDocAdapter`.
|
|
||||||
Then, create a `LazyProvider` with `createLazyProvider(rootDoc, datasource)`.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "y-provider",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.14.0",
|
|
||||||
"description": "Yjs provider protocol for multi document support",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"import": "./dist/index.js",
|
|
||||||
"require": "./dist/index.cjs",
|
|
||||||
"default": "./dist/index.umd.cjs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@blocksuite/store": "0.14.0-canary-202404280529-c8e5f89",
|
|
||||||
"vite": "^5.1.4",
|
|
||||||
"vite-plugin-dts": "3.7.3",
|
|
||||||
"vitest": "1.4.0",
|
|
||||||
"yjs": "^13.6.14"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@blocksuite/global": "*",
|
|
||||||
"yjs": "^13"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
|
|
||||||
import { describe, expect, test, vi } from 'vitest';
|
|
||||||
import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from 'yjs';
|
|
||||||
|
|
||||||
import type { DocDataSource } from '../data-source';
|
|
||||||
import { createLazyProvider } from '../lazy-provider';
|
|
||||||
import { getDoc } from '../utils';
|
|
||||||
|
|
||||||
const createMemoryDatasource = (rootDoc: Doc) => {
|
|
||||||
const selfUpdateOrigin = Symbol('self-origin');
|
|
||||||
const listeners = new Set<(guid: string, update: Uint8Array) => void>();
|
|
||||||
|
|
||||||
function trackDoc(doc: Doc) {
|
|
||||||
doc.on('update', (update, origin) => {
|
|
||||||
if (origin === selfUpdateOrigin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const listener of listeners) {
|
|
||||||
listener(doc.guid, update);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
doc.on('subdocs', () => {
|
|
||||||
for (const subdoc of rootDoc.subdocs) {
|
|
||||||
trackDoc(subdoc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
trackDoc(rootDoc);
|
|
||||||
|
|
||||||
const adapter = {
|
|
||||||
queryDocState: async (guid, options) => {
|
|
||||||
const subdoc = getDoc(rootDoc, guid);
|
|
||||||
if (!subdoc) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
missing: encodeStateAsUpdate(subdoc, options?.stateVector),
|
|
||||||
state: encodeStateVector(subdoc),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
sendDocUpdate: async (guid, update) => {
|
|
||||||
const subdoc = getDoc(rootDoc, guid);
|
|
||||||
if (!subdoc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyUpdate(subdoc, update, selfUpdateOrigin);
|
|
||||||
},
|
|
||||||
onDocUpdate: callback => {
|
|
||||||
listeners.add(callback);
|
|
||||||
return () => {
|
|
||||||
listeners.delete(callback);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} satisfies DocDataSource;
|
|
||||||
return {
|
|
||||||
rootDoc, // expose rootDoc for testing
|
|
||||||
...adapter,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('y-provider', () => {
|
|
||||||
test('should sync a subdoc if it is loaded after connect', async () => {
|
|
||||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
|
||||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
|
||||||
|
|
||||||
const remotesubdoc = new Doc();
|
|
||||||
remotesubdoc.getText('text').insert(0, 'test-subdoc-value');
|
|
||||||
// populate remote doc with simple data
|
|
||||||
remoteRootDoc.getMap('map').set('test-0', 'test-0-value');
|
|
||||||
remoteRootDoc.getMap('map').set('subdoc', remotesubdoc);
|
|
||||||
|
|
||||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
|
||||||
const provider = createLazyProvider(rootDoc, datasource);
|
|
||||||
|
|
||||||
provider.connect();
|
|
||||||
|
|
||||||
await setTimeout(); // wait for the provider to sync
|
|
||||||
|
|
||||||
const subdoc = rootDoc.getMap('map').get('subdoc') as Doc;
|
|
||||||
|
|
||||||
expect(rootDoc.getMap('map').get('test-0')).toBe('test-0-value');
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('');
|
|
||||||
|
|
||||||
// onload, the provider should sync the subdoc
|
|
||||||
subdoc.load();
|
|
||||||
await setTimeout();
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
|
||||||
|
|
||||||
remotesubdoc.getText('text').insert(0, 'prefix-');
|
|
||||||
await setTimeout();
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('prefix-test-subdoc-value');
|
|
||||||
|
|
||||||
// disconnect then reconnect
|
|
||||||
provider.disconnect();
|
|
||||||
remotesubdoc.getText('text').delete(0, 'prefix-'.length);
|
|
||||||
await setTimeout();
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('prefix-test-subdoc-value');
|
|
||||||
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout();
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should sync a shouldLoad=true subdoc on connect', async () => {
|
|
||||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
|
||||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
|
||||||
|
|
||||||
const remotesubdoc = new Doc();
|
|
||||||
remotesubdoc.getText('text').insert(0, 'test-subdoc-value');
|
|
||||||
|
|
||||||
// populate remote doc with simple data
|
|
||||||
remoteRootDoc.getMap('map').set('test-0', 'test-0-value');
|
|
||||||
remoteRootDoc.getMap('map').set('subdoc', remotesubdoc);
|
|
||||||
|
|
||||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
|
||||||
applyUpdate(rootDoc, encodeStateAsUpdate(remoteRootDoc)); // sync rootDoc with remoteRootDoc
|
|
||||||
|
|
||||||
const subdoc = rootDoc.getMap('map').get('subdoc') as Doc;
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('');
|
|
||||||
|
|
||||||
subdoc.load();
|
|
||||||
const provider = createLazyProvider(rootDoc, datasource);
|
|
||||||
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout(); // wait for the provider to sync
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should send existing local update to remote on connect', async () => {
|
|
||||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
|
||||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
|
||||||
|
|
||||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
|
||||||
applyUpdate(rootDoc, encodeStateAsUpdate(remoteRootDoc)); // sync rootDoc with remoteRootDoc
|
|
||||||
|
|
||||||
rootDoc.getText('text').insert(0, 'test-value');
|
|
||||||
const provider = createLazyProvider(rootDoc, datasource);
|
|
||||||
provider.connect();
|
|
||||||
await setTimeout(); // wait for the provider to sync
|
|
||||||
|
|
||||||
expect(remoteRootDoc.getText('text').toJSON()).toBe('test-value');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should send local update to remote for subdoc after connect', async () => {
|
|
||||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
|
||||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
|
||||||
|
|
||||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
|
||||||
const provider = createLazyProvider(rootDoc, datasource);
|
|
||||||
|
|
||||||
provider.connect();
|
|
||||||
|
|
||||||
await setTimeout(); // wait for the provider to sync
|
|
||||||
|
|
||||||
const subdoc = new Doc();
|
|
||||||
rootDoc.getMap('map').set('subdoc', subdoc);
|
|
||||||
subdoc.getText('text').insert(0, 'test-subdoc-value');
|
|
||||||
|
|
||||||
await setTimeout(); // wait for the provider to sync
|
|
||||||
|
|
||||||
const remoteSubdoc = remoteRootDoc.getMap('map').get('subdoc') as Doc;
|
|
||||||
expect(remoteSubdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not send local update to remote for subdoc after disconnect', async () => {
|
|
||||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
|
||||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
|
||||||
|
|
||||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
|
||||||
const provider = createLazyProvider(rootDoc, datasource);
|
|
||||||
|
|
||||||
provider.connect();
|
|
||||||
|
|
||||||
await setTimeout(); // wait for the provider to sync
|
|
||||||
|
|
||||||
const subdoc = new Doc();
|
|
||||||
rootDoc.getMap('map').set('subdoc', subdoc);
|
|
||||||
|
|
||||||
await setTimeout(); // wait for the provider to sync
|
|
||||||
|
|
||||||
const remoteSubdoc = remoteRootDoc.getMap('map').get('subdoc') as Doc;
|
|
||||||
expect(remoteSubdoc.getText('text').toJSON()).toBe('');
|
|
||||||
|
|
||||||
provider.disconnect();
|
|
||||||
subdoc.getText('text').insert(0, 'test-subdoc-value');
|
|
||||||
await setTimeout();
|
|
||||||
expect(remoteSubdoc.getText('text').toJSON()).toBe('');
|
|
||||||
|
|
||||||
expect(provider.connected).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not send remote update back', async () => {
|
|
||||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
|
||||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
|
||||||
const spy = vi.spyOn(datasource, 'sendDocUpdate');
|
|
||||||
|
|
||||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
|
||||||
const provider = createLazyProvider(rootDoc, datasource);
|
|
||||||
|
|
||||||
provider.connect();
|
|
||||||
|
|
||||||
remoteRootDoc.getText('text').insert(0, 'test-value');
|
|
||||||
|
|
||||||
expect(spy).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('only sync', async () => {
|
|
||||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
|
||||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
|
||||||
remoteRootDoc.getText().insert(0, 'hello, world!');
|
|
||||||
|
|
||||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
|
||||||
const provider = createLazyProvider(rootDoc, datasource);
|
|
||||||
|
|
||||||
await provider.sync(true);
|
|
||||||
expect(rootDoc.getText().toJSON()).toBe('hello, world!');
|
|
||||||
|
|
||||||
const remotesubdoc = new Doc();
|
|
||||||
remotesubdoc.getText('text').insert(0, 'test-subdoc-value');
|
|
||||||
remoteRootDoc.getMap('map').set('subdoc', remotesubdoc);
|
|
||||||
expect(rootDoc.subdocs.size).toBe(0);
|
|
||||||
|
|
||||||
await provider.sync(true);
|
|
||||||
expect(rootDoc.subdocs.size).toBe(1);
|
|
||||||
const subdoc = rootDoc.getMap('map').get('subdoc') as Doc;
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('');
|
|
||||||
await provider.sync(true);
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('');
|
|
||||||
await provider.sync(false);
|
|
||||||
expect(subdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import type { Doc as YDoc } from 'yjs';
|
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
import type { DocState } from './types';
|
|
||||||
|
|
||||||
export interface DocDataSource {
|
|
||||||
/**
|
|
||||||
* request diff update from other clients
|
|
||||||
*/
|
|
||||||
queryDocState: (
|
|
||||||
guid: string,
|
|
||||||
options?: {
|
|
||||||
stateVector?: Uint8Array;
|
|
||||||
targetClientId?: number;
|
|
||||||
}
|
|
||||||
) => Promise<DocState | false>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* send update to the datasource
|
|
||||||
*/
|
|
||||||
sendDocUpdate: (guid: string, update: Uint8Array) => Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* listen to update from the datasource. Returns a function to unsubscribe.
|
|
||||||
* this is optional because some datasource might not support it
|
|
||||||
*/
|
|
||||||
onDocUpdate?(
|
|
||||||
callback: (guid: string, update: Uint8Array) => void
|
|
||||||
): () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncDocFromDataSource(
|
|
||||||
rootDoc: YDoc,
|
|
||||||
datasource: DocDataSource
|
|
||||||
) {
|
|
||||||
const downloadDocStateRecursively = async (doc: YDoc) => {
|
|
||||||
const docState = await datasource.queryDocState(doc.guid);
|
|
||||||
if (docState) {
|
|
||||||
applyUpdate(doc, docState.missing, 'sync-doc-from-datasource');
|
|
||||||
}
|
|
||||||
await Promise.all(
|
|
||||||
[...doc.subdocs].map(async subdoc => {
|
|
||||||
await downloadDocStateRecursively(subdoc);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
await downloadDocStateRecursively(rootDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncDataSourceFromDoc(
|
|
||||||
rootDoc: YDoc,
|
|
||||||
datasource: DocDataSource
|
|
||||||
) {
|
|
||||||
const uploadDocStateRecursively = async (doc: YDoc) => {
|
|
||||||
await datasource.sendDocUpdate(doc.guid, encodeStateAsUpdate(doc));
|
|
||||||
await Promise.all(
|
|
||||||
[...doc.subdocs].map(async subdoc => {
|
|
||||||
await uploadDocStateRecursively(subdoc);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
await uploadDocStateRecursively(rootDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* query the datasource from source, and save the latest update to target
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* bindDataSource(socketIO, indexedDB)
|
|
||||||
* bindDataSource(socketIO, sqlite)
|
|
||||||
*/
|
|
||||||
export async function syncDataSource(
|
|
||||||
listDocGuids: () => string[],
|
|
||||||
remoteDataSource: DocDataSource,
|
|
||||||
localDataSource: DocDataSource
|
|
||||||
) {
|
|
||||||
const guids = listDocGuids();
|
|
||||||
await Promise.all(
|
|
||||||
guids.map(guid => {
|
|
||||||
return localDataSource.queryDocState(guid).then(async docState => {
|
|
||||||
const remoteDocState = await (async () => {
|
|
||||||
if (docState) {
|
|
||||||
return remoteDataSource.queryDocState(guid, {
|
|
||||||
stateVector: docState.state,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return remoteDataSource.queryDocState(guid);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
if (remoteDocState) {
|
|
||||||
const missing = remoteDocState.missing;
|
|
||||||
if (missing.length === 2 && missing[0] === 0 && missing[1] === 0) {
|
|
||||||
// empty update
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await localDataSource.sendDocUpdate(guid, remoteDocState.missing);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './data-source';
|
|
||||||
export * from './lazy-provider';
|
|
||||||
export * from './types';
|
|
||||||
export * from './utils';
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import type { Doc } from 'yjs';
|
|
||||||
import { applyUpdate, encodeStateAsUpdate, encodeStateVector } from 'yjs';
|
|
||||||
|
|
||||||
import type { DocDataSource } from './data-source';
|
|
||||||
import type { DataSourceAdapter, Status } from './types';
|
|
||||||
|
|
||||||
function getDoc(doc: Doc, guid: string): Doc | undefined {
|
|
||||||
if (doc.guid === guid) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
for (const subdoc of doc.subdocs) {
|
|
||||||
const found = getDoc(subdoc, guid);
|
|
||||||
if (found) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LazyProviderOptions {
|
|
||||||
origin?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DocProvider = {
|
|
||||||
// backport from `@blocksuite/store`
|
|
||||||
passive: true;
|
|
||||||
|
|
||||||
sync(onlyRootDoc?: boolean): Promise<void>;
|
|
||||||
|
|
||||||
get connected(): boolean;
|
|
||||||
connect(): void;
|
|
||||||
disconnect(): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a lazy provider that connects to a datasource and synchronizes a root document.
|
|
||||||
*/
|
|
||||||
export const createLazyProvider = (
|
|
||||||
rootDoc: Doc,
|
|
||||||
datasource: DocDataSource,
|
|
||||||
options: LazyProviderOptions = {}
|
|
||||||
): DocProvider & DataSourceAdapter => {
|
|
||||||
let connected = false;
|
|
||||||
const pendingMap = new Map<string, Uint8Array[]>(); // guid -> pending-updates
|
|
||||||
const disposableMap = new Map<string, Set<() => void>>();
|
|
||||||
const connectedDocs = new Set<string>();
|
|
||||||
let abortController: AbortController | null = null;
|
|
||||||
|
|
||||||
const { origin = 'lazy-provider' } = options;
|
|
||||||
|
|
||||||
// todo: should we use a real state machine here like `xstate`?
|
|
||||||
let currentStatus: Status = {
|
|
||||||
type: 'idle',
|
|
||||||
};
|
|
||||||
let syncingStack = 0;
|
|
||||||
const callbackSet = new Set<() => void>();
|
|
||||||
const changeStatus = (newStatus: Status) => {
|
|
||||||
// simulate a stack, each syncing and synced should be paired
|
|
||||||
if (newStatus.type === 'syncing') {
|
|
||||||
syncingStack++;
|
|
||||||
} else if (newStatus.type === 'synced' || newStatus.type === 'error') {
|
|
||||||
syncingStack--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncingStack < 0) {
|
|
||||||
console.error(
|
|
||||||
'syncingStatus < 0, this should not happen',
|
|
||||||
options.origin
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncingStack === 0) {
|
|
||||||
currentStatus = newStatus;
|
|
||||||
}
|
|
||||||
if (newStatus.type !== 'synced') {
|
|
||||||
currentStatus = newStatus;
|
|
||||||
}
|
|
||||||
if (syncingStack === 0) {
|
|
||||||
if (!connected) {
|
|
||||||
currentStatus = {
|
|
||||||
type: 'idle',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
currentStatus = {
|
|
||||||
type: 'synced',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callbackSet.forEach(cb => cb());
|
|
||||||
};
|
|
||||||
|
|
||||||
async function syncDoc(doc: Doc) {
|
|
||||||
const guid = doc.guid;
|
|
||||||
{
|
|
||||||
const update = await datasource.queryDocState(guid);
|
|
||||||
let hasUpdate = false;
|
|
||||||
if (
|
|
||||||
update &&
|
|
||||||
update.missing.length !== 2 &&
|
|
||||||
update.missing[0] !== 0 &&
|
|
||||||
update.missing[1] !== 0
|
|
||||||
) {
|
|
||||||
applyUpdate(doc, update.missing, origin);
|
|
||||||
hasUpdate = true;
|
|
||||||
}
|
|
||||||
if (hasUpdate) {
|
|
||||||
await datasource.sendDocUpdate(
|
|
||||||
guid,
|
|
||||||
encodeStateAsUpdate(doc, update ? update.state : undefined)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeStatus({
|
|
||||||
type: 'syncing',
|
|
||||||
});
|
|
||||||
const remoteUpdate = await datasource
|
|
||||||
.queryDocState(guid, {
|
|
||||||
stateVector: encodeStateVector(doc),
|
|
||||||
})
|
|
||||||
.then(remoteUpdate => {
|
|
||||||
changeStatus({
|
|
||||||
type: 'synced',
|
|
||||||
});
|
|
||||||
return remoteUpdate;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
changeStatus({
|
|
||||||
type: 'error',
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
pendingMap.set(guid, []);
|
|
||||||
|
|
||||||
if (remoteUpdate) {
|
|
||||||
applyUpdate(doc, remoteUpdate.missing, origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// perf: optimize me
|
|
||||||
// it is possible the doc is only in memory but not yet in the datasource
|
|
||||||
// we need to send the whole update to the datasource
|
|
||||||
await datasource.sendDocUpdate(
|
|
||||||
guid,
|
|
||||||
encodeStateAsUpdate(doc, remoteUpdate ? remoteUpdate.state : undefined)
|
|
||||||
);
|
|
||||||
|
|
||||||
doc.emit('sync', [true, doc]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up event listeners for a Yjs document.
|
|
||||||
* @param doc - The Yjs document to set up listeners for.
|
|
||||||
*/
|
|
||||||
function setupDocListener(doc: Doc) {
|
|
||||||
const disposables = new Set<() => void>();
|
|
||||||
disposableMap.set(doc.guid, disposables);
|
|
||||||
const updateHandler = async (update: Uint8Array, updateOrigin: unknown) => {
|
|
||||||
if (origin === updateOrigin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
changeStatus({
|
|
||||||
type: 'syncing',
|
|
||||||
});
|
|
||||||
datasource
|
|
||||||
.sendDocUpdate(doc.guid, update)
|
|
||||||
.then(() => {
|
|
||||||
changeStatus({
|
|
||||||
type: 'synced',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
changeStatus({
|
|
||||||
type: 'error',
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const subdocsHandler = (event: {
|
|
||||||
loaded: Set<Doc>;
|
|
||||||
removed: Set<Doc>;
|
|
||||||
added: Set<Doc>;
|
|
||||||
}) => {
|
|
||||||
event.loaded.forEach(subdoc => {
|
|
||||||
connectDoc(subdoc).catch(console.error);
|
|
||||||
});
|
|
||||||
event.removed.forEach(subdoc => {
|
|
||||||
disposeDoc(subdoc);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
doc.on('update', updateHandler);
|
|
||||||
doc.on('subdocs', subdocsHandler);
|
|
||||||
// todo: handle destroy?
|
|
||||||
disposables.add(() => {
|
|
||||||
doc.off('update', updateHandler);
|
|
||||||
doc.off('subdocs', subdocsHandler);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up event listeners for the datasource.
|
|
||||||
* Specifically, listens for updates to documents and applies them to the corresponding Yjs document.
|
|
||||||
*/
|
|
||||||
function setupDatasourceListeners() {
|
|
||||||
assertExists(abortController, 'abortController should be defined');
|
|
||||||
const unsubscribe = datasource.onDocUpdate?.((guid, update) => {
|
|
||||||
changeStatus({
|
|
||||||
type: 'syncing',
|
|
||||||
});
|
|
||||||
const doc = getDoc(rootDoc, guid);
|
|
||||||
if (doc) {
|
|
||||||
applyUpdate(doc, update, origin);
|
|
||||||
//
|
|
||||||
if (pendingMap.has(guid)) {
|
|
||||||
pendingMap
|
|
||||||
.get(guid)
|
|
||||||
?.forEach(update => applyUpdate(doc, update, origin));
|
|
||||||
pendingMap.delete(guid);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This case happens when the father doc is not yet updated,
|
|
||||||
// so that the child doc is not yet created.
|
|
||||||
// We need to put it into cache so that it can be applied later.
|
|
||||||
console.warn('doc not found', guid);
|
|
||||||
pendingMap.set(guid, (pendingMap.get(guid) ?? []).concat(update));
|
|
||||||
}
|
|
||||||
changeStatus({
|
|
||||||
type: 'synced',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
abortController.signal.addEventListener('abort', () => {
|
|
||||||
unsubscribe?.();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// when a subdoc is loaded, we need to sync it with the datasource and setup listeners
|
|
||||||
async function connectDoc(doc: Doc) {
|
|
||||||
// skip if already connected
|
|
||||||
if (connectedDocs.has(doc.guid)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connectedDocs.add(doc.guid);
|
|
||||||
setupDocListener(doc);
|
|
||||||
await syncDoc(doc);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
[...doc.subdocs]
|
|
||||||
.filter(subdoc => subdoc.shouldLoad)
|
|
||||||
.map(subdoc => connectDoc(subdoc))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function disposeDoc(doc: Doc) {
|
|
||||||
connectedDocs.delete(doc.guid);
|
|
||||||
const disposables = disposableMap.get(doc.guid);
|
|
||||||
if (disposables) {
|
|
||||||
disposables.forEach(dispose => dispose());
|
|
||||||
disposableMap.delete(doc.guid);
|
|
||||||
}
|
|
||||||
// also dispose all subdocs
|
|
||||||
doc.subdocs.forEach(disposeDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
function disposeAll() {
|
|
||||||
disposableMap.forEach(disposables => {
|
|
||||||
disposables.forEach(dispose => dispose());
|
|
||||||
});
|
|
||||||
disposableMap.clear();
|
|
||||||
connectedDocs.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects to the datasource and sets up event listeners for document updates.
|
|
||||||
*/
|
|
||||||
function connect() {
|
|
||||||
connected = true;
|
|
||||||
abortController = new AbortController();
|
|
||||||
|
|
||||||
changeStatus({
|
|
||||||
type: 'syncing',
|
|
||||||
});
|
|
||||||
// root doc should be already loaded,
|
|
||||||
// but we want to populate the cache for later update events
|
|
||||||
connectDoc(rootDoc)
|
|
||||||
.then(() => {
|
|
||||||
changeStatus({
|
|
||||||
type: 'synced',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
changeStatus({
|
|
||||||
type: 'error',
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
setupDatasourceListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function disconnect() {
|
|
||||||
connected = false;
|
|
||||||
disposeAll();
|
|
||||||
assertExists(abortController, 'abortController should be defined');
|
|
||||||
abortController.abort();
|
|
||||||
abortController = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncDocRecursive = async (doc: Doc) => {
|
|
||||||
await syncDoc(doc);
|
|
||||||
await Promise.all(
|
|
||||||
[...doc.subdocs.values()].map(subdoc => syncDocRecursive(subdoc))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
sync: async onlyRootDoc => {
|
|
||||||
connected = true;
|
|
||||||
try {
|
|
||||||
if (onlyRootDoc) {
|
|
||||||
await syncDoc(rootDoc);
|
|
||||||
} else {
|
|
||||||
await syncDocRecursive(rootDoc);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
connected = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get status() {
|
|
||||||
return currentStatus;
|
|
||||||
},
|
|
||||||
subscribeStatusChange(cb: () => void) {
|
|
||||||
callbackSet.add(cb);
|
|
||||||
return () => {
|
|
||||||
callbackSet.delete(cb);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
get connected() {
|
|
||||||
return connected;
|
|
||||||
},
|
|
||||||
passive: true,
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
|
|
||||||
datasource,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import type { DocDataSource } from './data-source';
|
|
||||||
|
|
||||||
export type Status =
|
|
||||||
| {
|
|
||||||
type: 'idle';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'syncing';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'synced';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'error';
|
|
||||||
error: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface DataSourceAdapter {
|
|
||||||
datasource: DocDataSource;
|
|
||||||
readonly status: Status;
|
|
||||||
|
|
||||||
subscribeStatusChange(onStatusChange: () => void): () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocState {
|
|
||||||
/**
|
|
||||||
* The missing structs of client queries with self state.
|
|
||||||
*/
|
|
||||||
missing: Uint8Array;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The full state of remote, used to prepare for diff sync.
|
|
||||||
*/
|
|
||||||
state?: Uint8Array;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import type { Doc } from 'yjs';
|
|
||||||
|
|
||||||
export function getDoc(doc: Doc, guid: string): Doc | undefined {
|
|
||||||
if (doc.guid === guid) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
for (const subdoc of doc.subdocs) {
|
|
||||||
const found = getDoc(subdoc, guid);
|
|
||||||
if (found) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveAlert = (event: BeforeUnloadEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
return (event.returnValue =
|
|
||||||
'Data is not saved. Are you sure you want to leave?');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const writeOperation = async (op: Promise<unknown>) => {
|
|
||||||
window.addEventListener('beforeunload', saveAlert, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
await op;
|
|
||||||
window.removeEventListener('beforeunload', saveAlert, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"include": ["./src"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"noEmit": false,
|
|
||||||
"outDir": "lib"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { resolve } from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import dts from 'vite-plugin-dts';
|
|
||||||
|
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
build: {
|
|
||||||
minify: 'esbuild',
|
|
||||||
sourcemap: true,
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'src/index.ts'),
|
|
||||||
fileName: 'index',
|
|
||||||
name: 'ToEverythingIndexedDBProvider',
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: ['idb', 'yjs'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
dts({
|
|
||||||
entryRoot: resolve(__dirname, 'src'),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -7,10 +7,15 @@ import { listHistoryQuery, recoverDocMutation } from '@affine/graphql';
|
|||||||
import { assertEquals } from '@blocksuite/global/utils';
|
import { assertEquals } from '@blocksuite/global/utils';
|
||||||
import { DocCollection } from '@blocksuite/store';
|
import { DocCollection } from '@blocksuite/store';
|
||||||
import { globalBlockSuiteSchema } from '@toeverything/infra';
|
import { globalBlockSuiteSchema } from '@toeverything/infra';
|
||||||
import { revertUpdate } from '@toeverything/y-indexeddb';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import useSWRImmutable from 'swr/immutable';
|
import useSWRImmutable from 'swr/immutable';
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
import {
|
||||||
|
applyUpdate,
|
||||||
|
Doc as YDoc,
|
||||||
|
encodeStateAsUpdate,
|
||||||
|
encodeStateVector,
|
||||||
|
UndoManager,
|
||||||
|
} from 'yjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useMutateQueryResource,
|
useMutateQueryResource,
|
||||||
@@ -180,6 +185,43 @@ export const historyListGroupByDay = (histories: DocHistory[]) => {
|
|||||||
return [...map.entries()];
|
return [...map.entries()];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function revertUpdate(
|
||||||
|
doc: YDoc,
|
||||||
|
snapshotUpdate: Uint8Array,
|
||||||
|
getMetadata: (key: string) => 'Text' | 'Map' | 'Array'
|
||||||
|
) {
|
||||||
|
const snapshotDoc = new YDoc();
|
||||||
|
applyUpdate(snapshotDoc, snapshotUpdate);
|
||||||
|
|
||||||
|
const currentStateVector = encodeStateVector(doc);
|
||||||
|
const snapshotStateVector = encodeStateVector(snapshotDoc);
|
||||||
|
|
||||||
|
const changesSinceSnapshotUpdate = encodeStateAsUpdate(
|
||||||
|
doc,
|
||||||
|
snapshotStateVector
|
||||||
|
);
|
||||||
|
const undoManager = new UndoManager(
|
||||||
|
[...snapshotDoc.share.keys()].map(key => {
|
||||||
|
const type = getMetadata(key);
|
||||||
|
if (type === 'Text') {
|
||||||
|
return snapshotDoc.getText(key);
|
||||||
|
} else if (type === 'Map') {
|
||||||
|
return snapshotDoc.getMap(key);
|
||||||
|
} else if (type === 'Array') {
|
||||||
|
return snapshotDoc.getArray(key);
|
||||||
|
}
|
||||||
|
throw new Error('Unknown type');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
applyUpdate(snapshotDoc, changesSinceSnapshotUpdate);
|
||||||
|
undoManager.undo();
|
||||||
|
const revertChangesSinceSnapshotUpdate = encodeStateAsUpdate(
|
||||||
|
snapshotDoc,
|
||||||
|
currentStateVector
|
||||||
|
);
|
||||||
|
applyUpdate(doc, revertChangesSinceSnapshotUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
export const useRestorePage = (
|
export const useRestorePage = (
|
||||||
docCollection: DocCollection,
|
docCollection: DocCollection,
|
||||||
pageId: string
|
pageId: string
|
||||||
|
|||||||
@@ -26,9 +26,6 @@
|
|||||||
{
|
{
|
||||||
"path": "../../common/env"
|
"path": "../../common/env"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "../../common/y-indexeddb"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"path": "./tsconfig.node.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
"i18n",
|
"i18n",
|
||||||
"native",
|
"native",
|
||||||
"templates",
|
"templates",
|
||||||
"y-indexeddb",
|
|
||||||
"y-provider",
|
|
||||||
"debug",
|
"debug",
|
||||||
"storage",
|
"storage",
|
||||||
"infra"
|
"infra"
|
||||||
|
|||||||
@@ -65,7 +65,6 @@
|
|||||||
"@affine/electron/scripts/*": ["./packages/frontend/electron/scripts/*"],
|
"@affine/electron/scripts/*": ["./packages/frontend/electron/scripts/*"],
|
||||||
"@affine-test/kit/*": ["./tests/kit/*"],
|
"@affine-test/kit/*": ["./tests/kit/*"],
|
||||||
"@affine-test/fixtures/*": ["./tests/fixtures/*"],
|
"@affine-test/fixtures/*": ["./tests/fixtures/*"],
|
||||||
"@toeverything/y-indexeddb": ["./packages/common/y-indexeddb/src"],
|
|
||||||
"@toeverything/infra": ["./packages/common/infra/src"],
|
"@toeverything/infra": ["./packages/common/infra/src"],
|
||||||
"@affine/native": ["./packages/frontend/native/index.d.ts"],
|
"@affine/native": ["./packages/frontend/native/index.d.ts"],
|
||||||
"@affine/native/*": ["./packages/frontend/native/*"],
|
"@affine/native/*": ["./packages/frontend/native/*"],
|
||||||
@@ -115,9 +114,6 @@
|
|||||||
{
|
{
|
||||||
"path": "./packages/common/infra"
|
"path": "./packages/common/infra"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "./packages/common/y-indexeddb"
|
|
||||||
},
|
|
||||||
// Tools
|
// Tools
|
||||||
{
|
{
|
||||||
"path": "./tools/cli"
|
"path": "./tools/cli"
|
||||||
|
|||||||
@@ -7436,39 +7436,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@microsoft/api-extractor-model@npm:7.28.3":
|
|
||||||
version: 7.28.3
|
|
||||||
resolution: "@microsoft/api-extractor-model@npm:7.28.3"
|
|
||||||
dependencies:
|
|
||||||
"@microsoft/tsdoc": "npm:0.14.2"
|
|
||||||
"@microsoft/tsdoc-config": "npm:~0.16.1"
|
|
||||||
"@rushstack/node-core-library": "npm:3.62.0"
|
|
||||||
checksum: 10/704b8bfbf0b93c1d0605506a5a34ba3c68794d451f4b1dbfdc58fc142200c4d4391a435dd13d2d9470daaf4263ccdcee35f7e1806d1978cc64df6d0483481f94
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@microsoft/api-extractor@npm:7.39.0":
|
|
||||||
version: 7.39.0
|
|
||||||
resolution: "@microsoft/api-extractor@npm:7.39.0"
|
|
||||||
dependencies:
|
|
||||||
"@microsoft/api-extractor-model": "npm:7.28.3"
|
|
||||||
"@microsoft/tsdoc": "npm:0.14.2"
|
|
||||||
"@microsoft/tsdoc-config": "npm:~0.16.1"
|
|
||||||
"@rushstack/node-core-library": "npm:3.62.0"
|
|
||||||
"@rushstack/rig-package": "npm:0.5.1"
|
|
||||||
"@rushstack/ts-command-line": "npm:4.17.1"
|
|
||||||
colors: "npm:~1.2.1"
|
|
||||||
lodash: "npm:~4.17.15"
|
|
||||||
resolve: "npm:~1.22.1"
|
|
||||||
semver: "npm:~7.5.4"
|
|
||||||
source-map: "npm:~0.6.1"
|
|
||||||
typescript: "npm:5.3.3"
|
|
||||||
bin:
|
|
||||||
api-extractor: bin/api-extractor
|
|
||||||
checksum: 10/b05f525b428cbacf26bc45394b84d7a73ab61b0bce7b77f82d0e43a332f019e94ca24d83f94d5083e9150efbc916cc35aa195d4f62ceca6fa168eb796d0d8af2
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@microsoft/api-extractor@npm:7.43.0":
|
"@microsoft/api-extractor@npm:7.43.0":
|
||||||
version: 7.43.0
|
version: 7.43.0
|
||||||
resolution: "@microsoft/api-extractor@npm:7.43.0"
|
resolution: "@microsoft/api-extractor@npm:7.43.0"
|
||||||
@@ -11922,26 +11889,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@rushstack/node-core-library@npm:3.62.0":
|
|
||||||
version: 3.62.0
|
|
||||||
resolution: "@rushstack/node-core-library@npm:3.62.0"
|
|
||||||
dependencies:
|
|
||||||
colors: "npm:~1.2.1"
|
|
||||||
fs-extra: "npm:~7.0.1"
|
|
||||||
import-lazy: "npm:~4.0.0"
|
|
||||||
jju: "npm:~1.4.0"
|
|
||||||
resolve: "npm:~1.22.1"
|
|
||||||
semver: "npm:~7.5.4"
|
|
||||||
z-schema: "npm:~5.0.2"
|
|
||||||
peerDependencies:
|
|
||||||
"@types/node": "*"
|
|
||||||
peerDependenciesMeta:
|
|
||||||
"@types/node":
|
|
||||||
optional: true
|
|
||||||
checksum: 10/61e22a1a04cf194f12b05acb643a361a74a34944a48380f61ba9d5f4b6c3684a7ae5669af5013b5549101647c6862548e11e1b8c60bdb687541f09133bbdd976
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@rushstack/node-core-library@npm:4.0.2":
|
"@rushstack/node-core-library@npm:4.0.2":
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
resolution: "@rushstack/node-core-library@npm:4.0.2"
|
resolution: "@rushstack/node-core-library@npm:4.0.2"
|
||||||
@@ -11961,16 +11908,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@rushstack/rig-package@npm:0.5.1":
|
|
||||||
version: 0.5.1
|
|
||||||
resolution: "@rushstack/rig-package@npm:0.5.1"
|
|
||||||
dependencies:
|
|
||||||
resolve: "npm:~1.22.1"
|
|
||||||
strip-json-comments: "npm:~3.1.1"
|
|
||||||
checksum: 10/9e5d425f60bb1e23371ecc086eaca838651ced904da33b690103ac731820e65a8a3720243f9e03578dfd1efa067fec9c6d762f16b3bb8cf92b56254d5f906989
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@rushstack/rig-package@npm:0.5.2":
|
"@rushstack/rig-package@npm:0.5.2":
|
||||||
version: 0.5.2
|
version: 0.5.2
|
||||||
resolution: "@rushstack/rig-package@npm:0.5.2"
|
resolution: "@rushstack/rig-package@npm:0.5.2"
|
||||||
@@ -11996,18 +11933,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@rushstack/ts-command-line@npm:4.17.1":
|
|
||||||
version: 4.17.1
|
|
||||||
resolution: "@rushstack/ts-command-line@npm:4.17.1"
|
|
||||||
dependencies:
|
|
||||||
"@types/argparse": "npm:1.0.38"
|
|
||||||
argparse: "npm:~1.0.9"
|
|
||||||
colors: "npm:~1.2.1"
|
|
||||||
string-argv: "npm:~0.3.1"
|
|
||||||
checksum: 10/75407f6a42fda364ec9f945ebd346c632a23dd97d7ed5ad108c264d72ee0370d3d912cc6c16af6973bbc3f5f92b845b63fb13da75a077d61f7e34e69f00b8823
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@rushstack/ts-command-line@npm:4.19.1":
|
"@rushstack/ts-command-line@npm:4.19.1":
|
||||||
version: 4.19.1
|
version: 4.19.1
|
||||||
resolution: "@rushstack/ts-command-line@npm:4.19.1"
|
resolution: "@rushstack/ts-command-line@npm:4.19.1"
|
||||||
@@ -14459,27 +14384,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@toeverything/y-indexeddb@workspace:packages/common/y-indexeddb":
|
|
||||||
version: 0.0.0-use.local
|
|
||||||
resolution: "@toeverything/y-indexeddb@workspace:packages/common/y-indexeddb"
|
|
||||||
dependencies:
|
|
||||||
"@blocksuite/blocks": "npm:0.14.0-canary-202404280529-c8e5f89"
|
|
||||||
"@blocksuite/global": "npm:0.14.0-canary-202404280529-c8e5f89"
|
|
||||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
|
||||||
fake-indexeddb: "npm:^5.0.2"
|
|
||||||
idb: "npm:^8.0.0"
|
|
||||||
nanoid: "npm:^5.0.7"
|
|
||||||
vite: "npm:^5.2.8"
|
|
||||||
vite-plugin-dts: "npm:3.8.1"
|
|
||||||
vitest: "npm:1.4.0"
|
|
||||||
y-indexeddb: "npm:^9.0.12"
|
|
||||||
y-provider: "workspace:*"
|
|
||||||
yjs: "npm:^13.6.14"
|
|
||||||
peerDependencies:
|
|
||||||
yjs: ^13
|
|
||||||
languageName: unknown
|
|
||||||
linkType: soft
|
|
||||||
|
|
||||||
"@tokenizer/token@npm:^0.3.0":
|
"@tokenizer/token@npm:^0.3.0":
|
||||||
version: 0.3.0
|
version: 0.3.0
|
||||||
resolution: "@tokenizer/token@npm:0.3.0"
|
resolution: "@tokenizer/token@npm:0.3.0"
|
||||||
@@ -16121,7 +16025,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vue/language-core@npm:1.8.27, @vue/language-core@npm:^1.8.26, @vue/language-core@npm:^1.8.27":
|
"@vue/language-core@npm:1.8.27, @vue/language-core@npm:^1.8.27":
|
||||||
version: 1.8.27
|
version: 1.8.27
|
||||||
resolution: "@vue/language-core@npm:1.8.27"
|
resolution: "@vue/language-core@npm:1.8.27"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18964,13 +18868,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"colors@npm:~1.2.1":
|
|
||||||
version: 1.2.5
|
|
||||||
resolution: "colors@npm:1.2.5"
|
|
||||||
checksum: 10/fe30007df0f62abedc2726990d0951f19292d85686dffcc76fa96ee9dc4e1a987d50b34aa02796e88627709c54a52f07c057bf1da4b7302c06eda8e1afd2f09a
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"columnify@npm:^1.6.0":
|
"columnify@npm:^1.6.0":
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
resolution: "columnify@npm:1.6.0"
|
resolution: "columnify@npm:1.6.0"
|
||||||
@@ -26782,7 +26679,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lib0@npm:^0.2.74, lib0@npm:^0.2.85, lib0@npm:^0.2.86, lib0@npm:^0.2.93":
|
"lib0@npm:^0.2.85, lib0@npm:^0.2.86, lib0@npm:^0.2.93":
|
||||||
version: 0.2.93
|
version: 0.2.93
|
||||||
resolution: "lib0@npm:0.2.93"
|
resolution: "lib0@npm:0.2.93"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -35713,16 +35610,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"typescript@npm:5.3.3":
|
|
||||||
version: 5.3.3
|
|
||||||
resolution: "typescript@npm:5.3.3"
|
|
||||||
bin:
|
|
||||||
tsc: bin/tsc
|
|
||||||
tsserver: bin/tsserver
|
|
||||||
checksum: 10/6e4e6a14a50c222b3d14d4ea2f729e79f972fa536ac1522b91202a9a65af3605c2928c4a790a4a50aa13694d461c479ba92cedaeb1e7b190aadaa4e4b96b8e18
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"typescript@npm:5.4.2":
|
"typescript@npm:5.4.2":
|
||||||
version: 5.4.2
|
version: 5.4.2
|
||||||
resolution: "typescript@npm:5.4.2"
|
resolution: "typescript@npm:5.4.2"
|
||||||
@@ -35743,16 +35630,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>":
|
|
||||||
version: 5.3.3
|
|
||||||
resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin<compat/typescript>::version=5.3.3&hash=e012d7"
|
|
||||||
bin:
|
|
||||||
tsc: bin/tsc
|
|
||||||
tsserver: bin/tsserver
|
|
||||||
checksum: 10/c93786fcc9a70718ba1e3819bab56064ead5817004d1b8186f8ca66165f3a2d0100fee91fa64c840dcd45f994ca5d615d8e1f566d39a7470fc1e014dbb4cf15d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"typescript@patch:typescript@npm%3A5.4.2#optional!builtin<compat/typescript>":
|
"typescript@patch:typescript@npm%3A5.4.2#optional!builtin<compat/typescript>":
|
||||||
version: 5.4.2
|
version: 5.4.2
|
||||||
resolution: "typescript@patch:typescript@npm%3A5.4.2#optional!builtin<compat/typescript>::version=5.4.2&hash=5adc0c"
|
resolution: "typescript@patch:typescript@npm%3A5.4.2#optional!builtin<compat/typescript>::version=5.4.2&hash=5adc0c"
|
||||||
@@ -36603,26 +36480,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vite-plugin-dts@npm:3.7.3":
|
|
||||||
version: 3.7.3
|
|
||||||
resolution: "vite-plugin-dts@npm:3.7.3"
|
|
||||||
dependencies:
|
|
||||||
"@microsoft/api-extractor": "npm:7.39.0"
|
|
||||||
"@rollup/pluginutils": "npm:^5.1.0"
|
|
||||||
"@vue/language-core": "npm:^1.8.26"
|
|
||||||
debug: "npm:^4.3.4"
|
|
||||||
kolorist: "npm:^1.8.0"
|
|
||||||
vue-tsc: "npm:^1.8.26"
|
|
||||||
peerDependencies:
|
|
||||||
typescript: "*"
|
|
||||||
vite: "*"
|
|
||||||
peerDependenciesMeta:
|
|
||||||
vite:
|
|
||||||
optional: true
|
|
||||||
checksum: 10/b6adf0934a219b5b6a56f6ddf13b388533856eb55d167c245fed7de30352c285eeadc3387df6f5c6617c0c94640bd7b32bf65640a53484d50c6f0d2218918b51
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"vite-plugin-dts@npm:3.8.1":
|
"vite-plugin-dts@npm:3.8.1":
|
||||||
version: 3.8.1
|
version: 3.8.1
|
||||||
resolution: "vite-plugin-dts@npm:3.8.1"
|
resolution: "vite-plugin-dts@npm:3.8.1"
|
||||||
@@ -36674,7 +36531,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vite@npm:^5.0.0, vite@npm:^5.0.11, vite@npm:^5.1.4, vite@npm:^5.2.8":
|
"vite@npm:^5.0.0, vite@npm:^5.0.11, vite@npm:^5.2.8":
|
||||||
version: 5.2.9
|
version: 5.2.9
|
||||||
resolution: "vite@npm:5.2.9"
|
resolution: "vite@npm:5.2.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -36842,7 +36699,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vue-tsc@npm:^1.8.26, vue-tsc@npm:^1.8.27":
|
"vue-tsc@npm:^1.8.27":
|
||||||
version: 1.8.27
|
version: 1.8.27
|
||||||
resolution: "vue-tsc@npm:1.8.27"
|
resolution: "vue-tsc@npm:1.8.27"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -37605,17 +37462,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"y-indexeddb@npm:^9.0.12":
|
|
||||||
version: 9.0.12
|
|
||||||
resolution: "y-indexeddb@npm:9.0.12"
|
|
||||||
dependencies:
|
|
||||||
lib0: "npm:^0.2.74"
|
|
||||||
peerDependencies:
|
|
||||||
yjs: ^13.0.0
|
|
||||||
checksum: 10/6468ebdcb2936a5fe10e4fb57cbe2d90260c44b63c6ecf6a26cc3652d21bd3be58bb76dfb56dbe56dd71b320042bfd3663274217b89300f2f0db92611fc9e7c6
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"y-protocols@npm:^1.0.6":
|
"y-protocols@npm:^1.0.6":
|
||||||
version: 1.0.6
|
version: 1.0.6
|
||||||
resolution: "y-protocols@npm:1.0.6"
|
resolution: "y-protocols@npm:1.0.6"
|
||||||
@@ -37627,21 +37473,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"y-provider@workspace:*, y-provider@workspace:packages/common/y-provider":
|
|
||||||
version: 0.0.0-use.local
|
|
||||||
resolution: "y-provider@workspace:packages/common/y-provider"
|
|
||||||
dependencies:
|
|
||||||
"@blocksuite/store": "npm:0.14.0-canary-202404280529-c8e5f89"
|
|
||||||
vite: "npm:^5.1.4"
|
|
||||||
vite-plugin-dts: "npm:3.7.3"
|
|
||||||
vitest: "npm:1.4.0"
|
|
||||||
yjs: "npm:^13.6.14"
|
|
||||||
peerDependencies:
|
|
||||||
"@blocksuite/global": "*"
|
|
||||||
yjs: ^13
|
|
||||||
languageName: unknown
|
|
||||||
linkType: soft
|
|
||||||
|
|
||||||
"y18n@npm:^4.0.0":
|
"y18n@npm:^4.0.0":
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
resolution: "y18n@npm:4.0.3"
|
resolution: "y18n@npm:4.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user