@ny4n/durable 0.0.0-stage → 1.0.0

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 (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -2
  3. package/dist/documents.d.ts +66 -0
  4. package/dist/documents.d.ts.map +1 -0
  5. package/dist/documents.js +98 -0
  6. package/dist/documents.js.map +1 -0
  7. package/dist/entries.d.ts +31 -0
  8. package/dist/entries.d.ts.map +1 -0
  9. package/dist/entries.js +31 -0
  10. package/dist/entries.js.map +1 -0
  11. package/dist/env/decode.d.ts +16 -0
  12. package/dist/env/decode.d.ts.map +1 -0
  13. package/dist/env/decode.js +28 -0
  14. package/dist/env/decode.js.map +1 -0
  15. package/dist/env/index.d.ts +263 -0
  16. package/dist/env/index.d.ts.map +1 -0
  17. package/dist/env/index.js +49 -0
  18. package/dist/env/index.js.map +1 -0
  19. package/dist/env/line-scan.d.ts +13 -0
  20. package/dist/env/line-scan.d.ts.map +1 -0
  21. package/dist/env/line-scan.js +147 -0
  22. package/dist/env/line-scan.js.map +1 -0
  23. package/dist/env/node-watch.d.ts +29 -0
  24. package/dist/env/node-watch.d.ts.map +1 -0
  25. package/dist/env/node-watch.js +485 -0
  26. package/dist/env/node-watch.js.map +1 -0
  27. package/dist/env/node.d.ts +56 -0
  28. package/dist/env/node.d.ts.map +1 -0
  29. package/dist/env/node.js +1166 -0
  30. package/dist/env/node.js.map +1 -0
  31. package/dist/errors.d.ts +32 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +53 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/harness/agent.d.ts +33 -0
  36. package/dist/harness/agent.d.ts.map +1 -0
  37. package/dist/harness/agent.js +235 -0
  38. package/dist/harness/agent.js.map +1 -0
  39. package/dist/harness/assistant.d.ts +15 -0
  40. package/dist/harness/assistant.d.ts.map +1 -0
  41. package/dist/harness/assistant.js +23 -0
  42. package/dist/harness/assistant.js.map +1 -0
  43. package/dist/harness/compaction.d.ts +56 -0
  44. package/dist/harness/compaction.d.ts.map +1 -0
  45. package/dist/harness/compaction.js +384 -0
  46. package/dist/harness/compaction.js.map +1 -0
  47. package/dist/harness/context.d.ts +63 -0
  48. package/dist/harness/context.d.ts.map +1 -0
  49. package/dist/harness/context.js +221 -0
  50. package/dist/harness/context.js.map +1 -0
  51. package/dist/harness/define.d.ts +18 -0
  52. package/dist/harness/define.d.ts.map +1 -0
  53. package/dist/harness/define.js +25 -0
  54. package/dist/harness/define.js.map +1 -0
  55. package/dist/harness/events.d.ts +180 -0
  56. package/dist/harness/events.d.ts.map +1 -0
  57. package/dist/harness/events.js +370 -0
  58. package/dist/harness/events.js.map +1 -0
  59. package/dist/harness/generation.d.ts +54 -0
  60. package/dist/harness/generation.d.ts.map +1 -0
  61. package/dist/harness/generation.js +514 -0
  62. package/dist/harness/generation.js.map +1 -0
  63. package/dist/harness/harness.d.ts +17 -0
  64. package/dist/harness/harness.d.ts.map +1 -0
  65. package/dist/harness/harness.js +332 -0
  66. package/dist/harness/harness.js.map +1 -0
  67. package/dist/harness/inbox.d.ts +59 -0
  68. package/dist/harness/inbox.d.ts.map +1 -0
  69. package/dist/harness/inbox.js +97 -0
  70. package/dist/harness/inbox.js.map +1 -0
  71. package/dist/harness/json.d.ts +10 -0
  72. package/dist/harness/json.d.ts.map +1 -0
  73. package/dist/harness/json.js +33 -0
  74. package/dist/harness/json.js.map +1 -0
  75. package/dist/harness/live.d.ts +142 -0
  76. package/dist/harness/live.d.ts.map +1 -0
  77. package/dist/harness/live.js +149 -0
  78. package/dist/harness/live.js.map +1 -0
  79. package/dist/harness/output.d.ts +69 -0
  80. package/dist/harness/output.d.ts.map +1 -0
  81. package/dist/harness/output.js +318 -0
  82. package/dist/harness/output.js.map +1 -0
  83. package/dist/harness/prompt.d.ts +27 -0
  84. package/dist/harness/prompt.d.ts.map +1 -0
  85. package/dist/harness/prompt.js +131 -0
  86. package/dist/harness/prompt.js.map +1 -0
  87. package/dist/harness/provider.d.ts +14 -0
  88. package/dist/harness/provider.d.ts.map +1 -0
  89. package/dist/harness/provider.js +30 -0
  90. package/dist/harness/provider.js.map +1 -0
  91. package/dist/harness/registry.d.ts +6 -0
  92. package/dist/harness/registry.d.ts.map +1 -0
  93. package/dist/harness/registry.js +101 -0
  94. package/dist/harness/registry.js.map +1 -0
  95. package/dist/harness/run.d.ts +17 -0
  96. package/dist/harness/run.d.ts.map +1 -0
  97. package/dist/harness/run.js +26 -0
  98. package/dist/harness/run.js.map +1 -0
  99. package/dist/harness/scheduler.d.ts +106 -0
  100. package/dist/harness/scheduler.d.ts.map +1 -0
  101. package/dist/harness/scheduler.js +1743 -0
  102. package/dist/harness/scheduler.js.map +1 -0
  103. package/dist/harness/submissions.d.ts +29 -0
  104. package/dist/harness/submissions.d.ts.map +1 -0
  105. package/dist/harness/submissions.js +175 -0
  106. package/dist/harness/submissions.js.map +1 -0
  107. package/dist/harness/task-graph.d.ts +49 -0
  108. package/dist/harness/task-graph.d.ts.map +1 -0
  109. package/dist/harness/task-graph.js +168 -0
  110. package/dist/harness/task-graph.js.map +1 -0
  111. package/dist/harness/tool.d.ts +76 -0
  112. package/dist/harness/tool.d.ts.map +1 -0
  113. package/dist/harness/tool.js +736 -0
  114. package/dist/harness/tool.js.map +1 -0
  115. package/dist/harness/types.d.ts +713 -0
  116. package/dist/harness/types.d.ts.map +1 -0
  117. package/dist/harness/types.js +2 -0
  118. package/dist/harness/types.js.map +1 -0
  119. package/dist/harness/usage.d.ts +18 -0
  120. package/dist/harness/usage.d.ts.map +1 -0
  121. package/dist/harness/usage.js +57 -0
  122. package/dist/harness/usage.js.map +1 -0
  123. package/dist/harness/util.d.ts +19 -0
  124. package/dist/harness/util.d.ts.map +1 -0
  125. package/dist/harness/util.js +58 -0
  126. package/dist/harness/util.js.map +1 -0
  127. package/dist/harness/view.d.ts +47 -0
  128. package/dist/harness/view.d.ts.map +1 -0
  129. package/dist/harness/view.js +206 -0
  130. package/dist/harness/view.js.map +1 -0
  131. package/dist/ids.d.ts +6 -0
  132. package/dist/ids.d.ts.map +1 -0
  133. package/dist/ids.js +9 -0
  134. package/dist/ids.js.map +1 -0
  135. package/dist/images/cloudflare.d.ts +4 -0
  136. package/dist/images/cloudflare.d.ts.map +1 -0
  137. package/dist/images/cloudflare.js +9 -0
  138. package/dist/images/cloudflare.js.map +1 -0
  139. package/dist/images/exif.d.ts +3 -0
  140. package/dist/images/exif.d.ts.map +1 -0
  141. package/dist/images/exif.js +71 -0
  142. package/dist/images/exif.js.map +1 -0
  143. package/dist/images/index.d.ts +18 -0
  144. package/dist/images/index.d.ts.map +1 -0
  145. package/dist/images/index.js +125 -0
  146. package/dist/images/index.js.map +1 -0
  147. package/dist/images/node-worker.d.ts +17 -0
  148. package/dist/images/node-worker.d.ts.map +1 -0
  149. package/dist/images/node-worker.js +16 -0
  150. package/dist/images/node-worker.js.map +1 -0
  151. package/dist/images/node.d.ts +21 -0
  152. package/dist/images/node.d.ts.map +1 -0
  153. package/dist/images/node.js +121 -0
  154. package/dist/images/node.js.map +1 -0
  155. package/dist/index.d.ts +24 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +20 -0
  158. package/dist/index.js.map +1 -0
  159. package/dist/session/forks.d.ts +10 -0
  160. package/dist/session/forks.d.ts.map +1 -0
  161. package/dist/session/forks.js +42 -0
  162. package/dist/session/forks.js.map +1 -0
  163. package/dist/session/observation.d.ts +42 -0
  164. package/dist/session/observation.d.ts.map +1 -0
  165. package/dist/session/observation.js +281 -0
  166. package/dist/session/observation.js.map +1 -0
  167. package/dist/session/session.d.ts +133 -0
  168. package/dist/session/session.d.ts.map +1 -0
  169. package/dist/session/session.js +811 -0
  170. package/dist/session/session.js.map +1 -0
  171. package/dist/session/transaction.d.ts +119 -0
  172. package/dist/session/transaction.d.ts.map +1 -0
  173. package/dist/session/transaction.js +795 -0
  174. package/dist/session/transaction.js.map +1 -0
  175. package/dist/storage/jsonl/index.d.ts +2 -0
  176. package/dist/storage/jsonl/index.d.ts.map +1 -0
  177. package/dist/storage/jsonl/index.js +2 -0
  178. package/dist/storage/jsonl/index.js.map +1 -0
  179. package/dist/storage/jsonl/node.d.ts +6 -0
  180. package/dist/storage/jsonl/node.d.ts.map +1 -0
  181. package/dist/storage/jsonl/node.js +8 -0
  182. package/dist/storage/jsonl/node.js.map +1 -0
  183. package/dist/storage/jsonl/storage.d.ts +143 -0
  184. package/dist/storage/jsonl/storage.d.ts.map +1 -0
  185. package/dist/storage/jsonl/storage.js +682 -0
  186. package/dist/storage/jsonl/storage.js.map +1 -0
  187. package/dist/storage/memory.d.ts +62 -0
  188. package/dist/storage/memory.d.ts.map +1 -0
  189. package/dist/storage/memory.js +733 -0
  190. package/dist/storage/memory.js.map +1 -0
  191. package/dist/storage/scan.d.ts +18 -0
  192. package/dist/storage/scan.d.ts.map +1 -0
  193. package/dist/storage/scan.js +29 -0
  194. package/dist/storage/scan.js.map +1 -0
  195. package/dist/storage/sqlite/cloudflare.d.ts +46 -0
  196. package/dist/storage/sqlite/cloudflare.d.ts.map +1 -0
  197. package/dist/storage/sqlite/cloudflare.js +106 -0
  198. package/dist/storage/sqlite/cloudflare.js.map +1 -0
  199. package/dist/storage/sqlite/database.d.ts +37 -0
  200. package/dist/storage/sqlite/database.d.ts.map +1 -0
  201. package/dist/storage/sqlite/database.js +2 -0
  202. package/dist/storage/sqlite/database.js.map +1 -0
  203. package/dist/storage/sqlite/index.d.ts +4 -0
  204. package/dist/storage/sqlite/index.d.ts.map +1 -0
  205. package/dist/storage/sqlite/index.js +3 -0
  206. package/dist/storage/sqlite/index.js.map +1 -0
  207. package/dist/storage/sqlite/migrations.d.ts +11 -0
  208. package/dist/storage/sqlite/migrations.d.ts.map +1 -0
  209. package/dist/storage/sqlite/migrations.js +111 -0
  210. package/dist/storage/sqlite/migrations.js.map +1 -0
  211. package/dist/storage/sqlite/node.d.ts +41 -0
  212. package/dist/storage/sqlite/node.d.ts.map +1 -0
  213. package/dist/storage/sqlite/node.js +179 -0
  214. package/dist/storage/sqlite/node.js.map +1 -0
  215. package/dist/storage/sqlite/storage.d.ts +65 -0
  216. package/dist/storage/sqlite/storage.d.ts.map +1 -0
  217. package/dist/storage/sqlite/storage.js +670 -0
  218. package/dist/storage/sqlite/storage.js.map +1 -0
  219. package/dist/tasks.d.ts +6 -0
  220. package/dist/tasks.d.ts.map +1 -0
  221. package/dist/tasks.js +5 -0
  222. package/dist/tasks.js.map +1 -0
  223. package/dist/testing/assertions.d.ts +5 -0
  224. package/dist/testing/assertions.d.ts.map +1 -0
  225. package/dist/testing/assertions.js +25 -0
  226. package/dist/testing/assertions.js.map +1 -0
  227. package/dist/testing/env-conformance.d.ts +7 -0
  228. package/dist/testing/env-conformance.d.ts.map +1 -0
  229. package/dist/testing/env-conformance.js +431 -0
  230. package/dist/testing/env-conformance.js.map +1 -0
  231. package/dist/testing/index.d.ts +7 -0
  232. package/dist/testing/index.d.ts.map +1 -0
  233. package/dist/testing/index.js +6 -0
  234. package/dist/testing/index.js.map +1 -0
  235. package/dist/testing/runner.d.ts +12 -0
  236. package/dist/testing/runner.d.ts.map +1 -0
  237. package/dist/testing/runner.js +23 -0
  238. package/dist/testing/runner.js.map +1 -0
  239. package/dist/testing/storage-benchmark.d.ts +41 -0
  240. package/dist/testing/storage-benchmark.d.ts.map +1 -0
  241. package/dist/testing/storage-benchmark.js +350 -0
  242. package/dist/testing/storage-benchmark.js.map +1 -0
  243. package/dist/testing/storage-conformance.d.ts +4 -0
  244. package/dist/testing/storage-conformance.d.ts.map +1 -0
  245. package/dist/testing/storage-conformance.js +1213 -0
  246. package/dist/testing/storage-conformance.js.map +1 -0
  247. package/dist/testing/types.d.ts +37 -0
  248. package/dist/testing/types.d.ts.map +1 -0
  249. package/dist/testing/types.js +2 -0
  250. package/dist/testing/types.js.map +1 -0
  251. package/dist/tools/bash.d.ts +55 -0
  252. package/dist/tools/bash.d.ts.map +1 -0
  253. package/dist/tools/bash.js +138 -0
  254. package/dist/tools/bash.js.map +1 -0
  255. package/dist/tools/edit-diff.d.ts +89 -0
  256. package/dist/tools/edit-diff.d.ts.map +1 -0
  257. package/dist/tools/edit-diff.js +386 -0
  258. package/dist/tools/edit-diff.js.map +1 -0
  259. package/dist/tools/edit.d.ts +18 -0
  260. package/dist/tools/edit.d.ts.map +1 -0
  261. package/dist/tools/edit.js +117 -0
  262. package/dist/tools/edit.js.map +1 -0
  263. package/dist/tools/env.d.ts +5 -0
  264. package/dist/tools/env.d.ts.map +1 -0
  265. package/dist/tools/env.js +7 -0
  266. package/dist/tools/env.js.map +1 -0
  267. package/dist/tools/file-mutation-queue.d.ts +9 -0
  268. package/dist/tools/file-mutation-queue.d.ts.map +1 -0
  269. package/dist/tools/file-mutation-queue.js +52 -0
  270. package/dist/tools/file-mutation-queue.js.map +1 -0
  271. package/dist/tools/image-processor.d.ts +47 -0
  272. package/dist/tools/image-processor.d.ts.map +1 -0
  273. package/dist/tools/image-processor.js +30 -0
  274. package/dist/tools/image-processor.js.map +1 -0
  275. package/dist/tools/image.d.ts +20 -0
  276. package/dist/tools/image.d.ts.map +1 -0
  277. package/dist/tools/image.js +178 -0
  278. package/dist/tools/image.js.map +1 -0
  279. package/dist/tools/index.d.ts +46 -0
  280. package/dist/tools/index.d.ts.map +1 -0
  281. package/dist/tools/index.js +23 -0
  282. package/dist/tools/index.js.map +1 -0
  283. package/dist/tools/path-utils.d.ts +5 -0
  284. package/dist/tools/path-utils.d.ts.map +1 -0
  285. package/dist/tools/path-utils.js +26 -0
  286. package/dist/tools/path-utils.js.map +1 -0
  287. package/dist/tools/read.d.ts +25 -0
  288. package/dist/tools/read.d.ts.map +1 -0
  289. package/dist/tools/read.js +235 -0
  290. package/dist/tools/read.js.map +1 -0
  291. package/dist/tools/write.d.ts +10 -0
  292. package/dist/tools/write.d.ts.map +1 -0
  293. package/dist/tools/write.js +31 -0
  294. package/dist/tools/write.js.map +1 -0
  295. package/dist/truncate.d.ts +64 -0
  296. package/dist/truncate.d.ts.map +1 -0
  297. package/dist/truncate.js +154 -0
  298. package/dist/truncate.js.map +1 -0
  299. package/dist/types.d.ts +932 -0
  300. package/dist/types.d.ts.map +1 -0
  301. package/dist/types.js +3 -0
  302. package/dist/types.js.map +1 -0
  303. package/package.json +140 -5
@@ -0,0 +1,795 @@
1
+ import { copyJson } from "@ny4n/chord";
2
+ import { track } from "@ny4n/chord/delta";
3
+ import { addressId, checkRecordScope, checkRecordVersion, documentCreate, materializeDocumentValue, resolveAddress, } from "../documents.js";
4
+ import { ReadAfterWrite } from "../errors.js";
5
+ import { ROOT_CONVERSATION_ID } from "../types.js";
6
+ import { prepareForkDocumentCopies } from "./forks.js";
7
+ /**
8
+ * Complete record after applying one change. Placement turns a queued input `placed` and a queued write `done`; only a
9
+ * placed input can be answered. A settled record stays.
10
+ */
11
+ function applySubmissionChange(current, change) {
12
+ if (current.status === "done" || current.status === "unanswered")
13
+ return current;
14
+ if (change.status === "placed") {
15
+ if (current.status !== "queued")
16
+ throw new Error(`Submission ${current.id} is not queued`);
17
+ const status = current.type === "input" ? "placed" : "done";
18
+ return { ...current, status, entry: change.entry };
19
+ }
20
+ if (change.status === "done" && current.status !== "placed") {
21
+ throw new Error(`Submission ${current.id} is not a placed input`);
22
+ }
23
+ // Queued and placed records carry no answer, reason, or detail; an unanswered input keeps its entry.
24
+ return { ...current, ...change };
25
+ }
26
+ const INTERNAL_SCAN_PAGE_SIZE = 256;
27
+ const EMPTY_OPERATIONS = [];
28
+ const TABLE_JSON_COPY_OPTIONS = { omitUndefinedProperties: true };
29
+ /**
30
+ * Transaction for one Session commit callback.
31
+ *
32
+ * Every asynchronous operation is tracked so callback settlement can reject and drain unfinished work. Session calls
33
+ * one settlement method, then either discards prepared changes or adopts them once after Storage succeeds.
34
+ */
35
+ export class Transaction {
36
+ #host;
37
+ #context;
38
+ #scope;
39
+ #pendingOperations = new Set();
40
+ #sealed = false;
41
+ #hasTableWrite = false;
42
+ /** Atomic batch; conversation and entry writes stage eagerly, while task and document writes assemble later. */
43
+ #writes = [];
44
+ #createdConversationIds = new Set();
45
+ #forkSourceConversationIds = new Set();
46
+ #forkSourceDocumentIds = new Set();
47
+ /** One entry per task touched by a public read, candidate write, or document-owner lookup. */
48
+ #tasksById = new Map();
49
+ /** Submissions created by this transaction, by ID. */
50
+ #submissions = new Map();
51
+ /** Submission settlements and placements in staging order; resolved against the latest candidate record during assembly. */
52
+ #submissionChanges = [];
53
+ /** Write and publication plans of every staged incarnation, built during assembly. */
54
+ #plans = [];
55
+ /** Every document acquisition or retirement marker in staging order. */
56
+ #documents = [];
57
+ /** Latest transaction-local incarnation or retirement marker at each logical address. */
58
+ #latestDocumentByAddress = new Map();
59
+ constructor(host, context, scope = {}) {
60
+ this.#host = host;
61
+ this.#context = context;
62
+ this.#scope = scope;
63
+ }
64
+ // ─── Table reads ────────────────────────────────────────────────────────
65
+ conversation(id) {
66
+ return this.#read("conversation", () => this.#host.storage.conversation(id, this.#context));
67
+ }
68
+ entry(first, second) {
69
+ const kind = typeof first === "number" ? undefined : first.kind;
70
+ const id = typeof first === "number" ? first : second;
71
+ return this.#read("entry", async () => {
72
+ const entry = (await this.#host.storage.entry(id, this.#context))?.entry;
73
+ return kind === undefined || entry?.kind === kind ? entry : undefined;
74
+ });
75
+ }
76
+ task(id) {
77
+ return this.#read("task", () => this.#committedTask(id));
78
+ }
79
+ scanConversations(query, limit, cursor) {
80
+ return this.#read("scanConversations", () => this.#host.storage.scanConversations(query, limit, cursor, this.#context));
81
+ }
82
+ scanEntries(query, limit, cursor) {
83
+ return this.#read("scanEntries", () => this.#host.storage.scanEntries(query, limit, cursor, this.#context));
84
+ }
85
+ latestHeadMarker(conversationId) {
86
+ return this.#read("latestHeadMarker", () => this.#host.storage.findLatestHeadMarker(conversationId, undefined, this.#context));
87
+ }
88
+ scanTasks(query, limit, cursor) {
89
+ return this.#read("scanTasks", () => this.#host.storage.scanTasks(query, limit, cursor, this.#context));
90
+ }
91
+ /** Internal: committed submission record. */
92
+ submission(id) {
93
+ return this.#read("submission", () => this.#host.storage.submission(id, this.#context));
94
+ }
95
+ /** Committed submission with a conversation-scoped request ID. */
96
+ submissionByRequest(conversationId, requestId) {
97
+ return this.#read("submissionByRequest", () => this.#host.storage.submissionByRequest(conversationId, requestId, this.#context));
98
+ }
99
+ // ─── Table writes ───────────────────────────────────────────────────────
100
+ createConversation(options) {
101
+ return this.#write(() => this.#stageConversation(undefined, options.ownership));
102
+ }
103
+ /** Internal final-form bootstrap path for the reserved root identity. */
104
+ createRootConversation() {
105
+ return this.#write(() => this.#stageConversation(undefined, { kind: "ownerless" }, ROOT_CONVERSATION_ID));
106
+ }
107
+ forkConversation(parentConversationId, at, options) {
108
+ return this.#write(() => this.#stageConversation({ conversationId: parentConversationId, at }, options.ownership));
109
+ }
110
+ async #stageConversation(parent, ownership, reservedId) {
111
+ const ownerTaskId = ownership.kind === "task" ? ownership.taskId : undefined;
112
+ const id = reservedId ?? (await this.#host.storage.mintId());
113
+ this.#assertOpen();
114
+ let owner;
115
+ if (ownerTaskId !== undefined) {
116
+ const task = await this.#currentTask(ownerTaskId);
117
+ this.#assertOpen();
118
+ if (task === undefined)
119
+ throw new Error(`Conversation owner task ${ownerTaskId} does not exist`);
120
+ owner = { conversationId: task.conversationId, taskId: ownerTaskId };
121
+ }
122
+ const record = {
123
+ id,
124
+ ...(parent === undefined ? {} : { parent }),
125
+ ...(owner === undefined ? {} : { owner }),
126
+ };
127
+ const copies = parent === undefined
128
+ ? []
129
+ : await prepareForkDocumentCopies(this.#host.storage, parent.conversationId, parent.at, id, this.#context);
130
+ this.#assertOpen();
131
+ for (const copy of copies) {
132
+ this.#forkSourceDocumentIds.add(copy.source.id);
133
+ const entry = {
134
+ addressId: addressId(copy.record),
135
+ address: copy.record,
136
+ target: { kind: "fork-copy", ...copy },
137
+ retireOnCommit: false,
138
+ };
139
+ this.#documents.push(entry);
140
+ this.#latestDocumentByAddress.set(entry.addressId, entry);
141
+ }
142
+ if (parent !== undefined)
143
+ this.#forkSourceConversationIds.add(parent.conversationId);
144
+ this.#createdConversationIds.add(id);
145
+ this.#writes.push({ type: "conversation", value: record });
146
+ await this.#host.conversationCreated(this, record);
147
+ this.#assertOpen();
148
+ return record;
149
+ }
150
+ appendEntry(first, second, third) {
151
+ if (typeof first === "number")
152
+ return this.#appendEntry(first, second);
153
+ return this.#appendEntry(second, { ...third, kind: first.kind });
154
+ }
155
+ #appendEntry(conversationId, value) {
156
+ return this.#write(async () => {
157
+ await this.#requireConversation(conversationId);
158
+ this.#assertOpen();
159
+ const id = await this.#host.storage.mintId();
160
+ this.#assertOpen();
161
+ const { head, ...rest } = value;
162
+ // Undefined `head` and `byTaskId` are omitted by the copy.
163
+ const record = copyJson({
164
+ ...rest,
165
+ id,
166
+ conversationId,
167
+ head: head === "self" ? id : head,
168
+ byTaskId: this.#scope.taskId,
169
+ }, TABLE_JSON_COPY_OPTIONS);
170
+ this.#writes.push({ type: "entry", value: record });
171
+ return record;
172
+ });
173
+ }
174
+ createTask(task, input, options) {
175
+ return this.#write(async () => {
176
+ const ownership = options.ownership;
177
+ let owner;
178
+ if (ownership.kind === "task") {
179
+ // Validated again against the owner's final candidate during assembly.
180
+ owner = await this.#currentTask(ownership.taskId);
181
+ this.#assertOpen();
182
+ if (owner === undefined)
183
+ throw new Error(`Task owner ${ownership.taskId} does not exist`);
184
+ if (options.background === true)
185
+ throw new TypeError("A child task cannot be background");
186
+ if (options.conversationId !== undefined && options.conversationId !== owner.conversationId) {
187
+ throw new Error(`A child task lives in its owner's conversation ${owner.conversationId}`);
188
+ }
189
+ }
190
+ const conversationId = owner?.conversationId ?? options.conversationId ?? this.#scope.conversationId;
191
+ if (conversationId === undefined)
192
+ throw new TypeError("Tx.createTask() requires options.conversationId");
193
+ await this.#requireConversation(conversationId);
194
+ this.#assertOpen();
195
+ const definition = task.definition;
196
+ const checkpoint = definition.initial(input);
197
+ const id = await this.#host.storage.mintId();
198
+ this.#assertOpen();
199
+ const record = copyJson({
200
+ id,
201
+ conversationId,
202
+ kind: definition.name,
203
+ version: definition.version,
204
+ input,
205
+ ...(owner === undefined ? {} : { owner: owner.id }),
206
+ background: options.background ?? false,
207
+ abortRequested: false,
208
+ ...(options.abandonOnRestart === true ? { abandonOnRestart: true } : {}),
209
+ state: { status: "pending", checkpoint },
210
+ }, TABLE_JSON_COPY_OPTIONS);
211
+ this.#tasksById.set(id, { write: { kind: "create", record } });
212
+ return id;
213
+ });
214
+ }
215
+ /** Create a raw submission record with a fresh ID; no admission rules apply. */
216
+ createSubmission(create) {
217
+ return this.#write(async () => {
218
+ await this.#requireConversation(create.conversationId);
219
+ this.#assertOpen();
220
+ const id = await this.#host.storage.mintId();
221
+ this.#assertOpen();
222
+ const record = copyJson({ ...create, id }, TABLE_JSON_COPY_OPTIONS);
223
+ this.#submissions.set(id, record);
224
+ return record;
225
+ });
226
+ }
227
+ /**
228
+ * Settle a submission. Resolved during assembly against the transaction's latest candidate record, falling back to
229
+ * committed state, so it is not a caller table read and works after the first table write.
230
+ */
231
+ settleSubmission(id, settlement) {
232
+ this.#assertOpen();
233
+ this.#hasTableWrite = true;
234
+ this.#submissionChanges.push({ id, change: copyJson(settlement) });
235
+ }
236
+ /** Place a queued submission at `entry`; resolved during assembly like `settleSubmission()`. */
237
+ placeSubmission(id, entry) {
238
+ this.#assertOpen();
239
+ this.#hasTableWrite = true;
240
+ this.#submissionChanges.push({ id, change: { status: "placed", entry } });
241
+ }
242
+ /** Internal: replace one task record completely. Tasks change their own state through their runtime. */
243
+ setTask(value) {
244
+ this.#assertOpen();
245
+ this.#hasTableWrite = true;
246
+ const task = this.#taskEntry(value.id);
247
+ const candidate = task.write?.record;
248
+ if (candidate?.state.status === "terminal") {
249
+ throw new Error(`Task ${value.id} already has a terminal candidate`);
250
+ }
251
+ if (candidate !== undefined && candidate.conversationId !== value.conversationId) {
252
+ throw new Error(`Task ${value.id} cannot change conversations`);
253
+ }
254
+ task.write = {
255
+ kind: task.write?.kind === "create" ? "create" : "replace",
256
+ record: copyJson(this.#stampTimes(value, candidate), TABLE_JSON_COPY_OPTIONS),
257
+ };
258
+ }
259
+ /** Internal: candidate records of the tasks this transaction created or replaced so far. */
260
+ stagedTasks() {
261
+ const records = [];
262
+ for (const task of this.#tasksById.values())
263
+ if (task.write !== undefined)
264
+ records.push(task.write.record);
265
+ return records;
266
+ }
267
+ /** Internal: conversations this transaction created or forked so far. */
268
+ stagedConversations() {
269
+ const records = [];
270
+ for (const write of this.#writes)
271
+ if (write.type === "conversation")
272
+ records.push(write.value);
273
+ return records;
274
+ }
275
+ doc(token, ...args) {
276
+ try {
277
+ this.#assertOpen();
278
+ const definition = token.definition;
279
+ const resolved = resolveAddress(definition, args);
280
+ this.#assertTaskDocumentsOpen(resolved);
281
+ const latest = this.#latestDocumentByAddress.get(resolved.id);
282
+ if (latest !== undefined && !latest.retireOnCommit) {
283
+ if (latest.draftPromise !== undefined)
284
+ return latest.draftPromise;
285
+ if (latest.target?.kind === "fork-copy") {
286
+ latest.draftPromise = this.#track(this.#acquireForkCopy(latest, definition, latest.target));
287
+ return latest.draftPromise;
288
+ }
289
+ }
290
+ const seed = definition.family === true ? copyJson(args[resolved.nextArgument]) : undefined;
291
+ const docEntry = {
292
+ addressId: resolved.id,
293
+ address: resolved.address,
294
+ definition,
295
+ retireOnCommit: false,
296
+ };
297
+ this.#documents.push(docEntry);
298
+ this.#latestDocumentByAddress.set(docEntry.addressId, docEntry);
299
+ // Capture retirement before awaiting so a pending old acquisition and its replacement stay distinct.
300
+ docEntry.draftPromise = this.#track(this.#acquire(docEntry, seed, latest?.retireOnCommit === true));
301
+ return docEntry.draftPromise;
302
+ }
303
+ catch (error) {
304
+ return Promise.reject(error);
305
+ }
306
+ }
307
+ retireDoc(token, ...args) {
308
+ try {
309
+ this.#assertOpen();
310
+ const definition = token.definition;
311
+ const resolved = resolveAddress(definition, args);
312
+ const latest = this.#latestDocumentByAddress.get(resolved.id);
313
+ if (latest?.retireOnCommit)
314
+ return Promise.resolve();
315
+ if (latest?.target?.kind === "fork-copy") {
316
+ checkRecordScope(definition, latest.target.record);
317
+ latest.retireOnCommit = true;
318
+ return Promise.resolve();
319
+ }
320
+ if (latest?.draftPromise !== undefined) {
321
+ // Retirement of an acquired draft persists its final content before retirement.
322
+ latest.retireOnCommit = true;
323
+ return this.#track(latest.draftPromise.then(() => undefined));
324
+ }
325
+ const entry = {
326
+ addressId: resolved.id,
327
+ address: resolved.address,
328
+ definition,
329
+ retireOnCommit: true,
330
+ };
331
+ this.#documents.push(entry);
332
+ this.#latestDocumentByAddress.set(entry.addressId, entry);
333
+ return this.#track(this.#findRetirement(entry));
334
+ }
335
+ catch (error) {
336
+ return Promise.reject(error);
337
+ }
338
+ }
339
+ async #acquire(entry, seed, skipLoad) {
340
+ const definition = entry.definition;
341
+ const loaded = skipLoad
342
+ ? undefined
343
+ : await this.#host.load(definition, entry.addressId, entry.address, this.#context);
344
+ this.#assertOpen();
345
+ if (loaded !== undefined) {
346
+ checkRecordScope(definition, loaded.record);
347
+ checkRecordVersion(definition, loaded.record, loaded.storedVersion);
348
+ entry.target = { kind: "loaded", document: loaded };
349
+ entry.change = loaded.tracker.beginChange();
350
+ return entry.change.state;
351
+ }
352
+ const scope = entry.address.scope;
353
+ if (scope.kind === "conversation")
354
+ await this.#requireConversation(scope.conversationId);
355
+ if (scope.kind === "task") {
356
+ const task = await this.#currentTask(scope.taskId);
357
+ if (task === undefined)
358
+ throw new Error(`Task ${scope.taskId} does not exist`);
359
+ if (task.state.status === "terminal")
360
+ throw new Error(`Task ${scope.taskId} is terminal`);
361
+ }
362
+ this.#assertOpen();
363
+ const value = copyJson(definition.family === true ? definition.initial(seed) : definition.initial());
364
+ const id = await this.#host.storage.mintId();
365
+ this.#assertOpen();
366
+ const tracker = track(value);
367
+ entry.target = {
368
+ kind: "created",
369
+ record: documentCreate(definition, entry.address, id),
370
+ version: definition.version,
371
+ tracker,
372
+ };
373
+ entry.change = tracker.beginChange();
374
+ return entry.change.state;
375
+ }
376
+ async #acquireForkCopy(entry, definition, target) {
377
+ const stored = await this.#host.storage.document(target.source.id, target.source.at, this.#context);
378
+ this.#assertOpen();
379
+ if (stored === undefined) {
380
+ throw new Error(`Fork source document ${target.source.id} cannot be read`);
381
+ }
382
+ if (stored.record.scope.kind !== "conversation" ||
383
+ stored.record.kind !== target.record.kind ||
384
+ stored.record.key !== target.record.key ||
385
+ stored.record.history !== target.record.history ||
386
+ stored.record.fork !== target.record.fork) {
387
+ throw new Error(`Fork source document ${target.source.id} does not match the copied record`);
388
+ }
389
+ const value = materializeDocumentValue(definition, target.record, stored.version, stored.value);
390
+ const tracker = track(value);
391
+ entry.definition = definition;
392
+ entry.target = { kind: "created", record: target.record, version: definition.version, tracker };
393
+ entry.change = tracker.beginChange();
394
+ return entry.change.state;
395
+ }
396
+ async #findRetirement(entry) {
397
+ const record = this.#host.cached(entry.addressId)?.record ??
398
+ (await this.#host.storage.findDocument(entry.address, "current", this.#context));
399
+ this.#assertOpen();
400
+ if (record === undefined)
401
+ return;
402
+ checkRecordScope(entry.definition, record);
403
+ entry.target = { kind: "retire-only", record };
404
+ }
405
+ // ─── Settlement ─────────────────────────────────────────────────────────
406
+ /** Seal after callback failure: abort every change and observe every pending operation. */
407
+ async settleFailure() {
408
+ this.#sealed = true;
409
+ this.#abortChanges();
410
+ await Promise.allSettled(this.#pendingOperations);
411
+ }
412
+ /**
413
+ * Seal after callback success, prepare every open change, and assemble the atomic batch.
414
+ * Any failure aborts every change before Storage admission.
415
+ */
416
+ async settleSuccess() {
417
+ this.#sealed = true;
418
+ if (this.#pendingOperations.size > 0) {
419
+ this.#abortChanges();
420
+ await Promise.allSettled(this.#pendingOperations);
421
+ throw new Error("Session commit callback settled before its pending Tx operations");
422
+ }
423
+ try {
424
+ // Synchronously prepare every open change; this revokes every draft.
425
+ for (const document of this.#documents) {
426
+ if (document.change !== undefined)
427
+ document.prepared = document.change.prepare();
428
+ }
429
+ return await this.#assemble();
430
+ }
431
+ catch (error) {
432
+ this.#abortChanges();
433
+ throw error;
434
+ }
435
+ }
436
+ /** Abort every prepared change after Storage failure or when no write is required. */
437
+ discard() {
438
+ this.#abortChanges();
439
+ }
440
+ /** Adopt every prepared change by pointer swap after Storage success and describe the publication. */
441
+ adopt(seq) {
442
+ const publications = [];
443
+ for (const plan of this.#plans) {
444
+ const committed = "createdAt" in plan.record;
445
+ let record = committed ? plan.record : { ...plan.record, createdAt: seq };
446
+ if (plan.retire)
447
+ record = { ...record, retiredAt: seq };
448
+ const change = plan.change;
449
+ if (change !== undefined) {
450
+ const { tracker, prepared, loaded, version } = change;
451
+ // A new incarnation is adopted unless it retires in the same commit; a loaded one only when it changed.
452
+ if (loaded === undefined ? !plan.retire : prepared.ops.length > 0)
453
+ tracker.adopt(prepared);
454
+ else
455
+ prepared.abort();
456
+ if (loaded !== undefined) {
457
+ if (loaded.storedVersion < version)
458
+ loaded.storedVersion = version;
459
+ if (plan.content?.type === "document.change") {
460
+ if (plan.content.content.kind === "base")
461
+ loaded.deltasSinceBase = 0;
462
+ else
463
+ loaded.deltasSinceBase++;
464
+ }
465
+ }
466
+ else if (!plan.retire) {
467
+ this.#host.install({
468
+ addressId: plan.addressId,
469
+ record,
470
+ storedVersion: version,
471
+ valueVersion: version,
472
+ deltasSinceBase: 0,
473
+ tracker,
474
+ });
475
+ }
476
+ }
477
+ const conversationId = plan.conversationId;
478
+ if (plan.retire) {
479
+ if (committed)
480
+ this.#host.evict(plan.addressId, record.id);
481
+ const retired = { version: undefined, value: null, ops: EMPTY_OPERATIONS };
482
+ publications.push({ type: "document", record, conversationId, ...retired });
483
+ }
484
+ else if (plan.content?.type === "document.copy") {
485
+ publications.push({
486
+ type: "document.copy",
487
+ record,
488
+ conversationId: conversationId,
489
+ source: plan.content.source,
490
+ });
491
+ }
492
+ else if (change !== undefined && publishes(plan)) {
493
+ const ops = change.loaded === undefined ? EMPTY_OPERATIONS : change.prepared.ops;
494
+ publications.push({
495
+ type: "document",
496
+ record,
497
+ conversationId,
498
+ version: change.version,
499
+ value: change.prepared.value,
500
+ ops,
501
+ });
502
+ }
503
+ }
504
+ return publications;
505
+ }
506
+ async #assemble() {
507
+ const storage = this.#host.storage;
508
+ const plans = this.#plans;
509
+ for (const document of this.#documents) {
510
+ const plan = planDocument(document);
511
+ if (plan !== undefined)
512
+ plans.push(plan);
513
+ }
514
+ this.#rejectForkSourceWrites(plans);
515
+ await this.#validateOwners();
516
+ for (const [id, task] of this.#tasksById) {
517
+ if (task.write?.kind !== "replace")
518
+ continue;
519
+ const committed = await this.#committedTask(id);
520
+ if (committed === undefined)
521
+ throw new Error(`Task ${id} does not exist`);
522
+ if (committed.state.status === "terminal")
523
+ throw new Error(`Task ${id} is already terminal`);
524
+ if (committed.conversationId !== task.write.record.conversationId) {
525
+ throw new Error(`Task ${id} cannot change conversations`);
526
+ }
527
+ }
528
+ // Terminal settlement retires every task document, including ones created by this transaction.
529
+ const terminalTaskIds = new Set();
530
+ for (const task of this.#tasksById.values()) {
531
+ if (task.write?.record.state.status === "terminal")
532
+ terminalTaskIds.add(task.write.record.id);
533
+ }
534
+ if (terminalTaskIds.size > 0) {
535
+ const retiring = new Set();
536
+ for (const plan of plans) {
537
+ const scope = plan.record.scope;
538
+ if (scope.kind !== "task" || !terminalTaskIds.has(scope.taskId))
539
+ continue;
540
+ plan.retire = true;
541
+ retiring.add(plan.record.id);
542
+ }
543
+ for (const taskId of terminalTaskIds) {
544
+ if (this.#tasksById.get(taskId)?.write?.kind === "create")
545
+ continue;
546
+ let cursor;
547
+ do {
548
+ const page = await storage.scanDocuments({ scope: { kind: "task", taskId }, at: "current" }, INTERNAL_SCAN_PAGE_SIZE, cursor, this.#context);
549
+ for (const record of page.items) {
550
+ if (retiring.has(record.id))
551
+ continue;
552
+ plans.push({ addressId: addressId(record), record, retire: true });
553
+ retiring.add(record.id);
554
+ }
555
+ cursor = page.next;
556
+ } while (cursor !== undefined);
557
+ }
558
+ }
559
+ // Resolve publication ownership before Storage admission so adoption remains synchronous.
560
+ for (const plan of plans) {
561
+ if (!publishes(plan))
562
+ continue;
563
+ const scope = plan.record.scope;
564
+ if (scope.kind === "conversation")
565
+ plan.conversationId = scope.conversationId;
566
+ if (scope.kind !== "task")
567
+ continue;
568
+ const task = this.#taskEntry(scope.taskId);
569
+ if (task.publicationConversationId === undefined) {
570
+ const current = await this.#currentTask(scope.taskId);
571
+ if (current !== undefined)
572
+ task.publicationConversationId = current.conversationId;
573
+ }
574
+ plan.conversationId = task.publicationConversationId;
575
+ }
576
+ for (const { id, change } of this.#submissionChanges) {
577
+ const current = this.#submissions.get(id) ?? (await storage.submission(id, this.#context));
578
+ if (current === undefined)
579
+ throw new Error(`Submission ${id} does not exist`);
580
+ const next = applySubmissionChange(current, change);
581
+ if (next !== current)
582
+ this.#submissions.set(id, next);
583
+ }
584
+ const writes = this.#writes;
585
+ for (const value of this.#submissions.values())
586
+ writes.push({ type: "submission", value });
587
+ for (const task of this.#tasksById.values()) {
588
+ if (task.write !== undefined)
589
+ writes.push({ type: "task", value: task.write.record });
590
+ }
591
+ for (const plan of plans) {
592
+ const change = plan.change;
593
+ // Checkpoint predicates run last, after every validation.
594
+ if (plan.content?.type === "document.change" && plan.content.content.kind === "delta" && change?.loaded) {
595
+ const { prepared, version } = change;
596
+ const info = { deltasSinceBase: change.loaded.deltasSinceBase };
597
+ if (change.definition?.checkpointWhen?.(prepared.value, prepared.ops, info)) {
598
+ plan.content = { ...plan.content, content: { version, kind: "base", value: prepared.value } };
599
+ }
600
+ }
601
+ if (plan.content !== undefined)
602
+ writes.push(plan.content);
603
+ if (plan.retire)
604
+ writes.push({ type: "document.retire", id: plan.record.id });
605
+ }
606
+ return writes;
607
+ }
608
+ // ─── Helpers ────────────────────────────────────────────────────────────
609
+ /**
610
+ * New owned work needs a live owner, judged on the owner's final candidate: not `completing`, terminal, or
611
+ * abort-marked. A task therefore cannot create owned work in the commit that finishes it (spec §5.5).
612
+ */
613
+ async #validateOwners() {
614
+ const owners = [];
615
+ for (const write of this.#writes) {
616
+ if (write.type !== "conversation" || write.value.owner === undefined)
617
+ continue;
618
+ owners.push({ what: "Conversation owner task", taskId: write.value.owner.taskId });
619
+ }
620
+ for (const task of this.#tasksById.values()) {
621
+ const record = task.write?.kind === "create" ? task.write.record : undefined;
622
+ if (record?.owner !== undefined)
623
+ owners.push({ what: "Task owner", taskId: record.owner });
624
+ }
625
+ for (const { what, taskId } of owners) {
626
+ const task = await this.#currentTask(taskId);
627
+ if (task === undefined)
628
+ throw new Error(`${what} ${taskId} does not exist`);
629
+ if (task.state.status === "terminal" || task.state.status === "completing") {
630
+ throw new Error(`${what} ${taskId} is ${task.state.status}`);
631
+ }
632
+ if (task.abortRequested)
633
+ throw new Error(`${what} ${taskId} is abort-marked`);
634
+ }
635
+ }
636
+ #rejectForkSourceWrites(plans) {
637
+ for (const plan of plans) {
638
+ if (plan.content === undefined && !plan.retire)
639
+ continue;
640
+ const record = plan.record;
641
+ if (this.#forkSourceDocumentIds.has(record.id)) {
642
+ throw new Error(`Cannot change fork source document ${record.id} in the fork transaction`);
643
+ }
644
+ const scope = plan.record.scope;
645
+ if (scope.kind === "conversation" &&
646
+ this.#forkSourceConversationIds.has(scope.conversationId) &&
647
+ record.scope.kind === "conversation" &&
648
+ record.fork === "current") {
649
+ throw new Error(`Cannot fork conversation ${scope.conversationId} while changing its current-policy documents`);
650
+ }
651
+ }
652
+ }
653
+ #abortChanges() {
654
+ for (const document of this.#documents)
655
+ document.change?.abort();
656
+ }
657
+ #assertOpen() {
658
+ if (this.#sealed)
659
+ throw new Error("Transaction has settled");
660
+ }
661
+ #assertTaskDocumentsOpen(resolved) {
662
+ const scope = resolved.address.scope;
663
+ if (scope.kind === "task" && this.#tasksById.get(scope.taskId)?.write?.record.state.status === "terminal") {
664
+ throw new Error(`Task ${scope.taskId} is terminal`);
665
+ }
666
+ }
667
+ /** Register an operation so callback settlement can reject and drain it. */
668
+ #track(operation) {
669
+ this.#pendingOperations.add(operation);
670
+ const settle = () => {
671
+ this.#pendingOperations.delete(operation);
672
+ };
673
+ operation.then(settle, settle);
674
+ return operation;
675
+ }
676
+ #read(method, read) {
677
+ try {
678
+ this.#assertOpen();
679
+ if (this.#hasTableWrite)
680
+ throw new ReadAfterWrite(method);
681
+ return this.#track(read());
682
+ }
683
+ catch (error) {
684
+ return Promise.reject(error);
685
+ }
686
+ }
687
+ #write(write) {
688
+ try {
689
+ this.#assertOpen();
690
+ this.#hasTableWrite = true;
691
+ return this.#track(write());
692
+ }
693
+ catch (error) {
694
+ return Promise.reject(error);
695
+ }
696
+ }
697
+ async #requireConversation(id) {
698
+ if (this.#createdConversationIds.has(id))
699
+ return;
700
+ if ((await this.#host.storage.conversation(id, this.#context)) === undefined) {
701
+ throw new Error(`Conversation ${id} does not exist`);
702
+ }
703
+ }
704
+ /**
705
+ * Lifecycle times: `startedAt` on the first change to `running`, `endedAt` on the change to `terminal`. Once set,
706
+ * they carry over from the replaced record; records written before they existed lack them.
707
+ */
708
+ #stampTimes(value, candidate) {
709
+ const status = value.state.status;
710
+ const now = (stamps) => (stamps ? this.#host.now() : undefined);
711
+ const startedAt = candidate?.startedAt ?? value.startedAt ?? now(status === "running");
712
+ const endedAt = candidate?.endedAt ?? value.endedAt ?? now(status === "terminal");
713
+ if (startedAt === value.startedAt && endedAt === value.endedAt)
714
+ return value;
715
+ return {
716
+ ...value,
717
+ ...(startedAt === undefined ? {} : { startedAt }),
718
+ ...(endedAt === undefined ? {} : { endedAt }),
719
+ };
720
+ }
721
+ #taskEntry(id) {
722
+ let task = this.#tasksById.get(id);
723
+ if (task === undefined) {
724
+ task = {};
725
+ this.#tasksById.set(id, task);
726
+ }
727
+ return task;
728
+ }
729
+ /** Latest candidate task record, falling back to committed state; not a caller table read. */
730
+ async #currentTask(id) {
731
+ return this.#tasksById.get(id)?.write?.record ?? (await this.#committedTask(id));
732
+ }
733
+ #committedTask(id) {
734
+ const task = this.#taskEntry(id);
735
+ task.committedRead ??= this.#host.storage.task(id, this.#context);
736
+ return task.committedRead;
737
+ }
738
+ }
739
+ /** Plan of one staged document: its record, content write, and prepared change. Retirement is decided later. */
740
+ function planDocument(document) {
741
+ const target = document.target;
742
+ if (target === undefined)
743
+ return undefined;
744
+ const plan = { addressId: document.addressId, retire: document.retireOnCommit };
745
+ switch (target.kind) {
746
+ case "created": {
747
+ const prepared = document.prepared;
748
+ const { record, version, tracker } = target;
749
+ const content = { version, kind: "base", value: prepared.value };
750
+ return {
751
+ ...plan,
752
+ record,
753
+ content: { type: "document.create", record, content },
754
+ change: { tracker, prepared, version },
755
+ };
756
+ }
757
+ case "fork-copy":
758
+ return {
759
+ ...plan,
760
+ record: target.record,
761
+ content: { type: "document.copy", record: target.record, source: target.source },
762
+ };
763
+ case "retire-only":
764
+ return { ...plan, record: target.record };
765
+ case "loaded": {
766
+ const loaded = target.document;
767
+ const definition = document.definition;
768
+ const prepared = document.prepared;
769
+ const version = definition.version;
770
+ const id = loaded.record.id;
771
+ // A version change stores a base even without operations; otherwise only a change stores a delta.
772
+ const content = loaded.storedVersion < version
773
+ ? { type: "document.change", id, content: { version, kind: "base", value: prepared.value } }
774
+ : prepared.ops.length > 0
775
+ ? { type: "document.change", id, content: { version, kind: "delta", ops: prepared.ops } }
776
+ : undefined;
777
+ const change = {
778
+ tracker: loaded.tracker,
779
+ prepared,
780
+ version,
781
+ loaded,
782
+ definition,
783
+ };
784
+ return { ...plan, record: loaded.record, change, ...(content === undefined ? {} : { content }) };
785
+ }
786
+ }
787
+ }
788
+ /**
789
+ * Whether adoption publishes the plan: every creation, copy, and retirement, and a loaded incarnation that writes
790
+ * content, which includes a migration-only base so observers of the older shape receive the new value.
791
+ */
792
+ function publishes(plan) {
793
+ return plan.retire || plan.change?.loaded === undefined || plan.content !== undefined;
794
+ }
795
+ //# sourceMappingURL=transaction.js.map