mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 09:36:17 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -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 () => {};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user