@tearleads/client-sdk 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentMoveIntentPersistence.js","sourceRoot":"","sources":["documentMoveIntentPersistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,4CAA4C,CAAC;AACvF,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iCAAiC,EAAE,MAAM,uCAAuC,CAAC;AAC1F,OAAO,EAEL,eAAe,EACf,wBAAwB,GACzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,kCAAkC,CAAC;AAE1C,MAAM,CAAC,MAAM,yBAAyB,GAAG,eAAe,CAAC;AACzD,MAAM,CAAC,MAAM,yBAAyB,GAAG,eAAe,CAAC;AACzD,MAAM,+BAA+B,GAAG;IACtC,yBAAyB;IACzB,yBAAyB;CAC1B,CAAC;AAEF,2EAA2E;AAC3E,mEAAmE;AACnE,yEAAyE;AACzE,oEAAoE;AACpE,6EAA6E;AAC7E,4EAA4E;AAC5E,wDAAwD;AACxD,MAAM,8BAA8B,GAAG;;;;EAIrC,CAAC;AAEH,MAAM,oCAAoC,GAAG;;;;;;yDAMY,CAAC;AAuD1D,SAAS,iCAAiC,CACxC,KAAc;IAEd,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;QACzE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,yBAAyB,CAAC,KAIlC;IACC,IAAI,KAAK,CAAC,WAAW;QAAE,OAAO,aAAa,CAAC;IAC5C,IAAI,KAAK,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAC;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,SAAS,2BAA2B,CAClC,GAAqC;IAErC,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;QACxB,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,UAAU,EACR,GAAG,CAAC,UAAU,KAAK,yBAAyB;YAC1C,CAAC,CAAC,yBAAyB;YAC3B,CAAC,CAAC,yBAAyB;QAC/B,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,uBAAuB,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;QAC7D,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QACxC,UAAU,EAAE,iCAAiC,CAAC,GAAG,CAAC,UAAU,CAAC;QAC7D,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QACxC,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,gCAAgC,GAAG;IAC9C,KAAK,CAAC,YAAY,CAAC,OAAgB;QACjC,MAAM,eAAe,CAAC,OAAO,EAAE,wBAAwB,CAAC,CAAC;IAC3D,CAAC;IAED,iBAAiB,EAAE,yBAAyB;IAC5C,iBAAiB,EAAE,yBAAyB;IAC5C,mBAAmB,EAAE,2BAA2B;IAEhD,KAAK,CAAC,sBAAsB,CAC1B,OAAgB;QAEhB,OAAO,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;YAC/D,MAAM,eAAe,CAAC,aAAa,EAAE,wBAAwB,CAAC,CAAC;YAC/D,OAAO,iCAAiC,CAAC,aAAa,CAAC,CAAC,WAAW,CACjE,KAAK,EAAE,EAAE,EAAE,EAAE;gBACX,MAAM,IAAI,GAAG,MAAM,EAAE;qBAClB,MAAM,EAAE;qBACR,IAAI,CAAC,mBAAmB,CAAC;oBAC1B,kEAAkE;oBAClE,qEAAqE;oBACrE,uEAAuE;oBACvE,qEAAqE;oBACrE,2DAA2D;oBAC3D,wEAAwE;oBACxE,mEAAmE;oBACnE,uEAAuE;qBACtE,KAAK,CACJ,GAAG,CACD,OAAO,CAAC,mBAAmB,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,EAC/D,OAAO,CACL,mBAAmB,CAAC,UAAU,EAC9B,+BAA+B,CAChC,CACF,CACF;qBACA,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC;gBAE/C,OAAO,OAAO,CAAC,GAAG,CAChB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;oBACrB,MAAM,OAAO,GAAG,MAAM,6BAA6B,CACjD,aAAa,EACb,GAAG,CAAC,EAAE,IAAI,EAAE,CACb,CAAC;oBACF,MAAM,KAAK,GAAG,OAAO;yBAClB,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC;yBAC/C,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC;yBACnC,IAAI,EAAE,CAAC;oBACV,MAAM,OAAO,GAAG,OAAO;yBACpB,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC;yBACjD,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC;yBACnC,IAAI,EAAE,CAAC;oBACV,OAAO;wBACL,GAAG,2BAA2B,CAAC,GAAG,CAAC;wBACnC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,0BAA0B,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC9D,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBAChE,CAAC;gBACJ,CAAC,CAAC,CACH,CAAC;YACJ,CAAC,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,oBAAoB,CACxB,OAAgB,EAChB,KAIC;QAED,OAAO,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;YAC/D,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC;;;;aAIK,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;sBACjC,EACd;gBACE,KAAK,CAAC,UAAU;gBAChB,GAAG,+BAA+B;gBAClC,KAAK,CAAC,iBAAiB;gBACvB,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CACF,CAAC;YACF,KAAK,MAAM,EAAE,EAAE,EAAE,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,iCAAiC,CAAC,aAAa,CAAC;qBACnD,EAAE,CAAC,MAAM,CAAC,yBAAyB,CAAC;qBACpC,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;qBACzD,GAAG,EAAE,CAAC;YACX,CAAC;YACD,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,qBAAqB,CACzB,OAAgB,EAChB,KA2BC;QAED,MAAM,iCAAiC,CAAC,OAAO,CAAC,CAAC,kBAAkB,CACjE,KAAK,EAAE,EAAE,EAAE,EAAE;YACX,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,EAAE;iBACL,MAAM,CAAC,mBAAmB,CAAC;iBAC3B,GAAG,CAAC;gBACH,eAAe,EAAE,SAAS;gBAC1B,SAAS,EAAE,KAAK,CAAC,OAAO;gBACxB,UAAU,EAAE,yBAAyB,CAAC,KAAK,CAAC;gBAC5C,SAAS;aACV,CAAC;iBACD,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,mBAAmB,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC;YACpD,4DAA4D;YAC5D,8DAA8D;YAC9D,kDAAkD;YAClD,OAAO,CAAC,mBAAmB,CAAC,UAAU,EAAE;gBACtC,SAAS;gBACT,SAAS;gBACT,QAAQ;aACT,CAAC,EACF,OAAO,CACL,mBAAmB,CAAC,UAAU,EAC9B,+BAA+B,CAChC,EACD,GAAG,CAAC,KAAK,CAAC,gBAAgB;gBACxB,CAAC,CAAC,CAAC,EAAE,CAAC,mBAAmB,CAAC,EAAE,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;gBACtD,CAAC,CAAC,EAAE,CAAC,EACP,GAAG,CAAC,KAAK,CAAC,iBAAiB;gBACzB,CAAC,CAAC,CAAC,EAAE,CAAC,mBAAmB,CAAC,SAAS,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;gBAC9D,CAAC,CAAC,EAAE,CAAC,CACR,CACF;iBACA,GAAG,EAAE,CAAC;QACX,CAAC,EACD,KAAK,CAAC,YAAY,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAClC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAC1B,CAAC;IACJ,CAAC;IACD;;;OAGG;IACH,KAAK,CAAC,oBAAoB,CACxB,OAAgB,EAChB,KAAmC;QAEnC,MAAM,gCAAgC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,eAAe,CAAC,OAAO,EAAE;YAC7B,GAAG,wBAAwB;YAC3B,GAAG,eAAe;SACnB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CACxB;;SAEG,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC,EAAE;;;WAG/D,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,QAAQ,8BAA8B,WAAW,8BAA8B,WAAW,CAAC,CAAC,CAAC,EAAE;eACnH,EACT,KAAK,EAAE,cAAc;YACnB,CAAC,CAAC,CAAC,GAAG,+BAA+B,EAAE,KAAK,CAAC,cAAc,CAAC;YAC5D,CAAC,CAAC,+BAA+B,CACpC,CAAC;QACF,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IACzB,CAAC;IACD;;;;OAIG;IACH,KAAK,CAAC,sBAAsB,CAC1B,OAAgB,EAChB,KAAqD;QAErD,MAAM,gCAAgC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,eAAe,CAAC,OAAO,EAAE;YAC7B,GAAG,wBAAwB;YAC3B,GAAG,eAAe;SACnB,CAAC,CAAC;QACH,MAAM,wBAAwB,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;YAC9D,uEAAuE;YACvE,sEAAsE;YACtE,qEAAqE;YACrE,8DAA8D;YAC9D,MAAM,aAAa,CACjB;;;;aAIK,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE;aAExC,KAAK,EAAE,cAAc;gBACnB,CAAC,CAAC;;sBAEK,oCAAoC;4BAC9B,8BAA8B;2BAC/B,8BAA8B;oBACrC;gBACL,CAAC,CAAC,EACjB,EAAE,EACM;gBACE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACxB,GAAG,+BAA+B;gBAClC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1C,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aACzD,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import { and, asc, eq, inArray } from \"drizzle-orm\";\nimport { documentIntentLinkTargets } from \"../../sqlite/documentPlacementIntentSchema\";\nimport {\n containerTables,\n documentMoveIntents,\n documentMoveIntentTables,\n documentProjectionTables,\n} from \"../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport {\n enqueueDocumentLinkIntent,\n enqueueDocumentMoveIntent,\n enqueueDocumentUnlinkIntent,\n loadDocumentIntentLinkTargets,\n} from \"./documentPlacementIntentEnqueue\";\n\nexport const DOCUMENT_MOVE_INTENT_TYPE = \"document.move\";\nexport const DOCUMENT_LINK_INTENT_TYPE = \"document.link\";\nconst DOCUMENT_PLACEMENT_INTENT_TYPES = [\n DOCUMENT_MOVE_INTENT_TYPE,\n DOCUMENT_LINK_INTENT_TYPE,\n];\n\n// Resolves the organization a parked move belongs to, in confidence order:\n// the move's target container, the document's preserved projection\n// attribution, then the container the document currently sits in. Normal\n// saves routinely leave projection attribution empty (deferred-tail\n// reporting resolves through the same container join), so the container rows\n// carry the common case; rows with no resolvable organization stay in every\n// scope (device-first shapes, matching orphan priming).\nconst DENIED_INTENT_ORGANIZATION_SQL = `COALESCE(\n NULLIF(target_container.organization_id, ''),\n NULLIF(projection.organization_id, ''),\n NULLIF(projection_container.organization_id, '')\n)`;\n\nconst DENIED_INTENT_ORGANIZATION_JOINS_SQL = `\n LEFT JOIN containers target_container\n ON target_container.id = intent.target_container_id\n LEFT JOIN document_projection projection\n ON projection.local_id = intent.local_id\n LEFT JOIN containers projection_container\n ON projection_container.id = projection.container_id`;\n\n/**\n * - `pending`: replays on every structural pass.\n * - `blocked`: the last pass could not proceed locally (missing local\n * document / destination); replays, since hydration can heal it.\n * - `denied`: parked on a 403 until the access-restored replay (row 7).\n * - `unavailable`: terminal — after a projection refresh the destination\n * itself is proven deleted (coded 404 on its own projection probe). No\n * mutation 409 is ever terminal: with the destination live it can only\n * name an ancestor or source, and the next pass's fresh paths outrun it.\n * Container ids never return, so no replay can commit the intent as\n * written; it leaves the replay set until the local container tombstone\n * cascade retargets it or the user re-enqueues the move.\n */\nexport type DocumentMoveIntentSyncStatus =\n | \"pending\"\n | \"blocked\"\n | \"denied\"\n | \"unavailable\";\n\nexport interface DocumentMoveIntentRecord {\n additionalLinkContainerIds?: readonly string[] | undefined;\n removedLinkContainerIds?: readonly string[] | undefined;\n id: string;\n documentId: string;\n intentType:\n | typeof DOCUMENT_MOVE_INTENT_TYPE\n | typeof DOCUMENT_LINK_INTENT_TYPE;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: DocumentMoveIntentSyncStatus;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SelectedDocumentMoveIntentRecord {\n id: string | null;\n documentId: string;\n intentType: string;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: string;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nfunction parseDocumentMoveIntentSyncStatus(\n value: unknown,\n): DocumentMoveIntentSyncStatus {\n if (value === \"blocked\" || value === \"denied\" || value === \"unavailable\") {\n return value;\n }\n return \"pending\";\n}\n\nfunction resolveRecordedSyncStatus(input: {\n blocked?: boolean | undefined;\n denied?: boolean | undefined;\n unavailable?: boolean | undefined;\n}): DocumentMoveIntentSyncStatus {\n if (input.unavailable) return \"unavailable\";\n if (input.denied) return \"denied\";\n return input.blocked ? \"blocked\" : \"pending\";\n}\n\nfunction mapDocumentMoveIntentRecord(\n row: SelectedDocumentMoveIntentRecord,\n): DocumentMoveIntentRecord {\n return {\n id: String(row.id ?? \"\"),\n documentId: row.documentId,\n intentType:\n row.intentType === DOCUMENT_LINK_INTENT_TYPE\n ? DOCUMENT_LINK_INTENT_TYPE\n : DOCUMENT_MOVE_INTENT_TYPE,\n lastAttemptedAt: row.lastAttemptedAt,\n lastError: row.lastError,\n localId: row.localId,\n replaceLinkedContainers: Boolean(row.replaceLinkedContainers),\n sourceContainerId: row.sourceContainerId,\n syncStatus: parseDocumentMoveIntentSyncStatus(row.syncStatus),\n targetContainerId: row.targetContainerId,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport const sqlDocumentMoveIntentPersistence = {\n async ensureSchema(execSql: ExecSql): Promise<void> {\n await ensureSqlTables(execSql, documentMoveIntentTables);\n },\n\n enqueueMoveIntent: enqueueDocumentMoveIntent,\n enqueueLinkIntent: enqueueDocumentLinkIntent,\n enqueueUnlinkIntent: enqueueDocumentUnlinkIntent,\n\n async listPendingMoveIntents(\n execSql: ExecSql,\n ): Promise<DocumentMoveIntentRecord[]> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureSqlTables(lockedExecSql, documentMoveIntentTables);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const rows = await tx\n .select()\n .from(documentMoveIntents)\n // Blocked intents replay too: \"blocked\" names the reason the last\n // attempt could not proceed (missing local doc / destination), not a\n // terminal verdict. The blocking condition can heal after hydration or\n // recovery, and re-checking is cheap — a still-blocked intent simply\n // re-records its reason without counting as lane progress.\n // Unavailable intents never replay: the server proved a cited container\n // is gone, so every replay would re-issue the same doomed requests\n // (#2278 #4). Only the tombstone cascade or a re-enqueue revives them.\n .where(\n and(\n inArray(documentMoveIntents.syncStatus, [\"pending\", \"blocked\"]),\n inArray(\n documentMoveIntents.intentType,\n DOCUMENT_PLACEMENT_INTENT_TYPES,\n ),\n ),\n )\n .orderBy(asc(documentMoveIntents.createdAt));\n\n return Promise.all(\n rows.map(async (row) => {\n const targets = await loadDocumentIntentLinkTargets(\n lockedExecSql,\n row.id ?? \"\",\n );\n const added = targets\n .filter((target) => target.operation === \"link\")\n .map((target) => target.containerId)\n .sort();\n const removed = targets\n .filter((target) => target.operation === \"unlink\")\n .map((target) => target.containerId)\n .sort();\n return {\n ...mapDocumentMoveIntentRecord(row),\n ...(added.length ? { additionalLinkContainerIds: added } : {}),\n ...(removed.length ? { removedLinkContainerIds: removed } : {}),\n };\n }),\n );\n },\n );\n });\n },\n\n async markMoveIntentSynced(\n execSql: ExecSql,\n input: {\n documentId: string;\n expectedIntentId?: string | undefined;\n expectedUpdatedAt: string;\n },\n ): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n const deleted = await lockedExecSql(\n `DELETE FROM document_move_intents\n WHERE document_id = ?\n AND intent_type IN (?, ?)\n AND updated_at = ?\n ${input.expectedIntentId ? \"AND id = ?\" : \"\"}\n RETURNING id`,\n [\n input.documentId,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n input.expectedUpdatedAt,\n ...(input.expectedIntentId ? [input.expectedIntentId] : []),\n ],\n );\n for (const { id } of deleted) {\n await getClientSQLitePersistenceRuntime(lockedExecSql)\n .db.delete(documentIntentLinkTargets)\n .where(eq(documentIntentLinkTargets.intentId, String(id)))\n .run();\n }\n return deleted.length > 0;\n });\n },\n\n async recordMoveIntentError(\n execSql: ExecSql,\n input: {\n blocked?: boolean | undefined;\n /**\n * A permission denial (403): the intent parks as `denied` — excluded\n * from routine structural replays — until the org-access-restored\n * signal or a manual retry flips it back to pending (edge-case row 7).\n */\n denied?: boolean | undefined;\n documentId: string;\n /** Exact intent revision read by the pass; preferred over wall-clock CAS. */\n expectedIntentId?: string | undefined;\n /**\n * Optimistic concurrency: when given, the error only records against\n * the intent revision the pass read — a re-enqueued move (new\n * updatedAt) is never parked by a stale in-flight failure.\n */\n expectedUpdatedAt?: string | undefined;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n /**\n * Server proof that a cited container was deleted (coded 404 or\n * `container_unavailable` 409): the intent parks terminally as\n * `unavailable`, outside every replay, until the tombstone cascade\n * retargets it or a re-enqueue replaces it. Outranks `denied`: a\n * restored permission cannot revive a deleted container.\n */\n unavailable?: boolean | undefined;\n },\n ): Promise<void> {\n await getClientSQLitePersistenceRuntime(execSql).guardedTransaction(\n async (db) => {\n const updatedAt = new Date().toISOString();\n await db\n .update(documentMoveIntents)\n .set({\n lastAttemptedAt: updatedAt,\n lastError: input.message,\n syncStatus: resolveRecordedSyncStatus(input),\n updatedAt,\n })\n .where(\n and(\n eq(documentMoveIntents.documentId, input.documentId),\n // Blocked/denied rows must stay updatable: a retried intent\n // records its fresh outcome, and a transient failure flips it\n // back to pending instead of freezing it forever.\n inArray(documentMoveIntents.syncStatus, [\n \"pending\",\n \"blocked\",\n \"denied\",\n ]),\n inArray(\n documentMoveIntents.intentType,\n DOCUMENT_PLACEMENT_INTENT_TYPES,\n ),\n ...(input.expectedIntentId\n ? [eq(documentMoveIntents.id, input.expectedIntentId)]\n : []),\n ...(input.expectedUpdatedAt\n ? [eq(documentMoveIntents.updatedAt, input.expectedUpdatedAt)]\n : []),\n ),\n )\n .run();\n },\n input.stillCurrent ?? (() => true),\n { behavior: \"immediate\" },\n );\n },\n /**\n * Evidence for the org-access-restored re-arm gate: a parked\n * permission-denied move proves a queued write was refused.\n */\n async hasDeniedMoveIntents(\n execSql: ExecSql,\n input?: { organizationId?: string },\n ): Promise<boolean> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n const rows = await execSql(\n `SELECT intent.document_id AS document_id\n FROM document_move_intents intent\n ${input?.organizationId ? DENIED_INTENT_ORGANIZATION_JOINS_SQL : \"\"}\n WHERE intent.sync_status = 'denied'\n AND intent.intent_type IN (?, ?)\n ${input?.organizationId ? `AND (${DENIED_INTENT_ORGANIZATION_SQL} = ? OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL)` : \"\"}\n LIMIT 1`,\n input?.organizationId\n ? [...DOCUMENT_PLACEMENT_INTENT_TYPES, input.organizationId]\n : DOCUMENT_PLACEMENT_INTENT_TYPES,\n );\n return rows.length > 0;\n },\n /**\n * Flip parked permission-denied moves back to pending so the re-armed\n * structural pass replays them (access restored, or a manual retry scoped\n * to one document).\n */\n async resetDeniedMoveIntents(\n execSql: ExecSql,\n input?: { localId?: string; organizationId?: string },\n ): Promise<void> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n await runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n // Organization scoping resolves each intent's organization through the\n // container joins above: restoring one organization's access must not\n // un-park moves that another organization still denies. Rows with no\n // resolvable organization are included (device-first shapes).\n await lockedExecSql(\n `UPDATE document_move_intents\n SET sync_status = 'pending', updated_at = ?\n WHERE sync_status = 'denied'\n AND intent_type IN (?, ?)\n ${input?.localId ? \"AND local_id = ?\" : \"\"}\n ${\n input?.organizationId\n ? `AND local_id IN (\n SELECT intent.local_id FROM document_move_intents intent\n ${DENIED_INTENT_ORGANIZATION_JOINS_SQL}\n WHERE ${DENIED_INTENT_ORGANIZATION_SQL} = ?\n OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL\n )`\n : \"\"\n}`,\n [\n new Date().toISOString(),\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n ...(input?.localId ? [input.localId] : []),\n ...(input?.organizationId ? [input.organizationId] : []),\n ],\n );\n });\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"documentMoveIntentPersistence.js","sourceRoot":"","sources":["documentMoveIntentPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, asc, eq, inArray } from \"drizzle-orm\";\nimport { documentIntentLinkTargets } from \"../../sqlite/documentPlacementIntentSchema\";\nimport {\n containerTables,\n documentMoveIntents,\n documentMoveIntentTables,\n documentProjectionTables,\n} from \"../../sqlite/schema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport {\n type ExecSql,\n ensureSqlTables,\n runSerializedSqlMutation,\n} from \"../../sqlite/sqlSchema\";\nimport {\n enqueueDocumentLinkIntent,\n enqueueDocumentMoveIntent,\n enqueueDocumentUnlinkIntent,\n loadDocumentIntentLinkTargets,\n} from \"./documentPlacementIntentEnqueue\";\n\nexport const DOCUMENT_MOVE_INTENT_TYPE = \"document.move\";\nexport const DOCUMENT_LINK_INTENT_TYPE = \"document.link\";\nconst DOCUMENT_PLACEMENT_INTENT_TYPES = [\n DOCUMENT_MOVE_INTENT_TYPE,\n DOCUMENT_LINK_INTENT_TYPE,\n];\n\n// Resolves the organization a parked move belongs to, in confidence order:\n// the move's target container, the document's preserved projection\n// attribution, then the container the document currently sits in. Normal\n// saves routinely leave projection attribution empty (deferred-tail\n// reporting resolves through the same container join), so the container rows\n// carry the common case; rows with no resolvable organization stay in every\n// scope (device-first shapes, matching orphan priming).\nconst DENIED_INTENT_ORGANIZATION_SQL = `COALESCE(\n NULLIF(target_container.organization_id, ''),\n NULLIF(projection.organization_id, ''),\n NULLIF(projection_container.organization_id, '')\n)`;\n\nconst DENIED_INTENT_ORGANIZATION_JOINS_SQL = `\n LEFT JOIN containers target_container\n ON target_container.id = intent.target_container_id\n LEFT JOIN document_projection projection\n ON projection.local_id = intent.local_id\n LEFT JOIN containers projection_container\n ON projection_container.id = projection.container_id`;\n\n/**\n * - `pending`: replays on every structural pass.\n * - `blocked`: the last pass could not proceed locally (missing local\n * document / destination); replays, since hydration can heal it.\n * - `denied`: parked on a 403 until the access-restored replay (row 7).\n * - `unavailable`: terminal — after a projection refresh the destination\n * itself is proven deleted (coded 404 on its own projection probe). No\n * mutation 409 is ever terminal: with the destination live it can only\n * name an ancestor or source, and the next pass's fresh paths outrun it.\n * Container ids never return, so no replay can commit the intent as\n * written; it leaves the replay set until the local container tombstone\n * cascade retargets it or the user re-enqueues the move.\n */\nexport type DocumentMoveIntentSyncStatus =\n | \"pending\"\n | \"blocked\"\n | \"denied\"\n | \"unavailable\";\n\nexport interface DocumentMoveIntentRecord {\n additionalLinkContainerIds?: readonly string[] | undefined;\n removedLinkContainerIds?: readonly string[] | undefined;\n id: string;\n documentId: string;\n intentType:\n | typeof DOCUMENT_MOVE_INTENT_TYPE\n | typeof DOCUMENT_LINK_INTENT_TYPE;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: DocumentMoveIntentSyncStatus;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface SelectedDocumentMoveIntentRecord {\n id: string | null;\n documentId: string;\n intentType: string;\n lastAttemptedAt: string | null;\n lastError: string | null;\n localId: string;\n replaceLinkedContainers: boolean;\n sourceContainerId: string | null;\n syncStatus: string;\n targetContainerId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nfunction parseDocumentMoveIntentSyncStatus(\n value: unknown,\n): DocumentMoveIntentSyncStatus {\n if (value === \"blocked\" || value === \"denied\" || value === \"unavailable\") {\n return value;\n }\n return \"pending\";\n}\n\nfunction resolveRecordedSyncStatus(input: {\n blocked?: boolean | undefined;\n denied?: boolean | undefined;\n unavailable?: boolean | undefined;\n}): DocumentMoveIntentSyncStatus {\n if (input.unavailable) return \"unavailable\";\n if (input.denied) return \"denied\";\n return input.blocked ? \"blocked\" : \"pending\";\n}\n\nfunction mapDocumentMoveIntentRecord(\n row: SelectedDocumentMoveIntentRecord,\n): DocumentMoveIntentRecord {\n return {\n id: String(row.id ?? \"\"),\n documentId: row.documentId,\n intentType:\n row.intentType === DOCUMENT_LINK_INTENT_TYPE\n ? DOCUMENT_LINK_INTENT_TYPE\n : DOCUMENT_MOVE_INTENT_TYPE,\n lastAttemptedAt: row.lastAttemptedAt,\n lastError: row.lastError,\n localId: row.localId,\n replaceLinkedContainers: Boolean(row.replaceLinkedContainers),\n sourceContainerId: row.sourceContainerId,\n syncStatus: parseDocumentMoveIntentSyncStatus(row.syncStatus),\n targetContainerId: row.targetContainerId,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport const sqlDocumentMoveIntentPersistence = {\n async ensureSchema(execSql: ExecSql): Promise<void> {\n await ensureSqlTables(execSql, documentMoveIntentTables);\n },\n\n enqueueMoveIntent: enqueueDocumentMoveIntent,\n enqueueLinkIntent: enqueueDocumentLinkIntent,\n enqueueUnlinkIntent: enqueueDocumentUnlinkIntent,\n\n async listPendingMoveIntents(\n execSql: ExecSql,\n ): Promise<DocumentMoveIntentRecord[]> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n await ensureSqlTables(lockedExecSql, documentMoveIntentTables);\n return getClientSQLitePersistenceRuntime(lockedExecSql).transaction(\n async (tx) => {\n const rows = await tx\n .select()\n .from(documentMoveIntents)\n // Blocked intents replay too: \"blocked\" names the reason the last\n // attempt could not proceed (missing local doc / destination), not a\n // terminal verdict. The blocking condition can heal after hydration or\n // recovery, and re-checking is cheap — a still-blocked intent simply\n // re-records its reason without counting as lane progress.\n // Unavailable intents never replay: the server proved a cited container\n // is gone, so every replay would re-issue the same doomed requests\n // (#2278 #4). Only the tombstone cascade or a re-enqueue revives them.\n .where(\n and(\n inArray(documentMoveIntents.syncStatus, [\"pending\", \"blocked\"]),\n inArray(\n documentMoveIntents.intentType,\n DOCUMENT_PLACEMENT_INTENT_TYPES,\n ),\n ),\n )\n .orderBy(asc(documentMoveIntents.createdAt));\n\n return Promise.all(\n rows.map(async (row) => {\n const targets = await loadDocumentIntentLinkTargets(\n lockedExecSql,\n row.id ?? \"\",\n );\n const added = targets\n .filter((target) => target.operation === \"link\")\n .map((target) => target.containerId)\n .sort();\n const removed = targets\n .filter((target) => target.operation === \"unlink\")\n .map((target) => target.containerId)\n .sort();\n return {\n ...mapDocumentMoveIntentRecord(row),\n ...(added.length ? { additionalLinkContainerIds: added } : {}),\n ...(removed.length ? { removedLinkContainerIds: removed } : {}),\n };\n }),\n );\n },\n );\n });\n },\n\n async markMoveIntentSynced(\n execSql: ExecSql,\n input: {\n documentId: string;\n expectedIntentId?: string | undefined;\n expectedUpdatedAt: string;\n },\n ): Promise<boolean> {\n return runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n const deleted = await lockedExecSql(\n `DELETE FROM document_move_intents\n WHERE document_id = ?\n AND intent_type IN (?, ?)\n AND updated_at = ?\n ${input.expectedIntentId ? \"AND id = ?\" : \"\"}\n RETURNING id`,\n [\n input.documentId,\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n input.expectedUpdatedAt,\n ...(input.expectedIntentId ? [input.expectedIntentId] : []),\n ],\n );\n for (const { id } of deleted) {\n await getClientSQLitePersistenceRuntime(lockedExecSql)\n .db.delete(documentIntentLinkTargets)\n .where(eq(documentIntentLinkTargets.intentId, String(id)))\n .run();\n }\n return deleted.length > 0;\n });\n },\n\n async recordMoveIntentError(\n execSql: ExecSql,\n input: {\n blocked?: boolean | undefined;\n /**\n * A permission denial (403): the intent parks as `denied` — excluded\n * from routine structural replays — until the org-access-restored\n * signal or a manual retry flips it back to pending (edge-case row 7).\n */\n denied?: boolean | undefined;\n documentId: string;\n /** Exact intent revision read by the pass; preferred over wall-clock CAS. */\n expectedIntentId?: string | undefined;\n /**\n * Optimistic concurrency: when given, the error only records against\n * the intent revision the pass read — a re-enqueued move (new\n * updatedAt) is never parked by a stale in-flight failure.\n */\n expectedUpdatedAt?: string | undefined;\n message: string;\n stillCurrent?: (() => boolean) | undefined;\n /**\n * Server proof that a cited container was deleted (coded 404 or\n * `container_unavailable` 409): the intent parks terminally as\n * `unavailable`, outside every replay, until the tombstone cascade\n * retargets it or a re-enqueue replaces it. Outranks `denied`: a\n * restored permission cannot revive a deleted container.\n */\n unavailable?: boolean | undefined;\n },\n ): Promise<void> {\n await getClientSQLitePersistenceRuntime(execSql).guardedTransaction(\n async (db) => {\n const updatedAt = new Date().toISOString();\n await db\n .update(documentMoveIntents)\n .set({\n lastAttemptedAt: updatedAt,\n lastError: input.message,\n syncStatus: resolveRecordedSyncStatus(input),\n updatedAt,\n })\n .where(\n and(\n eq(documentMoveIntents.documentId, input.documentId),\n // Blocked/denied rows must stay updatable: a retried intent\n // records its fresh outcome, and a transient failure flips it\n // back to pending instead of freezing it forever.\n inArray(documentMoveIntents.syncStatus, [\n \"pending\",\n \"blocked\",\n \"denied\",\n ]),\n inArray(\n documentMoveIntents.intentType,\n DOCUMENT_PLACEMENT_INTENT_TYPES,\n ),\n ...(input.expectedIntentId\n ? [eq(documentMoveIntents.id, input.expectedIntentId)]\n : []),\n ...(input.expectedUpdatedAt\n ? [eq(documentMoveIntents.updatedAt, input.expectedUpdatedAt)]\n : []),\n ),\n )\n .run();\n },\n input.stillCurrent ?? (() => true),\n { behavior: \"immediate\" },\n );\n },\n /**\n * Evidence for the org-access-restored re-arm gate: a parked\n * permission-denied move proves a queued write was refused.\n */\n async hasDeniedMoveIntents(\n execSql: ExecSql,\n input?: { organizationId?: string },\n ): Promise<boolean> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n const rows = await execSql(\n `SELECT intent.document_id AS document_id\n FROM document_move_intents intent\n ${input?.organizationId ? DENIED_INTENT_ORGANIZATION_JOINS_SQL : \"\"}\n WHERE intent.sync_status = 'denied'\n AND intent.intent_type IN (?, ?)\n ${input?.organizationId ? `AND (${DENIED_INTENT_ORGANIZATION_SQL} = ? OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL)` : \"\"}\n LIMIT 1`,\n input?.organizationId\n ? [...DOCUMENT_PLACEMENT_INTENT_TYPES, input.organizationId]\n : DOCUMENT_PLACEMENT_INTENT_TYPES,\n );\n return rows.length > 0;\n },\n /**\n * Flip parked permission-denied moves back to pending so the re-armed\n * structural pass replays them (access restored, or a manual retry scoped\n * to one document).\n */\n async resetDeniedMoveIntents(\n execSql: ExecSql,\n input?: { localId?: string; organizationId?: string },\n ): Promise<void> {\n await sqlDocumentMoveIntentPersistence.ensureSchema(execSql);\n await ensureSqlTables(execSql, [\n ...documentProjectionTables,\n ...containerTables,\n ]);\n await runSerializedSqlMutation(execSql, async (lockedExecSql) => {\n // Organization scoping resolves each intent's organization through the\n // container joins above: restoring one organization's access must not\n // un-park moves that another organization still denies. Rows with no\n // resolvable organization are included (device-first shapes).\n await lockedExecSql(\n `UPDATE document_move_intents\n SET sync_status = 'pending', updated_at = ?\n WHERE sync_status = 'denied'\n AND intent_type IN (?, ?)\n ${input?.localId ? \"AND local_id = ?\" : \"\"}\n ${\n input?.organizationId\n ? `AND local_id IN (\n SELECT intent.local_id FROM document_move_intents intent\n ${DENIED_INTENT_ORGANIZATION_JOINS_SQL}\n WHERE ${DENIED_INTENT_ORGANIZATION_SQL} = ?\n OR ${DENIED_INTENT_ORGANIZATION_SQL} IS NULL\n )`\n : \"\"\n }`,\n [\n new Date().toISOString(),\n ...DOCUMENT_PLACEMENT_INTENT_TYPES,\n ...(input?.localId ? [input.localId] : []),\n ...(input?.organizationId ? [input.organizationId] : []),\n ],\n );\n });\n },\n};\n"]}
|
|
@@ -196,31 +196,28 @@ export function getTextValue(doc, key = "text") {
|
|
|
196
196
|
* index, so callers can both blame and re-render the prose (the per-range view)
|
|
197
197
|
* from a single pass.
|
|
198
198
|
*
|
|
199
|
-
* One Loro op (one counter) is one code point, but an astral character
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* that opens with an emoji), but the low/last unit always resolves to the
|
|
204
|
-
* inserting op — and using one position per code point avoids double-counting
|
|
205
|
-
* the two halves of a surrogate pair.
|
|
199
|
+
* One Loro op (one counter) is one code point, but an astral character spans
|
|
200
|
+
* two UTF-16 units. Loro 1.16.4 resolves the cursor at the first unit and rejects
|
|
201
|
+
* the interior low-surrogate position. Iterate code points and advance by their
|
|
202
|
+
* UTF-16 length so attribution neither splits nor double-counts a character.
|
|
206
203
|
*/
|
|
207
204
|
function collectTextCharBlame(doc, key) {
|
|
208
205
|
const text = doc.getText(key);
|
|
209
206
|
const codePoints = [];
|
|
210
207
|
const opIds = [];
|
|
211
|
-
let
|
|
208
|
+
let utf16Start = 0;
|
|
212
209
|
for (const char of text.toString()) {
|
|
213
|
-
utf16End += char.length;
|
|
214
210
|
// getCursor mints a WASM-backed Cursor each call; free it immediately after
|
|
215
211
|
// reading its op id so a long document does not pile up cursors on the heap.
|
|
216
|
-
const cursor = text.getCursor(
|
|
212
|
+
const cursor = text.getCursor(utf16Start, 1);
|
|
217
213
|
const opId = cursor?.pos();
|
|
218
214
|
cursor?.free();
|
|
219
215
|
if (opId === undefined) {
|
|
220
|
-
throw new Error(`LoroText "${key}" code point
|
|
216
|
+
throw new Error(`LoroText "${key}" code point starting at UTF-16 index ${utf16Start} has no op id.`);
|
|
221
217
|
}
|
|
222
218
|
codePoints.push(char);
|
|
223
219
|
opIds.push({ peerId: opId.peer, counter: opId.counter });
|
|
220
|
+
utf16Start += char.length;
|
|
224
221
|
}
|
|
225
222
|
return { codePoints, opIds };
|
|
226
223
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document.js","sourceRoot":"","sources":["document.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64ToBytes, bytesToBase64 } from \"@tearleads/encoding\";\nimport { LoroDoc, VersionVector } from \"loro-crdt\";\nimport { serializeCanonicalHistory } from \"./historyCanonicalization\";\nimport { getImportBlobMetadata } from \"./importMetadata\";\n\nexport { getImportBlobMetadata, type ImportBlobMode } from \"./importMetadata\";\n\nfunction isPeerIdString(value: string): value is `${number}` {\n return /^\\d+$/.test(value);\n}\n\nconst MAX_POSTGRES_INTEGER = 2_147_483_647;\n\nexport class LoroImportUnresolvedDependenciesError extends Error {\n readonly importKind: \"snapshot\" | \"updates\";\n\n constructor(importKind: \"snapshot\" | \"updates\") {\n super(\n `import${importKind === \"snapshot\" ? \"Snapshot\" : \"Updates\"} received ${\n importKind === \"snapshot\" ? \"a snapshot\" : \"updates\"\n } with unresolved pending dependencies`,\n );\n this.name = \"LoroImportUnresolvedDependenciesError\";\n this.importKind = importKind;\n }\n}\n\nexport interface VersionVectorSpan {\n peerId: `${number}`;\n startCounter: number;\n endCounter: number;\n}\n\nfunction assertSupportedVersionVectorCounter(counter: number): void {\n if (\n !Number.isInteger(counter) ||\n counter < 0 ||\n counter > MAX_POSTGRES_INTEGER\n ) {\n throw new Error(\n \"Version vector counter is outside the supported integer range.\",\n );\n }\n}\n\nfunction normalizeVersionVectorEntries(\n versionVector: VersionVector,\n): Map<`${number}`, number> {\n const entries = new Map<`${number}`, number>();\n\n for (const [peerId, counter] of versionVector.toJSON()) {\n const normalizedPeerId = String(peerId);\n if (!isPeerIdString(normalizedPeerId)) {\n throw new Error(\"Version vector contains a non-numeric peer ID.\");\n }\n assertSupportedVersionVectorCounter(counter);\n entries.set(normalizedPeerId, counter);\n }\n\n return entries;\n}\n\nexport async function derivePeerId(\n seed: string | Uint8Array,\n): Promise<`${number}`> {\n const bytes =\n typeof seed === \"string\" ? new TextEncoder().encode(seed) : seed;\n const digest = new Uint8Array(\n await crypto.subtle.digest(\"SHA-256\", bytes.slice()),\n );\n const view = new DataView(\n digest.buffer,\n digest.byteOffset,\n digest.byteLength,\n );\n const peerId = (view.getBigUint64(0, false) || 1n).toString();\n\n if (!isPeerIdString(peerId)) {\n throw new Error(\"Failed to derive a numeric peer ID.\");\n }\n\n return peerId;\n}\n\nexport async function createDocument(peerSeed: string | Uint8Array) {\n const doc = new LoroDoc();\n doc.setPeerId(await derivePeerId(peerSeed));\n return doc;\n}\n\nexport function exportAllUpdates(doc: LoroDoc): Uint8Array {\n return doc.export({ mode: \"update\" });\n}\n\n/** Deterministic, peer-uncompressed identity for an operation-log prefix. */\nexport function exportFullHistoryIdentity(\n doc: LoroDoc,\n endVersion?: string,\n): string {\n return serializeCanonicalHistory(\n doc.exportJsonUpdates(\n undefined,\n endVersion === undefined ? undefined : decodeVersionVector(endVersion),\n false,\n ),\n );\n}\n\n/**\n * Export a mergeable, full-history snapshot — the only snapshot form this\n * codebase produces. Every document is born with full durable history and\n * restored from it, so a partial (gc'd) export here is an invariant\n * violation, not a state to accommodate; fail loudly instead of publishing a\n * blob that could not safely merge concurrent frontiers.\n */\nexport function exportFullHistorySnapshot(doc: LoroDoc): Uint8Array {\n const snapshot = doc.export({ mode: \"snapshot\" });\n const metadata = getImportBlobMetadata(snapshot);\n if (\n metadata.mode !== \"snapshot\" ||\n !versionVectorsEqual(\n metadata.partialStartVersionVector,\n emptyVersionVector(),\n )\n ) {\n throw new Error(\n \"Document export lost full history; this document was restored from an incomplete source\",\n );\n }\n return snapshot;\n}\n\nexport function exportUpdatesSince(\n doc: LoroDoc,\n encodedVersionVector?: string | null,\n): Uint8Array {\n if (!encodedVersionVector) {\n return exportAllUpdates(doc);\n }\n\n return doc.export({\n mode: \"update\",\n from: VersionVector.decode(base64ToBytes(encodedVersionVector)),\n });\n}\n\nexport function encodeEncodedVersionVector(\n versionVector: VersionVector,\n): string {\n return bytesToBase64(versionVector.encode());\n}\n\nexport function encodeVersionVector(doc: LoroDoc): string {\n return encodeEncodedVersionVector(doc.oplogVersion());\n}\n\nexport function emptyVersionVector(): string {\n return encodeEncodedVersionVector(new VersionVector(undefined));\n}\n\nexport function decodeVersionVector(\n encodedVersionVector: string | null | undefined,\n): VersionVector {\n if (!encodedVersionVector) {\n return new VersionVector(undefined);\n }\n\n return VersionVector.decode(base64ToBytes(encodedVersionVector));\n}\n\nexport function getUpdateVersionVectors(update: Uint8Array): {\n partialStartVersionVector: string;\n partialEndVersionVector: string;\n} {\n const metadata = getImportBlobMetadata(update);\n\n return {\n partialStartVersionVector: metadata.partialStartVersionVector,\n partialEndVersionVector: metadata.partialEndVersionVector,\n };\n}\n\nexport function satisfiesVersionVector(\n encodedVersionVector: string | null | undefined,\n partialVersionVector: string,\n): boolean {\n const versionVector = decodeVersionVector(encodedVersionVector);\n const requiredVersionVector = decodeVersionVector(partialVersionVector);\n\n for (const [peerId, counter] of requiredVersionVector.toJSON()) {\n if ((versionVector.get(peerId) ?? 0) < counter) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function mergeVersionVectors(\n encodedVersionVectors: ReadonlyArray<string>,\n): string {\n const merged = new Map<`${number}`, number>();\n\n for (const encodedVersionVector of encodedVersionVectors) {\n for (const [normalizedPeerId, counter] of normalizeVersionVectorEntries(\n decodeVersionVector(encodedVersionVector),\n )) {\n merged.set(\n normalizedPeerId,\n Math.max(merged.get(normalizedPeerId) ?? 0, counter),\n );\n }\n }\n\n return encodeEncodedVersionVector(VersionVector.parseJSON(merged));\n}\n\nexport function listVersionVectorSpans(input: {\n partialStartVersionVector: string;\n partialEndVersionVector: string;\n}): VersionVectorSpan[] {\n const startEntries = normalizeVersionVectorEntries(\n decodeVersionVector(input.partialStartVersionVector),\n );\n const endEntries = normalizeVersionVectorEntries(\n decodeVersionVector(input.partialEndVersionVector),\n );\n const spans: VersionVectorSpan[] = [];\n\n for (const [peerId, startCounter] of startEntries) {\n const endCounter = endEntries.get(peerId) ?? 0;\n if (startCounter > endCounter) {\n throw new Error(\"Version vector end must cover start.\");\n }\n }\n\n for (const [peerId, endCounter] of endEntries) {\n const startCounter = startEntries.get(peerId) ?? 0;\n if (endCounter <= startCounter) {\n continue;\n }\n\n spans.push({ peerId, startCounter, endCounter });\n }\n\n return spans.sort((left, right) => left.peerId.localeCompare(right.peerId));\n}\n\nexport function versionVectorsEqual(\n left: string | null | undefined,\n right: string | null | undefined,\n): boolean {\n const leftEntries = decodeVersionVector(left).toJSON();\n const rightEntries = decodeVersionVector(right).toJSON();\n\n if (leftEntries.size !== rightEntries.size) {\n return false;\n }\n\n for (const [peerId, leftCounter] of leftEntries) {\n if (rightEntries.get(peerId) !== leftCounter) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function importUpdates(doc: LoroDoc, updates: Uint8Array[]): void {\n // Reject an obsolete member before importBatch can mutate the document.\n for (const update of updates) getImportBlobMetadata(update);\n const status = doc.importBatch(updates);\n if (status.pending != null && status.pending.size > 0) {\n // importBatch applies out-of-order batches correctly but can still report\n // the intermediate pending set it saw mid-batch. Trust the document, not\n // the report: the import only genuinely failed when the document's final\n // version does not cover some batched update's end version.\n const documentVersion = encodeVersionVector(doc);\n const covered = updates.every((update) =>\n satisfiesVersionVector(\n documentVersion,\n getImportBlobMetadata(update).partialEndVersionVector,\n ),\n );\n if (!covered) {\n throw new LoroImportUnresolvedDependenciesError(\"updates\");\n }\n }\n}\n\n/**\n * Load a snapshot blob into a document. Snapshots MUST go through a single\n * `import()`, never `importBatch()`. A non-empty `pending` means the import\n * target is incompatible or dependencies are missing, so fail loudly instead\n * of silently loading a short document.\n */\nexport function importSnapshot(doc: LoroDoc, snapshot: Uint8Array): void {\n getImportBlobMetadata(snapshot);\n const status = doc.import(snapshot);\n // Reject only a genuinely incomplete import: `pending` can be an empty\n // VersionVector (size 0) rather than null when everything resolved, so test\n // for at least one unresolved dependency instead of mere presence.\n if (status.pending != null && status.pending.size > 0) {\n throw new LoroImportUnresolvedDependenciesError(\"snapshot\");\n }\n}\n\nexport function getTextValue(doc: LoroDoc, key = \"text\"): string {\n return doc.getText(key).toString();\n}\n\nexport interface TextCharOpId {\n /** Peer of the op that inserted this character. */\n peerId: `${number}`;\n /** Counter of that op — identifies the character within the peer's stream. */\n counter: number;\n}\n\nexport interface TextCharBlameSource {\n /** One entry per Unicode code point of the prose, in document order. */\n codePoints: string[];\n /** The op id that inserted each code point — same length and order as `codePoints`. */\n opIds: TextCharOpId[];\n}\n\n/**\n * Walk a LoroText's prose once, collecting each Unicode code point alongside the\n * op id `(peerId, counter)` of the op that inserted it. The two arrays share an\n * index, so callers can both blame and re-render the prose (the per-range view)\n * from a single pass.\n *\n * One Loro op (one counter) is one code point, but an astral character (emoji,\n * etc.) spans two UTF-16 units. We therefore iterate code points and index\n * `getCursor` by each code point's LAST UTF-16 unit: a leading high-surrogate\n * position yields no cursor (so iterating raw UTF-16 units would throw on prose\n * that opens with an emoji), but the low/last unit always resolves to the\n * inserting op — and using one position per code point avoids double-counting\n * the two halves of a surrogate pair.\n */\nfunction collectTextCharBlame(doc: LoroDoc, key: string): TextCharBlameSource {\n const text = doc.getText(key);\n const codePoints: string[] = [];\n const opIds: TextCharOpId[] = [];\n let utf16End = 0;\n for (const char of text.toString()) {\n utf16End += char.length;\n // getCursor mints a WASM-backed Cursor each call; free it immediately after\n // reading its op id so a long document does not pile up cursors on the heap.\n const cursor = text.getCursor(utf16End - 1, 1);\n const opId = cursor?.pos();\n cursor?.free();\n if (opId === undefined) {\n throw new Error(\n `LoroText \"${key}\" code point ending at UTF-16 index ${utf16End - 1} has no op id.`,\n );\n }\n codePoints.push(char);\n opIds.push({ peerId: opId.peer, counter: opId.counter });\n }\n return { codePoints, opIds };\n}\n\n/**\n * Per-character op-id attribution for a LoroText container's prose. Returns one\n * `(peerId, counter)` per Unicode code point — the op that inserted that\n * character — in document order; the array length equals the code-point count\n * (`[...text].length`). This is the client-side primitive for character-level\n * \"blame\": each op id resolves to a writer by intersecting it against the\n * document's edit-attribution segments.\n */\nexport function listTextCharOpIds(doc: LoroDoc, key = \"text\"): TextCharOpId[] {\n return collectTextCharBlame(doc, key).opIds;\n}\n\n/**\n * {@link listTextCharOpIds} for a persisted snapshot blob, without opening an\n * editor: rebuilds a throwaway read-only `LoroDoc` from the snapshot and reads\n * each character's op id, so blame is exact. The doc is only read, never\n * edited, so no peer id is set.\n *\n * Extraction is linear: `getCursor()` is an O(log n) b-tree lookup per code\n * point (measured ~1.8µs/char — ~90ms for a 50k-char snapshot, flat per char and\n * unaffected by tombstones). `maxCharacters` (UTF-16 length) is therefore a soft\n * guard against pathologically large prose, not a quadratic cliff: over it we\n * return `null` (\"skipped, too large\", distinct from `[]` for an empty document)\n * rather than do unbounded synchronous work on the render thread.\n */\nexport function listSnapshotCharOpIds(\n snapshot: Uint8Array,\n maxCharacters: number,\n key = \"text\",\n): TextCharOpId[] | null {\n const source = listSnapshotCharBlameSource(snapshot, maxCharacters, key);\n return source === null ? null : source.opIds;\n}\n\n/**\n * {@link listSnapshotCharBlameSource} is {@link listSnapshotCharOpIds} that also\n * returns each code point's text, so a caller can render the prose tinted by\n * writer (the per-range \"blame\" view) without a second snapshot reconstruction.\n * Same linear cost and `maxCharacters` guard.\n */\nexport function listSnapshotCharBlameSource(\n snapshot: Uint8Array,\n maxCharacters: number,\n key = \"text\",\n): TextCharBlameSource | null {\n const doc = new LoroDoc();\n try {\n importSnapshot(doc, snapshot);\n if (doc.getText(key).length > maxCharacters) {\n return null;\n }\n return collectTextCharBlame(doc, key);\n } finally {\n // The throwaway doc is WASM-backed and never returned — free it immediately\n // rather than waiting for GC, since blame rebuilds one per Info-panel load.\n doc.free();\n }\n}\n\nexport interface FieldEditor {\n /** The map key (e.g. `firstName`, `cardNumber`) of a structured document field. */\n key: string;\n /** Peer of the op that last set this field, or null if none is recorded. */\n peerId: `${number}` | null;\n}\n\n/**\n * The structured-document counterpart to {@link listSnapshotCharBlameSource}:\n * for a persisted snapshot, name the peer that last wrote each key of a LoroMap\n * (the document's `fields` map). Structured documents (contacts, cards, licenses)\n * store each field as a `LoroMap` entry — a last-writer-wins register that keeps\n * the setting op's peer, which `getLastEditor` reads. That peer resolves to a\n * writer via the same attribution segments,\n * giving per-field \"blame\". Fields carry only the peer (no op counter), so\n * resolution is peer-level. A note (or any document without this map) yields an\n * empty array. Reconstructing the small map is cheap, so there is no size cap.\n */\nexport function listSnapshotFieldEditors(\n snapshot: Uint8Array,\n mapKey: string,\n): FieldEditor[] {\n const doc = new LoroDoc();\n try {\n importSnapshot(doc, snapshot);\n const map = doc.getMap(mapKey);\n return map.keys().map((key) => ({\n key,\n peerId: map.getLastEditor(key) ?? null,\n }));\n } finally {\n doc.free();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["document.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64ToBytes, bytesToBase64 } from \"@tearleads/encoding\";\nimport { LoroDoc, VersionVector } from \"loro-crdt\";\nimport { serializeCanonicalHistory } from \"./historyCanonicalization\";\nimport { getImportBlobMetadata } from \"./importMetadata\";\n\nexport { getImportBlobMetadata, type ImportBlobMode } from \"./importMetadata\";\n\nfunction isPeerIdString(value: string): value is `${number}` {\n return /^\\d+$/.test(value);\n}\n\nconst MAX_POSTGRES_INTEGER = 2_147_483_647;\n\nexport class LoroImportUnresolvedDependenciesError extends Error {\n readonly importKind: \"snapshot\" | \"updates\";\n\n constructor(importKind: \"snapshot\" | \"updates\") {\n super(\n `import${importKind === \"snapshot\" ? \"Snapshot\" : \"Updates\"} received ${\n importKind === \"snapshot\" ? \"a snapshot\" : \"updates\"\n } with unresolved pending dependencies`,\n );\n this.name = \"LoroImportUnresolvedDependenciesError\";\n this.importKind = importKind;\n }\n}\n\nexport interface VersionVectorSpan {\n peerId: `${number}`;\n startCounter: number;\n endCounter: number;\n}\n\nfunction assertSupportedVersionVectorCounter(counter: number): void {\n if (\n !Number.isInteger(counter) ||\n counter < 0 ||\n counter > MAX_POSTGRES_INTEGER\n ) {\n throw new Error(\n \"Version vector counter is outside the supported integer range.\",\n );\n }\n}\n\nfunction normalizeVersionVectorEntries(\n versionVector: VersionVector,\n): Map<`${number}`, number> {\n const entries = new Map<`${number}`, number>();\n\n for (const [peerId, counter] of versionVector.toJSON()) {\n const normalizedPeerId = String(peerId);\n if (!isPeerIdString(normalizedPeerId)) {\n throw new Error(\"Version vector contains a non-numeric peer ID.\");\n }\n assertSupportedVersionVectorCounter(counter);\n entries.set(normalizedPeerId, counter);\n }\n\n return entries;\n}\n\nexport async function derivePeerId(\n seed: string | Uint8Array,\n): Promise<`${number}`> {\n const bytes =\n typeof seed === \"string\" ? new TextEncoder().encode(seed) : seed;\n const digest = new Uint8Array(\n await crypto.subtle.digest(\"SHA-256\", bytes.slice()),\n );\n const view = new DataView(\n digest.buffer,\n digest.byteOffset,\n digest.byteLength,\n );\n const peerId = (view.getBigUint64(0, false) || 1n).toString();\n\n if (!isPeerIdString(peerId)) {\n throw new Error(\"Failed to derive a numeric peer ID.\");\n }\n\n return peerId;\n}\n\nexport async function createDocument(peerSeed: string | Uint8Array) {\n const doc = new LoroDoc();\n doc.setPeerId(await derivePeerId(peerSeed));\n return doc;\n}\n\nexport function exportAllUpdates(doc: LoroDoc): Uint8Array {\n return doc.export({ mode: \"update\" });\n}\n\n/** Deterministic, peer-uncompressed identity for an operation-log prefix. */\nexport function exportFullHistoryIdentity(\n doc: LoroDoc,\n endVersion?: string,\n): string {\n return serializeCanonicalHistory(\n doc.exportJsonUpdates(\n undefined,\n endVersion === undefined ? undefined : decodeVersionVector(endVersion),\n false,\n ),\n );\n}\n\n/**\n * Export a mergeable, full-history snapshot — the only snapshot form this\n * codebase produces. Every document is born with full durable history and\n * restored from it, so a partial (gc'd) export here is an invariant\n * violation, not a state to accommodate; fail loudly instead of publishing a\n * blob that could not safely merge concurrent frontiers.\n */\nexport function exportFullHistorySnapshot(doc: LoroDoc): Uint8Array {\n const snapshot = doc.export({ mode: \"snapshot\" });\n const metadata = getImportBlobMetadata(snapshot);\n if (\n metadata.mode !== \"snapshot\" ||\n !versionVectorsEqual(\n metadata.partialStartVersionVector,\n emptyVersionVector(),\n )\n ) {\n throw new Error(\n \"Document export lost full history; this document was restored from an incomplete source\",\n );\n }\n return snapshot;\n}\n\nexport function exportUpdatesSince(\n doc: LoroDoc,\n encodedVersionVector?: string | null,\n): Uint8Array {\n if (!encodedVersionVector) {\n return exportAllUpdates(doc);\n }\n\n return doc.export({\n mode: \"update\",\n from: VersionVector.decode(base64ToBytes(encodedVersionVector)),\n });\n}\n\nexport function encodeEncodedVersionVector(\n versionVector: VersionVector,\n): string {\n return bytesToBase64(versionVector.encode());\n}\n\nexport function encodeVersionVector(doc: LoroDoc): string {\n return encodeEncodedVersionVector(doc.oplogVersion());\n}\n\nexport function emptyVersionVector(): string {\n return encodeEncodedVersionVector(new VersionVector(undefined));\n}\n\nexport function decodeVersionVector(\n encodedVersionVector: string | null | undefined,\n): VersionVector {\n if (!encodedVersionVector) {\n return new VersionVector(undefined);\n }\n\n return VersionVector.decode(base64ToBytes(encodedVersionVector));\n}\n\nexport function getUpdateVersionVectors(update: Uint8Array): {\n partialStartVersionVector: string;\n partialEndVersionVector: string;\n} {\n const metadata = getImportBlobMetadata(update);\n\n return {\n partialStartVersionVector: metadata.partialStartVersionVector,\n partialEndVersionVector: metadata.partialEndVersionVector,\n };\n}\n\nexport function satisfiesVersionVector(\n encodedVersionVector: string | null | undefined,\n partialVersionVector: string,\n): boolean {\n const versionVector = decodeVersionVector(encodedVersionVector);\n const requiredVersionVector = decodeVersionVector(partialVersionVector);\n\n for (const [peerId, counter] of requiredVersionVector.toJSON()) {\n if ((versionVector.get(peerId) ?? 0) < counter) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function mergeVersionVectors(\n encodedVersionVectors: ReadonlyArray<string>,\n): string {\n const merged = new Map<`${number}`, number>();\n\n for (const encodedVersionVector of encodedVersionVectors) {\n for (const [normalizedPeerId, counter] of normalizeVersionVectorEntries(\n decodeVersionVector(encodedVersionVector),\n )) {\n merged.set(\n normalizedPeerId,\n Math.max(merged.get(normalizedPeerId) ?? 0, counter),\n );\n }\n }\n\n return encodeEncodedVersionVector(VersionVector.parseJSON(merged));\n}\n\nexport function listVersionVectorSpans(input: {\n partialStartVersionVector: string;\n partialEndVersionVector: string;\n}): VersionVectorSpan[] {\n const startEntries = normalizeVersionVectorEntries(\n decodeVersionVector(input.partialStartVersionVector),\n );\n const endEntries = normalizeVersionVectorEntries(\n decodeVersionVector(input.partialEndVersionVector),\n );\n const spans: VersionVectorSpan[] = [];\n\n for (const [peerId, startCounter] of startEntries) {\n const endCounter = endEntries.get(peerId) ?? 0;\n if (startCounter > endCounter) {\n throw new Error(\"Version vector end must cover start.\");\n }\n }\n\n for (const [peerId, endCounter] of endEntries) {\n const startCounter = startEntries.get(peerId) ?? 0;\n if (endCounter <= startCounter) {\n continue;\n }\n\n spans.push({ peerId, startCounter, endCounter });\n }\n\n return spans.sort((left, right) => left.peerId.localeCompare(right.peerId));\n}\n\nexport function versionVectorsEqual(\n left: string | null | undefined,\n right: string | null | undefined,\n): boolean {\n const leftEntries = decodeVersionVector(left).toJSON();\n const rightEntries = decodeVersionVector(right).toJSON();\n\n if (leftEntries.size !== rightEntries.size) {\n return false;\n }\n\n for (const [peerId, leftCounter] of leftEntries) {\n if (rightEntries.get(peerId) !== leftCounter) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function importUpdates(doc: LoroDoc, updates: Uint8Array[]): void {\n // Reject an obsolete member before importBatch can mutate the document.\n for (const update of updates) getImportBlobMetadata(update);\n const status = doc.importBatch(updates);\n if (status.pending != null && status.pending.size > 0) {\n // importBatch applies out-of-order batches correctly but can still report\n // the intermediate pending set it saw mid-batch. Trust the document, not\n // the report: the import only genuinely failed when the document's final\n // version does not cover some batched update's end version.\n const documentVersion = encodeVersionVector(doc);\n const covered = updates.every((update) =>\n satisfiesVersionVector(\n documentVersion,\n getImportBlobMetadata(update).partialEndVersionVector,\n ),\n );\n if (!covered) {\n throw new LoroImportUnresolvedDependenciesError(\"updates\");\n }\n }\n}\n\n/**\n * Load a snapshot blob into a document. Snapshots MUST go through a single\n * `import()`, never `importBatch()`. A non-empty `pending` means the import\n * target is incompatible or dependencies are missing, so fail loudly instead\n * of silently loading a short document.\n */\nexport function importSnapshot(doc: LoroDoc, snapshot: Uint8Array): void {\n getImportBlobMetadata(snapshot);\n const status = doc.import(snapshot);\n // Reject only a genuinely incomplete import: `pending` can be an empty\n // VersionVector (size 0) rather than null when everything resolved, so test\n // for at least one unresolved dependency instead of mere presence.\n if (status.pending != null && status.pending.size > 0) {\n throw new LoroImportUnresolvedDependenciesError(\"snapshot\");\n }\n}\n\nexport function getTextValue(doc: LoroDoc, key = \"text\"): string {\n return doc.getText(key).toString();\n}\n\nexport interface TextCharOpId {\n /** Peer of the op that inserted this character. */\n peerId: `${number}`;\n /** Counter of that op — identifies the character within the peer's stream. */\n counter: number;\n}\n\nexport interface TextCharBlameSource {\n /** One entry per Unicode code point of the prose, in document order. */\n codePoints: string[];\n /** The op id that inserted each code point — same length and order as `codePoints`. */\n opIds: TextCharOpId[];\n}\n\n/**\n * Walk a LoroText's prose once, collecting each Unicode code point alongside the\n * op id `(peerId, counter)` of the op that inserted it. The two arrays share an\n * index, so callers can both blame and re-render the prose (the per-range view)\n * from a single pass.\n *\n * One Loro op (one counter) is one code point, but an astral character spans\n * two UTF-16 units. Loro 1.16.4 resolves the cursor at the first unit and rejects\n * the interior low-surrogate position. Iterate code points and advance by their\n * UTF-16 length so attribution neither splits nor double-counts a character.\n */\nfunction collectTextCharBlame(doc: LoroDoc, key: string): TextCharBlameSource {\n const text = doc.getText(key);\n const codePoints: string[] = [];\n const opIds: TextCharOpId[] = [];\n let utf16Start = 0;\n for (const char of text.toString()) {\n // getCursor mints a WASM-backed Cursor each call; free it immediately after\n // reading its op id so a long document does not pile up cursors on the heap.\n const cursor = text.getCursor(utf16Start, 1);\n const opId = cursor?.pos();\n cursor?.free();\n if (opId === undefined) {\n throw new Error(\n `LoroText \"${key}\" code point starting at UTF-16 index ${utf16Start} has no op id.`,\n );\n }\n codePoints.push(char);\n opIds.push({ peerId: opId.peer, counter: opId.counter });\n utf16Start += char.length;\n }\n return { codePoints, opIds };\n}\n\n/**\n * Per-character op-id attribution for a LoroText container's prose. Returns one\n * `(peerId, counter)` per Unicode code point — the op that inserted that\n * character — in document order; the array length equals the code-point count\n * (`[...text].length`). This is the client-side primitive for character-level\n * \"blame\": each op id resolves to a writer by intersecting it against the\n * document's edit-attribution segments.\n */\nexport function listTextCharOpIds(doc: LoroDoc, key = \"text\"): TextCharOpId[] {\n return collectTextCharBlame(doc, key).opIds;\n}\n\n/**\n * {@link listTextCharOpIds} for a persisted snapshot blob, without opening an\n * editor: rebuilds a throwaway read-only `LoroDoc` from the snapshot and reads\n * each character's op id, so blame is exact. The doc is only read, never\n * edited, so no peer id is set.\n *\n * Extraction is linear: `getCursor()` is an O(log n) b-tree lookup per code\n * point (measured ~1.8µs/char — ~90ms for a 50k-char snapshot, flat per char and\n * unaffected by tombstones). `maxCharacters` (UTF-16 length) is therefore a soft\n * guard against pathologically large prose, not a quadratic cliff: over it we\n * return `null` (\"skipped, too large\", distinct from `[]` for an empty document)\n * rather than do unbounded synchronous work on the render thread.\n */\nexport function listSnapshotCharOpIds(\n snapshot: Uint8Array,\n maxCharacters: number,\n key = \"text\",\n): TextCharOpId[] | null {\n const source = listSnapshotCharBlameSource(snapshot, maxCharacters, key);\n return source === null ? null : source.opIds;\n}\n\n/**\n * {@link listSnapshotCharBlameSource} is {@link listSnapshotCharOpIds} that also\n * returns each code point's text, so a caller can render the prose tinted by\n * writer (the per-range \"blame\" view) without a second snapshot reconstruction.\n * Same linear cost and `maxCharacters` guard.\n */\nexport function listSnapshotCharBlameSource(\n snapshot: Uint8Array,\n maxCharacters: number,\n key = \"text\",\n): TextCharBlameSource | null {\n const doc = new LoroDoc();\n try {\n importSnapshot(doc, snapshot);\n if (doc.getText(key).length > maxCharacters) {\n return null;\n }\n return collectTextCharBlame(doc, key);\n } finally {\n // The throwaway doc is WASM-backed and never returned — free it immediately\n // rather than waiting for GC, since blame rebuilds one per Info-panel load.\n doc.free();\n }\n}\n\nexport interface FieldEditor {\n /** The map key (e.g. `firstName`, `cardNumber`) of a structured document field. */\n key: string;\n /** Peer of the op that last set this field, or null if none is recorded. */\n peerId: `${number}` | null;\n}\n\n/**\n * The structured-document counterpart to {@link listSnapshotCharBlameSource}:\n * for a persisted snapshot, name the peer that last wrote each key of a LoroMap\n * (the document's `fields` map). Structured documents (contacts, cards, licenses)\n * store each field as a `LoroMap` entry — a last-writer-wins register that keeps\n * the setting op's peer, which `getLastEditor` reads. That peer resolves to a\n * writer via the same attribution segments,\n * giving per-field \"blame\". Fields carry only the peer (no op counter), so\n * resolution is peer-level. A note (or any document without this map) yields an\n * empty array. Reconstructing the small map is cheap, so there is no size cap.\n */\nexport function listSnapshotFieldEditors(\n snapshot: Uint8Array,\n mapKey: string,\n): FieldEditor[] {\n const doc = new LoroDoc();\n try {\n importSnapshot(doc, snapshot);\n const map = doc.getMap(mapKey);\n return map.keys().map((key) => ({\n key,\n peerId: map.getLastEditor(key) ?? null,\n }));\n } finally {\n doc.free();\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tearleads/client-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "React-free Tearleads client runtime: local SQLite persistence, keys, sync, encrypted blobs, and domain workflows.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@noble/hashes": "^2.4.0",
|
|
34
34
|
"@noble/post-quantum": "^0.7.1",
|
|
35
35
|
"@scure/bip39": "^2.4.0",
|
|
36
|
-
"drizzle-orm": "^0.45.
|
|
37
|
-
"loro-crdt": "^1.16.
|
|
38
|
-
"zod": "^4.5
|
|
36
|
+
"drizzle-orm": "^0.45.3",
|
|
37
|
+
"loro-crdt": "^1.16.4",
|
|
38
|
+
"zod": "^4.6.5"
|
|
39
39
|
}
|
|
40
40
|
}
|