rspace-online/server/sync-instance.ts

71 lines
2.5 KiB
TypeScript

/**
* SyncServer singleton — shared across server/index.ts and modules.
*
* Participant mode: server maintains its own Automerge docs.
* On any doc change, debounced-save to disk via doc-persistence.
*
* When a doc belongs to an encrypted space (meta.encrypted === true),
* the save is encrypted at rest using the space's encryptionKeyId.
*
* Relay mode: for encrypted spaces, the server stores opaque blobs
* it cannot decrypt, enabling cross-device restore.
*/
import { SyncServer } from "./local-first/sync-server";
import { saveDoc, saveDocImmediate, saveEncryptedBlob, loadEncryptedBlob } from "./local-first/doc-persistence";
import { getDocumentData } from "./community-store";
import { spaceKnowledgeIndex } from "./space-knowledge";
/**
* Look up the encryption key ID for a doc's space.
* DocIds are formatted as "spaceSlug:module:collection[:itemId]".
* Returns the encryptionKeyId if the space has encryption enabled, else undefined.
*/
function getEncryptionKeyId(docId: string): string | undefined {
const spaceSlug = docId.split(":")[0];
if (!spaceSlug || spaceSlug === "global") return undefined;
const data = getDocumentData(spaceSlug);
if (data?.meta?.encrypted && data.meta.encryptionKeyId) {
return data.meta.encryptionKeyId;
}
return undefined;
}
export const syncServer = new SyncServer({
participantMode: true,
maxDocs: 500,
onDocChange: (docId, doc) => {
const spaceSlug = docId.split(":")[0];
if (spaceSlug && spaceSlug !== "global") spaceKnowledgeIndex.invalidate(spaceSlug);
const encryptionKeyId = getEncryptionKeyId(docId);
saveDoc(docId, doc, encryptionKeyId);
},
onDocEvict: (docId, doc) => {
// Persist to disk immediately before evicting from memory (no debounce!)
const encryptionKeyId = getEncryptionKeyId(docId);
saveDocImmediate(docId, doc, encryptionKeyId).catch(e => {
console.error(`[SyncInstance] Eviction save failed for ${docId}:`, e);
});
},
onRelayBackup: (docId, blob) => {
saveEncryptedBlob(docId, blob);
},
onRelayLoad: (docId) => {
return loadEncryptedBlob(docId);
},
});
/**
* Flush all in-memory docs to disk immediately, then resolve.
* Called on SIGTERM/SIGINT before process exit.
*/
export async function flushAndShutdown(): Promise<void> {
console.log("[SyncInstance] Flushing all docs to disk before shutdown...");
const start = Date.now();
await syncServer.flushAll(async (docId, doc) => {
const encryptionKeyId = getEncryptionKeyId(docId);
await saveDocImmediate(docId, doc, encryptionKeyId);
});
console.log(`[SyncInstance] Flush complete (${Date.now() - start}ms)`);
}