@superbee/core 0.2.0-pre.17 → 0.2.0-pre.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.
Files changed (202) hide show
  1. package/README.md +515 -1
  2. package/dist/blob-journal.d.ts +187 -2
  3. package/dist/blob-journal.d.ts.map +1 -1
  4. package/dist/blob-journal.js +377 -5
  5. package/dist/blob-journal.js.map +1 -1
  6. package/dist/body-replace-guards.d.ts +50 -0
  7. package/dist/body-replace-guards.d.ts.map +1 -0
  8. package/dist/body-replace-guards.js +158 -0
  9. package/dist/body-replace-guards.js.map +1 -0
  10. package/dist/builtin-recipes/content.d.ts +93 -0
  11. package/dist/builtin-recipes/content.d.ts.map +1 -0
  12. package/dist/builtin-recipes/content.js +288 -0
  13. package/dist/builtin-recipes/content.js.map +1 -0
  14. package/dist/builtin-recipes.d.ts +10 -0
  15. package/dist/builtin-recipes.d.ts.map +1 -0
  16. package/dist/builtin-recipes.js +97 -0
  17. package/dist/builtin-recipes.js.map +1 -0
  18. package/dist/code-point-order.d.ts +8 -0
  19. package/dist/code-point-order.d.ts.map +1 -0
  20. package/dist/code-point-order.js +20 -0
  21. package/dist/code-point-order.js.map +1 -0
  22. package/dist/connectors.d.ts +223 -0
  23. package/dist/connectors.d.ts.map +1 -0
  24. package/dist/connectors.js +1143 -0
  25. package/dist/connectors.js.map +1 -0
  26. package/dist/document-field-actions.d.ts +18 -0
  27. package/dist/document-field-actions.d.ts.map +1 -1
  28. package/dist/document-field-actions.js +91 -29
  29. package/dist/document-field-actions.js.map +1 -1
  30. package/dist/document-mutation.d.ts +7 -9
  31. package/dist/document-mutation.d.ts.map +1 -1
  32. package/dist/document-mutation.js +16 -16
  33. package/dist/document-mutation.js.map +1 -1
  34. package/dist/file-journaled-backend.d.ts +31 -5
  35. package/dist/file-journaled-backend.d.ts.map +1 -1
  36. package/dist/file-journaled-backend.js +131 -118
  37. package/dist/file-journaled-backend.js.map +1 -1
  38. package/dist/hosted-client/blob-delivery.d.ts +6 -0
  39. package/dist/hosted-client/blob-delivery.d.ts.map +1 -1
  40. package/dist/hosted-client/blob-delivery.js +23 -1
  41. package/dist/hosted-client/blob-delivery.js.map +1 -1
  42. package/dist/hosted-client/blob-write.d.ts +386 -0
  43. package/dist/hosted-client/blob-write.d.ts.map +1 -0
  44. package/dist/hosted-client/blob-write.js +770 -0
  45. package/dist/hosted-client/blob-write.js.map +1 -0
  46. package/dist/hosted-client/client.d.ts +64 -0
  47. package/dist/hosted-client/client.d.ts.map +1 -1
  48. package/dist/hosted-client/client.js +31 -0
  49. package/dist/hosted-client/client.js.map +1 -1
  50. package/dist/hosted-client/directory.d.ts +46 -0
  51. package/dist/hosted-client/directory.d.ts.map +1 -0
  52. package/dist/hosted-client/directory.js +74 -0
  53. package/dist/hosted-client/directory.js.map +1 -0
  54. package/dist/hosted-client/errors.d.ts +10 -0
  55. package/dist/hosted-client/errors.d.ts.map +1 -1
  56. package/dist/hosted-client/errors.js +13 -0
  57. package/dist/hosted-client/errors.js.map +1 -1
  58. package/dist/hosted-client/inbox.d.ts +320 -0
  59. package/dist/hosted-client/inbox.d.ts.map +1 -0
  60. package/dist/hosted-client/inbox.js +379 -0
  61. package/dist/hosted-client/inbox.js.map +1 -0
  62. package/dist/hosted-client/index.d.ts +11 -4
  63. package/dist/hosted-client/index.d.ts.map +1 -1
  64. package/dist/hosted-client/index.js +11 -4
  65. package/dist/hosted-client/index.js.map +1 -1
  66. package/dist/hosted-client/journal.d.ts +103 -0
  67. package/dist/hosted-client/journal.d.ts.map +1 -0
  68. package/dist/hosted-client/journal.js +464 -0
  69. package/dist/hosted-client/journal.js.map +1 -0
  70. package/dist/hosted-client/node.d.ts +5 -1
  71. package/dist/hosted-client/node.d.ts.map +1 -1
  72. package/dist/hosted-client/node.js +6 -1
  73. package/dist/hosted-client/node.js.map +1 -1
  74. package/dist/hosted-client/write.d.ts +18 -1
  75. package/dist/hosted-client/write.d.ts.map +1 -1
  76. package/dist/hosted-client/write.js +24 -7
  77. package/dist/hosted-client/write.js.map +1 -1
  78. package/dist/hosted-transport/carrier.d.ts +10 -0
  79. package/dist/hosted-transport/carrier.d.ts.map +1 -1
  80. package/dist/hosted-transport/carrier.js +22 -2
  81. package/dist/hosted-transport/carrier.js.map +1 -1
  82. package/dist/index.d.ts +4 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +3 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/kind-authoring.d.ts +170 -0
  87. package/dist/kind-authoring.d.ts.map +1 -0
  88. package/dist/kind-authoring.js +451 -0
  89. package/dist/kind-authoring.js.map +1 -0
  90. package/dist/kind-conformance-error.d.ts +15 -0
  91. package/dist/kind-conformance-error.d.ts.map +1 -0
  92. package/dist/kind-conformance-error.js +20 -0
  93. package/dist/kind-conformance-error.js.map +1 -0
  94. package/dist/kinds.d.ts +2 -0
  95. package/dist/kinds.d.ts.map +1 -1
  96. package/dist/kinds.js +7 -0
  97. package/dist/kinds.js.map +1 -1
  98. package/dist/recipe-lifecycle/stamp.d.ts +11 -0
  99. package/dist/recipe-lifecycle/stamp.d.ts.map +1 -1
  100. package/dist/recipe-lifecycle/stamp.js +6 -0
  101. package/dist/recipe-lifecycle/stamp.js.map +1 -1
  102. package/dist/record-overlay/apply.d.ts.map +1 -1
  103. package/dist/record-overlay/apply.js +52 -15
  104. package/dist/record-overlay/apply.js.map +1 -1
  105. package/dist/record-overlay/canonical.d.ts +8 -0
  106. package/dist/record-overlay/canonical.d.ts.map +1 -1
  107. package/dist/record-overlay/canonical.js +16 -0
  108. package/dist/record-overlay/canonical.js.map +1 -1
  109. package/dist/record-overlay/errors.d.ts +7 -3
  110. package/dist/record-overlay/errors.d.ts.map +1 -1
  111. package/dist/record-overlay/errors.js +7 -3
  112. package/dist/record-overlay/errors.js.map +1 -1
  113. package/dist/record-overlay/plan.d.ts.map +1 -1
  114. package/dist/record-overlay/plan.js +2 -1
  115. package/dist/record-overlay/plan.js.map +1 -1
  116. package/dist/record-overlay/policy.d.ts +10 -1
  117. package/dist/record-overlay/policy.d.ts.map +1 -1
  118. package/dist/record-overlay/policy.js +33 -14
  119. package/dist/record-overlay/policy.js.map +1 -1
  120. package/dist/record-overlay/types.d.ts +5 -0
  121. package/dist/record-overlay/types.d.ts.map +1 -1
  122. package/dist/search/contract.d.ts +151 -0
  123. package/dist/search/contract.d.ts.map +1 -0
  124. package/dist/search/contract.js +78 -0
  125. package/dist/search/contract.js.map +1 -0
  126. package/dist/search/errors.d.ts +30 -0
  127. package/dist/search/errors.d.ts.map +1 -0
  128. package/dist/search/errors.js +50 -0
  129. package/dist/search/errors.js.map +1 -0
  130. package/dist/search/identity.d.ts +12 -0
  131. package/dist/search/identity.d.ts.map +1 -0
  132. package/dist/search/identity.js +90 -0
  133. package/dist/search/identity.js.map +1 -0
  134. package/dist/search/index.d.ts +15 -0
  135. package/dist/search/index.d.ts.map +1 -0
  136. package/dist/search/index.js +14 -0
  137. package/dist/search/index.js.map +1 -0
  138. package/dist/search/memory-source.d.ts +25 -0
  139. package/dist/search/memory-source.d.ts.map +1 -0
  140. package/dist/search/memory-source.js +230 -0
  141. package/dist/search/memory-source.js.map +1 -0
  142. package/dist/search/rank.d.ts +34 -0
  143. package/dist/search/rank.d.ts.map +1 -0
  144. package/dist/search/rank.js +129 -0
  145. package/dist/search/rank.js.map +1 -0
  146. package/dist/search/row-text.d.ts +38 -0
  147. package/dist/search/row-text.d.ts.map +1 -0
  148. package/dist/search/row-text.js +88 -0
  149. package/dist/search/row-text.js.map +1 -0
  150. package/dist/search/search.d.ts +28 -0
  151. package/dist/search/search.d.ts.map +1 -0
  152. package/dist/search/search.js +240 -0
  153. package/dist/search/search.js.map +1 -0
  154. package/dist/search/source.d.ts +67 -0
  155. package/dist/search/source.d.ts.map +1 -0
  156. package/dist/search/source.js +2 -0
  157. package/dist/search/source.js.map +1 -0
  158. package/dist/search/text.d.ts +47 -0
  159. package/dist/search/text.d.ts.map +1 -0
  160. package/dist/search/text.js +148 -0
  161. package/dist/search/text.js.map +1 -0
  162. package/dist/source-engine/guard.d.ts +82 -0
  163. package/dist/source-engine/guard.d.ts.map +1 -0
  164. package/dist/source-engine/guard.js +396 -0
  165. package/dist/source-engine/guard.js.map +1 -0
  166. package/dist/source-engine/index.d.ts +24 -0
  167. package/dist/source-engine/index.d.ts.map +1 -0
  168. package/dist/source-engine/index.js +20 -0
  169. package/dist/source-engine/index.js.map +1 -0
  170. package/dist/source-engine/machine-state.d.ts +74 -0
  171. package/dist/source-engine/machine-state.d.ts.map +1 -0
  172. package/dist/source-engine/machine-state.js +48 -0
  173. package/dist/source-engine/machine-state.js.map +1 -0
  174. package/dist/source-engine/observe.d.ts +18 -0
  175. package/dist/source-engine/observe.d.ts.map +1 -0
  176. package/dist/source-engine/observe.js +598 -0
  177. package/dist/source-engine/observe.js.map +1 -0
  178. package/dist/source-engine/robots.d.ts +53 -0
  179. package/dist/source-engine/robots.d.ts.map +1 -0
  180. package/dist/source-engine/robots.js +158 -0
  181. package/dist/source-engine/robots.js.map +1 -0
  182. package/dist/source-engine/types.d.ts +285 -0
  183. package/dist/source-engine/types.d.ts.map +1 -0
  184. package/dist/source-engine/types.js +2 -0
  185. package/dist/source-engine/types.js.map +1 -0
  186. package/dist/source-harness/conformance.d.ts +16 -0
  187. package/dist/source-harness/conformance.d.ts.map +1 -0
  188. package/dist/source-harness/conformance.js +265 -0
  189. package/dist/source-harness/conformance.js.map +1 -0
  190. package/dist/source-harness/fixture.d.ts +95 -0
  191. package/dist/source-harness/fixture.d.ts.map +1 -0
  192. package/dist/source-harness/fixture.js +261 -0
  193. package/dist/source-harness/fixture.js.map +1 -0
  194. package/dist/source-harness/index.d.ts +22 -0
  195. package/dist/source-harness/index.d.ts.map +1 -0
  196. package/dist/source-harness/index.js +19 -0
  197. package/dist/source-harness/index.js.map +1 -0
  198. package/dist/source-harness/peer.d.ts +74 -0
  199. package/dist/source-harness/peer.d.ts.map +1 -0
  200. package/dist/source-harness/peer.js +106 -0
  201. package/dist/source-harness/peer.js.map +1 -0
  202. package/package.json +25 -1
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Experimental (`0.2.0-pre`): the API and the on-disk format may change before a stable release.
2
+ * The API is experimental (`0.2.0-pre`) and may change before a stable release. The on-disk
3
+ * format is a supported compatibility promise, because the hosted client's durable journal
4
+ * (`openHostedClientJournal`) keeps app state in it: a format bump ships a reader for the older
5
+ * layout, or a migration.
3
6
  *
4
7
  * A Node {@link JournaledBackend}: the private store behind a CLI working copy, proven by the
5
8
  * same contract kit as the IndexedDB adapter. It is an append-only, checksummed transaction log
@@ -35,24 +38,30 @@
35
38
  * entry of every file the store created. On macOS `fsync` does not force the drive's own cache
36
39
  * (Node exposes no `F_FULLFSYNC`), which is the same promise every Node program gets there.
37
40
  */
41
+ var _a;
38
42
  import { promises as fs } from "node:fs";
39
43
  import path from "node:path";
40
44
  import { assertBlobKey } from "./blob-contract.js";
41
- import { BLOB_JOURNAL_BOUNDS, assertBlobJournalGuard, captureBlobIntentRecord, captureBlobMutationOptions, decodePreparedBlobIntent } from "./blob-journal.js";
45
+ import { BLOB_JOURNAL_BOUNDS, captureBlobAdmission, captureBlobForget, captureBlobIntentRecord, captureBlobMetaWrite, captureBlobRetirement, captureBlobUpdate, decideBlobAdmission, decideBlobForget, decideBlobMetaWrite, decideBlobRetirement, decideBlobUpdate, readBlobJournal, } from "./blob-journal.js";
42
46
  import { resolveContentType } from "./content-type.js";
43
47
  import { OPEN_LOG, STORE_RAW, STORE_BYTE_LIMIT } from "./file-journaled-backend-internal.js";
44
48
  import { captureFilesystemHostPolicy } from "./filesystem-host.js";
45
49
  import { acquireFilesystemMutationLock } from "./filesystem-lock.js";
46
50
  import { MalformedDocumentError, parseMarkdown, stringifyDoc } from "./frontmatter.js";
47
- import { assertDeletionIntent, assertJournalGuard, assertJournalIntentChanges, assertJournalMetaChanges, assertJournalResolutionOptions, assertJournalSnapshot, assertMetaWrite, equalJournalValue, captureIntentUpdate, captureJournalDeleteOptions, captureJournalGuardOption, captureJournalValue, captureJournalWriteOptions, captureMetaWrite, captureRetireAcknowledged, deletionIntentRecord, IntentHoldConflict, IntentStateConflict, JournalGuardConflict, JournalSnapshotConflict, } from "./journaled-backend.js";
51
+ import { assertDeletionIntent, assertJournalGuard, assertJournalIntentChanges, assertJournalMetaChanges, assertJournalResolutionOptions, assertJournalSnapshot, assertMetaWrite, captureIntentUpdate, captureJournalDeleteOptions, captureJournalGuardOption, captureJournalValue, captureJournalWriteOptions, captureMetaWrite, captureRetireAcknowledged, deletionIntentRecord, IntentHoldConflict, IntentStateConflict, JournalGuardConflict, JournalSnapshotConflict, } from "./journaled-backend.js";
48
52
  import { mutationActorFromFrontmatter } from "./mutation-attribution.js";
49
53
  import { assertSafeBlobKey, assertSafeConceptId, assertSafeReservedDir, assertSafeReservedFilename, compareStorageKeys, pathFromConceptId } from "./paths.js";
50
54
  import { parseLeadingFrontmatter } from "./portable-frontmatter.js";
51
55
  import { sha256HexOfBytes } from "./sha256.js";
52
56
  import { blobVersion, defaultActor, VersionConflict, versionOfBytes } from "./versioning.js";
53
57
  // ── on-disk format ─────────────────────────────────────────────────────────────────────────
54
- /** Bumping this requires a reader for the older layout; open refuses any other format. */
55
- export const FILE_JOURNAL_FORMAT_VERSION = 2;
58
+ /**
59
+ * The newest layout this adapter writes and reads; it reads every earlier one too. A store stays
60
+ * at format 1 until it holds a blob intent (format 2), and at 2 until a blob intent carries
61
+ * not-dispatched evidence (format 3); the store keeps the highest format it has written. Bumping
62
+ * this requires a reader for the older layouts; open refuses any newer format.
63
+ */
64
+ export const FILE_JOURNAL_FORMAT_VERSION = 3;
56
65
  export const FILE_JOURNAL_LOG = "store.log";
57
66
  export const FILE_JOURNAL_SNAPSHOT = "store.snapshot";
58
67
  const SNAPSHOT_TEMP = "store.snapshot.tmp";
@@ -128,7 +137,7 @@ export class FileJournalFormatError extends FileJournalCorruptError {
128
137
  name = "FileJournalFormatError";
129
138
  format;
130
139
  constructor(file, format, offset) {
131
- super(file, `written in store format ${format}; this adapter reads formats 1 and ${FILE_JOURNAL_FORMAT_VERSION}`, offset);
140
+ super(file, `written in store format ${format}; this adapter reads formats 1 to ${FILE_JOURNAL_FORMAT_VERSION}`, offset);
132
141
  this.format = format;
133
142
  }
134
143
  }
@@ -355,7 +364,7 @@ function scanLog(bytes, file) {
355
364
  throw new FileJournalCorruptError(file, "a damaged record is followed by a whole one", at);
356
365
  return { records, validBytes: at, totalBytes: bytes.byteLength };
357
366
  }
358
- if (found.format !== 1 && found.format !== 2)
367
+ if (!readableFormat(found.format))
359
368
  throw new FileJournalFormatError(file, found.format, at);
360
369
  let record;
361
370
  try {
@@ -366,7 +375,7 @@ function scanLog(bytes, file) {
366
375
  }
367
376
  if (!record || !Number.isSafeInteger(record.txn) || !Array.isArray(record.changes))
368
377
  throw new FileJournalCorruptError(file, "record has no transaction number or changes", at);
369
- if ((found.format === 2 ? record.format !== 2 : record.format !== undefined && record.format !== 1))
378
+ if ((found.format >= 2 ? record.format !== found.format : record.format !== undefined && record.format !== 1))
370
379
  throw new FileJournalCorruptError(file, "record format does not match its frame", at);
371
380
  validateIntentChanges(record.changes, found.format, file);
372
381
  record.format = found.format;
@@ -381,7 +390,7 @@ function decodeSnapshot(bytes, file) {
381
390
  const found = frameAt(bytes, 0, SNAPSHOT_MAGIC);
382
391
  if (!found || found.end !== bytes.byteLength)
383
392
  throw new FileJournalCorruptError(file, "snapshot checksum or length does not match");
384
- if (found.format !== 1 && found.format !== 2)
393
+ if (!readableFormat(found.format))
385
394
  throw new FileJournalFormatError(file, found.format);
386
395
  let decoded;
387
396
  try {
@@ -397,14 +406,19 @@ function decodeSnapshot(bytes, file) {
397
406
  validateIntentChanges(decoded.intents.map(([key, value]) => ({ family: "intent", key, value })), found.format, file);
398
407
  return decoded;
399
408
  }
409
+ const readableFormat = (format) => Number.isSafeInteger(format) && format >= 1 && format <= FILE_JOURNAL_FORMAT_VERSION;
410
+ /** The layout a blob intent needs: not-dispatched evidence first appeared in format 3. */
411
+ const blobIntentFormat = (row) => ("notDispatched" in row ? 3 : 2);
400
412
  function validateIntentChanges(changes, format, file) {
401
413
  for (const change of changes) {
402
414
  if (change.family !== "intent" || change.value === null)
403
415
  continue;
404
416
  const row = change.value;
405
417
  if ("recordFamily" in row) {
406
- if (row.recordFamily !== "blob" || format !== 2)
418
+ if (row.recordFamily !== "blob" || format < 2)
407
419
  throw new FileJournalCorruptError(file, "blob intent is not allowed in this format");
420
+ if (blobIntentFormat(row) > format)
421
+ throw new FileJournalCorruptError(file, "blob not-dispatched evidence is not allowed in this format");
408
422
  try {
409
423
  captureBlobIntentRecord(row);
410
424
  assertReceiptBound(row);
@@ -440,7 +454,7 @@ function settlementReserve(state, txn) {
440
454
  // replay. Unknown/nonterminal evidence can never release this allocation.
441
455
  if (row.receipt !== undefined && ["acknowledged", "refused", "conflict"].includes(row.state))
442
456
  continue;
443
- const mutableBytes = encodePayload({ state: row.state, attempts: row.attempts, updatedAt: row.updatedAt, transaction: txn, ...(row.notAppliedThrough === undefined ? {} : { notAppliedThrough: row.notAppliedThrough }), ...(row.deletionHold === undefined ? {} : { deletionHold: row.deletionHold }) }).byteLength;
457
+ const mutableBytes = encodePayload({ state: row.state, attempts: row.attempts, updatedAt: row.updatedAt, transaction: txn, ...(row.notAppliedThrough === undefined ? {} : { notAppliedThrough: row.notAppliedThrough }), ...(row.deletionHold === undefined ? {} : { deletionHold: row.deletionHold }), ...(row.notDispatched === undefined ? {} : { notDispatched: row.notDispatched }) }).byteLength;
444
458
  const remaining = BLOB_JOURNAL_BOUNDS.terminalReceiptBytes + BLOB_JOURNAL_BOUNDS.settlementStructureBytes + BLOB_JOURNAL_BOUNDS.baselineBytes - mutableBytes;
445
459
  if (!Number.isSafeInteger(remaining) || remaining < 0 || !Number.isSafeInteger(reserved + remaining))
446
460
  throw new RangeError("blob settlement reserve is not representable");
@@ -579,6 +593,9 @@ export class FileJournaledBackend {
579
593
  #maximumBytes = MAXIMUM_RECORD_BYTES;
580
594
  #logBytes;
581
595
  #queue = Promise.resolve();
596
+ /** Blob claims this handle made, by request id, at their attempt count. The exclusive lock means
597
+ * any `in_flight` row found at open was claimed by a process that has since died. */
598
+ #claims = new Map();
582
599
  #unavailable = null;
583
600
  #closing = null;
584
601
  #readOnly = false;
@@ -597,11 +614,11 @@ export class FileJournaledBackend {
597
614
  * file. Compacts when the log is larger than `compactAfterBytes`.
598
615
  */
599
616
  static open(options) {
600
- return FileJournaledBackend.#open(options, (file) => fs.open(file, "r+"));
617
+ return _a.#open(options, (file) => fs.open(file, "r+"));
601
618
  }
602
619
  /** @internal The test seam: open with a substitute log handle. Reachable only through {@link OPEN_LOG}. */
603
620
  static [OPEN_LOG](options, openLog) {
604
- return FileJournaledBackend.#open(options, openLog);
621
+ return _a.#open(options, openLog);
605
622
  }
606
623
  static async #open(options, openLog) {
607
624
  if (typeof options.directory !== "string" || options.directory.trim() === "")
@@ -664,7 +681,7 @@ export class FileJournaledBackend {
664
681
  txn = record.txn;
665
682
  }
666
683
  if (options.readOnly) {
667
- const backend = new FileJournaledBackend(directory, state, READ_ONLY_LOG, release, txn, scanned.validBytes, policy);
684
+ const backend = new _a(directory, state, READ_ONLY_LOG, release, txn, scanned.validBytes, policy);
668
685
  backend.#maximumBytes = maximumBytes;
669
686
  backend.#readOnly = true;
670
687
  return backend;
@@ -679,7 +696,7 @@ export class FileJournaledBackend {
679
696
  await log.truncate(scanned.validBytes);
680
697
  await log.sync();
681
698
  }
682
- const backend = new FileJournaledBackend(directory, state, log, release, txn, scanned.validBytes, policy);
699
+ const backend = new _a(directory, state, log, release, txn, scanned.validBytes, policy);
683
700
  backend.#maximumBytes = maximumBytes;
684
701
  if (scanned.validBytes > compactAfter)
685
702
  await backend.compact();
@@ -769,18 +786,20 @@ export class FileJournaledBackend {
769
786
  */
770
787
  #mutate(decide) {
771
788
  return this.#serial(async () => {
772
- const { changes, result, format } = decide(this.#state);
773
- if (changes.length === 0)
789
+ const { changes, result, format, afterCommit } = decide(this.#state);
790
+ if (changes.length === 0) {
791
+ afterCommit?.();
774
792
  return result;
793
+ }
775
794
  const nextFormat = Math.max(this.#state.format, format ?? 1);
776
- const record = { ...(nextFormat === 2 ? { format: 2 } : {}), txn: this.#txn + 1, changes };
795
+ const record = { ...(nextFormat >= 2 ? { format: nextFormat } : {}), txn: this.#txn + 1, changes };
777
796
  const payload = encodePayload(record);
778
797
  assertByteBound(payload, this.#maximumBytes, "transaction");
779
798
  const projected = { ...this.#state, format: nextFormat, documents: new Map(this.#state.documents), reserved: new Map(this.#state.reserved), blobs: new Map(this.#state.blobs), intents: new Map(this.#state.intents), meta: new Map(this.#state.meta) };
780
799
  applyChanges(projected, changes);
781
800
  const snapshotBytes = encodePayload(snapshotPayload(projected, record.txn));
782
801
  assertByteBound(snapshotBytes, this.#maximumBytes, "projected snapshot");
783
- if (nextFormat === 2) {
802
+ if (nextFormat >= 2) {
784
803
  const reserved = settlementReserve(projected, record.txn);
785
804
  if (snapshotBytes.byteLength + reserved > this.#maximumBytes)
786
805
  throw new RangeError(`a projected snapshot of ${snapshotBytes.byteLength} bytes plus ${reserved} bytes of blob settlement reserve exceeds the store's ${this.#maximumBytes} byte bound`);
@@ -805,6 +824,7 @@ export class FileJournaledBackend {
805
824
  this.#txn = record.txn;
806
825
  this.#state.format = nextFormat;
807
826
  applyChanges(this.#state, changes);
827
+ afterCommit?.();
808
828
  return result;
809
829
  });
810
830
  }
@@ -1006,7 +1026,7 @@ export class FileJournaledBackend {
1006
1026
  if (resolveIntents)
1007
1027
  assertJournalSnapshot(id, resolveIntents.expected, documentIntents(state), intent?.requestId);
1008
1028
  if (requireSettled) {
1009
- const holder = FileJournaledBackend.#holder(state, id);
1029
+ const holder = _a.#holder(state, id);
1010
1030
  if (holder)
1011
1031
  throw new IntentHoldConflict(id, holder.requestId, holder.state);
1012
1032
  }
@@ -1029,7 +1049,7 @@ export class FileJournaledBackend {
1029
1049
  record = { ...intent, local: version, content: raw, sequence, attempts: 0, state: "pending", updatedAt: now };
1030
1050
  changes.push({ family: "sequence", value: sequence }, { family: "intent", key: record.requestId, value: structuredClone(record) });
1031
1051
  }
1032
- changes.push(...FileJournaledBackend.#metaChanges(meta, removeMeta));
1052
+ changes.push(..._a.#metaChanges(meta, removeMeta));
1033
1053
  return { changes, result: { version, raw, intent: record } };
1034
1054
  });
1035
1055
  }
@@ -1051,11 +1071,11 @@ export class FileJournaledBackend {
1051
1071
  if (captured.intent && state.intents.has(captured.intent.requestId))
1052
1072
  throw new JournalSnapshotConflict(id);
1053
1073
  if (requireSettled) {
1054
- const holder = FileJournaledBackend.#holder(state, id);
1074
+ const holder = _a.#holder(state, id);
1055
1075
  if (holder) {
1056
1076
  if (!captured.onHeld)
1057
1077
  throw new IntentHoldConflict(id, holder.requestId, holder.state);
1058
- return { changes: FileJournaledBackend.#metaChanges(captured.onHeld.meta), result: { outcome: "held", requestId: holder.requestId, state: holder.state } };
1078
+ return { changes: _a.#metaChanges(captured.onHeld.meta), result: { outcome: "held", requestId: holder.requestId, state: holder.state } };
1059
1079
  }
1060
1080
  }
1061
1081
  const current = state.documents.get(id)?.version ?? null;
@@ -1080,7 +1100,7 @@ export class FileJournaledBackend {
1080
1100
  record = deletionIntentRecord(captured.intent, sequence, now);
1081
1101
  changes.push({ family: "sequence", value: sequence }, { family: "intent", key: record.requestId, value: structuredClone(record) });
1082
1102
  }
1083
- changes.push(...FileJournaledBackend.#metaChanges(captured.meta, captured.removeMeta));
1103
+ changes.push(..._a.#metaChanges(captured.meta, captured.removeMeta));
1084
1104
  return { changes, result: { outcome: current !== null ? "deleted" : "absent", ...(record ? { intent: record } : {}) } };
1085
1105
  });
1086
1106
  }
@@ -1164,7 +1184,7 @@ export class FileJournaledBackend {
1164
1184
  const changes = [];
1165
1185
  if (replacement)
1166
1186
  changes.push({ family: "document", key: replacement.id, value: replacement.record });
1167
- changes.push({ family: "intent", key: requestId, value: structuredClone(next) }, ...FileJournaledBackend.#metaChanges(meta));
1187
+ changes.push({ family: "intent", key: requestId, value: structuredClone(next) }, ..._a.#metaChanges(meta));
1168
1188
  return { changes, result: next };
1169
1189
  });
1170
1190
  }
@@ -1174,64 +1194,63 @@ export class FileJournaledBackend {
1174
1194
  await this.#mutate((state) => {
1175
1195
  this.#checkGuard(state, guard);
1176
1196
  const changes = requestIds.map((key) => ({ family: "intent", key, value: null }));
1177
- return { changes: [...changes, ...FileJournaledBackend.#metaChanges([], removeMeta ?? [])], result: undefined };
1197
+ return { changes: [...changes, ..._a.#metaChanges([], removeMeta ?? [])], result: undefined };
1178
1198
  });
1179
1199
  }
1180
1200
  // ── optional ordinary-file journal (no filesystem/body mirror) ─────────────────────────
1181
- #blobGuard(state, guard) {
1182
- assertBlobJournalGuard(guard, {
1183
- key: guard.key,
1184
- intents: [...state.intents.values()].filter((row) => !isDocumentIntent(row) && row.target === guard.key),
1185
- meta: guard.meta.map(row => ({ key: row.key, expected: state.meta.has(row.key) ? { present: true, value: state.meta.get(row.key) } : { present: false } })),
1186
- });
1201
+ /** The committed index as the pure blob transition owner reads it. */
1202
+ #blobView(state) {
1203
+ return {
1204
+ blobIntentsFor: key => [...state.intents.values()].filter((row) => !isDocumentIntent(row) && row.target === key),
1205
+ blobIntent: id => { const row = state.intents.get(id); return row && !isDocumentIntent(row) ? row : undefined; },
1206
+ intentState: id => state.intents.get(id)?.state ?? null,
1207
+ meta: key => state.meta.has(key) ? { present: true, value: state.meta.get(key) } : { present: false },
1208
+ sequence: state.sequence,
1209
+ claimedInThisSession: (id, attempts) => this.#claims.get(id) === attempts,
1210
+ };
1211
+ }
1212
+ /** A decided blob verb as one mutation: its changes, the format they need, and the session claims. */
1213
+ #blobDecision(decision) {
1214
+ const formats = decision.intents.flatMap(({ value }) => (value === null ? [] : [blobIntentFormat(value)]));
1215
+ const format = formats.length === 0 ? undefined : Math.max(...formats);
1216
+ return {
1217
+ changes: _a.#blobChanges(decision),
1218
+ result: decision.result,
1219
+ ...(format === undefined ? {} : { format }),
1220
+ afterCommit: () => {
1221
+ if (decision.session) {
1222
+ if (decision.session.claimed === null)
1223
+ this.#claims.delete(decision.session.requestId);
1224
+ else
1225
+ this.#claims.set(decision.session.requestId, decision.session.claimed);
1226
+ }
1227
+ for (const { requestId, value } of decision.intents)
1228
+ if (value === null)
1229
+ this.#claims.delete(requestId);
1230
+ },
1231
+ };
1232
+ }
1233
+ /** A decided blob verb as one log record's changes. */
1234
+ static #blobChanges(decision) {
1235
+ return [
1236
+ ...(decision.sequence === undefined ? [] : [{ family: "sequence", value: decision.sequence }]),
1237
+ ...decision.intents.map(({ requestId, value }) => ({ family: "intent", key: requestId, value: value === null ? null : structuredClone(value) })),
1238
+ ..._a.#metaChanges(decision.meta, decision.removeMeta),
1239
+ ];
1187
1240
  }
1188
1241
  async readBlobWithJournal(key, options = {}) {
1189
1242
  assertBlobKey(key);
1190
1243
  this.#assertOpen();
1191
- const state = this.#state;
1192
- const intents = [...state.intents.values()].filter((row) => !isDocumentIntent(row) && row.target === key).sort(bySequence).map(row => structuredClone(row));
1193
- const meta = new Map();
1194
- const premises = [];
1195
- for (const name of new Set(options.meta ?? [])) {
1196
- if (typeof name !== "string")
1197
- throw new JournalGuardConflict(key);
1198
- if (state.meta.has(name)) {
1199
- const value = structuredClone(state.meta.get(name));
1200
- meta.set(name, value);
1201
- premises.push({ key: name, expected: { present: true, value: structuredClone(value) } });
1202
- }
1203
- else
1204
- premises.push({ key: name, expected: { present: false } });
1205
- }
1206
- return { key, intents, meta, guard: { key, intents: structuredClone(intents), meta: premises } };
1244
+ return readBlobJournal(this.#blobView(this.#state), key, options);
1207
1245
  }
1208
1246
  async writeBlobMeta(key, changes, options) {
1209
- assertBlobKey(key);
1210
- const captured = captureBlobMutationOptions({ ...captureJournalValue(changes), guard: options.guard });
1211
- if (captured.guard.key !== key)
1212
- throw new JournalGuardConflict(key);
1213
- await this.#mutate(state => {
1214
- this.#blobGuard(state, captured.guard);
1215
- return { changes: FileJournaledBackend.#metaChanges(captured.meta ?? [], captured.removeMeta ?? []), result: undefined };
1216
- });
1247
+ const captured = captureBlobMetaWrite(key, changes, options);
1248
+ await this.#mutate(state => this.#blobDecision(decideBlobMetaWrite(this.#blobView(state), captured)));
1217
1249
  }
1218
1250
  async admitPreparedBlob(prepared, options) {
1219
- const captured = decodePreparedBlobIntent(prepared);
1220
- options = captureBlobMutationOptions(options);
1221
- const { guard, meta = [], removeMeta = [] } = options;
1222
- if (guard.key !== captured.intent.key)
1223
- throw new JournalGuardConflict(captured.intent.key);
1251
+ const captured = captureBlobAdmission(prepared, options);
1224
1252
  const now = new Date().toISOString();
1225
- return this.#mutate(state => {
1226
- this.#blobGuard(state, guard);
1227
- if (state.intents.has(captured.identity.requestId))
1228
- throw new JournalSnapshotConflict(guard.key);
1229
- const sequence = state.sequence + 1;
1230
- if (!Number.isSafeInteger(sequence))
1231
- throw new RangeError("journal sequence exhausted");
1232
- const record = captureBlobIntentRecord({ recordFamily: "blob", requestId: captured.identity.requestId, target: captured.intent.key, prepared, sequence, attempts: 0, state: "pending", updatedAt: now });
1233
- return { format: 2, changes: [{ family: "sequence", value: sequence }, { family: "intent", key: record.requestId, value: structuredClone(record) }, ...FileJournaledBackend.#metaChanges(meta, removeMeta)], result: record };
1234
- });
1253
+ return this.#mutate(state => this.#blobDecision(decideBlobAdmission(this.#blobView(state), captured, now)));
1235
1254
  }
1236
1255
  async listBlobIntents(state) {
1237
1256
  this.#assertOpen();
@@ -1244,56 +1263,17 @@ export class FileJournaledBackend {
1244
1263
  return row && !isDocumentIntent(row) ? structuredClone(row) : undefined;
1245
1264
  }
1246
1265
  async updateBlobIntent(requestId, expectedState, patch, options) {
1247
- patch = captureJournalValue(patch);
1248
- options = captureBlobMutationOptions(options);
1249
- if (!patch || typeof patch !== "object" || Array.isArray(patch) || Object.keys(patch).some(key => !["state", "attempts", "receipt", "notAppliedThrough", "deletionHold"].includes(key)))
1250
- throw new JournalGuardConflict(options.guard.key);
1251
- const { guard, meta = [], removeMeta = [] } = options;
1266
+ const captured = captureBlobUpdate(requestId, expectedState, patch, options);
1252
1267
  const now = new Date().toISOString();
1253
- return this.#mutate(state => {
1254
- this.#blobGuard(state, guard);
1255
- const current = state.intents.get(requestId);
1256
- if (!current || isDocumentIntent(current) || current.state !== expectedState)
1257
- throw new IntentStateConflict(requestId, expectedState, current?.state ?? null);
1258
- if (current.target !== guard.key)
1259
- throw new JournalGuardConflict(guard.key);
1260
- const next = captureBlobIntentRecord({ ...current, ...patch, updatedAt: now });
1261
- assertReceiptBound(next);
1262
- const beforePrefix = current.notAppliedThrough ?? 0, afterPrefix = next.notAppliedThrough ?? 0;
1263
- const sameHold = equalJournalValue(current.deletionHold, next.deletionHold);
1264
- if (current.receipt && (next.attempts !== current.attempts || next.state !== current.state || !equalJournalValue(current.receipt, next.receipt) || afterPrefix !== beforePrefix || !sameHold))
1265
- throw new JournalGuardConflict(guard.key);
1266
- if (next.attempts !== current.attempts) {
1267
- // The durable claim consumes exactly one eligibility before any wire operation.
1268
- if (current.receipt || !["pending", "unknown"].includes(current.state) || next.state !== "in_flight" || next.attempts !== current.attempts + 1 || beforePrefix !== current.attempts || afterPrefix !== beforePrefix || !sameHold)
1269
- throw new JournalGuardConflict(guard.key);
1270
- }
1271
- else if (afterPrefix !== beforePrefix) {
1272
- // Typed evidence belongs to this exact claimed count and must extend the complete prefix.
1273
- if (current.receipt || !["in_flight", "unknown"].includes(current.state) || next.state !== "pending" || patch.attempts !== current.attempts || beforePrefix !== current.attempts - 1 || afterPrefix !== current.attempts || patch.deletionHold === undefined || next.receipt !== undefined)
1274
- throw new JournalGuardConflict(guard.key);
1275
- }
1276
- else if (!sameHold || patch.state === "in_flight")
1277
- throw new JournalGuardConflict(guard.key);
1278
- return { changes: [{ family: "intent", key: requestId, value: structuredClone(next) }, ...FileJournaledBackend.#metaChanges(meta, removeMeta)], result: next };
1279
- });
1268
+ return this.#mutate(state => this.#blobDecision(decideBlobUpdate(this.#blobView(state), captured, now, assertReceiptBound)));
1280
1269
  }
1281
1270
  async retireBlobIntents(key, requestIds, options) {
1282
- assertBlobKey(key);
1283
- const ids = captureJournalValue([...requestIds]);
1284
- options = captureBlobMutationOptions(options);
1285
- const { guard, meta = [], removeMeta = [] } = options;
1286
- if (guard.key !== key || ids.length === 0 || ids.some(id => typeof id !== "string") || new Set(ids).size !== ids.length)
1287
- throw new JournalGuardConflict(key);
1288
- await this.#mutate(state => {
1289
- this.#blobGuard(state, guard);
1290
- for (const id of ids) {
1291
- const row = state.intents.get(id);
1292
- if (!row || isDocumentIntent(row) || row.target !== key || !row.receipt || !["acknowledged", "refused", "conflict"].includes(row.state))
1293
- throw new JournalGuardConflict(key);
1294
- }
1295
- return { changes: [...ids.map((id) => ({ family: "intent", key: id, value: null })), ...FileJournaledBackend.#metaChanges(meta, removeMeta)], result: undefined };
1296
- });
1271
+ const captured = captureBlobRetirement(key, requestIds, options);
1272
+ await this.#mutate(state => this.#blobDecision(decideBlobRetirement(this.#blobView(state), captured)));
1273
+ }
1274
+ async forgetBlobIntent(requestId, options) {
1275
+ const captured = captureBlobForget(requestId, options);
1276
+ await this.#mutate(state => this.#blobDecision(decideBlobForget(this.#blobView(state), captured)));
1297
1277
  }
1298
1278
  async readMeta(key) {
1299
1279
  this.#assertOpen();
@@ -1317,5 +1297,38 @@ export class FileJournaledBackend {
1317
1297
  return { changes: [{ family: "meta", key, present: true, value: captured }], result: undefined };
1318
1298
  });
1319
1299
  }
1300
+ /**
1301
+ * Every meta row whose key starts with `prefix` (all rows when it is empty), in code-unit key
1302
+ * order, read in one synchronous pass over the last committed transaction. Values are copies.
1303
+ */
1304
+ async listMeta(prefix = "") {
1305
+ if (typeof prefix !== "string")
1306
+ throw new TypeError("a meta prefix is a string");
1307
+ this.#assertOpen();
1308
+ const meta = this.#state.meta;
1309
+ const keys = [...meta.keys()].filter((key) => key.startsWith(prefix)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1310
+ return keys.map((key) => ({ key, value: structuredClone(meta.get(key)) }));
1311
+ }
1312
+ /**
1313
+ * Remove one meta row, compare-and-swap: `expected` is the row's presence and value as the
1314
+ * caller last read it, compared in the same transaction as the removal. A mismatch is
1315
+ * `JournalGuardConflict` and writes nothing. It resolves true when it removed the row and false
1316
+ * when the row was absent as expected, which writes nothing either.
1317
+ */
1318
+ async removeMeta(key, options) {
1319
+ if (typeof key !== "string")
1320
+ throw new TypeError("a meta key is a string");
1321
+ if (!options || typeof options !== "object" || options.expected === undefined)
1322
+ throw new JournalGuardConflict(key);
1323
+ const { expected } = captureMetaWrite({ expected: options.expected });
1324
+ return this.#mutate((state) => {
1325
+ const actual = state.meta.has(key) ? { present: true, value: state.meta.get(key) } : { present: false };
1326
+ assertMetaWrite(key, { expected }, actual, []);
1327
+ if (!actual.present)
1328
+ return { changes: [], result: false };
1329
+ return { changes: [{ family: "meta", key, present: false }], result: true };
1330
+ });
1331
+ }
1320
1332
  }
1333
+ _a = FileJournaledBackend;
1321
1334
  //# sourceMappingURL=file-journaled-backend.js.map