@korajs/store 1.0.0-beta.11 → 1.0.0-beta.13

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.
Files changed (92) hide show
  1. package/README.md +29 -37
  2. package/dist/adapters/better-sqlite3.cjs +90 -4
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +21 -12
  5. package/dist/adapters/better-sqlite3.d.ts +21 -12
  6. package/dist/adapters/better-sqlite3.js +69 -5
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +1316 -610
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +29 -3
  11. package/dist/adapters/indexeddb.d.ts +29 -3
  12. package/dist/adapters/indexeddb.js +247 -221
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm-worker.cjs +803 -62
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +480 -62
  17. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  18. package/dist/adapters/sqlite-wasm.cjs +1032 -344
  19. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  20. package/dist/adapters/sqlite-wasm.d.cts +239 -18
  21. package/dist/adapters/sqlite-wasm.d.ts +239 -18
  22. package/dist/adapters/sqlite-wasm.js +111 -5
  23. package/dist/adapters/sqlite-wasm.js.map +1 -1
  24. package/dist/backup-4NUPDEMB.js +25 -0
  25. package/dist/chunk-32EDKWMI.js +876 -0
  26. package/dist/chunk-32EDKWMI.js.map +1 -0
  27. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  28. package/dist/chunk-A6GFH5IG.js +36 -0
  29. package/dist/chunk-A6GFH5IG.js.map +1 -0
  30. package/dist/chunk-DUKTWVZW.js +175 -0
  31. package/dist/chunk-DUKTWVZW.js.map +1 -0
  32. package/dist/chunk-HZTDBLDB.js +607 -0
  33. package/dist/chunk-HZTDBLDB.js.map +1 -0
  34. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  35. package/dist/chunk-J5AGXF3D.js.map +1 -0
  36. package/dist/chunk-MCQKEHOD.js +112 -0
  37. package/dist/chunk-MCQKEHOD.js.map +1 -0
  38. package/dist/chunk-P4V375FY.js +810 -0
  39. package/dist/chunk-P4V375FY.js.map +1 -0
  40. package/dist/chunk-QDUSJMFV.js +1095 -0
  41. package/dist/chunk-QDUSJMFV.js.map +1 -0
  42. package/dist/chunk-SJP2HTYZ.js +1003 -0
  43. package/dist/chunk-SJP2HTYZ.js.map +1 -0
  44. package/dist/chunk-W5F2LWG6.js +591 -0
  45. package/dist/chunk-W5F2LWG6.js.map +1 -0
  46. package/dist/chunk-Y5LRH7KI.js +5341 -0
  47. package/dist/chunk-Y5LRH7KI.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9354 -4157
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1064 -108
  52. package/dist/index.d.ts +1064 -108
  53. package/dist/index.js +249 -3092
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +580 -264
  56. package/dist/internal.cjs.map +1 -1
  57. package/dist/internal.d.cts +15 -85
  58. package/dist/internal.d.ts +15 -85
  59. package/dist/internal.js +10 -8
  60. package/dist/restore-FR7XIKT2.js +299 -0
  61. package/dist/restore-FR7XIKT2.js.map +1 -0
  62. package/dist/sql-builder-C4u55jKW.d.cts +608 -0
  63. package/dist/sql-builder-CGckdBDL.d.ts +608 -0
  64. package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
  65. package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
  66. package/dist/sqlite-wasm-channel-MLHLKEED.js +12 -0
  67. package/dist/store-IMWKZLWB.js +16 -0
  68. package/dist/store-IMWKZLWB.js.map +1 -0
  69. package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
  70. package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
  71. package/package.json +3 -2
  72. package/dist/backup-5S7K2Z35.js +0 -14
  73. package/dist/chunk-GSJ7EEUC.js.map +0 -1
  74. package/dist/chunk-HS6DBMST.js +0 -383
  75. package/dist/chunk-HS6DBMST.js.map +0 -1
  76. package/dist/chunk-RJIEKEN4.js +0 -751
  77. package/dist/chunk-RJIEKEN4.js.map +0 -1
  78. package/dist/chunk-TUR7RGEP.js +0 -84
  79. package/dist/chunk-TUR7RGEP.js.map +0 -1
  80. package/dist/chunk-VDBJQ75K.js +0 -78
  81. package/dist/chunk-VDBJQ75K.js.map +0 -1
  82. package/dist/chunk-YT7MRFCP.js +0 -687
  83. package/dist/chunk-YT7MRFCP.js.map +0 -1
  84. package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
  85. package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
  86. package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
  87. package/dist/types-BOO9EPt1.d.ts +0 -170
  88. package/dist/types-C_GDavG-.d.cts +0 -170
  89. /package/dist/{backup-5S7K2Z35.js.map → backup-4NUPDEMB.js.map} +0 -0
  90. /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
  91. /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
  92. /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-MLHLKEED.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mutations/write-context.ts","../src/lww/field-versions.ts","../src/state-machine/state-validator.ts","../src/mutations/run-local-write.ts","../src/mutations/write-ops.ts","../src/mutations/secret-write.ts","../src/mutations/execute-delete.ts","../src/mutations/execute-insert.ts","../src/mutations/execute-update.ts"],"sourcesContent":["import type {\n\tCausalTracker,\n\tHybridLogicalClock,\n\tOperation,\n\tSchemaDefinition,\n\tSecretKeyProvider,\n} from '@korajs/core'\nimport { KoraError } from '@korajs/core'\nimport type { RecordFolder } from '../fold/record-folder'\nimport type { RelationEnforcer } from '../relations/relation-enforcer'\nimport type { Transaction } from '../types'\n\n/**\n * Called inside the write transaction for every local delete, after the delete\n * operation is built and before referential side effects run. Lets the unified\n * apply pipeline run its referential checks (and emit their traces) against the\n * rows the transaction sees. Throwing aborts the whole write.\n */\nexport type LocalDeleteHook = (operation: Operation, tx: Transaction) => Promise<void>\n\n/**\n * Everything the single local write path needs to build and persist an\n * operation inside a storage transaction. One environment serves single-record\n * writes, `app.transaction` commits and referential cascades, so every local\n * operation is built the same way (W6).\n */\nexport interface WriteEnv {\n\treadonly schema: SchemaDefinition\n\treadonly clock: HybridLogicalClock\n\treadonly nodeId: string\n\treadonly relationEnforcer: RelationEnforcer | null\n\treadonly secretKeyProvider?: SecretKeyProvider\n\t/** Shared by every operation of an `app.transaction` / `app.mutation` commit. */\n\treadonly transactionId?: string\n\t/** Human-readable mutation name propagated to every operation (DevTools). */\n\treadonly mutationName?: string\n\t/** See {@link LocalDeleteHook}. */\n\treadonly beforeLocalDelete?: LocalDeleteHook\n\t/**\n\t * The W7 record fold. When set, a local write appends its operation and then\n\t * merges it into the record's fold state, which re-materializes the row (the\n\t * same path remote operations take). Absent only under\n\t * `experimental.legacyMerge` (and while a pre-W7 database is migrated, before its\n\t * re-materialization): the row is then written directly, as in beta.12.\n\t */\n\treadonly fold?: RecordFolder\n\t/**\n\t * Largest operation a local write may produce (RT-86), the server's\n\t * `maxOperationBytes`. Default `DEFAULT_MAX_OPERATION_BYTES` (256 KiB).\n\t */\n\treadonly maxOperationBytes?: number\n}\n\n/**\n * Causal bookkeeping for the operations built inside ONE storage transaction.\n *\n * Dependencies are computed from the committed causal heads (the shared\n * {@link CausalTracker}) overlaid with the operations already built in this\n * transaction. Heads are published to the shared tracker only after the\n * transaction commits, so a rolled-back write never leaves a dependency on an\n * operation that does not exist.\n */\nexport class CausalScope {\n\tprivate readonly lastByCollection = new Map<string, string>()\n\tprivate lastInScope: string | null = null\n\tprivate readonly recorded: Array<{ collection: string; id: string }> = []\n\n\tconstructor(\n\t\tprivate readonly tracker: CausalTracker | null,\n\t\t/** When true, every operation also depends on the previous one in this scope. */\n\t\tprivate readonly chainOperations: boolean,\n\t) {}\n\n\t/**\n\t * Direct causal parents for the next operation in `collection`.\n\t *\n\t * @param collection - Collection of the operation being built\n\t * @param extra - Explicit parents (e.g. the delete that caused a cascade)\n\t */\n\tdepsFor(collection: string, extra: readonly string[] = []): string[] {\n\t\tconst deps: string[] = []\n\t\tconst add = (id: string | undefined | null): void => {\n\t\t\tif (id && !deps.includes(id)) deps.push(id)\n\t\t}\n\t\tfor (const id of extra) add(id)\n\t\tconst local = this.lastByCollection.get(collection)\n\t\tif (local !== undefined) {\n\t\t\tadd(local)\n\t\t} else {\n\t\t\tfor (const id of this.tracker?.nextCausalDeps(collection, false) ?? []) add(id)\n\t\t}\n\t\tif (this.chainOperations) add(this.lastInScope)\n\t\treturn deps\n\t}\n\n\t/** Record an operation built in this scope. */\n\trecord(collection: string, operationId: string): void {\n\t\tthis.lastByCollection.set(collection, operationId)\n\t\tthis.lastInScope = operationId\n\t\tthis.recorded.push({ collection, id: operationId })\n\t}\n\n\t/** Publish this scope's heads to the shared tracker. Call only after commit. */\n\tpublish(): void {\n\t\tfor (const { collection, id } of this.recorded) {\n\t\t\tthis.tracker?.afterOperation(collection, id, false)\n\t\t}\n\t}\n}\n\n/**\n * The sequence-number block of ONE write transaction.\n *\n * The first number is reserved with one `UPSERT ... RETURNING` on\n * `_kora_version_vector`, inside the transaction; later numbers continue the block\n * in memory, and {@link SequenceBlock.close} persists the block's end with one\n * write before the transaction commits. Write transactions are serialized and no\n * code path allocates numbers outside one, so the block is contiguous and never\n * shared; the UNIQUE `(node_id, sequence_number)` index on every op table is the\n * backstop. A rollback discards the reservation with everything else.\n */\nexport class SequenceBlock {\n\tprivate last: number | null = null\n\tprivate persisted = 0\n\n\tconstructor(\n\t\tprivate readonly tx: Transaction,\n\t\tprivate readonly nodeId: string,\n\t) {}\n\n\t/** The next number of the block. */\n\tasync take(): Promise<number> {\n\t\tif (this.last === null) {\n\t\t\tconst rows = await this.tx.query<{ sequence_number: number }>(\n\t\t\t\t`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)\n ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1\n RETURNING sequence_number`,\n\t\t\t\t[this.nodeId],\n\t\t\t)\n\t\t\tconst first = rows[0]?.sequence_number\n\t\t\tif (first === undefined) {\n\t\t\t\tthrow new KoraError(\n\t\t\t\t\t`Failed to reserve a sequence number for node \"${this.nodeId}\"`,\n\t\t\t\t\t'SEQUENCE_RESERVATION_FAILED',\n\t\t\t\t\t{ nodeId: this.nodeId },\n\t\t\t\t)\n\t\t\t}\n\t\t\tthis.last = first\n\t\t\tthis.persisted = first\n\t\t\treturn first\n\t\t}\n\t\tthis.last += 1\n\t\treturn this.last\n\t}\n\n\t/** Persist the end of the block. Must run inside the transaction, before commit. */\n\tasync close(): Promise<void> {\n\t\tif (this.last === null || this.last === this.persisted) return\n\t\tawait this.tx.execute(\n\t\t\t'UPDATE _kora_version_vector SET sequence_number = MAX(sequence_number, ?) WHERE node_id = ?',\n\t\t\t[this.last, this.nodeId],\n\t\t)\n\t\tthis.persisted = this.last\n\t}\n}\n\n/**\n * The per-transaction state of a local write: the transaction handle, its\n * sequence block and its causal scope. Every operation built in one transaction\n * takes the next number of the same contiguous block.\n */\nexport interface WriteScope {\n\treadonly tx: Transaction\n\treadonly sequence: SequenceBlock\n\treadonly causal: CausalScope\n}\n\n/**\n * Run `write` with a fresh {@link WriteScope} on `tx`, then close the sequence\n * block (still inside the transaction).\n */\nexport async function withWriteScope<T>(\n\ttx: Transaction,\n\tnodeId: string,\n\tcausal: CausalScope,\n\twrite: (scope: WriteScope) => Promise<T>,\n): Promise<T> {\n\tconst scope: WriteScope = { tx, sequence: new SequenceBlock(tx, nodeId), causal }\n\tconst result = await write(scope)\n\tawait scope.sequence.close()\n\treturn result\n}\n\n/**\n * Whether a storage error means the device ran out of space: SQLite's\n * `SQLITE_FULL` (\"database or disk is full\") or a browser quota error from OPFS /\n * IndexedDB. The store maps these to a `store:quota-exceeded` event (STORE-15).\n */\nexport function isStorageFullError(error: unknown): boolean {\n\tif (error === null || typeof error !== 'object') return false\n\tconst candidate = error as { name?: unknown; code?: unknown; message?: unknown; cause?: unknown }\n\tif (candidate.name === 'QuotaExceededError') return true\n\tif (candidate.code === 'SQLITE_FULL' || candidate.code === 'QUOTA_EXCEEDED') return true\n\tconst message = typeof candidate.message === 'string' ? candidate.message : ''\n\tif (/SQLITE_FULL|database or disk is full|quota ?exceeded/i.test(message)) return true\n\treturn candidate.cause !== undefined && candidate.cause !== error\n\t\t? isStorageFullError(candidate.cause)\n\t\t: false\n}\n","import { EMPTY_ROW_VERSION } from './row-version'\n\n/**\n * Per-field last-writer versions for a materialized row.\n *\n * Each entry maps a field name to a serialized HLC timestamp (the same\n * lexicographically-sortable form used by the row `_version` column). Because\n * the serialized form sorts identically to {@link HybridLogicalClock.compare},\n * per-field last-write-wins is a plain string comparison: the greater string is\n * the newer writer. This makes field-level convergence deterministic and\n * independent of the order operations happen to arrive in.\n */\nexport type FieldVersions = Record<string, string>\n\n/**\n * Parse the `_field_versions` JSON column into a map. Tolerates null, empty,\n * and malformed values by returning an empty map — a row that predates\n * per-field tracking simply has no field versions and falls back to `_version`.\n */\nexport function parseFieldVersions(raw: unknown): FieldVersions {\n\tif (typeof raw !== 'string' || raw.length === 0) {\n\t\treturn {}\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as unknown\n\t\tif (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n\t\t\treturn {}\n\t\t}\n\t\tconst result: FieldVersions = {}\n\t\tfor (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {\n\t\t\tif (typeof value === 'string') {\n\t\t\t\tresult[key] = value\n\t\t\t}\n\t\t}\n\t\treturn result\n\t} catch {\n\t\treturn {}\n\t}\n}\n\n/**\n * Serialize a field-version map for storage in the `_field_versions` column.\n */\nexport function serializeFieldVersions(versions: FieldVersions): string {\n\treturn JSON.stringify(versions)\n}\n\n/**\n * Build the initial field-version map for an insert: every supplied field is\n * stamped with the operation's version.\n */\nexport function fieldVersionsForFields(\n\tfieldNames: Iterable<string>,\n\tversion: string,\n): FieldVersions {\n\tconst versions: FieldVersions = {}\n\tfor (const field of fieldNames) {\n\t\tversions[field] = version\n\t}\n\treturn versions\n}\n\n/**\n * The effective stored version for a field: its own tracked version if present,\n * otherwise the row-level `_version` (so rows written before per-field tracking\n * still compare correctly), otherwise the empty sentinel (older than anything).\n */\nexport function effectiveFieldVersion(\n\tcurrent: FieldVersions,\n\tfield: string,\n\trowVersion: string | undefined,\n): string {\n\tconst own = current[field]\n\tif (own !== undefined) {\n\t\treturn own\n\t}\n\treturn rowVersion ?? EMPTY_ROW_VERSION\n}\n\n/**\n * Result of resolving an incoming operation against the current per-field\n * versions of a row.\n */\nexport interface PerFieldLwwResult {\n\t/** Fields the incoming operation wins (strictly newer than the stored version). */\n\twinners: string[]\n\t/** The merged field-version map to persist (winners advanced to `incomingVersion`). */\n\tmerged: FieldVersions\n}\n\n/**\n * Resolve per-field last-write-wins for an incoming operation.\n *\n * For each incoming field, the operation wins only when its version is strictly\n * greater than the field's effective stored version. Ties (identical version,\n * i.e. the same operation re-applied) are losses, keeping the resolution\n * idempotent. The comparison is a total order, so every node resolves the same\n * winner regardless of the order operations arrive.\n *\n * @param current - Parsed `_field_versions` of the target row\n * @param incomingFields - Field names carried by the incoming operation\n * @param incomingVersion - Serialized HLC version of the incoming operation\n * @param rowVersion - The row's `_version` (fallback for untracked fields)\n */\nexport function resolvePerFieldLww(\n\tcurrent: FieldVersions,\n\tincomingFields: Iterable<string>,\n\tincomingVersion: string,\n\trowVersion: string | undefined,\n): PerFieldLwwResult {\n\tconst winners: string[] = []\n\tconst merged: FieldVersions = { ...current }\n\tfor (const field of incomingFields) {\n\t\tconst stored = effectiveFieldVersion(current, field, rowVersion)\n\t\tif (incomingVersion > stored) {\n\t\t\twinners.push(field)\n\t\t\tmerged[field] = incomingVersion\n\t\t}\n\t\t// Losing fields are left untouched: their existing tracked version (or the\n\t\t// row-level `_version` fallback) already reflects the newer writer.\n\t}\n\treturn { winners, merged }\n}\n\n/**\n * Stamp `fields` with `version` on top of a stored `_field_versions` value and\n * return the serialized column. This is the one per-field stamp every local\n * write uses (insert, update, transaction entries, cascades), so every field a\n * device writes carries the HLC of the operation that wrote it.\n *\n * Stamps only move forward: a field already holding a newer version keeps it.\n * The map is keyed by field name with one HLC register per field, the shape the\n * per-field fold (W7) builds on.\n *\n * @param currentRaw - The stored `_field_versions` column (or null for a new row)\n * @param fields - Field names this write sets\n * @param version - The writing operation's serialized HLC version\n */\nexport function stampFieldVersions(\n\tcurrentRaw: unknown,\n\tfields: Iterable<string>,\n\tversion: string,\n): string {\n\tconst versions = parseFieldVersions(currentRaw)\n\tfor (const field of fields) {\n\t\tconst existing = versions[field]\n\t\tif (existing === undefined || existing < version) {\n\t\t\tversions[field] = version\n\t\t}\n\t}\n\treturn serializeFieldVersions(versions)\n}\n","import { KoraError, validateTransition } from '@korajs/core'\nimport type {\n\tCollectionDefinition,\n\tStateMachineConstraint,\n\tStateMachineDefinition,\n} from '@korajs/core'\n\n/**\n * Error thrown when a local mutation attempts an invalid state transition.\n *\n * Contains enough context to debug without reproduction:\n * the collection, record, field, current state, attempted state, and allowed transitions.\n */\nexport class InvalidStateTransitionError extends KoraError {\n\tconstructor(\n\t\tpublic readonly collection: string,\n\t\tpublic readonly recordId: string,\n\t\tpublic readonly field: string,\n\t\tpublic readonly fromState: string,\n\t\tpublic readonly toState: string,\n\t\tpublic readonly allowedStates: string[],\n\t) {\n\t\tsuper(\n\t\t\t`Invalid state transition in collection \"${collection}\": ` +\n\t\t\t\t`cannot transition field \"${field}\" from \"${fromState}\" to \"${toState}\". ` +\n\t\t\t\t`Allowed transitions from \"${fromState}\": ${allowedStates.length > 0 ? allowedStates.join(', ') : '(none -- terminal state)'}`,\n\t\t\t'INVALID_STATE_TRANSITION',\n\t\t\t{ collection, recordId, field, fromState, toState, allowedStates },\n\t\t)\n\t\tthis.name = 'InvalidStateTransitionError'\n\t}\n}\n\n/**\n * Validates a state machine transition for a local mutation (insert or update).\n *\n * For inserts: validates that the initial state (from the data or the default value)\n * is a known state in the state machine.\n *\n * For updates: looks up the current state field value and checks whether\n * the transition to the new value is allowed.\n *\n * @param collectionName - Name of the collection\n * @param recordId - The record being mutated\n * @param stateMachine - The state machine definition\n * @param currentState - The current value of the state field (null for inserts)\n * @param newState - The new value being set for the state field\n * @returns An object indicating whether the transition is valid, and if not, the allowed states.\n * When `onInvalidTransition` is 'last-valid-state', callers should suppress the field update\n * rather than throwing.\n */\nexport function validateStateTransition(\n\tcollectionName: string,\n\trecordId: string,\n\tstateMachine: StateMachineDefinition,\n\tcurrentState: string | null,\n\tnewState: string,\n): { valid: boolean; allowedStates: string[] } {\n\t// For inserts, any valid enum value is acceptable as the initial state\n\t// (schema validation already ensures the value is a valid enum value)\n\tif (currentState === null) {\n\t\treturn { valid: true, allowedStates: [] }\n\t}\n\n\t// Same-state transitions are always valid (idempotent updates)\n\tif (currentState === newState) {\n\t\treturn { valid: true, allowedStates: stateMachine.transitions[currentState] ?? [] }\n\t}\n\n\tconst constraint: StateMachineConstraint = {\n\t\tfield: stateMachine.field,\n\t\tcollection: collectionName,\n\t\ttransitions: stateMachine.transitions,\n\t}\n\tconst transitionResult = validateTransition(constraint, currentState, newState)\n\tif (transitionResult.valid) {\n\t\treturn { valid: true, allowedStates: transitionResult.allowedTargets }\n\t}\n\n\tconst allowedStates = transitionResult.allowedTargets\n\n\tif (stateMachine.onInvalidTransition === 'reject') {\n\t\tthrow new InvalidStateTransitionError(\n\t\t\tcollectionName,\n\t\t\trecordId,\n\t\t\tstateMachine.field,\n\t\t\tcurrentState,\n\t\t\tnewState,\n\t\t\tallowedStates,\n\t\t)\n\t}\n\n\t// 'last-valid-state': return invalid so the caller can suppress the field update\n\treturn { valid: false, allowedStates }\n}\n\n/**\n * Checks whether a collection update data object contains a change to the state machine field,\n * and if so, validates the transition.\n *\n * If the state field is not in the update data, returns the data unchanged.\n * If the transition is invalid and mode is 'last-valid-state', removes the field from the data.\n * If the transition is invalid and mode is 'reject', throws InvalidStateTransitionError.\n *\n * @param collectionName - Name of the collection\n * @param recordId - The record being updated\n * @param collectionDef - The collection definition from the schema\n * @param currentRecord - The current record data (must include the state field)\n * @param updateData - The partial update data\n * @returns The update data, potentially with the state field removed if invalid and mode is 'last-valid-state'\n */\nexport function validateUpdateStateMachine(\n\tcollectionName: string,\n\trecordId: string,\n\tcollectionDef: CollectionDefinition,\n\tcurrentRecord: Record<string, unknown>,\n\tupdateData: Record<string, unknown>,\n): Record<string, unknown> {\n\tlet result = updateData\n\tconst stateMachine = collectionDef.stateMachine\n\tif (stateMachine !== undefined) {\n\t\tresult = applyStateMachine(collectionName, recordId, stateMachine, currentRecord, result)\n\t}\n\t// Field-level `t.enum(...).transitions()` maps are state machines too, in the\n\t// default 'reject' mode (NEW-STORE-3). A collection-level machine on the same\n\t// field takes precedence.\n\tfor (const [field, descriptor] of Object.entries(collectionDef.fields)) {\n\t\tif (descriptor.kind !== 'enum' || !descriptor.transitions) continue\n\t\tif (stateMachine?.field === field) continue\n\t\tresult = applyStateMachine(\n\t\t\tcollectionName,\n\t\t\trecordId,\n\t\t\t{ field, transitions: descriptor.transitions, onInvalidTransition: 'reject' },\n\t\t\tcurrentRecord,\n\t\t\tresult,\n\t\t)\n\t}\n\treturn result\n}\n\nfunction applyStateMachine(\n\tcollectionName: string,\n\trecordId: string,\n\tstateMachine: StateMachineDefinition,\n\tcurrentRecord: Record<string, unknown>,\n\tupdateData: Record<string, unknown>,\n): Record<string, unknown> {\n\tconst stateField = stateMachine.field\n\tif (!(stateField in updateData)) {\n\t\t// State field not being changed -- no validation needed\n\t\treturn updateData\n\t}\n\n\tconst currentState = currentRecord[stateField]\n\tconst newState = updateData[stateField]\n\n\t// Both values must be strings for state machine validation\n\tif (typeof currentState !== 'string' || typeof newState !== 'string') {\n\t\treturn updateData\n\t}\n\n\tconst result = validateStateTransition(\n\t\tcollectionName,\n\t\trecordId,\n\t\tstateMachine,\n\t\tcurrentState,\n\t\tnewState,\n\t)\n\n\tif (result.valid) {\n\t\treturn updateData\n\t}\n\n\t// Mode is 'last-valid-state': silently remove the state field from the update\n\tconst filtered = { ...updateData }\n\tdelete filtered[stateField]\n\n\t// If no fields remain after removing the state field, the update becomes a no-op.\n\t// Return the empty object -- the caller can decide whether to proceed.\n\treturn filtered\n}\n","import type { Operation } from '@korajs/core'\nimport { KoraError } from '@korajs/core'\nimport type { LocalMutationContext } from './types'\nimport { CausalScope, type WriteScope, withWriteScope } from './write-context'\nimport type { WriteResult } from './write-ops'\n\n/**\n * Run one local write in its own storage transaction, then publish it.\n *\n * Post-commit work (causal heads, in-memory version vector, subscriptions,\n * `operation:created`) runs only after the transaction commits, so nothing\n * observable ever points at an operation that was rolled back (STORE-15).\n */\nexport async function runLocalWrite(\n\tctx: LocalMutationContext,\n\twrite: (scope: WriteScope) => Promise<WriteResult>,\n): Promise<WriteResult> {\n\tconst causal = new CausalScope(ctx.causalTracker, false)\n\tlet result: WriteResult | undefined\n\ttry {\n\t\tawait ctx.adapter.transaction(async (tx) => {\n\t\t\tresult = await withWriteScope(tx, ctx.nodeId, causal, write)\n\t\t})\n\t} catch (error) {\n\t\tctx.onStorageError?.(error)\n\t\tthrow error\n\t}\n\tif (!result) {\n\t\tthrow new KoraError('Local write transaction completed without a result', 'WRITE_INCOMPLETE', {\n\t\t\tcollection: ctx.collection,\n\t\t})\n\t}\n\tcausal.publish()\n\tpublishOperations(ctx, result)\n\treturn result\n}\n\n/** Notify the store about every operation of a committed write, in creation order. */\nfunction publishOperations(ctx: LocalMutationContext, result: WriteResult): void {\n\tconst ops: Operation[] = []\n\tif (result.operation) ops.push(result.operation)\n\tops.push(...result.sideEffects)\n\tfor (const op of ops) {\n\t\tctx.onMutation(op.collection, op)\n\t}\n}\n","import type { AtomicOp, CollectionDefinition, Operation } from '@korajs/core'\nimport {\n\tDEFAULT_MAX_OPERATION_BYTES,\n\tKoraError,\n\tOperationTooLargeError,\n\tassertResolvedFieldValue,\n\tcreateOperation,\n\tgenerateUUIDv7,\n\tisAtomicOp,\n\tmeasureOperationBytes,\n\tquoteIdent,\n\tresolveAtomicOp,\n\ttoAtomicOp,\n\tvalidateRecord,\n} from '@korajs/core'\nimport { RecordNotFoundError } from '../errors'\nimport { stampFieldVersions } from '../lww/field-versions'\nimport { serializeRowVersion } from '../lww/row-version'\nimport { buildInsertQuery, buildSoftDeleteQuery, buildUpdateQuery } from '../query/sql-builder'\nimport { encodeRichtextFieldsForOpData } from '../serialization/op-data-encoding'\nimport {\n\tdeserializeRecord,\n\tserializeOperation,\n\tserializeRecord,\n\ttoRichtextReadShape,\n} from '../serialization/serializer'\nimport { validateUpdateStateMachine } from '../state-machine/state-validator'\nimport type { CollectionRecord, RawCollectionRow } from '../types'\nimport { toAtRestWriteData } from './secret-write'\nimport type { WriteEnv, WriteScope } from './write-context'\n\n/*\n * THE local write path (W6). Every local operation — a single-record write, each\n * entry of an `app.transaction` / `app.mutation` commit, and every referential\n * cascade — is built and persisted by the functions in this file, inside the\n * storage transaction that commits it:\n *\n * 1. the current row is read through the transaction handle, so validation\n * (existence, state machine) and atomic ops resolve against committed state;\n * 2. the sequence number comes from the transaction's SequenceBlock (reserved\n * with UPSERT ... RETURNING on `_kora_version_vector` inside the transaction,\n * end persisted before commit); transactions are serialized, so the numbers of\n * one commit form one contiguous block and two commits can never share one;\n * 3. the operation is built only AFTER its sequence number is reserved, and is\n * never re-stamped (protocol v2 hashes the sequence number into the id);\n * 4. the row is written with per-field version stamps and the operation row is\n * appended, in the same transaction.\n */\n\n/** An insert whose values are validated and in their at-rest form. */\nexport interface PreparedInsert {\n\treadonly collection: string\n\treadonly recordId: string\n\t/** Record-shaped, at-rest values (secret fields hashed / encrypted). */\n\treadonly data: Record<string, unknown>\n}\n\n/**\n * Validate insert data, fill auto fields and transform secrets. Pure with\n * respect to storage, so `app.transaction` can do it when the developer calls\n * `insert` (returning the new id immediately) and write it at commit.\n */\nexport async function prepareInsert(\n\tenv: WriteEnv,\n\tcollection: string,\n\tdata: Record<string, unknown>,\n): Promise<PreparedInsert> {\n\tconst definition = requireDefinition(env, collection)\n\tconst validated = validateRecord(collection, definition, data, 'insert')\n\tconst now = Date.now()\n\tfor (const [fieldName, descriptor] of Object.entries(definition.fields)) {\n\t\tif (descriptor.auto && descriptor.kind === 'timestamp') {\n\t\t\tvalidated[fieldName] = now\n\t\t}\n\t}\n\t// Secret fields reach their at-rest form BEFORE the operation is built, so\n\t// plaintext never enters the op log, the row, or the wire (STORE-4).\n\tconst writeData = await toAtRestWriteData(validated, definition, env.secretKeyProvider)\n\treturn { collection, recordId: generateUUIDv7(), data: writeData }\n}\n\n/** Result of writing one local operation. */\nexport interface WriteResult {\n\t/** The operation, or null when the write turned out to be a no-op. */\n\treadonly operation: Operation | null\n\t/** The record as stored after the write. */\n\treadonly record: CollectionRecord\n\t/** Operations created as side effects (referential cascades), in creation order. */\n\treadonly sideEffects: Operation[]\n}\n\n/**\n * Write a prepared insert inside `scope.tx`.\n */\nexport async function writeInsertInTx(\n\tenv: WriteEnv,\n\tscope: WriteScope,\n\tinsert: PreparedInsert,\n\textraCausalDeps: readonly string[] = [],\n): Promise<WriteResult> {\n\tconst definition = requireDefinition(env, insert.collection)\n\tconst operation = await buildLocalOperation(env, scope, {\n\t\ttype: 'insert',\n\t\tcollection: insert.collection,\n\t\trecordId: insert.recordId,\n\t\t// Binary richtext values are tagged as canonical JSON BEFORE hashing, so the\n\t\t// hash input, persisted JSON and wire payload are the identical value.\n\t\tdata: encodeRichtextFieldsForOpData(insert.data, definition.fields),\n\t\tpreviousData: null,\n\t\textraCausalDeps,\n\t})\n\n\tif (env.fold) {\n\t\t// Append, then merge: the row is the materialization of the record's state.\n\t\tawait appendOperationRow(scope, operation)\n\t\tawait env.fold.applyInTx(scope.tx, operation, 'local')\n\t} else {\n\t\tconst serialized = serializeRecord(insert.data, definition.fields)\n\t\tconst version = serializeRowVersion(operation.timestamp)\n\t\tconst row: Record<string, unknown> = {\n\t\t\tid: insert.recordId,\n\t\t\t...serialized,\n\t\t\t_created_at: operation.timestamp.wallTime,\n\t\t\t_updated_at: operation.timestamp.wallTime,\n\t\t\t_version: version,\n\t\t\t// Every inserted field is stamped with this operation's version, so later\n\t\t\t// per-field LWW compares against a real writer, not the row fallback.\n\t\t\t_field_versions: stampFieldVersions(null, Object.keys(serialized), version),\n\t\t}\n\t\tconst rowInsert = buildInsertQuery(insert.collection, row)\n\t\tawait scope.tx.execute(rowInsert.sql, rowInsert.params)\n\t\tawait appendOperationRow(scope, operation)\n\t}\n\n\treturn {\n\t\toperation,\n\t\trecord: {\n\t\t\tid: insert.recordId,\n\t\t\t// A richtext value written as a string returns as the bytes reads return.\n\t\t\t...toRichtextReadShape(insert.data, definition.fields),\n\t\t\tcreatedAt: operation.timestamp.wallTime,\n\t\t\tupdatedAt: operation.timestamp.wallTime,\n\t\t},\n\t\tsideEffects: [],\n\t}\n}\n\n/**\n * Write a local update inside `scope.tx`.\n *\n * `data` must already be schema-validated (it may contain atomic op descriptors).\n * The state machine, atomic ops and `previousData` are all resolved against the\n * row read through the transaction, so concurrent writers serialize correctly\n * (STORE-9, NEW-STORE-2).\n */\nexport async function writeUpdateInTx(\n\tenv: WriteEnv,\n\tscope: WriteScope,\n\tcollection: string,\n\tid: string,\n\tdata: Record<string, unknown>,\n\textraCausalDeps: readonly string[] = [],\n): Promise<WriteResult> {\n\tconst definition = requireDefinition(env, collection)\n\tconst currentRow = await readLiveRow(scope, collection, id)\n\tif (!currentRow) {\n\t\tthrow new RecordNotFoundError(collection, id)\n\t}\n\tconst currentRecord = deserializeRecord(currentRow, definition.fields)\n\tconst allowed = validateUpdateStateMachine(collection, id, definition, currentRecord, data)\n\tif (Object.keys(allowed).length === 0) {\n\t\treturn { operation: null, record: currentRecord, sideEffects: [] }\n\t}\n\n\tconst previousData: Record<string, unknown> = {}\n\tconst resolved: Record<string, unknown> = {}\n\tconst atomicOps: Record<string, AtomicOp> = {}\n\tfor (const [key, value] of Object.entries(allowed)) {\n\t\tpreviousData[key] = currentRecord[key]\n\t\tif (isAtomicOp(value)) {\n\t\t\tresolved[key] = resolveAtomicOp(currentRecord[key], value)\n\t\t\t// The resolved value must be in the field's domain like any written value\n\t\t\t// (RT-87): `op.increment(0.5)` on a timestamp, an increment past the largest\n\t\t\t// finite number.\n\t\t\tconst descriptor = definition.fields[key]\n\t\t\tif (descriptor) assertResolvedFieldValue(collection, key, descriptor, resolved[key])\n\t\t\tatomicOps[key] = toAtomicOp(value)\n\t\t} else {\n\t\t\tresolved[key] = value\n\t\t}\n\t}\n\t// previousData already holds the stored (at-rest) values, so only the new\n\t// values need the secret transform.\n\tconst writeData = await toAtRestWriteData(resolved, definition, env.secretKeyProvider)\n\n\tconst operation = await buildLocalOperation(env, scope, {\n\t\ttype: 'update',\n\t\tcollection,\n\t\trecordId: id,\n\t\tdata: encodeRichtextFieldsForOpData(writeData, definition.fields),\n\t\tpreviousData: encodeRichtextFieldsForOpData(previousData, definition.fields),\n\t\textraCausalDeps,\n\t\t...(Object.keys(atomicOps).length > 0 ? { atomicOps } : {}),\n\t})\n\n\tif (env.fold) {\n\t\tawait appendOperationRow(scope, operation)\n\t\tawait env.fold.applyInTx(scope.tx, operation, 'local')\n\t} else {\n\t\tconst serializedChanges = serializeRecord(writeData, definition.fields)\n\t\tconst version = serializeRowVersion(operation.timestamp)\n\t\t// A local edit is the newest writer of every field it touches (the HLC is past\n\t\t// every timestamp this device has seen), so each changed field gets its stamp.\n\t\tconst rowUpdate = buildUpdateQuery(collection, id, {\n\t\t\t...serializedChanges,\n\t\t\t_updated_at: operation.timestamp.wallTime,\n\t\t\t_version: version,\n\t\t\t_field_versions: stampFieldVersions(\n\t\t\t\tcurrentRow._field_versions,\n\t\t\t\tObject.keys(serializedChanges),\n\t\t\t\tversion,\n\t\t\t),\n\t\t})\n\t\tawait scope.tx.execute(rowUpdate.sql, rowUpdate.params)\n\t\tawait appendOperationRow(scope, operation)\n\t}\n\n\tconst updatedRow = await readLiveRow(scope, collection, id)\n\tif (!updatedRow) {\n\t\tthrow new RecordNotFoundError(collection, id)\n\t}\n\treturn {\n\t\toperation,\n\t\trecord: deserializeRecord(updatedRow, definition.fields),\n\t\tsideEffects: [],\n\t}\n}\n\n/** Options for {@link writeDeleteInTx}. */\nexport interface WriteDeleteOptions {\n\t/** Skip referential checks and cascades (the caller already applied them). */\n\treadonly skipReferentialEnforcement?: boolean\n\t/** Explicit causal parents (e.g. the delete that caused this cascade). */\n\treadonly extraCausalDeps?: readonly string[]\n}\n\n/**\n * Soft-delete a record inside `scope.tx`, then apply the schema's referential\n * policies (cascade, set-null, restrict) in the same transaction. Cascaded\n * operations are built through this same write path, after the parent, so their\n * sequence numbers follow the parent's in the commit's block (STORE-2).\n */\nexport async function writeDeleteInTx(\n\tenv: WriteEnv,\n\tscope: WriteScope,\n\tcollection: string,\n\tid: string,\n\toptions: WriteDeleteOptions = {},\n): Promise<WriteResult> {\n\tconst definition = requireDefinition(env, collection)\n\tconst currentRow = await readLiveRow(scope, collection, id)\n\tif (!currentRow) {\n\t\tthrow new RecordNotFoundError(collection, id)\n\t}\n\tconst operation = await buildLocalOperation(env, scope, {\n\t\ttype: 'delete',\n\t\tcollection,\n\t\trecordId: id,\n\t\tdata: null,\n\t\tpreviousData: null,\n\t\textraCausalDeps: options.extraCausalDeps ?? [],\n\t})\n\n\tif (!options.skipReferentialEnforcement && env.beforeLocalDelete) {\n\t\tawait env.beforeLocalDelete(operation, scope.tx)\n\t}\n\n\t// The row is tombstoned BEFORE side effects run, so a reference cycle\n\t// (self-referencing or mutually-referencing records) cannot cascade back\n\t// into this record.\n\tif (env.fold) {\n\t\tawait appendOperationRow(scope, operation)\n\t\tawait env.fold.applyInTx(scope.tx, operation, 'local')\n\t} else {\n\t\tconst version = serializeRowVersion(operation.timestamp)\n\t\tconst softDelete = buildSoftDeleteQuery(collection, id, operation.timestamp.wallTime, version)\n\t\tawait scope.tx.execute(softDelete.sql, softDelete.params)\n\t\tawait appendOperationRow(scope, operation)\n\t}\n\n\tconst sideEffects: Operation[] = []\n\tif (!options.skipReferentialEnforcement && env.relationEnforcer) {\n\t\tconst result = await env.relationEnforcer.enforceDelete(\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tscope,\n\t\t\tenv,\n\t\t\toperation.id,\n\t\t)\n\t\tsideEffects.push(...result.operations)\n\t}\n\n\treturn {\n\t\toperation,\n\t\trecord: deserializeRecord(currentRow, definition.fields),\n\t\tsideEffects,\n\t}\n}\n\ninterface LocalOperationInput {\n\ttype: Operation['type']\n\tcollection: string\n\trecordId: string\n\tdata: Record<string, unknown> | null\n\tpreviousData: Record<string, unknown> | null\n\textraCausalDeps: readonly string[]\n\tatomicOps?: Record<string, AtomicOp>\n}\n\n/**\n * Reserve the next sequence number through the transaction, THEN build the\n * operation. The order matters: the operation is final the moment it exists.\n */\nasync function buildLocalOperation(\n\tenv: WriteEnv,\n\tscope: WriteScope,\n\tinput: LocalOperationInput,\n): Promise<Operation> {\n\tconst sequenceNumber = await scope.sequence.take()\n\tconst causalDeps = scope.causal.depsFor(input.collection, input.extraCausalDeps)\n\tconst operation = await createOperation(\n\t\t{\n\t\t\tnodeId: env.nodeId,\n\t\t\ttype: input.type,\n\t\t\tcollection: input.collection,\n\t\t\trecordId: input.recordId,\n\t\t\tdata: input.data,\n\t\t\tpreviousData: input.previousData,\n\t\t\tsequenceNumber,\n\t\t\tcausalDeps,\n\t\t\tschemaVersion: env.schema.version,\n\t\t\t...(input.atomicOps !== undefined ? { atomicOps: input.atomicOps } : {}),\n\t\t\t...(env.transactionId !== undefined ? { transactionId: env.transactionId } : {}),\n\t\t\t...(env.mutationName !== undefined ? { mutationName: env.mutationName } : {}),\n\t\t},\n\t\tenv.clock,\n\t)\n\t// Every operation must fit the server's maxOperationBytes (RT-86): refused here, in\n\t// the write's transaction (nothing is written), rather than accepted and then refused\n\t// by the server.\n\tconst maxBytes = env.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES\n\tconst bytes = measureOperationBytes(operation)\n\tif (bytes > maxBytes) {\n\t\tthrow new OperationTooLargeError(input.collection, input.recordId, bytes, maxBytes)\n\t}\n\tscope.causal.record(input.collection, operation.id)\n\treturn operation\n}\n\nasync function appendOperationRow(scope: WriteScope, operation: Operation): Promise<void> {\n\tconst opInsert = buildInsertQuery(\n\t\t`_kora_ops_${operation.collection}`,\n\t\tserializeOperation(operation) as unknown as Record<string, unknown>,\n\t)\n\tawait scope.tx.execute(opInsert.sql, opInsert.params)\n}\n\nasync function readLiveRow(\n\tscope: WriteScope,\n\tcollection: string,\n\tid: string,\n): Promise<RawCollectionRow | undefined> {\n\tconst rows = await scope.tx.query<RawCollectionRow>(\n\t\t`SELECT * FROM ${quoteIdent(collection)} WHERE id = ? AND _deleted = 0`,\n\t\t[id],\n\t)\n\treturn rows[0]\n}\n\nfunction requireDefinition(env: WriteEnv, collection: string): CollectionDefinition {\n\tconst definition = env.schema.collections[collection]\n\tif (!definition) {\n\t\tthrow new KoraError(\n\t\t\t`Unknown collection \"${collection}\". Available: ${Object.keys(env.schema.collections).join(', ')}`,\n\t\t\t'UNKNOWN_COLLECTION',\n\t\t\t{ collection },\n\t\t)\n\t}\n\treturn definition\n}\n","import type { CollectionDefinition, SecretKeyProvider } from '@korajs/core'\nimport { transformSecretFieldsForWrite } from '@korajs/core'\n\n/**\n * Convert the values a local write is about to persist into their at-rest form.\n *\n * Every local write path (single-record insert/update and the buffered\n * `TransactionContext` used by `app.transaction` / `app.mutation`) MUST route\n * its data through this helper BEFORE the operation is built, so plaintext\n * secrets never reach the operation log, the materialized row, or the wire.\n * Keeping one helper means a new write path cannot forget the transform\n * without also bypassing this function (STORE-4).\n *\n * @param data - Validated (and atomic-op-resolved) field values for the write\n * @param definition - The collection definition, used to find secret fields\n * @param secretKeyProvider - Key provider for `t.secret()` encrypted fields\n * @returns A copy of `data` with secret fields hashed or encrypted\n */\nexport function toAtRestWriteData(\n\tdata: Record<string, unknown>,\n\tdefinition: CollectionDefinition,\n\tsecretKeyProvider: SecretKeyProvider | undefined,\n): Promise<Record<string, unknown>> {\n\treturn transformSecretFieldsForWrite(data, definition, secretKeyProvider)\n}\n","import type { Operation } from '@korajs/core'\nimport { runLocalWrite } from './run-local-write'\nimport type { LocalMutationContext } from './types'\nimport { writeDeleteInTx } from './write-ops'\n\nexport interface ExecuteDeleteOptions {\n\t/** When true, skip referential checks and cascades (caller already enforced them). */\n\tskipReferentialEnforcement?: boolean\n}\n\n/**\n * Soft-delete a record and persist the operation log entry atomically, together\n * with any referential side effects (cascades / set-null), in one transaction.\n * Returns the primary delete operation followed by the side-effect operations.\n */\nexport async function executeDelete(\n\tctx: LocalMutationContext,\n\tid: string,\n\toptions?: ExecuteDeleteOptions,\n): Promise<Operation[]> {\n\tconst result = await runLocalWrite(ctx, (scope) =>\n\t\twriteDeleteInTx(ctx, scope, ctx.collection, id, {\n\t\t\tskipReferentialEnforcement: options?.skipReferentialEnforcement ?? false,\n\t\t\textraCausalDeps: ctx.extraCausalDeps ?? [],\n\t\t}),\n\t)\n\treturn result.operation ? [result.operation, ...result.sideEffects] : result.sideEffects\n}\n","import type { CollectionRecord } from '../types'\nimport { runLocalWrite } from './run-local-write'\nimport type { LocalMutationContext } from './types'\nimport { prepareInsert, writeInsertInTx } from './write-ops'\n\n/**\n * Insert a record and persist its operation, atomically, through the single\n * local write path (sequence reserved and operation built inside the write\n * transaction).\n */\nexport async function executeInsert(\n\tctx: LocalMutationContext,\n\tdata: Record<string, unknown>,\n): Promise<CollectionRecord> {\n\tconst prepared = await prepareInsert(ctx, ctx.collection, data)\n\tconst result = await runLocalWrite(ctx, (scope) =>\n\t\twriteInsertInTx(ctx, scope, prepared, ctx.extraCausalDeps ?? []),\n\t)\n\treturn result.record\n}\n","import { validateRecord } from '@korajs/core'\nimport type { CollectionRecord } from '../types'\nimport { runLocalWrite } from './run-local-write'\nimport type { LocalMutationContext } from './types'\nimport { writeUpdateInTx } from './write-ops'\n\n/**\n * Update a record and persist its operation, atomically. The row read, the\n * state-machine check and atomic-op resolution all happen inside the write\n * transaction, so concurrent increments compose (STORE-9).\n */\nexport async function executeUpdate(\n\tctx: LocalMutationContext,\n\tid: string,\n\tdata: Record<string, unknown>,\n): Promise<CollectionRecord> {\n\tconst validated = validateRecord(ctx.collection, ctx.definition, data, 'update')\n\tconst result = await runLocalWrite(ctx, (scope) =>\n\t\twriteUpdateInTx(ctx, scope, ctx.collection, id, validated, ctx.extraCausalDeps ?? []),\n\t)\n\treturn result.record\n}\n"],"mappings":"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","names":["KoraError","KoraError","KoraError","KoraError","KoraError","validateRecord","validateRecord"]}
@@ -91,7 +91,7 @@ function richtextStatesEqual(a, b) {
91
91
  }
92
92
 
93
93
  // src/serialization/serializer.ts
94
- import { HybridLogicalClock } from "@korajs/core";
94
+ import { HybridLogicalClock, decodeStoredText, encodeStoredText } from "@korajs/core";
95
95
  function serializeRecord(data, fields) {
96
96
  const result = {};
97
97
  for (const [key, value] of Object.entries(data)) {
@@ -123,8 +123,10 @@ function deserializeRecord(row, fields) {
123
123
  var ATOMIC_OPS_KEY = "__kora_atomic_ops__";
124
124
  var TX_ID_KEY = "__kora_tx_id__";
125
125
  var MUTATION_NAME_KEY = "__kora_mutation__";
126
+ var FIELD_VERSIONS_KEY = "__kora_field_versions__";
127
+ var HASH_VERSION_KEY = "__kora_hash_version__";
126
128
  function serializeOperation(op) {
127
- const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
129
+ const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0 || op.hashVersion !== void 0;
128
130
  let dataPayload = null;
129
131
  if (op.data) {
130
132
  dataPayload = { ...op.data };
@@ -137,6 +139,12 @@ function serializeOperation(op) {
137
139
  if (op.mutationName !== void 0) {
138
140
  dataPayload[MUTATION_NAME_KEY] = op.mutationName;
139
141
  }
142
+ if (op.fieldVersions !== void 0) {
143
+ dataPayload[FIELD_VERSIONS_KEY] = op.fieldVersions;
144
+ }
145
+ if (op.hashVersion !== void 0) {
146
+ dataPayload[HASH_VERSION_KEY] = op.hashVersion;
147
+ }
140
148
  } else if (hasMetadata) {
141
149
  dataPayload = {};
142
150
  if (op.transactionId !== void 0) {
@@ -145,6 +153,9 @@ function serializeOperation(op) {
145
153
  if (op.mutationName !== void 0) {
146
154
  dataPayload[MUTATION_NAME_KEY] = op.mutationName;
147
155
  }
156
+ if (op.hashVersion !== void 0) {
157
+ dataPayload[HASH_VERSION_KEY] = op.hashVersion;
158
+ }
148
159
  }
149
160
  return {
150
161
  id: op.id,
@@ -164,6 +175,8 @@ function deserializeOperation(row) {
164
175
  let atomicOps;
165
176
  let transactionId;
166
177
  let mutationName;
178
+ let fieldVersions;
179
+ let hashVersion;
167
180
  if (row.data) {
168
181
  const parsed = JSON.parse(row.data);
169
182
  if (ATOMIC_OPS_KEY in parsed) {
@@ -175,8 +188,21 @@ function deserializeOperation(row) {
175
188
  if (MUTATION_NAME_KEY in parsed) {
176
189
  mutationName = parsed[MUTATION_NAME_KEY];
177
190
  }
178
- const { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed;
179
- data = Object.keys(rest).length > 0 ? rest : null;
191
+ if (FIELD_VERSIONS_KEY in parsed) {
192
+ fieldVersions = parsed[FIELD_VERSIONS_KEY];
193
+ }
194
+ if (HASH_VERSION_KEY in parsed) {
195
+ hashVersion = parsed[HASH_VERSION_KEY];
196
+ }
197
+ const {
198
+ [ATOMIC_OPS_KEY]: _a,
199
+ [TX_ID_KEY]: _t,
200
+ [MUTATION_NAME_KEY]: _m,
201
+ [FIELD_VERSIONS_KEY]: _f,
202
+ [HASH_VERSION_KEY]: _h,
203
+ ...rest
204
+ } = parsed;
205
+ data = row.type !== "delete" || Object.keys(rest).length > 0 ? rest : null;
180
206
  }
181
207
  return {
182
208
  id: row.id,
@@ -193,13 +219,26 @@ function deserializeOperation(row) {
193
219
  schemaVersion: row.schema_version,
194
220
  ...atomicOps !== void 0 ? { atomicOps } : {},
195
221
  ...transactionId !== void 0 ? { transactionId } : {},
196
- ...mutationName !== void 0 ? { mutationName } : {}
222
+ ...mutationName !== void 0 ? { mutationName } : {},
223
+ ...fieldVersions !== void 0 ? { fieldVersions } : {},
224
+ ...hashVersion !== void 0 ? { hashVersion } : {}
197
225
  };
198
226
  }
199
227
  function deserializeOperationWithCollection(row, collection) {
200
228
  const op = deserializeOperation(row);
201
229
  return { ...op, collection };
202
230
  }
231
+ function toRichtextReadShape(data, fields) {
232
+ let out = data;
233
+ for (const [name, descriptor] of Object.entries(fields)) {
234
+ if (descriptor.kind !== "richtext" || !(name in data)) continue;
235
+ const value = data[name];
236
+ if (value === null || value === void 0 || value instanceof Uint8Array) continue;
237
+ if (out === data) out = { ...data };
238
+ out[name] = encodeRichtext(value);
239
+ }
240
+ return out;
241
+ }
203
242
  function serializeValue(value, descriptor) {
204
243
  if (value === null || value === void 0) {
205
244
  return null;
@@ -215,9 +254,16 @@ function serializeValue(value, descriptor) {
215
254
  case "richtext":
216
255
  return encodeRichtext(value);
217
256
  default:
218
- return value;
257
+ return typeof value === "string" && isRawTextKind(descriptor.kind) ? encodeStoredText(value) : value;
219
258
  }
220
259
  }
260
+ var RAW_TEXT_KINDS = /* @__PURE__ */ new Set(["string", "secret"]);
261
+ function isRawTextKind(kind) {
262
+ return RAW_TEXT_KINDS.has(kind);
263
+ }
264
+ function encodeStoredFilterValue(value, descriptor) {
265
+ return typeof value === "string" && descriptor !== void 0 && isRawTextKind(descriptor.kind) ? encodeStoredText(value) : value;
266
+ }
221
267
  function deserializeValue(value, descriptor) {
222
268
  switch (descriptor.kind) {
223
269
  case "boolean":
@@ -237,7 +283,7 @@ function deserializeValue(value, descriptor) {
237
283
  case "richtext":
238
284
  return decodeRichtext(value);
239
285
  default:
240
- return value;
286
+ return typeof value === "string" && isRawTextKind(descriptor.kind) ? decodeStoredText(value) : value;
241
287
  }
242
288
  }
243
289
 
@@ -252,6 +298,9 @@ export {
252
298
  deserializeRecord,
253
299
  serializeOperation,
254
300
  deserializeOperation,
255
- deserializeOperationWithCollection
301
+ deserializeOperationWithCollection,
302
+ toRichtextReadShape,
303
+ isRawTextKind,
304
+ encodeStoredFilterValue
256
305
  };
257
- //# sourceMappingURL=chunk-GSJ7EEUC.js.map
306
+ //# sourceMappingURL=chunk-J5AGXF3D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serialization/richtext-serializer.ts","../src/serialization/op-data-encoding.ts","../src/serialization/serializer.ts"],"sourcesContent":["import * as Y from 'yjs'\nimport {\n\ttype KoraBytesValue,\n\tdecodeRichtextOpDataValue,\n\tisKoraBytesValue,\n\tisLegacyNumericByteObject,\n} from './op-data-encoding'\n\nconst TEXT_KEY = 'content'\n\nexport type RichtextInput = string | Uint8Array | ArrayBuffer | null | undefined\n\n/**\n * Encodes richtext values into Yjs document updates.\n *\n * Also accepts the tagged `{ $koraBytes }` form that binary richtext values\n * take inside `op.data` (and, for old dev databases, the pre-fix numeric-key\n * object shape), so every consumer that writes op-data values to columns\n * inherits the decoding. Plain strings keep their exact historical behavior:\n * encoded as a fresh Yjs document containing the string.\n */\nexport function encodeRichtext(value: RichtextInput | KoraBytesValue): Uint8Array | null {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst doc = new Y.Doc()\n\t\t// Deterministic clientId so the same string encodes to the same bytes on every\n\t\t// device (a plain string is a replacement, not a collaborative actor). A random\n\t\t// clientId would make a string-derived richtext column diverge byte-wise across\n\t\t// replicas even when the text is identical.\n\t\tdoc.clientID = 0\n\t\tdoc.getText(TEXT_KEY).insert(0, value)\n\t\treturn Y.encodeStateAsUpdate(doc)\n\t}\n\n\tif (value instanceof Uint8Array) {\n\t\treturn value\n\t}\n\n\tif (value instanceof ArrayBuffer) {\n\t\treturn new Uint8Array(value)\n\t}\n\n\tif (isKoraBytesValue(value) || isLegacyNumericByteObject(value)) {\n\t\tconst decoded = decodeRichtextOpDataValue(value)\n\t\t// Both accepted shapes decode to bytes; strings were handled above.\n\t\treturn decoded instanceof Uint8Array ? decoded : null\n\t}\n\n\tthrow new Error('Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.')\n}\n\n/**\n * Decodes driver-provided richtext values into Uint8Array.\n */\nexport function decodeRichtext(value: unknown): Uint8Array | null {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\tif (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {\n\t\treturn new Uint8Array(value)\n\t}\n\n\tif (value instanceof Uint8Array) {\n\t\treturn value\n\t}\n\n\tif (value instanceof ArrayBuffer) {\n\t\treturn new Uint8Array(value)\n\t}\n\n\tthrow new Error(\n\t\t'Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined.',\n\t)\n}\n\n/**\n * Reads plain text from a richtext Yjs state update.\n */\nexport function richtextToPlainText(value: RichtextInput): string {\n\tconst encoded = encodeRichtext(value)\n\tif (!encoded) return ''\n\n\tconst doc = new Y.Doc()\n\tY.applyUpdate(doc, encoded)\n\treturn doc.getText(TEXT_KEY).toString()\n}\n\n/**\n * Compares two richtext values by decoded plain text.\n * Yjs update bytes are not stable across separate encodes of the same string.\n */\nexport function richtextStatesEqual(a: unknown, b: unknown): boolean {\n\ttry {\n\t\treturn richtextToPlainText(a as RichtextInput) === richtextToPlainText(b as RichtextInput)\n\t} catch {\n\t\treturn false\n\t}\n}\n","import {\n\ttype KoraBytesValue,\n\tbase64ToBytes,\n\tbytesToBase64,\n\tdecodeBytesFromOpData,\n\tencodeBytesForOpData,\n\tisKoraBytesValue,\n\tisLegacyNumericByteObject,\n} from '@korajs/core'\nimport type { FieldDescriptor } from '@korajs/core'\n\n/**\n * The canonical binary op-data encoding lives in `@korajs/core` because\n * `op.data` is a core concept shared by store (creation, persistence) and merge\n * (CRDT merge). These re-exports keep existing store-internal import paths\n * working while the single source of truth is the core module.\n */\nexport type { KoraBytesValue }\nexport { base64ToBytes, bytesToBase64, isKoraBytesValue, isLegacyNumericByteObject }\n\n/** @deprecated Use {@link encodeRichtextForOpData}; kept for existing imports. */\nexport const encodeRichtextForOpData = encodeBytesForOpData\n/** @deprecated Use {@link decodeRichtextOpDataValue}; kept for existing imports. */\nexport const decodeRichtextOpDataValue = decodeBytesFromOpData\n\n/**\n * Apply {@link encodeBytesForOpData} to every richtext-typed field in an\n * operation's data payload. Called at operation creation, BEFORE the payload\n * is content-hashed, so the hash input, persisted JSON, wire payload, and the\n * value the merge engine sees are the identical canonical value. Non-richtext\n * fields are untouched.\n */\nexport function encodeRichtextFieldsForOpData(\n\tdata: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = { ...data }\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tif (fields[key]?.kind !== 'richtext') {\n\t\t\tcontinue\n\t\t}\n\t\tif (typeof value === 'string' || value instanceof Uint8Array || value instanceof ArrayBuffer) {\n\t\t\tresult[key] = encodeBytesForOpData(value)\n\t\t}\n\t}\n\treturn result\n}\n\n/**\n * Reverse of {@link encodeRichtextFieldsForOpData}: decode tagged (or legacy\n * numeric-key) richtext values in an op-data payload back to record-shaped\n * values (string or Uint8Array). Null/undefined and plain strings pass\n * through, preserving pre-fix behavior exactly.\n */\nexport function decodeRichtextFieldsFromOpData(\n\tdata: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = { ...data }\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tif (fields[key]?.kind !== 'richtext' || value === null || value === undefined) {\n\t\t\tcontinue\n\t\t}\n\t\tresult[key] = decodeBytesFromOpData(value)\n\t}\n\treturn result\n}\n","import { HybridLogicalClock, decodeStoredText, encodeStoredText } from '@korajs/core'\nimport type { CollectionDefinition, FieldDescriptor, Operation } from '@korajs/core'\nimport type { CollectionRecord, OperationRow, RawCollectionRow } from '../types'\nimport { decodeRichtext, encodeRichtext } from './richtext-serializer'\n\n/**\n * Serialize a JS record to SQL-compatible values for INSERT/UPDATE.\n * Transforms: boolean → 0/1, array → JSON string, richtext → Yjs binary update.\n *\n * @param data - The record data with JS-native types\n * @param fields - The field descriptors from the schema\n * @returns An object with SQL-compatible values\n */\nexport function serializeRecord(\n\tdata: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {}\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tconst descriptor = fields[key]\n\t\tif (!descriptor) {\n\t\t\tresult[key] = value\n\t\t\tcontinue\n\t\t}\n\t\tresult[key] = serializeValue(value, descriptor)\n\t}\n\treturn result\n}\n\n/**\n * Deserialize a SQL row to JS-native types for the application layer.\n * Transforms: 0/1 → boolean, JSON string → array, strips _deleted, maps _created_at/_updated_at.\n *\n * @param row - The raw SQL row\n * @param fields - The field descriptors from the schema\n * @returns A CollectionRecord with JS-native types\n */\nexport function deserializeRecord(\n\trow: RawCollectionRow,\n\tfields: Record<string, FieldDescriptor>,\n): CollectionRecord {\n\tconst result: CollectionRecord = {\n\t\tid: row.id,\n\t\tcreatedAt: row._created_at,\n\t\tupdatedAt: row._updated_at,\n\t}\n\n\tfor (const [key, descriptor] of Object.entries(fields)) {\n\t\tconst rawValue = row[key]\n\t\tif (rawValue === undefined || rawValue === null) {\n\t\t\tresult[key] = rawValue ?? null\n\t\t\tcontinue\n\t\t}\n\t\tresult[key] = deserializeValue(rawValue, descriptor)\n\t}\n\n\treturn result\n}\n\n/**\n * Internal key used to embed atomicOps metadata in the data JSON column.\n * This avoids adding a new column to the ops table (no migration needed).\n */\nconst ATOMIC_OPS_KEY = '__kora_atomic_ops__'\n\n/**\n * Internal key used to embed transactionId in the data JSON column.\n */\nconst TX_ID_KEY = '__kora_tx_id__'\n\n/**\n * Internal key used to embed mutationName in the data JSON column.\n */\nconst MUTATION_NAME_KEY = '__kora_mutation__'\n\n/**\n * Internal key used to embed a scope-entry insert's per-field versions (RT-27) in the\n * data JSON column, so a later fold of the record's log places each field correctly.\n */\nconst FIELD_VERSIONS_KEY = '__kora_field_versions__'\n\n/**\n * Internal key used to embed the operation's content-hash version (CORE-1, protocol\n * v2) in the data JSON column. Absent means version 1. Kept in the row so id\n * verification and re-hashing (rebase, rotation, resequence) use the right version.\n */\nconst HASH_VERSION_KEY = '__kora_hash_version__'\n\n/**\n * Serialize an Operation to a row for the operations log table.\n *\n * @param op - The operation to serialize\n * @returns An OperationRow suitable for SQL INSERT\n */\nexport function serializeOperation(op: Operation): OperationRow {\n\tconst hasMetadata =\n\t\top.transactionId !== undefined || op.mutationName !== undefined || op.hashVersion !== undefined\n\tlet dataPayload: Record<string, unknown> | null = null\n\tif (op.data) {\n\t\t// Embed metadata in the data JSON when present\n\t\tdataPayload = { ...op.data }\n\t\tif (op.atomicOps !== undefined && Object.keys(op.atomicOps).length > 0) {\n\t\t\tdataPayload[ATOMIC_OPS_KEY] = op.atomicOps\n\t\t}\n\t\tif (op.transactionId !== undefined) {\n\t\t\tdataPayload[TX_ID_KEY] = op.transactionId\n\t\t}\n\t\tif (op.mutationName !== undefined) {\n\t\t\tdataPayload[MUTATION_NAME_KEY] = op.mutationName\n\t\t}\n\t\tif (op.fieldVersions !== undefined) {\n\t\t\tdataPayload[FIELD_VERSIONS_KEY] = op.fieldVersions\n\t\t}\n\t\tif (op.hashVersion !== undefined) {\n\t\t\tdataPayload[HASH_VERSION_KEY] = op.hashVersion\n\t\t}\n\t} else if (hasMetadata) {\n\t\t// For delete operations (data is null), we still need to store metadata\n\t\tdataPayload = {}\n\t\tif (op.transactionId !== undefined) {\n\t\t\tdataPayload[TX_ID_KEY] = op.transactionId\n\t\t}\n\t\tif (op.mutationName !== undefined) {\n\t\t\tdataPayload[MUTATION_NAME_KEY] = op.mutationName\n\t\t}\n\t\tif (op.hashVersion !== undefined) {\n\t\t\tdataPayload[HASH_VERSION_KEY] = op.hashVersion\n\t\t}\n\t}\n\n\treturn {\n\t\tid: op.id,\n\t\tnode_id: op.nodeId,\n\t\ttype: op.type,\n\t\trecord_id: op.recordId,\n\t\tdata: dataPayload ? JSON.stringify(dataPayload) : null,\n\t\tprevious_data: op.previousData ? JSON.stringify(op.previousData) : null,\n\t\ttimestamp: HybridLogicalClock.serialize(op.timestamp),\n\t\tsequence_number: op.sequenceNumber,\n\t\tcausal_deps: JSON.stringify(op.causalDeps),\n\t\tschema_version: op.schemaVersion,\n\t}\n}\n\n/**\n * Deserialize a row from the operations log table back to an Operation.\n *\n * @param row - The raw operation row from SQL\n * @returns The deserialized Operation object\n */\nexport function deserializeOperation(row: OperationRow): Operation {\n\tlet data: Record<string, unknown> | null = null\n\tlet atomicOps: Record<string, unknown> | undefined\n\tlet transactionId: string | undefined\n\tlet mutationName: string | undefined\n\tlet fieldVersions: Operation['fieldVersions']\n\tlet hashVersion: Operation['hashVersion']\n\n\tif (row.data) {\n\t\tconst parsed = JSON.parse(row.data) as Record<string, unknown>\n\t\t// Extract embedded metadata keys\n\t\tif (ATOMIC_OPS_KEY in parsed) {\n\t\t\tatomicOps = parsed[ATOMIC_OPS_KEY] as Record<string, unknown>\n\t\t}\n\t\tif (TX_ID_KEY in parsed) {\n\t\t\ttransactionId = parsed[TX_ID_KEY] as string\n\t\t}\n\t\tif (MUTATION_NAME_KEY in parsed) {\n\t\t\tmutationName = parsed[MUTATION_NAME_KEY] as string\n\t\t}\n\t\tif (FIELD_VERSIONS_KEY in parsed) {\n\t\t\tfieldVersions = parsed[FIELD_VERSIONS_KEY] as Operation['fieldVersions']\n\t\t}\n\t\tif (HASH_VERSION_KEY in parsed) {\n\t\t\thashVersion = parsed[HASH_VERSION_KEY] as Operation['hashVersion']\n\t\t}\n\t\t// Remove metadata keys from data\n\t\tconst {\n\t\t\t[ATOMIC_OPS_KEY]: _a,\n\t\t\t[TX_ID_KEY]: _t,\n\t\t\t[MUTATION_NAME_KEY]: _m,\n\t\t\t[FIELD_VERSIONS_KEY]: _f,\n\t\t\t[HASH_VERSION_KEY]: _h,\n\t\t\t...rest\n\t\t} = parsed\n\t\t// An insert or update always carries data, possibly `{}` (an insert of only\n\t\t// optional fields): its id covers `{}`, so `{}` is read back, never `null`. Only a\n\t\t// delete's payload is metadata alone (RT-80).\n\t\tdata = row.type !== 'delete' || Object.keys(rest).length > 0 ? rest : null\n\t}\n\n\treturn {\n\t\tid: row.id,\n\t\tnodeId: row.node_id,\n\t\ttype: row.type as Operation['type'],\n\t\tcollection: '', // Collection name is derived from the table name by the caller\n\t\trecordId: row.record_id,\n\t\tdata,\n\t\tpreviousData: row.previous_data\n\t\t\t? (JSON.parse(row.previous_data) as Record<string, unknown>)\n\t\t\t: null,\n\t\ttimestamp: HybridLogicalClock.deserialize(row.timestamp),\n\t\tsequenceNumber: row.sequence_number,\n\t\tcausalDeps: JSON.parse(row.causal_deps) as string[],\n\t\tschemaVersion: row.schema_version,\n\t\t...(atomicOps !== undefined ? { atomicOps: atomicOps as Operation['atomicOps'] } : {}),\n\t\t...(transactionId !== undefined ? { transactionId } : {}),\n\t\t...(mutationName !== undefined ? { mutationName } : {}),\n\t\t...(fieldVersions !== undefined ? { fieldVersions } : {}),\n\t\t...(hashVersion !== undefined ? { hashVersion } : {}),\n\t}\n}\n\n/**\n * Deserialize an operation row with collection name already known.\n */\nexport function deserializeOperationWithCollection(\n\trow: OperationRow,\n\tcollection: string,\n): Operation {\n\tconst op = deserializeOperation(row)\n\treturn { ...op, collection }\n}\n\n/**\n * The read form of record-shaped values the developer wrote: a richtext field written\n * as a string or ArrayBuffer is returned as the Yjs bytes it is stored (and read back)\n * as, so `insert()` / `update()` (and their transaction previews) return the same\n * value shape as `findById()` and queries. Other fields are returned as given.\n *\n * @param data - Record-shaped values (validated, at rest)\n * @param fields - The collection's field descriptors\n * @returns `data`, or a copy with richtext values in their read form\n */\nexport function toRichtextReadShape(\n\tdata: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): Record<string, unknown> {\n\tlet out = data\n\tfor (const [name, descriptor] of Object.entries(fields)) {\n\t\tif (descriptor.kind !== 'richtext' || !(name in data)) continue\n\t\tconst value = data[name]\n\t\tif (value === null || value === undefined || value instanceof Uint8Array) continue\n\t\tif (out === data) out = { ...data }\n\t\tout[name] = encodeRichtext(value as Parameters<typeof encodeRichtext>[0])\n\t}\n\treturn out\n}\n\nfunction serializeValue(value: unknown, descriptor: FieldDescriptor): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\tswitch (descriptor.kind) {\n\t\tcase 'boolean':\n\t\t\treturn value ? 1 : 0\n\t\tcase 'array':\n\t\tcase 'object':\n\t\tcase 'json':\n\t\tcase 'blob':\n\t\t\treturn JSON.stringify(value)\n\t\tcase 'richtext':\n\t\t\t// May be a record-shaped value (string/bytes) or an op-data value\n\t\t\t// (tagged { $koraBytes }) — encodeRichtext accepts every form.\n\t\t\treturn encodeRichtext(value as Parameters<typeof encodeRichtext>[0])\n\t\tdefault:\n\t\t\t// Raw string columns: SQLite binds strings as UTF-8, which cannot hold a lone\n\t\t\t// surrogate (stored as U+FFFD) and SQLite WASM reads TEXT up to a NUL. The\n\t\t\t// shared stored-text codec keeps every JS string (RT-65); JSON columns above\n\t\t\t// need none (JSON.stringify escapes both).\n\t\t\treturn typeof value === 'string' && isRawTextKind(descriptor.kind)\n\t\t\t\t? encodeStoredText(value)\n\t\t\t\t: value\n\t}\n}\n\n/**\n * Field kinds whose column holds the raw string (the stored-text codec applies). Not\n * `enum`: its column holds the schema's literal values verbatim; an enum value SQL text\n * cannot store exactly (U+0000, U+FFFF, a lone surrogate) is refused by `t.enum()` itself\n * (the table no longer has a `CHECK`, RT-101).\n */\nconst RAW_TEXT_KINDS: ReadonlySet<string> = new Set(['string', 'secret'])\n\n/** True for a field kind stored as a raw string, encoded with the stored-text codec. */\nexport function isRawTextKind(kind: string): boolean {\n\treturn RAW_TEXT_KINDS.has(kind)\n}\n\n/**\n * Encode a filter value compared against a field's column: a raw-string field stores\n * the encoded form, and the codec is injective, so equality and `IN` stay exact.\n *\n * @param value - The value from the developer's `where`\n * @param descriptor - The field's descriptor, when the field is known\n */\nexport function encodeStoredFilterValue(\n\tvalue: unknown,\n\tdescriptor: FieldDescriptor | undefined,\n): unknown {\n\treturn typeof value === 'string' && descriptor !== undefined && isRawTextKind(descriptor.kind)\n\t\t? encodeStoredText(value)\n\t\t: value\n}\n\nfunction deserializeValue(value: unknown, descriptor: FieldDescriptor): unknown {\n\tswitch (descriptor.kind) {\n\t\tcase 'boolean':\n\t\t\treturn value === 1 || value === true\n\t\tcase 'array':\n\t\t\tif (typeof value === 'string') {\n\t\t\t\treturn JSON.parse(value) as unknown[]\n\t\t\t}\n\t\t\treturn value\n\t\tcase 'object':\n\t\tcase 'json':\n\t\tcase 'blob':\n\t\t\tif (typeof value === 'string') {\n\t\t\t\treturn JSON.parse(value) as unknown\n\t\t\t}\n\t\t\treturn value\n\t\tcase 'richtext':\n\t\t\treturn decodeRichtext(value)\n\t\tdefault:\n\t\t\treturn typeof value === 'string' && isRawTextKind(descriptor.kind)\n\t\t\t\t? decodeStoredText(value)\n\t\t\t\t: value\n\t}\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,112 @@
1
+ import {
2
+ BridgeTerminatedError,
3
+ RequestAbortedError,
4
+ WorkerTimeoutError
5
+ } from "./chunk-DUKTWVZW.js";
6
+
7
+ // src/adapters/sqlite-wasm-channel.ts
8
+ var WebWorkerBridge = class {
9
+ worker;
10
+ pending = /* @__PURE__ */ new Map();
11
+ nextId = 1;
12
+ terminated = false;
13
+ timeoutMs;
14
+ /**
15
+ * @param workerUrl - URL to the sqlite-wasm-worker script
16
+ * @param timeoutMs - Timeout for worker responses in milliseconds (default: 30000)
17
+ * @param options - Status event listener
18
+ */
19
+ constructor(workerUrl, timeoutMs = 3e4, options = {}) {
20
+ this.timeoutMs = timeoutMs;
21
+ this.worker = new Worker(workerUrl, { type: "module" });
22
+ this.worker.onmessage = (event) => {
23
+ const message = event.data;
24
+ if (message.type === "event") {
25
+ options.onEvent?.(message.event);
26
+ return;
27
+ }
28
+ const entry = this.pending.get(message.id);
29
+ if (entry) {
30
+ this.pending.delete(message.id);
31
+ entry.resolve(message);
32
+ }
33
+ };
34
+ this.worker.onerror = (event) => {
35
+ const error = new Error(`Worker error: ${event.message}`);
36
+ for (const [id, entry] of this.pending) {
37
+ this.pending.delete(id);
38
+ entry.reject(error);
39
+ }
40
+ };
41
+ }
42
+ /** True once {@link terminate} ran. */
43
+ isTerminated() {
44
+ return this.terminated;
45
+ }
46
+ /** Post a request without awaiting a response (fire-and-forget control messages). */
47
+ post(request, clientId) {
48
+ if (this.terminated) return;
49
+ const message = { ...request, id: this.nextId++, clientId };
50
+ this.worker.postMessage(message);
51
+ }
52
+ async send(request, clientId, options = {}) {
53
+ if (this.terminated) {
54
+ return {
55
+ id: request.id,
56
+ type: "error",
57
+ message: "Worker has been terminated",
58
+ code: "WORKER_TERMINATED"
59
+ };
60
+ }
61
+ const id = this.nextId++;
62
+ const message = { ...request, id, clientId };
63
+ const timeoutMs = options.timeoutMs ?? this.timeoutMs;
64
+ const signal = options.signal;
65
+ if (signal?.aborted) {
66
+ throw new RequestAbortedError(request.type, request.requestId ?? String(id));
67
+ }
68
+ return new Promise((resolve, reject) => {
69
+ const timer = Number.isFinite(timeoutMs) ? setTimeout(() => {
70
+ this.pending.delete(id);
71
+ cleanup();
72
+ reject(new WorkerTimeoutError(message.type, timeoutMs));
73
+ }, timeoutMs) : void 0;
74
+ const onAbort = () => {
75
+ this.pending.delete(id);
76
+ cleanup();
77
+ reject(new RequestAbortedError(request.type, request.requestId ?? String(id)));
78
+ };
79
+ const cleanup = () => {
80
+ if (timer !== void 0) clearTimeout(timer);
81
+ signal?.removeEventListener("abort", onAbort);
82
+ };
83
+ signal?.addEventListener("abort", onAbort, { once: true });
84
+ this.pending.set(id, {
85
+ type: request.type,
86
+ resolve: (response) => {
87
+ cleanup();
88
+ resolve(response);
89
+ },
90
+ reject: (error) => {
91
+ cleanup();
92
+ reject(error);
93
+ }
94
+ });
95
+ this.worker.postMessage(message);
96
+ });
97
+ }
98
+ terminate() {
99
+ if (this.terminated) return;
100
+ this.terminated = true;
101
+ this.worker.terminate();
102
+ for (const [id, entry] of this.pending) {
103
+ this.pending.delete(id);
104
+ entry.reject(new BridgeTerminatedError(entry.type, "worker terminated"));
105
+ }
106
+ }
107
+ };
108
+
109
+ export {
110
+ WebWorkerBridge
111
+ };
112
+ //# sourceMappingURL=chunk-MCQKEHOD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/sqlite-wasm-channel.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\nimport { BridgeTerminatedError, RequestAbortedError, WorkerTimeoutError } from '../errors'\n\n// === Message Protocol ===\n\n/** Fields every request carries besides its type. */\ninterface RequestMeta {\n\t/** Correlates a worker response with its request on one postMessage channel. */\n\tid: number\n\t/**\n\t * Stable id for de-duplication. A follower retrying a request (after a\n\t * `LeaderUnresponsiveError`, say) sends the same id and the leader worker\n\t * answers from its response cache instead of applying it twice.\n\t */\n\trequestId?: string\n}\n\n/**\n * Request message sent from the main thread (or a follower tab) to the SQLite\n * WASM worker.\n */\nexport type WorkerRequest =\n\t| (RequestMeta & {\n\t\t\ttype: 'open'\n\t\t\tddlStatements: string[]\n\t\t\tdbName?: string\n\t\t\t/**\n\t\t\t * `memory` opens an in-memory database without touching OPFS (the\n\t\t\t * IndexedDB adapter persists snapshots itself). Defaults to `opfs`.\n\t\t\t */\n\t\t\tstorage?: 'opfs' | 'memory'\n\t\t\t/** Fail with `NOT_FOUND` instead of creating the database file. */\n\t\t\tmustExist?: boolean\n\t })\n\t| (RequestMeta & { type: 'close' })\n\t| (RequestMeta & { type: 'destroy' })\n\t| (RequestMeta & { type: 'execute'; sql: string; params?: unknown[] })\n\t| (RequestMeta & { type: 'query'; sql: string; params?: unknown[] })\n\t| (RequestMeta & { type: 'begin' })\n\t| (RequestMeta & { type: 'commit' })\n\t| (RequestMeta & { type: 'rollback' })\n\t| (RequestMeta & { type: 'migrate'; from: number; to: number; statements: string[] })\n\t| (RequestMeta & { type: 'export' })\n\t| (RequestMeta & { type: 'import'; data: Uint8Array })\n\t| (RequestMeta & {\n\t\t\t/**\n\t\t\t * Leader worker only: answer follower RPC, pings and heartbeats on this\n\t\t\t * BroadcastChannel directly, so a hung leader main thread never blocks\n\t\t\t * other tabs (NEW-STORE-9).\n\t\t\t */\n\t\t\ttype: 'serve'\n\t\t\tchannelName: string\n\t })\n\n/**\n * Response message sent from the worker back to the main thread.\n * Matches the request `id` for correlation.\n */\nexport type WorkerResponse =\n\t| { id: number; type: 'success'; data?: unknown }\n\t| { id: number; type: 'error'; message: string; code: string; context?: Record<string, unknown> }\n\n/**\n * Unsolicited status the worker reports while it works on a request, such as\n * waiting for another holder of the OPFS pool (a blocking state the app shows).\n */\nexport type WorkerStatusEvent =\n\t| {\n\t\t\tkind: 'storage-blocked'\n\t\t\tresource: 'pool' | 'legacy-pool'\n\t\t\tpoolName: string\n\t }\n\t| {\n\t\t\tkind: 'storage-unblocked'\n\t\t\tresource: 'pool' | 'legacy-pool'\n\t\t\tpoolName: string\n\t\t\twaitedMs: number\n\t }\n\n/** Envelope for {@link WorkerStatusEvent}s on the worker's postMessage channel. */\nexport interface WorkerEventMessage {\n\tid: -1\n\ttype: 'event'\n\tevent: WorkerStatusEvent\n}\n\n/** Message the main thread posts to a dedicated worker. */\nexport type WorkerInboundMessage = WorkerRequest & { clientId?: string }\n\n/** Per-request options accepted by bridges. */\nexport interface WorkerSendOptions {\n\t/** Cancels the wait for the response (the worker may still apply the request). */\n\tsignal?: AbortSignal\n\t/** Overrides the bridge timeout for this request; `Infinity` disables it. */\n\ttimeoutMs?: number\n}\n\n// === WorkerBridge Interface ===\n\n/**\n * Abstraction over the communication channel with the SQLite WASM worker.\n * In browsers, this is backed by a real Web Worker via MessagePort.\n * In Node.js tests, this is backed by better-sqlite3 via MockWorkerBridge.\n */\nexport interface WorkerBridge {\n\t/** Send a request to the worker and wait for a response. */\n\tsend(\n\t\trequest: WorkerRequest,\n\t\tclientId?: string,\n\t\toptions?: WorkerSendOptions,\n\t): Promise<WorkerResponse>\n\n\t/** Terminate the worker. Safe to call multiple times. */\n\tterminate(): void\n}\n\n// === Mutex ===\n\n// The Mutex now lives in its own module so non-browser adapters (better-sqlite3)\n// can serialize transactions without importing browser-worker code.\nexport { Mutex } from './mutex'\n\n// === WebWorkerBridge ===\n\n/** Options for {@link WebWorkerBridge}. */\nexport interface WebWorkerBridgeOptions {\n\t/** Receives status events (blocking state) the worker reports. */\n\tonEvent?: (event: WorkerStatusEvent) => void\n}\n\n/**\n * WorkerBridge implementation for browser environments.\n * Communicates with an actual Web Worker running SQLite WASM.\n */\nexport class WebWorkerBridge implements WorkerBridge {\n\tprivate worker: Worker\n\tprivate pending = new Map<\n\t\tnumber,\n\t\t{ resolve: (r: WorkerResponse) => void; reject: (e: Error) => void; type: string }\n\t>()\n\tprivate nextId = 1\n\tprivate terminated = false\n\tprivate timeoutMs: number\n\n\t/**\n\t * @param workerUrl - URL to the sqlite-wasm-worker script\n\t * @param timeoutMs - Timeout for worker responses in milliseconds (default: 30000)\n\t * @param options - Status event listener\n\t */\n\tconstructor(workerUrl: string | URL, timeoutMs = 30000, options: WebWorkerBridgeOptions = {}) {\n\t\tthis.timeoutMs = timeoutMs\n\t\tthis.worker = new Worker(workerUrl, { type: 'module' })\n\t\tthis.worker.onmessage = (event: MessageEvent<WorkerResponse | WorkerEventMessage>) => {\n\t\t\tconst message = event.data\n\t\t\tif (message.type === 'event') {\n\t\t\t\toptions.onEvent?.(message.event)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst entry = this.pending.get(message.id)\n\t\t\tif (entry) {\n\t\t\t\tthis.pending.delete(message.id)\n\t\t\t\tentry.resolve(message)\n\t\t\t}\n\t\t}\n\t\tthis.worker.onerror = (event) => {\n\t\t\t// Reject all pending requests on worker error\n\t\t\tconst error = new Error(`Worker error: ${event.message}`)\n\t\t\tfor (const [id, entry] of this.pending) {\n\t\t\t\tthis.pending.delete(id)\n\t\t\t\tentry.reject(error)\n\t\t\t}\n\t\t}\n\t}\n\n\t/** True once {@link terminate} ran. */\n\tisTerminated(): boolean {\n\t\treturn this.terminated\n\t}\n\n\t/** Post a request without awaiting a response (fire-and-forget control messages). */\n\tpost(request: WorkerRequest, clientId?: string): void {\n\t\tif (this.terminated) return\n\t\tconst message: WorkerInboundMessage = { ...request, id: this.nextId++, clientId }\n\t\tthis.worker.postMessage(message)\n\t}\n\n\tasync send(\n\t\trequest: WorkerRequest,\n\t\tclientId?: string,\n\t\toptions: WorkerSendOptions = {},\n\t): Promise<WorkerResponse> {\n\t\tif (this.terminated) {\n\t\t\treturn {\n\t\t\t\tid: request.id,\n\t\t\t\ttype: 'error',\n\t\t\t\tmessage: 'Worker has been terminated',\n\t\t\t\tcode: 'WORKER_TERMINATED',\n\t\t\t}\n\t\t}\n\n\t\tconst id = this.nextId++\n\t\tconst message: WorkerInboundMessage = { ...request, id, clientId }\n\t\tconst timeoutMs = options.timeoutMs ?? this.timeoutMs\n\t\tconst signal = options.signal\n\t\tif (signal?.aborted) {\n\t\t\tthrow new RequestAbortedError(request.type, request.requestId ?? String(id))\n\t\t}\n\n\t\treturn new Promise<WorkerResponse>((resolve, reject) => {\n\t\t\tconst timer = Number.isFinite(timeoutMs)\n\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\tthis.pending.delete(id)\n\t\t\t\t\t\tcleanup()\n\t\t\t\t\t\treject(new WorkerTimeoutError(message.type, timeoutMs))\n\t\t\t\t\t}, timeoutMs)\n\t\t\t\t: undefined\n\t\t\tconst onAbort = (): void => {\n\t\t\t\tthis.pending.delete(id)\n\t\t\t\tcleanup()\n\t\t\t\treject(new RequestAbortedError(request.type, request.requestId ?? String(id)))\n\t\t\t}\n\t\t\tconst cleanup = (): void => {\n\t\t\t\tif (timer !== undefined) clearTimeout(timer)\n\t\t\t\tsignal?.removeEventListener('abort', onAbort)\n\t\t\t}\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\n\t\t\tthis.pending.set(id, {\n\t\t\t\ttype: request.type,\n\t\t\t\tresolve: (response) => {\n\t\t\t\t\tcleanup()\n\t\t\t\t\tresolve(response)\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tcleanup()\n\t\t\t\t\treject(error)\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tthis.worker.postMessage(message)\n\t\t})\n\t}\n\n\tterminate(): void {\n\t\tif (this.terminated) return\n\t\tthis.terminated = true\n\t\t// Terminating the worker releases its OPFS handles and the pool Web Lock it\n\t\t// holds; callers release the tab leader lock only after this (NEW-STORE-10).\n\t\tthis.worker.terminate()\n\t\tfor (const [id, entry] of this.pending) {\n\t\t\tthis.pending.delete(id)\n\t\t\tentry.reject(new BridgeTerminatedError(entry.type, 'worker terminated'))\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}