mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 17:39:55 +08:00
feat(infra): framework
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { map } from 'rxjs';
|
||||
import type { Doc as YDoc } from 'yjs';
|
||||
|
||||
import { LiveData } from '../../livedata';
|
||||
import { MANUALLY_STOP } from '../../utils';
|
||||
import { DocEngineLocalPart } from './local';
|
||||
import { DocEngineRemotePart } from './remote';
|
||||
import type { DocServer } from './server';
|
||||
import type { DocStorage } from './storage';
|
||||
import { DocStorageInner } from './storage';
|
||||
|
||||
const logger = new DebugLogger('doc-engine');
|
||||
|
||||
export type { DocEvent, DocEventBus } from './event';
|
||||
export { MemoryDocEventBus } from './event';
|
||||
export type { DocServer } from './server';
|
||||
export type { DocStorage } from './storage';
|
||||
export {
|
||||
MemoryStorage as MemoryDocStorage,
|
||||
ReadonlyStorage as ReadonlyDocStorage,
|
||||
} from './storage';
|
||||
|
||||
export class DocEngine {
|
||||
localPart: DocEngineLocalPart;
|
||||
remotePart: DocEngineRemotePart | null;
|
||||
|
||||
storage: DocStorageInner;
|
||||
|
||||
engineState$ = LiveData.computed(get => {
|
||||
const localState = get(this.localPart.engineState$);
|
||||
if (this.remotePart) {
|
||||
const remoteState = get(this.remotePart?.engineState$);
|
||||
return {
|
||||
total: remoteState.total,
|
||||
syncing: remoteState.syncing,
|
||||
saving: localState.syncing,
|
||||
retrying: remoteState.retrying,
|
||||
errorMessage: remoteState.errorMessage,
|
||||
};
|
||||
}
|
||||
return {
|
||||
total: localState.total,
|
||||
syncing: localState.syncing,
|
||||
saving: localState.syncing,
|
||||
retrying: false,
|
||||
errorMessage: null,
|
||||
};
|
||||
});
|
||||
|
||||
docState$(docId: string) {
|
||||
const localState$ = this.localPart.docState$(docId);
|
||||
const remoteState$ = this.remotePart?.docState$(docId);
|
||||
return LiveData.computed(get => {
|
||||
const local = get(localState$);
|
||||
const remote = remoteState$ ? get(remoteState$) : null;
|
||||
return {
|
||||
ready: local.ready,
|
||||
saving: local.syncing,
|
||||
syncing: local.syncing || remote?.syncing,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
storage: DocStorage,
|
||||
private readonly server?: DocServer | null
|
||||
) {
|
||||
const clientId = nanoid();
|
||||
this.storage = new DocStorageInner(storage);
|
||||
this.localPart = new DocEngineLocalPart(clientId, this.storage);
|
||||
this.remotePart = this.server
|
||||
? new DocEngineRemotePart(clientId, this.storage, this.server)
|
||||
: null;
|
||||
}
|
||||
|
||||
abort = new AbortController();
|
||||
|
||||
start() {
|
||||
this.abort.abort(MANUALLY_STOP);
|
||||
this.abort = new AbortController();
|
||||
Promise.all([
|
||||
this.localPart.mainLoop(this.abort.signal),
|
||||
this.remotePart?.mainLoop(this.abort.signal),
|
||||
]).catch(err => {
|
||||
if (err === MANUALLY_STOP) {
|
||||
return;
|
||||
}
|
||||
logger.error('Doc engine error', err);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.abort.abort(MANUALLY_STOP);
|
||||
}
|
||||
|
||||
async resetSyncStatus() {
|
||||
this.stop();
|
||||
await this.storage.clearSyncMetadata();
|
||||
await this.storage.clearServerClock();
|
||||
}
|
||||
|
||||
addDoc(doc: YDoc, withSubDocs = true) {
|
||||
this.localPart.actions.addDoc(doc);
|
||||
this.remotePart?.actions.addDoc(doc.guid);
|
||||
|
||||
if (withSubDocs) {
|
||||
const subdocs = doc.getSubdocs();
|
||||
for (const subdoc of subdocs) {
|
||||
this.addDoc(subdoc, false);
|
||||
}
|
||||
doc.on('subdocs', ({ added }: { added: Set<YDoc> }) => {
|
||||
for (const subdoc of added) {
|
||||
this.addDoc(subdoc, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setPriority(docId: string, priority: number) {
|
||||
this.localPart.setPriority(docId, priority);
|
||||
this.remotePart?.setPriority(docId, priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Saved:
|
||||
* YDoc changes have been saved to storage, and the browser can be safely closed without losing data.
|
||||
*/
|
||||
waitForSaved() {
|
||||
return new Promise<void>(resolve => {
|
||||
this.engineState$
|
||||
.pipe(map(state => state.saving === 0))
|
||||
.subscribe(saved => {
|
||||
if (saved) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Synced:
|
||||
* is fully synchronized with the server
|
||||
*/
|
||||
waitForSynced() {
|
||||
return new Promise<void>(resolve => {
|
||||
this.engineState$
|
||||
.pipe(map(state => state.syncing === 0 && state.saving === 0))
|
||||
.subscribe(synced => {
|
||||
if (synced) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Ready:
|
||||
*
|
||||
* means that the doc has been loaded and the data can be modified.
|
||||
* (is not force, you can still modify it if you know you are creating some new data)
|
||||
*
|
||||
* this is a temporary solution to deal with the yjs overwrite issue.
|
||||
*
|
||||
* if content is loaded from storage
|
||||
* or if content is pulled from the server, it will be true, otherwise be false.
|
||||
*
|
||||
* For example, when opening a doc that is not in storage, ready = false until the content is pulled from the server.
|
||||
*/
|
||||
waitForReady(docId: string) {
|
||||
return new Promise<void>(resolve => {
|
||||
this.docState$(docId)
|
||||
.pipe(map(state => state.ready))
|
||||
.subscribe(ready => {
|
||||
if (ready) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user