heyquilt 0.3.121 → 0.3.138

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 (129) hide show
  1. package/RELEASES.md +292 -17
  2. package/bin/quilt.js +72 -5
  3. package/desktop/main.js +16 -2
  4. package/package.json +3 -1
  5. package/src/agent-task-workflow.js +2 -2
  6. package/src/agents/common.js +13 -0
  7. package/src/ai-vm.js +9 -1
  8. package/src/api/activity.js +5 -0
  9. package/src/api/memory-store.js +21 -10
  10. package/src/api/notify.js +134 -0
  11. package/src/api/routes/browsers.js +29 -0
  12. package/src/api/routes/github.js +0 -13
  13. package/src/api/routes/relay.js +26 -3
  14. package/src/api/routes/seam-connect.js +103 -0
  15. package/src/api/routes/session-migrate.js +39 -0
  16. package/src/api/routes/sessions.js +73 -1
  17. package/src/api/server.js +46 -12
  18. package/src/api/supabase-store.js +53 -4
  19. package/src/autowork.js +182 -16
  20. package/src/connection.js +39 -9
  21. package/src/control.js +139 -13
  22. package/src/force-date.js +27 -0
  23. package/src/fsutil.js +13 -3
  24. package/src/gitstate.js +38 -2
  25. package/src/history.js +25 -1
  26. package/src/mcp.js +93 -7
  27. package/src/merge3.js +11 -3
  28. package/src/now-summary.js +113 -0
  29. package/src/pathrules.js +12 -0
  30. package/src/protocol.js +10 -2
  31. package/src/quilt-default.js +84 -0
  32. package/src/quilt-history.js +106 -0
  33. package/src/quilt-remote.js +78 -0
  34. package/src/relay-commit.js +5 -0
  35. package/src/relay-lease.js +6 -2
  36. package/src/relay-mcp.js +172 -8
  37. package/src/relay-outbound.js +1 -1
  38. package/src/relay-prune.js +4 -1
  39. package/src/release-words.js +2 -2
  40. package/src/runner.js +20 -7
  41. package/src/seam-access.js +97 -0
  42. package/src/seam-claims.js +53 -0
  43. package/src/seam-url.js +15 -0
  44. package/src/server.js +1767 -33
  45. package/src/session-follow.js +515 -0
  46. package/src/session-layout3.js +466 -0
  47. package/src/session-migrate.js +123 -0
  48. package/src/session-quilt.js +145 -0
  49. package/src/session-seam.js +284 -0
  50. package/src/session.js +899 -46
  51. package/src/source-live/bring-in.js +283 -0
  52. package/src/source-live/live-block.js +507 -35
  53. package/src/source-live/live-square.js +17 -6
  54. package/src/status.js +5 -0
  55. package/src/ui/agent-guide.js +1 -1
  56. package/src/ui/app.css +85 -20
  57. package/src/ui/app.js +4 -1
  58. package/src/ui/chat.js +9 -3
  59. package/src/ui/cloud-ai-model.js +13 -0
  60. package/src/ui/cloud-ai.js +10 -2
  61. package/src/ui/common.js +6 -2
  62. package/src/ui/fileview.js +20 -7
  63. package/src/ui/force-words.js +19 -0
  64. package/src/ui/git-rule.js +9 -0
  65. package/src/ui/home.js +0 -147
  66. package/src/ui/loom-model.js +15 -1
  67. package/src/ui/quiltview.js +213 -0
  68. package/src/ui/seam-model.js +251 -0
  69. package/src/ui/session.js +413 -31
  70. package/src/ui/voice-call.js +83 -14
  71. package/src/ui/voice-model.js +39 -0
  72. package/src/ui/voice.js +167 -17
  73. package/src/ui-server.js +80 -28
  74. package/src/update-check.js +25 -1
  75. package/src/voice.js +13 -6
  76. package/src/git.js +0 -147
  77. package/src/quilts/AGENTS.md +0 -1
  78. package/src/quilts/CLAUDE.md +0 -1
  79. package/src/quilts/archiver.js +0 -175
  80. package/src/quilts/autorevert.js +0 -49
  81. package/src/quilts/bisect-run.js +0 -31
  82. package/src/quilts/blame.js +0 -103
  83. package/src/quilts/block.js +0 -347
  84. package/src/quilts/bucket.js +0 -25
  85. package/src/quilts/cbor.js +0 -92
  86. package/src/quilts/claims.js +0 -144
  87. package/src/quilts/cli.js +0 -449
  88. package/src/quilts/client.js +0 -128
  89. package/src/quilts/conflict-format.js +0 -190
  90. package/src/quilts/describe.js +0 -266
  91. package/src/quilts/dogfood.js +0 -82
  92. package/src/quilts/errors.js +0 -8
  93. package/src/quilts/events.js +0 -160
  94. package/src/quilts/export-git.js +0 -130
  95. package/src/quilts/export.js +0 -81
  96. package/src/quilts/fastmerge.js +0 -41
  97. package/src/quilts/hash.js +0 -44
  98. package/src/quilts/history.js +0 -266
  99. package/src/quilts/ids.js +0 -12
  100. package/src/quilts/import-git.js +0 -291
  101. package/src/quilts/import.js +0 -134
  102. package/src/quilts/index.js +0 -72
  103. package/src/quilts/limits.js +0 -67
  104. package/src/quilts/lines.js +0 -49
  105. package/src/quilts/live.js +0 -50
  106. package/src/quilts/merge-smart.js +0 -114
  107. package/src/quilts/merge.js +0 -486
  108. package/src/quilts/objects.js +0 -144
  109. package/src/quilts/pack.js +0 -118
  110. package/src/quilts/records.js +0 -211
  111. package/src/quilts/resolutions.js +0 -69
  112. package/src/quilts/reviews.js +0 -195
  113. package/src/quilts/routes.js +0 -126
  114. package/src/quilts/service.js +0 -923
  115. package/src/quilts/setup.js +0 -34
  116. package/src/quilts/signing.js +0 -149
  117. package/src/quilts/squares.js +0 -239
  118. package/src/quilts/store-memory.js +0 -683
  119. package/src/quilts/store-rpc.js +0 -79
  120. package/src/quilts/store-supabase.js +0 -13
  121. package/src/quilts/tags.js +0 -64
  122. package/src/quilts/textdiff.js +0 -96
  123. package/src/quilts/tokens.js +0 -66
  124. package/src/quilts/tools.js +0 -375
  125. package/src/quilts/tree.js +0 -351
  126. package/src/quilts/webhook.js +0 -116
  127. package/src/quilts/window.js +0 -119
  128. package/src/quilts/xdiff.js +0 -435
  129. package/src/ui/branches.js +0 -111
@@ -1,683 +0,0 @@
1
- // The quilt tables (plans/quilt-source-control/03, "The Postgres schema") in memory, for tests
2
- // and for running everything on one machine. Every write is one call that either applies whole
3
- // or throws, as one SQL function call does in the Supabase store (supabase/migrations/
4
- // *_quilts.sql has the same transactions). With `file`, every write is also appended to a
5
- // journal (one JSON line per transaction) and replayed at start, so a local server keeps its
6
- // quilts across restarts.
7
- import fs from 'node:fs'
8
- import path from 'node:path'
9
-
10
- export class StoreError extends Error {
11
- constructor (code, message, extra = {}) { super(message || code); this.code = code; Object.assign(this, extra) }
12
- }
13
-
14
- const k = (...parts) => parts.join('\u0000')
15
-
16
- export function createMemoryQuiltStore ({ file = null } = {}) {
17
- const t = {
18
- quilts: new Map(), // id → quilt
19
- slugs: new Map(), // owner + slug → quilt id
20
- members: new Map(), // quilt + member → { level }
21
- lines: new Map(), // quilt + name → line
22
- live: new Map(), // square id → its shared uncommitted work { head, changes, actor, at }
23
- reviews: new Map(), // quilt → review rows (13), oldest first
24
- signingKeys: new Map(), // key id → { id, member, public_key, name, added_at, removed_at }
25
- signatures: new Map(), // quilt + commit → signature rows
26
- tokens: new Map(), // token id → { id, quilt_id, name, level, hash, created_by, created_at, revoked_at } (tokens.js)
27
- lineRules: new Map(), // quilt → Map of line → { line, approvals, checks }
28
- squareChecks: new Map(), // square → Map of check name → its latest report
29
- claims: new Map(), // quilt → Map of claim id → claim (06: rows, not log entries)
30
- resolutions: new Map(), // quilt → Map of key → remembered resolution (07, D12)
31
- tags: new Map(), // quilt + name → tag (06: never moved or deleted)
32
- squares: new Map(), // id → square
33
- commits: new Map(), // quilt + id → commit row
34
- bySeq: new Map(), // quilt + seq → commit id
35
- files: new Map(), // quilt + commit id → [file rows] (staged before the commit row exists)
36
- linePaths: new Map(), // quilt + line + path → [{ patch_no, commit_id, seq, op }] oldest first
37
- linePathsPending: new Map(), // quilt → [{ line, patch_no, commit_id, seq }] (03: merged, rows not written yet)
38
- blobs: new Map(), // quilt + id → location
39
- objects: new Map(), // quilt + id → location
40
- packs: new Map(), // quilt + id → pack row
41
- log: new Map(), // quilt → [log rows] (index = seq − 1)
42
- patches: new Map(), // quilt + line + patch_no → seq of the merge row
43
- idem: new Map(), // key → { result, at }
44
- gitShas: new Map(), // quilt + sha → commit id
45
- stubs: new Map(), // quilt + member id → stub member (14: git import authors)
46
- gitOf: new Map(), // quilt + commit id → sha
47
- blocks: new Map(), // id → block
48
- events: new Map(), // quilt → [event rows] (index = event_seq − 1), each with its recipients
49
- eventCursors: new Map(), // quilt → the last log seq the events job read
50
- memberEvents: new Map(), // quilt + member → [event_seq] (event_recipients)
51
- webhooks: new Map(), // quilt + member → subscription
52
- leases: new Map(), // 'line' + quilt + line | 'square' + id → { holder, expires_at }
53
- // Indexes, so no read scans every row:
54
- byMember: new Map(), // member → Set of quilt ids
55
- quiltMembers: new Map(), // quilt → Set of member ids
56
- quiltLines: new Map(), // quilt → Set of line names
57
- quiltTags: new Map(), // quilt → Set of tag names
58
- quiltSquares: new Map(), // quilt → Set of square ids
59
- squareCommitIds: new Map(), // square → [commit ids] in seq order
60
- squareTail: new Map(), // quilt + last 4 hex digits of the id → Set of square ids
61
- idPrefix: new Map() // quilt + first 3 hex chars → [commit ids]
62
- }
63
- const clone = (v) => v == null ? null : structuredClone(v)
64
- const addTo = (m, key, v) => { let x = m.get(key); if (!x) { x = new Set(); m.set(key, x) } x.add(v) }
65
- const pushTo = (m, key, v) => { let x = m.get(key); if (!x) { x = []; m.set(key, x) } x.push(v) }
66
-
67
- // ---- writes (each one transaction) ----
68
-
69
- const logOf = (q) => { let l = t.log.get(q); if (!l) { l = []; t.log.set(q, l) } return l }
70
- const quiltRow = (q) => {
71
- const row = t.quilts.get(q)
72
- if (!row) throw new StoreError('not_found', 'no such quilt')
73
- return row
74
- }
75
- // The quilt row lock: in one process every transaction runs to its end before the next starts.
76
- function appendLog (q, row) {
77
- const quilt = quiltRow(q)
78
- const seq = quilt.head_seq + 1
79
- const full = { quilt_id: q, seq, ...row }
80
- logOf(q).push(full)
81
- quilt.head_seq = seq
82
- return full
83
- }
84
-
85
- const tx = {
86
- createQuilt ({ quilt, at }) {
87
- const sk = k(quilt.owner, quilt.slug)
88
- if (t.slugs.has(sk)) throw new StoreError('slug_taken', `you already have a quilt called ${quilt.slug}`)
89
- t.quilts.set(quilt.id, { ...quilt, head_seq: 0, created_at: at })
90
- t.slugs.set(sk, quilt.id)
91
- t.members.set(k(quilt.id, quilt.owner), { quilt_id: quilt.id, member_id: quilt.owner, level: 'admin' })
92
- addTo(t.byMember, quilt.owner, quilt.id); addTo(t.quiltMembers, quilt.id, quilt.owner); addTo(t.quiltLines, quilt.id, 'main')
93
- t.lines.set(k(quilt.id, 'main'), { quilt_id: quilt.id, name: 'main', tip: null, tip_seq: 0, patch_count: 0, frozen: false, updated_at: at })
94
- appendLog(quilt.id, { kind: 'policy', actor: quilt.owner, at, payload: { op: 'create', slug: quilt.slug } })
95
- return quiltRow(quilt.id)
96
- },
97
-
98
- setMember ({ quilt_id: q, member_id: m, level, actor, at }) {
99
- quiltRow(q)
100
- if (level) { t.members.set(k(q, m), { quilt_id: q, member_id: m, level }); addTo(t.byMember, m, q); addTo(t.quiltMembers, q, m) }
101
- else t.members.delete(k(q, m))
102
- appendLog(q, { kind: 'policy', actor, at, payload: { op: 'member', member: m, level: level || null } })
103
- return { member_id: m, level: level || null }
104
- },
105
-
106
- /** Packs, blobs and objects rows for a sealed pack (03: committed before any row names them). */
107
- sealPack ({ quilt_id: q, pack, blobs = [], objects = [], at }) {
108
- quiltRow(q)
109
- if (!t.packs.has(k(q, pack.id))) t.packs.set(k(q, pack.id), { ...pack, sealed_at: at })
110
- const seq = quiltRow(q).head_seq
111
- for (const b of blobs) if (!t.blobs.has(k(q, b.id))) t.blobs.set(k(q, b.id), { ...b, pack_id: pack.id, first_seen_seq: seq + 1 })
112
- for (const o of objects) if (!t.objects.has(k(q, o.id))) t.objects.set(k(q, o.id), { ...o, pack_id: pack.id })
113
- return { ok: true }
114
- },
115
-
116
- /** A commit's file rows, keyed by its content id, written before the lock (03). */
117
- stageFiles ({ quilt_id: q, commit_id: id, files }) {
118
- if (!t.files.has(k(q, id))) t.files.set(k(q, id), files)
119
- return { ok: true }
120
- },
121
-
122
- openSquare ({ square, at }) {
123
- quiltRow(square.quilt_id)
124
- if (t.squares.has(square.id)) throw new StoreError('exists', 'that square exists')
125
- if (square.task_id && square.task_primary) {
126
- for (const s of [...(t.quiltSquares.get(square.quilt_id) || [])].map((id) => t.squares.get(id))) if (s.quilt_id === square.quilt_id && s.task_id === square.task_id && s.task_primary && s.state === 'open') return { ...s, existed: true }
127
- }
128
- const row = appendLog(square.quilt_id, { kind: 'square', square: square.id, line: square.line, actor: square.opened_by, at, payload: { op: 'open', title: square.title, base: square.base } })
129
- const s = { ...square, state: 'open', head: null, opened_at: at, updated_at: at, last_seq: row.seq }
130
- t.squares.set(s.id, s)
131
- addTo(t.quiltSquares, s.quilt_id, s.id)
132
- addTo(t.squareTail, k(s.quilt_id, s.id.replace(/-/g, '').slice(-4)), s.id)
133
- return s
134
- },
135
-
136
- /**
137
- * The commit transaction (03): compare-and-swap of the square's head, then the commit row
138
- * and its log row, the predecessor hidden. `state` is the square state the write expects:
139
- * 'open', or 'merging' for a merge's own writes.
140
- */
141
- commit ({ commit, expect_head, state = 'open', actor, at, payload = {}, square_patch: patch = null }) {
142
- const q = commit.quilt_id
143
- const s = t.squares.get(commit.square_id)
144
- if (!s || s.quilt_id !== q) throw new StoreError('not_found', 'no such square')
145
- if (s.state !== state) throw new StoreError(s.state === 'merging' ? 'square_merging' : 'square_closed', `the square is ${s.state}`, { head: s.head })
146
- if ((s.head || null) !== (expect_head || null)) throw new StoreError('head_moved', 'the square moved on since you looked', { head: s.head })
147
- if (t.commits.has(k(q, commit.id))) throw new StoreError('exists', 'that commit exists')
148
- const c = writeCommit(commit, s, actor, at, payload, true)
149
- // A rebase moves the square's base in the same transaction (07).
150
- if (patch && patch.base !== undefined) s.base = patch.base
151
- return c
152
- },
153
-
154
- /** Moves a square to 'merging' (07 step 1), by compare-and-swap on state and head. */
155
- beginMerge ({ square_id, expect_head, actor, at }) {
156
- const s = t.squares.get(square_id)
157
- if (!s) throw new StoreError('not_found', 'no such square')
158
- if (s.state === 'merging') throw new StoreError('square_merging', 'another merge of this square is running')
159
- if (s.state !== 'open') throw new StoreError('square_closed', `the square is ${s.state}`)
160
- if ((s.head || null) !== (expect_head || null)) throw new StoreError('head_moved', 'the square moved on since you looked', { head: s.head })
161
- appendLog(s.quilt_id, { kind: 'square', square: s.id, actor, at, payload: { op: 'merging' } })
162
- s.state = 'merging'; s.updated_at = at
163
- return { ...s }
164
- },
165
-
166
- /** A refused or abandoned merge: the square goes back to 'open' with a merge_refused row. */
167
- endMerge ({ square_id, why, actor, at }) {
168
- const s = t.squares.get(square_id)
169
- if (!s || s.state !== 'merging') return null
170
- appendLog(s.quilt_id, { kind: 'square', square: s.id, actor, at, payload: { op: 'merge_refused', why } })
171
- s.state = 'open'; s.updated_at = at
172
- return { ...s }
173
- },
174
-
175
- /**
176
- * The merge transaction (07 step 7): the rebased commit, the merge row, the line's
177
- * compare-and-swap and the square's merged row, all or nothing. `tip_moved` means the
178
- * caller rebases again.
179
- */
180
- merge ({ commit, line, expected_tip, expect_head, snapshot_root, actor, at, payload = {}, import: imported = false, root = false }) {
181
- const q = commit.quilt_id
182
- const ln = t.lines.get(k(q, line))
183
- if (!ln) throw new StoreError('not_found', `no line ${line}`)
184
- if (ln.frozen && !imported) throw new StoreError('line_frozen', `${line} is frozen`)
185
- if ((ln.tip || null) !== (expected_tip || null)) throw new StoreError('tip_moved', `${line} moved on`, { tip: ln.tip })
186
- if (commit.conflicted) throw new StoreError('conflict', 'a line never takes a conflicted commit')
187
- const s = t.squares.get(commit.square_id)
188
- if (!s) throw new StoreError('not_found', 'no such square')
189
- if (!imported && (s.state !== 'merging' || (s.head || null) !== (expect_head || null))) throw new StoreError('head_moved', 'the square moved on since you looked', { head: s.head })
190
- const c = t.commits.get(k(q, commit.id)) || writeCommit({ ...commit, snapshot_root }, s, actor, at, payload, false)
191
- const patchNo = root && !ln.tip ? 0 : ln.patch_count + 1 // the root commit is patch 0 (ruling 2026-10-24)
192
- const mrow = appendLog(q, { kind: 'merge', commit: commit.id, square: s.id, line, patch_no: patchNo, from: ln.tip, to: commit.id, actor, at, snapshot_root, payload: { combined: payload.combined || [], ...(payload.reverts ? { reverts: payload.reverts } : {}) } })
193
- Object.assign(ln, { tip: commit.id, tip_seq: mrow.seq, patch_count: patchNo, updated_at: at })
194
- t.patches.set(k(q, line, patchNo), mrow.seq)
195
- if (snapshot_root) c.snapshot_root = snapshot_root
196
- c.patch = { line, patch_no: patchNo }
197
- const merged = { line, patch_no: patchNo, commit: commit.id, seq: mrow.seq }
198
- appendLog(q, { kind: 'square', square: s.id, actor, at, payload: { op: 'merged', ...merged } })
199
- Object.assign(s, { state: 'merged', head: commit.id, merged, updated_at: at, closed_at: at })
200
- // line_paths: written by the second short transaction (fillLinePaths, 03); until then the
201
- // patch is pending and readers take its rows from its file rows.
202
- pushTo(t.linePathsPending, q, { line, patch_no: patchNo, commit_id: commit.id, seq: c.seq })
203
- return { patch_no: patchNo, seq: mrow.seq, commit: c }
204
- },
205
-
206
- /** 03's second short transaction: the line_paths rows of every pending patch of a quilt. */
207
- fillLinePaths ({ quilt_id: q }) {
208
- const pending = t.linePathsPending.get(q) || []
209
- for (const pp of pending) {
210
- for (const [path, op] of pathRows(q, pp.commit_id)) {
211
- const lk = k(q, pp.line, path)
212
- let list = t.linePaths.get(lk)
213
- if (!list) { list = []; t.linePaths.set(lk, list) }
214
- if (!list.some((r) => r.patch_no === pp.patch_no)) list.push({ patch_no: pp.patch_no, commit_id: pp.commit_id, seq: pp.seq, op })
215
- }
216
- }
217
- t.linePathsPending.delete(q)
218
- return { filled: pending.length }
219
- },
220
-
221
- /** Writes an imported commit that is not a patch (a side branch's commit) into its square. */
222
- importCommit ({ commit, actor, at, payload = {} }) {
223
- const s = t.squares.get(commit.square_id)
224
- if (!s) throw new StoreError('not_found', 'no such square')
225
- if (t.commits.has(k(commit.quilt_id, commit.id))) return t.commits.get(k(commit.quilt_id, commit.id))
226
- return writeCommit(commit, s, actor, at, payload, false)
227
- },
228
-
229
- setSnapshot ({ quilt_id: q, commit_id: id, root }) {
230
- const c = t.commits.get(k(q, id))
231
- if (c && !c.snapshot_root) c.snapshot_root = root
232
- return { ok: true }
233
- },
234
-
235
- setSquare ({ square_id, patch, op, expect, log = {}, actor, at }) {
236
- const s = t.squares.get(square_id)
237
- if (!s) throw new StoreError('not_found', 'no such square')
238
- // Compare-and-swap on the square row (06): head, holder and state as the caller saw them.
239
- if (expect && 'head' in expect && (s.head || null) !== (expect.head || null)) throw Object.assign(new StoreError('head_moved', 'the square moved on since you looked'), { head: s.head })
240
- if (expect && 'holder' in expect && s.holder !== expect.holder) throw Object.assign(new StoreError('holder_moved', `the square is now held by ${s.holder}`), { holder: s.holder })
241
- if (expect && 'state' in expect && s.state !== expect.state) throw Object.assign(new StoreError('square_state', `the square is ${s.state}`), { state: s.state })
242
- appendLog(s.quilt_id, { kind: 'square', square: s.id, actor, at, payload: { op, ...patch, ...log } })
243
- Object.assign(s, patch, { updated_at: at })
244
- return { ...s }
245
- },
246
-
247
- /** Stores a square's conflicted paths for one head (07); a head that moved since is left alone. */
248
- setConflicted ({ square_id, head, paths }) {
249
- const s = t.squares.get(square_id)
250
- if (!s || (s.head || null) !== (head || null)) return { ok: false }
251
- s.conflicted_paths = [...paths]
252
- s.conflicted_at = head
253
- return { ok: true }
254
- },
255
-
256
- setGitSha ({ quilt_id: q, sha, commit_id: id }) {
257
- t.gitShas.set(k(q, sha), id)
258
- t.gitOf.set(k(q, id), sha)
259
- return { ok: true }
260
- },
261
-
262
- /** Stub person/agent members for git import authors (14); a known stub is left as it is. */
263
- addStubs ({ quilt_id: q, stubs, at }) {
264
- let added = 0
265
- for (const st of stubs) {
266
- if (t.stubs.has(k(q, st.member_id))) continue
267
- t.stubs.set(k(q, st.member_id), { quilt_id: q, member_id: st.member_id, kind: st.kind || 'person', name: st.name || null, email: st.email || null, stub: true, created_at: at })
268
- added++
269
- }
270
- return { added }
271
- },
272
-
273
- remember ({ key, result, at }) { t.idem.set(key, { result, at }); return { ok: true } },
274
-
275
- putBlock ({ block }) { t.blocks.set(block.id, { ...block }); return { ...block } },
276
-
277
- /**
278
- * The events job's transaction (12): the events derived from log rows up to `cursor`, each
279
- * with a dense event_seq and its recipients, and the cursor moved, only if it is still at
280
- * `expect_cursor` (so two workers never write one row's events twice).
281
- */
282
- addEvents ({ quilt_id: q, events, expect_cursor: expect, cursor }) {
283
- quiltRow(q)
284
- if ((t.eventCursors.get(q) || 0) !== (expect || 0)) throw new StoreError('cursor_moved', 'another events job ran')
285
- const list = t.events.get(q) || []
286
- t.events.set(q, list)
287
- const out = []
288
- for (const ev of events) {
289
- const row = { ...ev, quilt_id: q, event_seq: list.length + 1 }
290
- list.push(row)
291
- for (const m of new Set(ev.recipients || [])) pushTo(t.memberEvents, k(q, m), row.event_seq)
292
- out.push(row)
293
- }
294
- t.eventCursors.set(q, cursor)
295
- return out.map(clone)
296
- },
297
-
298
- /** revert (04): a new square and its one commit in one transaction. */
299
- openAndCommit ({ square, at, ...commitArgs }) {
300
- const q = commitArgs.commit.quilt_id
301
- for (const p of commitArgs.commit.parents) if (!t.commits.has(k(q, p))) throw new StoreError('bad_parent', `parent ${p} is not in this quilt`)
302
- tx.openSquare({ square, at })
303
- return tx.commit({ ...commitArgs, at, expect_head: null })
304
- },
305
-
306
- /** Takes (or renews) a line's or a square's lease (03's line_leases / square_leases). */
307
- takeLease ({ kind, quilt_id: q, line, square_id: sq, holder, ttl_ms: ttl, at }) {
308
- const key = kind === 'line' ? k('line', q, line) : k('square', sq)
309
- const cur = t.leases.get(key)
310
- if (!cur || cur.holder === holder || cur.expires_at <= at) t.leases.set(key, { holder, expires_at: at + ttl })
311
- const now = t.leases.get(key)
312
- return { held: now.holder === holder, holder: now.holder, expires_at: now.expires_at }
313
- },
314
- giveLease ({ kind, quilt_id: q, line, square_id: sq, holder }) {
315
- const key = kind === 'line' ? k('line', q, line) : k('square', sq)
316
- if (t.leases.get(key)?.holder === holder) t.leases.delete(key)
317
- return { ok: true }
318
- },
319
-
320
- /** One webhook subscription per member per quilt (null removes it). */
321
- setWebhook ({ quilt_id: q, member_id: m, sub }) {
322
- if (sub) t.webhooks.set(k(q, m), { ...sub }); else t.webhooks.delete(k(q, m))
323
- return sub ? { ...sub } : null
324
- },
325
- // A new line (release/<name>) starting at a commit: its next merge is patch 1.
326
- createLine ({ quilt_id: q, name, tip, actor, at }) {
327
- quiltRow(q)
328
- if (t.lines.has(k(q, name))) throw new StoreError('line_exists', `there is already a line ${name}`)
329
- if (tip && !t.commits.has(k(q, tip))) throw new StoreError('not_found', 'no such commit')
330
- const row = appendLog(q, { kind: 'policy', line: name, actor, at, payload: { op: 'line', tip: tip || null } })
331
- t.lines.set(k(q, name), { quilt_id: q, name, tip: tip || null, tip_seq: row.seq, patch_count: 0, frozen: false, updated_at: at })
332
- addTo(t.quiltLines, q, name)
333
- return clone(t.lines.get(k(q, name)))
334
- },
335
- // A tag (or a release: a tag with notes) on a commit; a name is taken once, for good.
336
- createTag ({ quilt_id: q, name, commit, release = false, notes = null, actor, at }) {
337
- quiltRow(q)
338
- if (t.tags.has(k(q, name))) throw new StoreError('tag_exists', `there is already a tag ${name}`)
339
- if (!t.commits.has(k(q, commit))) throw new StoreError('not_found', 'no such commit')
340
- const row = appendLog(q, { kind: 'release', commit, actor, at, payload: { op: release ? 'release' : 'tag', name, ...(release ? { notes } : {}) } })
341
- t.tags.set(k(q, name), { quilt_id: q, name, commit, release: !!release, notes: release ? notes : null, actor, at, seq: row.seq })
342
- addTo(t.quiltTags, q, name)
343
- return clone(t.tags.get(k(q, name)))
344
- },
345
- // A member's Ed25519 public key (signing.js): removed, never deleted.
346
- addSigningKey (r) {
347
- const had = t.signingKeys.get(r.id)
348
- if (had && !had.removed_at) throw new StoreError('key_exists', had.member === r.member ? 'you already added this key' : 'this key is someone else\'s')
349
- if (had && had.member !== r.member) throw new StoreError('key_exists', 'this key is someone else\'s')
350
- t.signingKeys.set(r.id, { ...r, removed_at: null })
351
- return clone(t.signingKeys.get(r.id))
352
- },
353
- removeSigningKey ({ id, member, at }) {
354
- const row = t.signingKeys.get(id)
355
- if (!row || row.member !== member) throw new StoreError('not_found', 'no such key')
356
- if (!row.removed_at) row.removed_at = at
357
- return clone(row)
358
- },
359
- // A quilt token (tokens.js): a row and its membership token:<id>, made and ended together.
360
- createToken (r) {
361
- quiltRow(r.quilt_id)
362
- if ([...t.tokens.values()].some((x) => x.hash === r.hash)) throw new StoreError('exists', 'this token exists')
363
- t.tokens.set(r.id, { ...r, revoked_at: null })
364
- const m = `token:${r.id}`
365
- t.members.set(k(r.quilt_id, m), { quilt_id: r.quilt_id, member_id: m, level: r.level }); addTo(t.byMember, m, r.quilt_id); addTo(t.quiltMembers, r.quilt_id, m)
366
- appendLog(r.quilt_id, { kind: 'policy', actor: r.created_by, at: r.created_at, payload: { op: 'token', token: r.id, name: r.name, level: r.level } })
367
- return clone(t.tokens.get(r.id))
368
- },
369
- revokeToken ({ quilt_id: q, id, actor, at }) {
370
- const row = t.tokens.get(id)
371
- if (!row || row.quilt_id !== q) throw new StoreError('not_found', 'no such token')
372
- if (!row.revoked_at) {
373
- row.revoked_at = at
374
- t.members.delete(k(q, `token:${id}`))
375
- appendLog(q, { kind: 'policy', actor, at, payload: { op: 'revoke_token', token: id } })
376
- }
377
- return clone(row)
378
- },
379
- // A signature on a commit: one per (commit, key), kept for good.
380
- addSignature (r) {
381
- if (!t.commits.has(k(r.quilt_id, r.commit))) throw new StoreError('not_found', 'no such commit')
382
- let a = t.signatures.get(k(r.quilt_id, r.commit)); if (!a) { a = []; t.signatures.set(k(r.quilt_id, r.commit), a) }
383
- if (!a.some((x) => x.key === r.key)) a.push({ ...r })
384
- return clone(r)
385
- },
386
- // A review row on a square (13): kept for good, never changed.
387
- addReview (r) {
388
- const row = t.squares.get(r.square_id)
389
- if (!row || row.quilt_id !== r.quilt_id) throw new StoreError('not_found', 'no such square')
390
- let a = t.reviews.get(r.quilt_id); if (!a) { a = []; t.reviews.set(r.quilt_id, a) }
391
- a.push({ ...r })
392
- return clone(r)
393
- },
394
- // What a line needs to take a merge (13): a number of approvals.
395
- setLineRule ({ quilt_id: q, line, approvals, checks = [] }) {
396
- if (!t.lines.has(k(q, line))) throw new StoreError('not_found', `no line ${line}`)
397
- let m = t.lineRules.get(q); if (!m) { m = new Map(); t.lineRules.set(q, m) }
398
- m.set(line, { line, approvals, checks: [...checks] })
399
- return clone(m.get(line))
400
- },
401
- // A check's latest result on a square (13): one row per (square, check), replaced by each report.
402
- setSquareCheck ({ quilt_id: q, square_id: sq, name, status, head, by, at }) {
403
- const row = t.squares.get(sq)
404
- if (!row || row.quilt_id !== q) throw new StoreError('not_found', 'no such square')
405
- let m = t.squareChecks.get(sq); if (!m) { m = new Map(); t.squareChecks.set(sq, m) }
406
- m.set(name, { name, status, head, by, at })
407
- return clone(m.get(name))
408
- },
409
- // A claim on a square's files (06): a mutable row, not a log entry.
410
- createClaim ({ quilt_id: q, id, square_id: sq, pattern, holder, note = '', at }) {
411
- const row = t.squares.get(sq)
412
- if (!row || row.quilt_id !== q) throw new StoreError('not_found', 'no such square')
413
- let m = t.claims.get(q); if (!m) { m = new Map(); t.claims.set(q, m) }
414
- m.set(id, { quilt_id: q, id, square_id: sq, pattern, holder, note, at })
415
- return clone(m.get(id))
416
- },
417
- deleteClaim ({ quilt_id: q, id }) {
418
- const m = t.claims.get(q)
419
- if (!m || !m.has(id)) throw new StoreError('not_found', 'no such claim')
420
- m.delete(id)
421
- return { ok: true }
422
- },
423
- // A square's shared uncommitted work, replaced whole; no changes clears it. Not history: no log row.
424
- setLive ({ quilt_id: q, square_id: sq, head = null, changes = [], actor, at }) {
425
- const row = t.squares.get(sq)
426
- if (!row || row.quilt_id !== q) throw new StoreError('not_found', 'no such square')
427
- if (!changes.length) { t.live.delete(sq); return null }
428
- t.live.set(sq, { quilt_id: q, square_id: sq, head, changes: clone(changes), actor, at })
429
- return clone(t.live.get(sq))
430
- },
431
- // Freezes or unfreezes a line: a frozen line refuses merges (line_frozen).
432
- setLineFrozen ({ quilt_id: q, name, frozen, actor, at }) {
433
- const ln = t.lines.get(k(q, name))
434
- if (!ln) throw new StoreError('not_found', `no line ${name}`)
435
- appendLog(q, { kind: 'policy', line: name, actor, at, payload: { op: frozen ? 'freeze' : 'unfreeze' } })
436
- Object.assign(ln, { frozen: !!frozen, updated_at: at })
437
- return clone(ln)
438
- },
439
- // The merge window's state on a line (07): current window, maximum, red checks.
440
- setLineWindow ({ quilt_id: q, line, merge_window: w, merge_window_max: m, red_checks: red }) {
441
- const ln = t.lines.get(k(q, line))
442
- if (!ln) throw new StoreError('not_found', `no line ${line}`)
443
- Object.assign(ln, { merge_window: w, merge_window_max: m, red_checks: { ...red } })
444
- return clone(ln)
445
- },
446
- // The archiver's step (03): archive_seq moves from `expect_seq` to `to_seq` (a CAS, so two
447
- // archivers never both advance it) and last_segment names the newest segment's hash.
448
- setArchive ({ quilt_id: q, expect_seq: expect, to_seq: to, segment }) {
449
- const row = quiltRow(q)
450
- if ((row.archive_seq || 0) !== expect) throw new StoreError('archive_moved', 'another archiver moved archive_seq', { archive_seq: row.archive_seq || 0 })
451
- if (to <= expect || to > row.head_seq) throw new StoreError('bad_archive', 'a segment must end after archive_seq and at or before head_seq')
452
- row.archive_seq = to
453
- row.last_segment = segment
454
- return { archive_seq: to, last_segment: segment }
455
- }
456
- }
457
-
458
- // A patch's line_paths rows: one per file, and one per directory it is in ('dir/'), so
459
- // `log -- dir` reads them too.
460
- function pathRows (q, commitId) {
461
- const rows = new Map()
462
- for (const f of t.files.get(k(q, commitId)) || []) {
463
- rows.set(f.path, f.op)
464
- const parts = f.path.split('/')
465
- for (let i = 1; i < parts.length; i++) rows.set(parts.slice(0, i).join('/') + '/', 'dir')
466
- }
467
- return rows
468
- }
469
-
470
- function writeCommit (commit, s, actor, at, payload, moveHead) {
471
- const q = commit.quilt_id
472
- const parentSeqs = commit.parents.map((p) => {
473
- const pc = t.commits.get(k(q, p))
474
- if (!pc) throw new StoreError('bad_parent', `parent ${p} is not in this quilt`)
475
- return pc.seq
476
- })
477
- if (commit.predecessor && !t.commits.has(k(q, commit.predecessor))) throw new StoreError('bad_parent', 'predecessor is not in this quilt')
478
- for (const f of t.files.get(k(q, commit.id)) || commit.changes || []) if (f.copy_from && !t.commits.has(k(q, f.copy_from.commit))) throw new StoreError('bad_parent', 'copy_from commit is not in this quilt')
479
- const row = appendLog(q, { kind: 'commit', commit: commit.id, square: s.id, actor, at, payload })
480
- const { changes, ...rest } = commit
481
- const c = { ...rest, seq: row.seq, parent_seqs: parentSeqs, hidden_seq: null }
482
- t.commits.set(k(q, commit.id), c)
483
- pushTo(t.squareCommitIds, c.square_id, c.id)
484
- pushTo(t.idPrefix, k(q, c.id.slice(0, 3)), c.id)
485
- t.bySeq.set(k(q, row.seq), commit.id)
486
- if (!t.files.has(k(q, commit.id))) t.files.set(k(q, commit.id), changes || [])
487
- if (commit.predecessor) { const p = t.commits.get(k(q, commit.predecessor)); if (p && p.hidden_seq == null) p.hidden_seq = row.seq }
488
- if (moveHead) { s.head = commit.id; s.updated_at = at; s.last_seq = row.seq }
489
- return c
490
- }
491
-
492
- // ---- the journal ----
493
-
494
- // Compaction: the whole state as one `state` line (Maps and Sets tagged), then new ops after it.
495
- // Leases are not journaled: they last seconds, and after a restart every holder is gone.
496
- const NOT_JOURNALED = new Set(['remember', 'takeLease', 'giveLease'])
497
- const COMPACT_AT = 5000
498
- const pack = (_, v) => v instanceof Map ? { $map: [...v] } : v instanceof Set ? { $set: [...v] } : v
499
- const unpack = (_, v) => v && typeof v === 'object' && !Array.isArray(v) ? (v.$map ? new Map(v.$map) : v.$set ? new Set(v.$set) : v) : v
500
- let journal = null
501
- let ops = 0
502
- if (file) {
503
- fs.mkdirSync(path.dirname(file), { recursive: true })
504
- if (fs.existsSync(file)) {
505
- for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
506
- if (!line) continue
507
- if (line.startsWith('{"state":')) { const st = JSON.parse(line, unpack).state; for (const key of Object.keys(t)) if (st[key]) t[key] = st[key]; ops = 0; continue }
508
- const { op, args } = JSON.parse(line)
509
- try { tx[op](args) } catch (err) { throw new Error(`quilt journal ${file}: replaying ${op} failed: ${err.message}`) }
510
- ops++
511
- }
512
- }
513
- journal = fs.openSync(file, 'a')
514
- if (ops >= COMPACT_AT) compact()
515
- }
516
-
517
- /** Rewrites the journal as one state line (written beside it, then renamed over it). */
518
- function compact () {
519
- if (!file) return
520
- const { leases, ...durable } = t
521
- const tmp = `${file}.compact`
522
- fs.writeFileSync(tmp, JSON.stringify({ state: durable }, pack) + '\n')
523
- fs.closeSync(journal)
524
- fs.renameSync(tmp, file)
525
- journal = fs.openSync(file, 'a')
526
- ops = 0
527
- }
528
-
529
- const store = { compact: async () => compact() }
530
- for (const [name, fn] of Object.entries(tx)) {
531
- store[name] = async (args) => {
532
- const out = fn(args)
533
- if (journal && !NOT_JOURNALED.has(name)) {
534
- fs.writeSync(journal, JSON.stringify({ op: name, args }) + '\n')
535
- if (++ops >= COMPACT_AT) compact()
536
- }
537
- return structuredClone(out)
538
- }
539
- }
540
-
541
- // ---- reads ----
542
- Object.assign(store, {
543
- async getQuilt (id) { return clone(t.quilts.get(id)) },
544
- async quiltBySlug (owner, slug) { const id = t.slugs.get(k(owner, slug)); return clone(id && t.quilts.get(id)) },
545
- async quiltsFor (member) {
546
- return [...(t.byMember.get(member) || [])].map((q) => t.members.get(k(q, member))).filter(Boolean).map((m) => ({ ...clone(t.quilts.get(m.quilt_id)), level: m.level }))
547
- },
548
- async memberLevel (q, member) { return t.members.get(k(q, member))?.level || null },
549
- async members (q) { return [...(t.quiltMembers.get(q) || [])].map((m) => t.members.get(k(q, m))).filter(Boolean).map(clone) },
550
- async getLine (q, name) { return clone(t.lines.get(k(q, name))) },
551
- async lines (q) { return [...(t.quiltLines.get(q) || [])].map((n) => t.lines.get(k(q, n))).filter(Boolean).map(clone) },
552
- async getLive (q, sq) { const r = t.live.get(sq); return r && r.quilt_id === q ? clone(r) : null },
553
- // A remembered resolution (07, D12): one row per conflict key, the latest answer wins.
554
- setResolution (r) {
555
- let m = t.resolutions.get(r.quilt_id); if (!m) { m = new Map(); t.resolutions.set(r.quilt_id, m) }
556
- m.set(r.key, { ...r })
557
- return clone(m.get(r.key))
558
- },
559
- deleteResolution ({ quilt_id: q, key }) {
560
- const m = t.resolutions.get(q)
561
- if (!m || !m.has(key)) throw new StoreError('not_found', 'no such resolution')
562
- m.delete(key)
563
- return { ok: true }
564
- },
565
- async resolution (q, key) { return clone(t.resolutions.get(q)?.get(key) || null) },
566
- async resolutions (q) { return [...(t.resolutions.get(q)?.values() || [])].map(clone) },
567
- async signingKey (id) { const r = t.signingKeys.get(id); return r ? clone(r) : null },
568
- async tokens (q) { return [...t.tokens.values()].filter((r) => r.quilt_id === q).sort((a, b) => a.created_at - b.created_at).map(clone) },
569
- async tokenByHash (hash) { const r = [...t.tokens.values()].find((x) => x.hash === hash); return r ? clone(r) : null },
570
- async signingKeys (member) { return [...t.signingKeys.values()].filter((r) => r.member === member).sort((a, b) => a.added_at - b.added_at).map(clone) },
571
- async signaturesOf (q, commits) { return commits.flatMap((c) => t.signatures.get(k(q, c)) || []).map(clone) },
572
- async reviews (q, sq) { return (t.reviews.get(q) || []).filter((r) => r.square_id === sq).map(clone) },
573
- async squareChecks (q, sq) { return t.squares.get(sq)?.quilt_id === q ? [...(t.squareChecks.get(sq)?.values() || [])].map(clone) : [] },
574
- async lineRules (q) { return [...(t.lineRules.get(q)?.values() || [])].map(clone) },
575
- async claims (q) { return [...(t.claims.get(q)?.values() || [])].map(clone) },
576
- async getTag (q, name) { return clone(t.tags.get(k(q, name))) },
577
- async tags (q) { return [...(t.quiltTags.get(q) || [])].map((n) => t.tags.get(k(q, n))).filter(Boolean).map(clone) },
578
- async getSquare (id) { return clone(t.squares.get(id)) },
579
- async squares (q, { state, holder, task } = {}) {
580
- return [...(t.quiltSquares.get(q) || [])].map((id) => t.squares.get(id)).filter((s) => s.quilt_id === q && (!state || s.state === state) && (!holder || s.holder === holder) && (!task || s.task_id === task)).map(clone)
581
- },
582
- /** Squares whose id (without dashes) ends in `tail` (4+ hex digits), at most two. */
583
- async squaresByTail (q, tail) {
584
- return [...(t.squareTail.get(k(q, tail.slice(-4))) || [])].filter((id) => id.replace(/-/g, '').endsWith(tail)).slice(0, 2).map((id) => clone(t.squares.get(id)))
585
- },
586
- async getCommit (q, id) {
587
- const c = t.commits.get(k(q, id))
588
- return c ? { ...clone(c), changes: clone(t.files.get(k(q, id)) || []) } : null
589
- },
590
- async commitAtSeq (q, seq) { const id = t.bySeq.get(k(q, seq)); return id ? store.getCommit(q, id) : null },
591
- async resolvePrefix (q, prefix) {
592
- const out = []
593
- // Prefixes shorter than 3 characters read the buckets they cover (at most 4096, never rows).
594
- const buckets = prefix.length >= 3 ? [prefix.slice(0, 3)] : [...Array(16 ** (3 - prefix.length)).keys()].map((n) => prefix + n.toString(16).padStart(3 - prefix.length, '0'))
595
- for (const b of buckets) {
596
- for (const id of t.idPrefix.get(k(q, b)) || []) if (id.startsWith(prefix)) { out.push(id); if (out.length > 1) return out }
597
- }
598
- return out
599
- },
600
- async squareCommits (q, squareId) {
601
- return (t.squareCommitIds.get(squareId) || []).map((id) => t.commits.get(k(q, id))).filter(Boolean).sort((a, b) => a.seq - b.seq).map(clone)
602
- },
603
- /**
604
- * A square's commits at or below seq `upTo` that touched `p` (a file, or every file under a
605
- * 'dir/' key), newest first: [{ commit_id, seq }]. Reads only that square's file rows.
606
- */
607
- async squarePathCommits (q, squareId, p, { upTo = Infinity, limit = 1000 } = {}) {
608
- const out = []
609
- const ids = t.squareCommitIds.get(squareId) || []
610
- for (let i = ids.length - 1; i >= 0 && out.length < limit; i--) {
611
- const c = t.commits.get(k(q, ids[i]))
612
- if (!c || c.seq > upTo) continue
613
- const files = t.files.get(k(q, c.id)) || []
614
- if (files.some((f) => f.path === p || (p.endsWith('/') && f.path.startsWith(p)))) out.push({ commit_id: c.id, seq: c.seq })
615
- }
616
- return out
617
- },
618
- async getBlob (q, id) { return clone(t.blobs.get(k(q, id))) },
619
- async hasBlobs (q, ids) { return ids.filter((id) => t.blobs.has(k(q, id))) },
620
- async getObject (q, id) { return clone(t.objects.get(k(q, id))) },
621
- async patch (q, line, n) { const seq = t.patches.get(k(q, line, n)); return seq ? clone(t.log.get(q)[seq - 1]) : null },
622
- async patches (q, line, { after = 0, before = Infinity, limit = 50, desc = true } = {}) {
623
- const ln = t.lines.get(k(q, line))
624
- if (!ln) return []
625
- const out = []
626
- // An imported quilt has no patch 0 (it takes git's root commit as patch 1).
627
- const add = (n) => { const seq = t.patches.get(k(q, line, n)); if (seq) out.push(clone(t.log.get(q)[seq - 1])) }
628
- if (desc) {
629
- for (let n = Math.min(ln.patch_count, before - 1); n > after && out.length < limit; n--) add(n)
630
- } else {
631
- for (let n = after + 1; n <= ln.patch_count && n < before && out.length < limit; n++) add(n)
632
- }
633
- return out
634
- },
635
- /** A line's patches, newest first, with their commits' authors and their squares' times (as quilt_line_patches_view). */
636
- async linePatchesView (q, line, { before = Infinity, limit = 100 } = {}) {
637
- const rows = await store.patches(q, line, { before, limit: Math.min(limit, 1001), desc: true })
638
- return rows.map((r) => {
639
- const c = t.commits.get(k(q, r.commit))
640
- const s = c && t.squares.get(c.square_id)
641
- if (!c) return null
642
- const out = { patch_no: r.patch_no, commit: c.id, square_id: c.square_id, author: clone(c.author), title: String(c.message || '').split('\n')[0].slice(0, 300), authored_at: c.authored_at, at: c.at, files: (t.files.get(k(q, c.id)) || []).length }
643
- if (s) Object.assign(out, { opened_at: s.opened_at }, s.import ? { import: true } : {}, s.closed_at ? { closed_at: s.closed_at } : {})
644
- return out
645
- }).filter(Boolean)
646
- },
647
- async eventCursor (q) { return t.eventCursors.get(q) || 0 },
648
- /** A member's events after `after`, oldest first (event_recipients, then the rows). */
649
- async eventsFor (q, member, { after = 0, kinds, limit = 100 } = {}) {
650
- const seqs = t.memberEvents.get(k(q, member)) || []
651
- let lo = 0; let hi = seqs.length
652
- while (lo < hi) { const mid = (lo + hi) >> 1; if (seqs[mid] <= after) lo = mid + 1; else hi = mid }
653
- const out = []
654
- for (let i = lo; i < seqs.length && out.length < limit; i++) { const e = t.events.get(q)[seqs[i] - 1]; if (!kinds || kinds.includes(e.kind)) out.push(clone(e)) }
655
- return out
656
- },
657
- async webhook (q, member) { return clone(t.webhooks.get(k(q, member)) || null) },
658
- async logRows (q, { after = 0, limit = 100, kinds } = {}) {
659
- return (t.log.get(q) || []).slice(after, after + 100000).filter((r) => !kinds || kinds.includes(r.kind)).slice(0, limit).map(clone)
660
- },
661
- /** A path's patches on a line, newest first, at or below patch `upTo` (03's line_paths). */
662
- async linePaths (q, line, p, { upTo = Infinity, limit = 1000 } = {}) {
663
- const list = t.linePaths.get(k(q, line, p)) || []
664
- const out = []
665
- // Pending patches (merged, rows not written yet) are the newest; read their file rows.
666
- const pending = (t.linePathsPending.get(q) || []).filter((pp) => pp.line === line && pp.patch_no <= upTo)
667
- for (let i = pending.length - 1; i >= 0 && out.length < limit; i--) {
668
- const op = pathRows(q, pending[i].commit_id).get(p)
669
- if (op && !list.some((r) => r.patch_no === pending[i].patch_no)) out.push({ patch_no: pending[i].patch_no, commit_id: pending[i].commit_id, seq: pending[i].seq, op })
670
- }
671
- for (let i = list.length - 1; i >= 0 && out.length < limit; i--) if (list[i].patch_no <= upTo) out.push(clone(list[i]))
672
- return out
673
- },
674
- async recall (key, maxAgeMs, now) { const r = t.idem.get(key); return r && now - r.at < maxAgeMs ? clone(r.result) : null },
675
- async stubMembers (q) { const out = []; for (const [key, v] of t.stubs) if (v.quilt_id === q) out.push(clone(v)); return out.sort((a, b) => a.member_id.localeCompare(b.member_id)) },
676
- async gitSha (q, sha) { return t.gitShas.get(k(q, sha)) || null },
677
- async gitShas (q, shas) { const out = {}; for (const sha of shas) { const id = t.gitShas.get(k(q, sha)); if (id) out[sha] = id } return out },
678
- async gitShaOf (q, id) { return t.gitOf.get(k(q, id)) || null },
679
- async getBlock (id) { return clone(t.blocks.get(id)) },
680
- close () { if (journal) { fs.closeSync(journal); journal = null } }
681
- })
682
- return store
683
- }