@earendil-works/pi-durable 1.4.0 → 1.5.1

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 (270) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/README.md +195 -85
  3. package/dist/codemode/codemode.d.ts +6 -5
  4. package/dist/codemode/codemode.d.ts.map +1 -1
  5. package/dist/codemode/codemode.js +10 -49
  6. package/dist/codemode/codemode.js.map +1 -1
  7. package/dist/codemode/models.d.ts +2 -1
  8. package/dist/codemode/models.d.ts.map +1 -1
  9. package/dist/codemode/models.js +4 -4
  10. package/dist/codemode/models.js.map +1 -1
  11. package/dist/documents.d.ts +1 -1
  12. package/dist/documents.d.ts.map +1 -1
  13. package/dist/documents.js +1 -1
  14. package/dist/documents.js.map +1 -1
  15. package/dist/entries.d.ts +1 -1
  16. package/dist/entries.d.ts.map +1 -1
  17. package/dist/entries.js.map +1 -1
  18. package/dist/env/index.d.ts +13 -11
  19. package/dist/env/index.d.ts.map +1 -1
  20. package/dist/env/index.js.map +1 -1
  21. package/dist/env/node-watch.d.ts +4 -4
  22. package/dist/env/node-watch.d.ts.map +1 -1
  23. package/dist/env/node-watch.js +18 -16
  24. package/dist/env/node-watch.js.map +1 -1
  25. package/dist/env/node.d.ts +3 -3
  26. package/dist/env/node.d.ts.map +1 -1
  27. package/dist/env/node.js +1 -1
  28. package/dist/env/node.js.map +1 -1
  29. package/dist/errors.d.ts +18 -2
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +26 -3
  32. package/dist/errors.js.map +1 -1
  33. package/dist/harness/agent.d.ts.map +1 -1
  34. package/dist/harness/agent.js +37 -3
  35. package/dist/harness/agent.js.map +1 -1
  36. package/dist/harness/compaction.d.ts +8 -3
  37. package/dist/harness/compaction.d.ts.map +1 -1
  38. package/dist/harness/compaction.js +40 -11
  39. package/dist/harness/compaction.js.map +1 -1
  40. package/dist/harness/context.d.ts +24 -8
  41. package/dist/harness/context.d.ts.map +1 -1
  42. package/dist/harness/context.js +71 -25
  43. package/dist/harness/context.js.map +1 -1
  44. package/dist/harness/define.d.ts +8 -4
  45. package/dist/harness/define.d.ts.map +1 -1
  46. package/dist/harness/define.js +4 -0
  47. package/dist/harness/define.js.map +1 -1
  48. package/dist/harness/generation.d.ts +39 -6
  49. package/dist/harness/generation.d.ts.map +1 -1
  50. package/dist/harness/generation.js +451 -100
  51. package/dist/harness/generation.js.map +1 -1
  52. package/dist/harness/harness.d.ts +3 -1
  53. package/dist/harness/harness.d.ts.map +1 -1
  54. package/dist/harness/harness.js +87 -62
  55. package/dist/harness/harness.js.map +1 -1
  56. package/dist/harness/inbox.d.ts +7 -2
  57. package/dist/harness/inbox.d.ts.map +1 -1
  58. package/dist/harness/inbox.js +5 -2
  59. package/dist/harness/inbox.js.map +1 -1
  60. package/dist/harness/json.d.ts +21 -5
  61. package/dist/harness/json.d.ts.map +1 -1
  62. package/dist/harness/json.js +23 -5
  63. package/dist/harness/json.js.map +1 -1
  64. package/dist/harness/live.d.ts +124 -17
  65. package/dist/harness/live.d.ts.map +1 -1
  66. package/dist/harness/live.js +93 -12
  67. package/dist/harness/live.js.map +1 -1
  68. package/dist/harness/partial-changes.d.ts +54 -0
  69. package/dist/harness/partial-changes.d.ts.map +1 -0
  70. package/dist/harness/partial-changes.js +163 -0
  71. package/dist/harness/partial-changes.js.map +1 -0
  72. package/dist/harness/prompt.d.ts +1 -1
  73. package/dist/harness/prompt.d.ts.map +1 -1
  74. package/dist/harness/prompt.js.map +1 -1
  75. package/dist/harness/provider.d.ts +1 -1
  76. package/dist/harness/provider.d.ts.map +1 -1
  77. package/dist/harness/provider.js.map +1 -1
  78. package/dist/harness/registry-reader.d.ts +11 -0
  79. package/dist/harness/registry-reader.d.ts.map +1 -0
  80. package/dist/harness/registry-reader.js +25 -0
  81. package/dist/harness/registry-reader.js.map +1 -0
  82. package/dist/harness/scheduler.d.ts +48 -20
  83. package/dist/harness/scheduler.d.ts.map +1 -1
  84. package/dist/harness/scheduler.js +1006 -380
  85. package/dist/harness/scheduler.js.map +1 -1
  86. package/dist/harness/submissions.d.ts +18 -11
  87. package/dist/harness/submissions.d.ts.map +1 -1
  88. package/dist/harness/submissions.js +63 -26
  89. package/dist/harness/submissions.js.map +1 -1
  90. package/dist/harness/task-graph.d.ts +14 -9
  91. package/dist/harness/task-graph.d.ts.map +1 -1
  92. package/dist/harness/task-graph.js +38 -28
  93. package/dist/harness/task-graph.js.map +1 -1
  94. package/dist/harness/telemetry.d.ts +11 -11
  95. package/dist/harness/telemetry.d.ts.map +1 -1
  96. package/dist/harness/telemetry.js +7 -7
  97. package/dist/harness/telemetry.js.map +1 -1
  98. package/dist/harness/tool.d.ts +3 -1
  99. package/dist/harness/tool.d.ts.map +1 -1
  100. package/dist/harness/tool.js +172 -56
  101. package/dist/harness/tool.js.map +1 -1
  102. package/dist/harness/transcript.d.ts +72 -0
  103. package/dist/harness/transcript.d.ts.map +1 -0
  104. package/dist/harness/transcript.js +264 -0
  105. package/dist/harness/transcript.js.map +1 -0
  106. package/dist/harness/types.d.ts +237 -61
  107. package/dist/harness/types.d.ts.map +1 -1
  108. package/dist/harness/types.js.map +1 -1
  109. package/dist/harness/usage.d.ts +2 -1
  110. package/dist/harness/usage.d.ts.map +1 -1
  111. package/dist/harness/usage.js +1 -1
  112. package/dist/harness/usage.js.map +1 -1
  113. package/dist/harness/util.d.ts +4 -2
  114. package/dist/harness/util.d.ts.map +1 -1
  115. package/dist/harness/util.js +9 -0
  116. package/dist/harness/util.js.map +1 -1
  117. package/dist/harness/view.d.ts +44 -22
  118. package/dist/harness/view.d.ts.map +1 -1
  119. package/dist/harness/view.js +130 -71
  120. package/dist/harness/view.js.map +1 -1
  121. package/dist/index.d.ts +9 -7
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +4 -3
  124. package/dist/index.js.map +1 -1
  125. package/dist/search.d.ts +19 -3
  126. package/dist/search.d.ts.map +1 -1
  127. package/dist/search.js +107 -44
  128. package/dist/search.js.map +1 -1
  129. package/dist/session/cached-storage.d.ts +72 -0
  130. package/dist/session/cached-storage.d.ts.map +1 -0
  131. package/dist/session/cached-storage.js +269 -0
  132. package/dist/session/cached-storage.js.map +1 -0
  133. package/dist/session/conversation-tails.d.ts +33 -0
  134. package/dist/session/conversation-tails.d.ts.map +1 -0
  135. package/dist/session/conversation-tails.js +101 -0
  136. package/dist/session/conversation-tails.js.map +1 -0
  137. package/dist/session/forks.d.ts +1 -1
  138. package/dist/session/forks.d.ts.map +1 -1
  139. package/dist/session/forks.js.map +1 -1
  140. package/dist/session/keys.d.ts +69 -0
  141. package/dist/session/keys.d.ts.map +1 -0
  142. package/dist/session/keys.js +161 -0
  143. package/dist/session/keys.js.map +1 -0
  144. package/dist/session/session.d.ts +169 -26
  145. package/dist/session/session.d.ts.map +1 -1
  146. package/dist/session/session.js +1029 -171
  147. package/dist/session/session.js.map +1 -1
  148. package/dist/session/transaction.d.ts +84 -5
  149. package/dist/session/transaction.d.ts.map +1 -1
  150. package/dist/session/transaction.js +227 -40
  151. package/dist/session/transaction.js.map +1 -1
  152. package/dist/session/writer.d.ts +74 -0
  153. package/dist/session/writer.d.ts.map +1 -0
  154. package/dist/session/writer.js +294 -0
  155. package/dist/session/writer.js.map +1 -0
  156. package/dist/storage/indexeddb.d.ts +12 -3
  157. package/dist/storage/indexeddb.d.ts.map +1 -1
  158. package/dist/storage/indexeddb.js +91 -41
  159. package/dist/storage/indexeddb.js.map +1 -1
  160. package/dist/storage/jsonl/index.d.ts +1 -1
  161. package/dist/storage/jsonl/index.d.ts.map +1 -1
  162. package/dist/storage/jsonl/index.js +1 -1
  163. package/dist/storage/jsonl/index.js.map +1 -1
  164. package/dist/storage/jsonl/node.d.ts +3 -3
  165. package/dist/storage/jsonl/node.d.ts.map +1 -1
  166. package/dist/storage/jsonl/node.js +2 -2
  167. package/dist/storage/jsonl/node.js.map +1 -1
  168. package/dist/storage/jsonl/storage.d.ts +46 -5
  169. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  170. package/dist/storage/jsonl/storage.js +158 -43
  171. package/dist/storage/jsonl/storage.js.map +1 -1
  172. package/dist/storage/memory.d.ts +9 -3
  173. package/dist/storage/memory.d.ts.map +1 -1
  174. package/dist/storage/memory.js +28 -2
  175. package/dist/storage/memory.js.map +1 -1
  176. package/dist/storage/sqlite/commit.d.ts +52 -0
  177. package/dist/storage/sqlite/commit.d.ts.map +1 -0
  178. package/dist/storage/sqlite/commit.js +371 -0
  179. package/dist/storage/sqlite/commit.js.map +1 -0
  180. package/dist/storage/sqlite/rows.d.ts +33 -0
  181. package/dist/storage/sqlite/rows.d.ts.map +1 -0
  182. package/dist/storage/sqlite/rows.js +53 -0
  183. package/dist/storage/sqlite/rows.js.map +1 -0
  184. package/dist/storage/sqlite/statements.d.ts +13 -0
  185. package/dist/storage/sqlite/statements.d.ts.map +1 -0
  186. package/dist/storage/sqlite/statements.js +88 -0
  187. package/dist/storage/sqlite/statements.js.map +1 -0
  188. package/dist/storage/sqlite/storage.d.ts +23 -10
  189. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  190. package/dist/storage/sqlite/storage.js +120 -261
  191. package/dist/storage/sqlite/storage.js.map +1 -1
  192. package/dist/storage/store/commit.d.ts +33 -0
  193. package/dist/storage/store/commit.d.ts.map +1 -0
  194. package/dist/storage/store/commit.js +286 -0
  195. package/dist/storage/store/commit.js.map +1 -0
  196. package/dist/storage/store/documents.d.ts +43 -0
  197. package/dist/storage/store/documents.d.ts.map +1 -0
  198. package/dist/storage/store/documents.js +102 -0
  199. package/dist/storage/store/documents.js.map +1 -0
  200. package/dist/storage/store/index.d.ts +2 -0
  201. package/dist/storage/store/index.d.ts.map +1 -0
  202. package/dist/storage/store/index.js +2 -0
  203. package/dist/storage/store/index.js.map +1 -0
  204. package/dist/storage/store/queries.d.ts +49 -0
  205. package/dist/storage/store/queries.d.ts.map +1 -0
  206. package/dist/storage/store/queries.js +93 -0
  207. package/dist/storage/store/queries.js.map +1 -0
  208. package/dist/storage/store/storage.d.ts +60 -0
  209. package/dist/storage/store/storage.d.ts.map +1 -0
  210. package/dist/storage/store/storage.js +438 -0
  211. package/dist/storage/store/storage.js.map +1 -0
  212. package/dist/storage/store/tables.d.ts +54 -0
  213. package/dist/storage/store/tables.d.ts.map +1 -0
  214. package/dist/storage/store/tables.js +108 -0
  215. package/dist/storage/store/tables.js.map +1 -0
  216. package/dist/testing/commit-writes.d.ts +5 -0
  217. package/dist/testing/commit-writes.d.ts.map +1 -0
  218. package/dist/testing/commit-writes.js +12 -0
  219. package/dist/testing/commit-writes.js.map +1 -0
  220. package/dist/testing/env-conformance.js +7 -7
  221. package/dist/testing/env-conformance.js.map +1 -1
  222. package/dist/testing/index.d.ts +1 -0
  223. package/dist/testing/index.d.ts.map +1 -1
  224. package/dist/testing/index.js +1 -0
  225. package/dist/testing/index.js.map +1 -1
  226. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  227. package/dist/testing/storage-benchmark.js +18 -17
  228. package/dist/testing/storage-benchmark.js.map +1 -1
  229. package/dist/testing/storage-conformance.d.ts.map +1 -1
  230. package/dist/testing/storage-conformance.js +354 -69
  231. package/dist/testing/storage-conformance.js.map +1 -1
  232. package/dist/tool-search/tool-search.d.ts +8 -14
  233. package/dist/tool-search/tool-search.d.ts.map +1 -1
  234. package/dist/tool-search/tool-search.js +25 -13
  235. package/dist/tool-search/tool-search.js.map +1 -1
  236. package/dist/tools/bash.d.ts +13 -13
  237. package/dist/tools/bash.d.ts.map +1 -1
  238. package/dist/tools/bash.js +16 -15
  239. package/dist/tools/bash.js.map +1 -1
  240. package/dist/tools/edit.d.ts +7 -7
  241. package/dist/tools/edit.d.ts.map +1 -1
  242. package/dist/tools/edit.js +9 -9
  243. package/dist/tools/edit.js.map +1 -1
  244. package/dist/tools/file-mutation-queue.d.ts +1 -1
  245. package/dist/tools/file-mutation-queue.d.ts.map +1 -1
  246. package/dist/tools/file-mutation-queue.js.map +1 -1
  247. package/dist/tools/index.d.ts +32 -32
  248. package/dist/tools/path-utils.d.ts +1 -1
  249. package/dist/tools/path-utils.d.ts.map +1 -1
  250. package/dist/tools/path-utils.js.map +1 -1
  251. package/dist/tools/read.d.ts +6 -6
  252. package/dist/tools/read.d.ts.map +1 -1
  253. package/dist/tools/read.js +19 -8
  254. package/dist/tools/read.js.map +1 -1
  255. package/dist/tools/write.d.ts +4 -4
  256. package/dist/tools/write.d.ts.map +1 -1
  257. package/dist/tools/write.js +5 -5
  258. package/dist/tools/write.js.map +1 -1
  259. package/dist/types.d.ts +228 -78
  260. package/dist/types.d.ts.map +1 -1
  261. package/dist/types.js.map +1 -1
  262. package/package.json +19 -12
  263. package/dist/harness/events.d.ts +0 -186
  264. package/dist/harness/events.d.ts.map +0 -1
  265. package/dist/harness/events.js +0 -373
  266. package/dist/harness/events.js.map +0 -1
  267. package/dist/session/observation.d.ts +0 -42
  268. package/dist/session/observation.d.ts.map +0 -1
  269. package/dist/session/observation.js +0 -281
  270. package/dist/session/observation.js.map +0 -1
@@ -0,0 +1,264 @@
1
+ import { idFromNumber } from "../ids.js";
2
+ const EARLIER = Object.freeze({ kind: "earlier" });
3
+ /**
4
+ * What a client has of one conversation: every entry it got from a view or fetched, and the gaps between (spec §9.3).
5
+ * A layer over any `ConversationView`, wherever it comes from: call `merge()` with every value of the view; it returns
6
+ * the view with the client's whole transcript as `items` instead of the window. Entries never change once committed,
7
+ * so the transcript only grows; a window that dropped its oldest entries keeps them here.
8
+ *
9
+ * A window whose `before` lies past the newest entry the client has does not continue it, after a stall, an overflow,
10
+ * or a reconnect: the entries in between become a gap. Nothing counts entries or computes ranges from entry IDs, which
11
+ * have holes; only exclusive bounds are turned into the inclusive bounds of `EntryQuery`. After a drop, a new watch
12
+ * with `resume` as its window continues the transcript.
13
+ *
14
+ * A merge costs O(items) and never re-sorts or re-fetches; a merge, `loadEarlier()`, or `fillGap()` that adds nothing
15
+ * returns the previous `items` array. Concurrent calls of `loadEarlier()`, or of `fillGap()` for the same gap, share one
16
+ * load, so a placeholder that stays visible does not fetch twice.
17
+ */
18
+ export class ConversationTranscript {
19
+ #load;
20
+ #pageSize;
21
+ /** Every entry held, in ID order, as items. */
22
+ #entries = [];
23
+ #ids = new Set();
24
+ /** Gaps in ID order; each is an immutable item, replaced when it shrinks. */
25
+ #gaps = [];
26
+ /** Whether anything was merged yet. */
27
+ #started = false;
28
+ /** Exclusive lower bound of the known range: entries at or below it are not loaded; `-Infinity` at the start. */
29
+ #low = Number.NEGATIVE_INFINITY;
30
+ /** Inclusive upper bound of the known range: the newest entry, or a later empty window's `before`. */
31
+ #tail = Number.NEGATIVE_INFINITY;
32
+ #items = [];
33
+ #last;
34
+ #view;
35
+ #earlier;
36
+ #filling = new Map();
37
+ constructor(load, options) {
38
+ const pageSize = options?.pageSize ?? 50;
39
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1) {
40
+ throw new RangeError(`Page size must be an integer >= 1, got ${pageSize}`);
41
+ }
42
+ this.#load = load;
43
+ this.#pageSize = pageSize;
44
+ }
45
+ /**
46
+ * The ID the transcript reaches: its newest entry, or the `before` of a later window that was empty. Undefined before
47
+ * the first merge and for an empty transcript.
48
+ */
49
+ get newest() {
50
+ return this.#tail === Number.NEGATIVE_INFINITY ? undefined : idFromNumber(this.#tail);
51
+ }
52
+ /**
53
+ * The window that resumes this transcript after a drop: `{ from }` right after `newest`, so a new watch returns only
54
+ * what was missed when it fits its window, otherwise its newest entries and a gap; `{}` for an empty transcript;
55
+ * undefined before the first merge. `conversation.watch(context, transcript.resume)`.
56
+ */
57
+ get resume() {
58
+ if (!this.#started)
59
+ return undefined;
60
+ const newest = this.newest;
61
+ return newest === undefined ? {} : { from: idFromNumber(newest + 1) };
62
+ }
63
+ /** Take one value of the view: its window's entries join the transcript. */
64
+ merge(view) {
65
+ this.#last = view;
66
+ const { entries, before } = view;
67
+ const hi = entries.at(-1)?.id ?? before ?? Number.NEGATIVE_INFINITY;
68
+ this.#cover(before ?? Number.NEGATIVE_INFINITY, hi, entries);
69
+ return this.#result();
70
+ }
71
+ /** Load the page before the oldest entry held. Without anything earlier, returns the current view. */
72
+ loadEarlier(context) {
73
+ const last = this.#last;
74
+ if (last === undefined)
75
+ return Promise.reject(new Error("ConversationTranscript: merge a view first"));
76
+ if (this.#low === Number.NEGATIVE_INFINITY)
77
+ return Promise.resolve(this.#result());
78
+ if (this.#earlier !== undefined)
79
+ return this.#earlier;
80
+ const low = this.#low;
81
+ // The loader runs in a later microtask, so even its synchronous throw settles a promise that is already shared.
82
+ const loading = Promise.resolve().then(async () => {
83
+ const query = { maxEntryId: idFromNumber(low), order: "descending" };
84
+ const page = await this.#load(query, this.#pageSize, context);
85
+ const items = [...page.items].reverse();
86
+ const oldest = items[0];
87
+ // No next page, or an empty one: the transcript starts here.
88
+ if (page.next === undefined || oldest === undefined)
89
+ this.#cover(Number.NEGATIVE_INFINITY, low, items);
90
+ else
91
+ this.#cover(oldest.id - 1, low, items);
92
+ return this.#result();
93
+ });
94
+ this.#earlier = loading;
95
+ const settled = () => {
96
+ if (this.#earlier === loading)
97
+ this.#earlier = undefined;
98
+ };
99
+ loading.then(settled, settled);
100
+ return loading;
101
+ }
102
+ /**
103
+ * Load the next page of `gap`, a gap item of `items`, shrinking it from its start, or removing it once nothing is
104
+ * missing. A gap that has shrunk since loads the gap it became; one that is no longer there returns the current view.
105
+ */
106
+ fillGap(gap, context) {
107
+ const current = this.#gaps.find((each) => each.after === gap.after && each.upTo === gap.upTo) ??
108
+ this.#gaps.find((each) => each.after < gap.upTo && gap.after < each.upTo);
109
+ if (current === undefined) {
110
+ const last = this.#last;
111
+ if (last === undefined)
112
+ return Promise.reject(new Error("ConversationTranscript: merge a view first"));
113
+ return Promise.resolve(this.#result());
114
+ }
115
+ // Keyed by the gap the call resolved to, so a stale descriptor and the current one share the load.
116
+ const key = `${current.after}:${current.upTo}`;
117
+ const running = this.#filling.get(key);
118
+ if (running !== undefined)
119
+ return running;
120
+ const filling = Promise.resolve().then(async () => {
121
+ const query = {
122
+ minEntryId: idFromNumber(current.after + 1),
123
+ maxEntryId: idFromNumber(current.upTo - 1),
124
+ order: "ascending",
125
+ };
126
+ const page = await this.#load(query, this.#pageSize, context);
127
+ const newest = page.items.at(-1);
128
+ // No next page, or an empty one: nothing else is missing.
129
+ if (page.next === undefined || newest === undefined)
130
+ this.#cover(current.after, current.upTo - 1, page.items);
131
+ else
132
+ this.#cover(current.after, newest.id, page.items);
133
+ return this.#result();
134
+ });
135
+ this.#filling.set(key, filling);
136
+ const settled = () => {
137
+ if (this.#filling.get(key) === filling)
138
+ this.#filling.delete(key);
139
+ };
140
+ filling.then(settled, settled);
141
+ return filling;
142
+ }
143
+ /**
144
+ * Record that every entry with an ID in (`lo`, `hi`] is known, `added` being those entries in ID order: hold the new
145
+ * ones, shrink the gaps they fill, extend the known range, and open a gap where the range does not continue it.
146
+ */
147
+ #cover(lo, hi, added) {
148
+ const fresh = added.filter((entry) => !this.#ids.has(entry.id));
149
+ for (const entry of fresh)
150
+ this.#ids.add(entry.id);
151
+ const held = this.#entries;
152
+ // New entries past every held one: the items grow at their end.
153
+ const appended = fresh.length === 0 || held.length === 0 || fresh[0].id > held.at(-1).entry.id;
154
+ const items = fresh.map((entry) => ({ kind: "entry", entry }));
155
+ if (items.length > 0)
156
+ this.#entries = appended ? held.concat(items) : mergeById(held, items);
157
+ let rebuild = false;
158
+ if (!this.#started || (held.length === 0 && this.#gaps.length === 0 && lo >= this.#tail)) {
159
+ // The first range, or one after an empty transcript: it is all the client knows.
160
+ rebuild = !this.#started || lo !== this.#low;
161
+ this.#started = true;
162
+ this.#low = lo;
163
+ this.#tail = Math.max(this.#tail, hi);
164
+ }
165
+ else if (lo > this.#tail) {
166
+ // Past the newest known ID: what lies between is missing.
167
+ const upTo = added[0]?.id ?? lo + 1;
168
+ this.#gaps = [...this.#gaps, gapOf(this.#tail, upTo)];
169
+ this.#tail = hi;
170
+ rebuild = true;
171
+ }
172
+ else if (hi !== Number.NEGATIVE_INFINITY && hi < this.#low) {
173
+ // Below the oldest known ID: what lies between is missing.
174
+ const upTo = held.find((item) => item.entry.id > hi)?.entry.id ?? this.#low + 1;
175
+ this.#gaps = [gapOf(hi, upTo), ...this.#gaps];
176
+ this.#low = lo;
177
+ rebuild = true;
178
+ }
179
+ else {
180
+ const gaps = this.#subtract(lo, hi);
181
+ if (gaps !== this.#gaps) {
182
+ this.#gaps = gaps;
183
+ rebuild = true;
184
+ }
185
+ if (lo < this.#low) {
186
+ this.#low = lo;
187
+ rebuild = true;
188
+ }
189
+ if (hi > this.#tail)
190
+ this.#tail = hi;
191
+ }
192
+ if (rebuild || !appended)
193
+ this.#items = this.#build();
194
+ else if (items.length > 0)
195
+ this.#items = this.#items.concat(items);
196
+ }
197
+ /** The gaps without (`lo`, `hi`]; the same array when none changes, as when the range is empty. */
198
+ #subtract(lo, hi) {
199
+ if (hi <= lo)
200
+ return this.#gaps;
201
+ let changed = false;
202
+ const gaps = [];
203
+ for (const gap of this.#gaps) {
204
+ // Missing: IDs in (after, upTo); known now: IDs in (lo, hi].
205
+ if (hi <= gap.after || lo + 1 >= gap.upTo) {
206
+ gaps.push(gap);
207
+ continue;
208
+ }
209
+ changed = true;
210
+ if (lo > gap.after) {
211
+ const firstKnown = this.#entries.find((item) => item.entry.id > lo)?.entry.id ?? lo + 1;
212
+ gaps.push(gapOf(gap.after, Math.min(gap.upTo, firstKnown)));
213
+ }
214
+ if (hi + 1 < gap.upTo)
215
+ gaps.push(gapOf(hi, gap.upTo));
216
+ }
217
+ return changed ? gaps : this.#gaps;
218
+ }
219
+ /** Items in ID order: "earlier" until the start is known, then entries with each gap before the entry after it. */
220
+ #build() {
221
+ const items = this.#low === Number.NEGATIVE_INFINITY ? [] : [EARLIER];
222
+ const gaps = this.#gaps;
223
+ let next = 0;
224
+ for (const item of this.#entries) {
225
+ while (next < gaps.length && gaps[next].after < item.entry.id)
226
+ items.push(gaps[next++]);
227
+ items.push(item);
228
+ }
229
+ while (next < gaps.length)
230
+ items.push(gaps[next++]);
231
+ return items;
232
+ }
233
+ /** The view with the current items; the previous one when nothing changed. */
234
+ #result() {
235
+ const last = this.#last;
236
+ const previous = this.#view;
237
+ if (previous !== undefined &&
238
+ previous.items === this.#items &&
239
+ previous.docs === last.docs &&
240
+ previous.conversation === last.conversation) {
241
+ return previous;
242
+ }
243
+ this.#view = { conversation: last.conversation, docs: last.docs, items: this.#items };
244
+ return this.#view;
245
+ }
246
+ }
247
+ function gapOf(after, upTo) {
248
+ return Object.freeze({ kind: "gap", after: idFromNumber(after), upTo: idFromNumber(upTo) });
249
+ }
250
+ /** Two ID-ordered item lists merged in one pass. */
251
+ function mergeById(left, right) {
252
+ const merged = [];
253
+ let i = 0;
254
+ let j = 0;
255
+ while (i < left.length && j < right.length) {
256
+ merged.push(left[i].entry.id < right[j].entry.id ? left[i++] : right[j++]);
257
+ }
258
+ while (i < left.length)
259
+ merged.push(left[i++]);
260
+ while (j < right.length)
261
+ merged.push(right[j++]);
262
+ return merged;
263
+ }
264
+ //# sourceMappingURL=transcript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../src/harness/transcript.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context } from \"@earendil-works/chord/context\";\nimport { idFromNumber } from \"../ids.ts\";\nimport type { Cursor, EntryId, EntryQuery, EntryRecord, Page } from \"../types.ts\";\nimport type { ConversationView, ConversationWindow } from \"./view.ts\";\n\n/** One row of a client's transcript. */\nexport type TranscriptItem =\n\t| { readonly kind: \"entry\"; readonly entry: EntryRecord }\n\t/** Entries strictly between `after` and `upTo` are missing: render \"load more\" and call `fillGap()`. */\n\t| { readonly kind: \"gap\"; readonly after: EntryId; readonly upTo: EntryId }\n\t/** The transcript continues above: render \"load earlier\" and call `loadEarlier()`. Always the first item. */\n\t| { readonly kind: \"earlier\" };\n\n/** The conversation view as a renderer sees it: `items`, the client's whole transcript, instead of the window. */\nexport type TranscriptView = Omit<ConversationView, \"entries\" | \"before\"> & {\n\treadonly items: readonly TranscriptItem[];\n};\n\n/** Pages a conversation's entries, as `Conversation.entries()` does without a cursor. */\nexport type EntryLoader = (\n\tquery: Omit<EntryQuery, \"conversationId\">,\n\tlimit: number,\n\tcontext: Context,\n) => Promise<Page<EntryRecord, Cursor>>;\n\nexport type ConversationTranscriptOptions = {\n\t/** Entries `loadEarlier()` and `fillGap()` load per call; default 50. */\n\treadonly pageSize?: number;\n};\n\ntype Gap = Extract<TranscriptItem, { kind: \"gap\" }>;\ntype EntryItem = Extract<TranscriptItem, { kind: \"entry\" }>;\n\nconst EARLIER: TranscriptItem = Object.freeze({ kind: \"earlier\" });\n\n/**\n * What a client has of one conversation: every entry it got from a view or fetched, and the gaps between (spec §9.3).\n * A layer over any `ConversationView`, wherever it comes from: call `merge()` with every value of the view; it returns\n * the view with the client's whole transcript as `items` instead of the window. Entries never change once committed,\n * so the transcript only grows; a window that dropped its oldest entries keeps them here.\n *\n * A window whose `before` lies past the newest entry the client has does not continue it, after a stall, an overflow,\n * or a reconnect: the entries in between become a gap. Nothing counts entries or computes ranges from entry IDs, which\n * have holes; only exclusive bounds are turned into the inclusive bounds of `EntryQuery`. After a drop, a new watch\n * with `resume` as its window continues the transcript.\n *\n * A merge costs O(items) and never re-sorts or re-fetches; a merge, `loadEarlier()`, or `fillGap()` that adds nothing\n * returns the previous `items` array. Concurrent calls of `loadEarlier()`, or of `fillGap()` for the same gap, share one\n * load, so a placeholder that stays visible does not fetch twice.\n */\nexport class ConversationTranscript {\n\treadonly #load: EntryLoader;\n\treadonly #pageSize: number;\n\t/** Every entry held, in ID order, as items. */\n\t#entries: EntryItem[] = [];\n\treadonly #ids = new Set<EntryId>();\n\t/** Gaps in ID order; each is an immutable item, replaced when it shrinks. */\n\t#gaps: Gap[] = [];\n\t/** Whether anything was merged yet. */\n\t#started = false;\n\t/** Exclusive lower bound of the known range: entries at or below it are not loaded; `-Infinity` at the start. */\n\t#low = Number.NEGATIVE_INFINITY;\n\t/** Inclusive upper bound of the known range: the newest entry, or a later empty window's `before`. */\n\t#tail = Number.NEGATIVE_INFINITY;\n\t#items: readonly TranscriptItem[] = [];\n\t#last: ConversationView | undefined;\n\t#view: TranscriptView | undefined;\n\t#earlier: Promise<TranscriptView> | undefined;\n\treadonly #filling = new Map<string, Promise<TranscriptView>>();\n\n\tconstructor(load: EntryLoader, options?: ConversationTranscriptOptions) {\n\t\tconst pageSize = options?.pageSize ?? 50;\n\t\tif (!Number.isSafeInteger(pageSize) || pageSize < 1) {\n\t\t\tthrow new RangeError(`Page size must be an integer >= 1, got ${pageSize}`);\n\t\t}\n\t\tthis.#load = load;\n\t\tthis.#pageSize = pageSize;\n\t}\n\n\t/**\n\t * The ID the transcript reaches: its newest entry, or the `before` of a later window that was empty. Undefined before\n\t * the first merge and for an empty transcript.\n\t */\n\tget newest(): EntryId | undefined {\n\t\treturn this.#tail === Number.NEGATIVE_INFINITY ? undefined : idFromNumber<EntryId>(this.#tail);\n\t}\n\n\t/**\n\t * The window that resumes this transcript after a drop: `{ from }` right after `newest`, so a new watch returns only\n\t * what was missed when it fits its window, otherwise its newest entries and a gap; `{}` for an empty transcript;\n\t * undefined before the first merge. `conversation.watch(context, transcript.resume)`.\n\t */\n\tget resume(): ConversationWindow | undefined {\n\t\tif (!this.#started) return undefined;\n\t\tconst newest = this.newest;\n\t\treturn newest === undefined ? {} : { from: idFromNumber<EntryId>(newest + 1) };\n\t}\n\n\t/** Take one value of the view: its window's entries join the transcript. */\n\tmerge(view: ConversationView): TranscriptView {\n\t\tthis.#last = view;\n\t\tconst { entries, before } = view;\n\t\tconst hi = entries.at(-1)?.id ?? before ?? Number.NEGATIVE_INFINITY;\n\t\tthis.#cover(before ?? Number.NEGATIVE_INFINITY, hi, entries);\n\t\treturn this.#result();\n\t}\n\n\t/** Load the page before the oldest entry held. Without anything earlier, returns the current view. */\n\tloadEarlier(context: Context): Promise<TranscriptView> {\n\t\tconst last = this.#last;\n\t\tif (last === undefined) return Promise.reject(new Error(\"ConversationTranscript: merge a view first\"));\n\t\tif (this.#low === Number.NEGATIVE_INFINITY) return Promise.resolve(this.#result());\n\t\tif (this.#earlier !== undefined) return this.#earlier;\n\t\tconst low = this.#low;\n\t\t// The loader runs in a later microtask, so even its synchronous throw settles a promise that is already shared.\n\t\tconst loading = Promise.resolve().then(async () => {\n\t\t\tconst query = { maxEntryId: idFromNumber<EntryId>(low), order: \"descending\" } as const;\n\t\t\tconst page = await this.#load(query, this.#pageSize, context);\n\t\t\tconst items = [...page.items].reverse();\n\t\t\tconst oldest = items[0];\n\t\t\t// No next page, or an empty one: the transcript starts here.\n\t\t\tif (page.next === undefined || oldest === undefined) this.#cover(Number.NEGATIVE_INFINITY, low, items);\n\t\t\telse this.#cover(oldest.id - 1, low, items);\n\t\t\treturn this.#result();\n\t\t});\n\t\tthis.#earlier = loading;\n\t\tconst settled = (): void => {\n\t\t\tif (this.#earlier === loading) this.#earlier = undefined;\n\t\t};\n\t\tloading.then(settled, settled);\n\t\treturn loading;\n\t}\n\n\t/**\n\t * Load the next page of `gap`, a gap item of `items`, shrinking it from its start, or removing it once nothing is\n\t * missing. A gap that has shrunk since loads the gap it became; one that is no longer there returns the current view.\n\t */\n\tfillGap(gap: Gap, context: Context): Promise<TranscriptView> {\n\t\tconst current =\n\t\t\tthis.#gaps.find((each) => each.after === gap.after && each.upTo === gap.upTo) ??\n\t\t\tthis.#gaps.find((each) => each.after < gap.upTo && gap.after < each.upTo);\n\t\tif (current === undefined) {\n\t\t\tconst last = this.#last;\n\t\t\tif (last === undefined) return Promise.reject(new Error(\"ConversationTranscript: merge a view first\"));\n\t\t\treturn Promise.resolve(this.#result());\n\t\t}\n\t\t// Keyed by the gap the call resolved to, so a stale descriptor and the current one share the load.\n\t\tconst key = `${current.after}:${current.upTo}`;\n\t\tconst running = this.#filling.get(key);\n\t\tif (running !== undefined) return running;\n\t\tconst filling = Promise.resolve().then(async () => {\n\t\t\tconst query = {\n\t\t\t\tminEntryId: idFromNumber<EntryId>(current.after + 1),\n\t\t\t\tmaxEntryId: idFromNumber<EntryId>(current.upTo - 1),\n\t\t\t\torder: \"ascending\",\n\t\t\t} as const;\n\t\t\tconst page = await this.#load(query, this.#pageSize, context);\n\t\t\tconst newest = page.items.at(-1);\n\t\t\t// No next page, or an empty one: nothing else is missing.\n\t\t\tif (page.next === undefined || newest === undefined) this.#cover(current.after, current.upTo - 1, page.items);\n\t\t\telse this.#cover(current.after, newest.id, page.items);\n\t\t\treturn this.#result();\n\t\t});\n\t\tthis.#filling.set(key, filling);\n\t\tconst settled = (): void => {\n\t\t\tif (this.#filling.get(key) === filling) this.#filling.delete(key);\n\t\t};\n\t\tfilling.then(settled, settled);\n\t\treturn filling;\n\t}\n\n\t/**\n\t * Record that every entry with an ID in (`lo`, `hi`] is known, `added` being those entries in ID order: hold the new\n\t * ones, shrink the gaps they fill, extend the known range, and open a gap where the range does not continue it.\n\t */\n\t#cover(lo: number, hi: number, added: readonly EntryRecord[]): void {\n\t\tconst fresh = added.filter((entry) => !this.#ids.has(entry.id));\n\t\tfor (const entry of fresh) this.#ids.add(entry.id);\n\t\tconst held = this.#entries;\n\t\t// New entries past every held one: the items grow at their end.\n\t\tconst appended = fresh.length === 0 || held.length === 0 || fresh[0]!.id > held.at(-1)!.entry.id;\n\t\tconst items = fresh.map((entry): EntryItem => ({ kind: \"entry\", entry }));\n\t\tif (items.length > 0) this.#entries = appended ? held.concat(items) : mergeById(held, items);\n\t\tlet rebuild = false;\n\t\tif (!this.#started || (held.length === 0 && this.#gaps.length === 0 && lo >= this.#tail)) {\n\t\t\t// The first range, or one after an empty transcript: it is all the client knows.\n\t\t\trebuild = !this.#started || lo !== this.#low;\n\t\t\tthis.#started = true;\n\t\t\tthis.#low = lo;\n\t\t\tthis.#tail = Math.max(this.#tail, hi);\n\t\t} else if (lo > this.#tail) {\n\t\t\t// Past the newest known ID: what lies between is missing.\n\t\t\tconst upTo = added[0]?.id ?? lo + 1;\n\t\t\tthis.#gaps = [...this.#gaps, gapOf(this.#tail, upTo)];\n\t\t\tthis.#tail = hi;\n\t\t\trebuild = true;\n\t\t} else if (hi !== Number.NEGATIVE_INFINITY && hi < this.#low) {\n\t\t\t// Below the oldest known ID: what lies between is missing.\n\t\t\tconst upTo = held.find((item) => item.entry.id > hi)?.entry.id ?? this.#low + 1;\n\t\t\tthis.#gaps = [gapOf(hi, upTo), ...this.#gaps];\n\t\t\tthis.#low = lo;\n\t\t\trebuild = true;\n\t\t} else {\n\t\t\tconst gaps = this.#subtract(lo, hi);\n\t\t\tif (gaps !== this.#gaps) {\n\t\t\t\tthis.#gaps = gaps;\n\t\t\t\trebuild = true;\n\t\t\t}\n\t\t\tif (lo < this.#low) {\n\t\t\t\tthis.#low = lo;\n\t\t\t\trebuild = true;\n\t\t\t}\n\t\t\tif (hi > this.#tail) this.#tail = hi;\n\t\t}\n\t\tif (rebuild || !appended) this.#items = this.#build();\n\t\telse if (items.length > 0) this.#items = this.#items.concat(items);\n\t}\n\n\t/** The gaps without (`lo`, `hi`]; the same array when none changes, as when the range is empty. */\n\t#subtract(lo: number, hi: number): Gap[] {\n\t\tif (hi <= lo) return this.#gaps;\n\t\tlet changed = false;\n\t\tconst gaps: Gap[] = [];\n\t\tfor (const gap of this.#gaps) {\n\t\t\t// Missing: IDs in (after, upTo); known now: IDs in (lo, hi].\n\t\t\tif (hi <= gap.after || lo + 1 >= gap.upTo) {\n\t\t\t\tgaps.push(gap);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tchanged = true;\n\t\t\tif (lo > gap.after) {\n\t\t\t\tconst firstKnown = this.#entries.find((item) => item.entry.id > lo)?.entry.id ?? lo + 1;\n\t\t\t\tgaps.push(gapOf(gap.after, Math.min(gap.upTo, firstKnown)));\n\t\t\t}\n\t\t\tif (hi + 1 < gap.upTo) gaps.push(gapOf(hi, gap.upTo));\n\t\t}\n\t\treturn changed ? gaps : this.#gaps;\n\t}\n\n\t/** Items in ID order: \"earlier\" until the start is known, then entries with each gap before the entry after it. */\n\t#build(): TranscriptItem[] {\n\t\tconst items: TranscriptItem[] = this.#low === Number.NEGATIVE_INFINITY ? [] : [EARLIER];\n\t\tconst gaps = this.#gaps;\n\t\tlet next = 0;\n\t\tfor (const item of this.#entries) {\n\t\t\twhile (next < gaps.length && gaps[next]!.after < item.entry.id) items.push(gaps[next++]!);\n\t\t\titems.push(item);\n\t\t}\n\t\twhile (next < gaps.length) items.push(gaps[next++]!);\n\t\treturn items;\n\t}\n\n\t/** The view with the current items; the previous one when nothing changed. */\n\t#result(): TranscriptView {\n\t\tconst last = this.#last!;\n\t\tconst previous = this.#view;\n\t\tif (\n\t\t\tprevious !== undefined &&\n\t\t\tprevious.items === this.#items &&\n\t\t\tprevious.docs === last.docs &&\n\t\t\tprevious.conversation === last.conversation\n\t\t) {\n\t\t\treturn previous;\n\t\t}\n\t\tthis.#view = { conversation: last.conversation, docs: last.docs, items: this.#items };\n\t\treturn this.#view;\n\t}\n}\n\nfunction gapOf(after: number, upTo: number): Gap {\n\treturn Object.freeze({ kind: \"gap\", after: idFromNumber<EntryId>(after), upTo: idFromNumber<EntryId>(upTo) });\n}\n\n/** Two ID-ordered item lists merged in one pass. */\nfunction mergeById(left: readonly EntryItem[], right: readonly EntryItem[]): EntryItem[] {\n\tconst merged: EntryItem[] = [];\n\tlet i = 0;\n\tlet j = 0;\n\twhile (i < left.length && j < right.length) {\n\t\tmerged.push(left[i]!.entry.id < right[j]!.entry.id ? left[i++]! : right[j++]!);\n\t}\n\twhile (i < left.length) merged.push(left[i++]!);\n\twhile (j < right.length) merged.push(right[j++]!);\n\treturn merged;\n}\n"]}