chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,91 @@
import { assertExists } from '@blocksuite/global/utils';
import { diffUpdate, encodeStateVectorFromUpdate, mergeUpdates } from 'yjs';
import type { DocSource } from '../source.js';
type ChannelMessage =
| {
type: 'init';
}
| {
type: 'update';
docId: string;
data: Uint8Array;
};
export class BroadcastChannelDocSource implements DocSource {
private _onMessage = (event: MessageEvent<ChannelMessage>) => {
if (event.data.type === 'init') {
for (const [docId, data] of this.docMap) {
this.channel.postMessage({
type: 'update',
docId,
data,
} satisfies ChannelMessage);
}
return;
}
const { docId, data } = event.data;
const update = this.docMap.get(docId);
if (update) {
this.docMap.set(docId, mergeUpdates([update, data]));
} else {
this.docMap.set(docId, data);
}
};
channel = new BroadcastChannel(this.channelName);
docMap = new Map<string, Uint8Array>();
name = 'broadcast-channel';
constructor(readonly channelName: string = 'blocksuite:doc') {
this.channel.addEventListener('message', this._onMessage);
this.channel.postMessage({
type: 'init',
});
}
pull(docId: string, state: Uint8Array) {
const update = this.docMap.get(docId);
if (!update) return null;
const diff = state.length ? diffUpdate(update, state) : update;
return { data: diff, state: encodeStateVectorFromUpdate(update) };
}
push(docId: string, data: Uint8Array) {
const update = this.docMap.get(docId);
if (update) {
this.docMap.set(docId, mergeUpdates([update, data]));
} else {
this.docMap.set(docId, data);
}
assertExists(this.docMap.get(docId));
this.channel.postMessage({
type: 'update',
docId,
data: this.docMap.get(docId)!,
} satisfies ChannelMessage);
}
subscribe(cb: (docId: string, data: Uint8Array) => void) {
const abortController = new AbortController();
this.channel.addEventListener(
'message',
(event: MessageEvent<ChannelMessage>) => {
if (event.data.type !== 'update') return;
const { docId, data } = event.data;
cb(docId, data);
},
{ signal: abortController.signal }
);
return () => {
abortController.abort();
};
}
}
@@ -0,0 +1,3 @@
export * from './broadcast.js';
export * from './indexeddb.js';
export * from './noop.js';
@@ -0,0 +1,116 @@
import { type DBSchema, type IDBPDatabase, openDB } from 'idb';
import { diffUpdate, encodeStateVectorFromUpdate, mergeUpdates } from 'yjs';
import type { DocSource } from '../source.js';
export const dbVersion = 1;
export const DEFAULT_DB_NAME = 'blocksuite-local';
type UpdateMessage = {
timestamp: number;
update: Uint8Array;
};
type DocCollectionPersist = {
id: string;
updates: UpdateMessage[];
};
interface BlockSuiteBinaryDB extends DBSchema {
collection: {
key: string;
value: DocCollectionPersist;
};
}
export function upgradeDB(db: IDBPDatabase<BlockSuiteBinaryDB>) {
db.createObjectStore('collection', { keyPath: 'id' });
}
type ChannelMessage = {
type: 'db-updated';
payload: { docId: string; update: Uint8Array };
};
export class IndexedDBDocSource implements DocSource {
// indexeddb could be shared between tabs, so we use broadcast channel to notify other tabs
channel = new BroadcastChannel('indexeddb:' + this.dbName);
dbPromise: Promise<IDBPDatabase<BlockSuiteBinaryDB>> | null = null;
mergeCount = 1;
name = 'indexeddb';
constructor(readonly dbName: string = DEFAULT_DB_NAME) {}
getDb() {
if (this.dbPromise === null) {
this.dbPromise = openDB<BlockSuiteBinaryDB>(this.dbName, dbVersion, {
upgrade: upgradeDB,
});
}
return this.dbPromise;
}
async pull(
docId: string,
state: Uint8Array
): Promise<{ data: Uint8Array; state?: Uint8Array | undefined } | null> {
const db = await this.getDb();
const store = db
.transaction('collection', 'readonly')
.objectStore('collection');
const data = await store.get(docId);
if (!data) {
return null;
}
const { updates } = data;
const update = mergeUpdates(updates.map(({ update }) => update));
const diff = state.length ? diffUpdate(update, state) : update;
return { data: diff, state: encodeStateVectorFromUpdate(update) };
}
async push(docId: string, data: Uint8Array): Promise<void> {
const db = await this.getDb();
const store = db
.transaction('collection', 'readwrite')
.objectStore('collection');
const { updates } = (await store.get(docId)) ?? { updates: [] };
let rows: UpdateMessage[] = [
...updates,
{ timestamp: Date.now(), update: data },
];
if (this.mergeCount && rows.length >= this.mergeCount) {
const merged = mergeUpdates(rows.map(({ update }) => update));
rows = [{ timestamp: Date.now(), update: merged }];
}
await store.put({
id: docId,
updates: rows,
});
this.channel.postMessage({
type: 'db-updated',
payload: { docId, update: data },
} satisfies ChannelMessage);
}
subscribe(cb: (docId: string, data: Uint8Array) => void) {
function onMessage(event: MessageEvent<ChannelMessage>) {
const { type, payload } = event.data;
if (type === 'db-updated') {
const { docId, update } = payload;
cb(docId, update);
}
}
this.channel.addEventListener('message', onMessage);
return () => {
this.channel.removeEventListener('message', onMessage);
};
}
}
@@ -0,0 +1,18 @@
import type { DocSource } from '../source.js';
export class NoopDocSource implements DocSource {
name = 'noop';
pull(_docId: string, _data: Uint8Array) {
return null;
}
push(_docId: string, _data: Uint8Array) {}
subscribe(
_cb: (docId: string, data: Uint8Array) => void,
_disconnect: (reason: string) => void
) {
return () => {};
}
}