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.
- package/RELEASES.md +292 -17
- package/bin/quilt.js +72 -5
- package/desktop/main.js +16 -2
- package/package.json +3 -1
- package/src/agent-task-workflow.js +2 -2
- package/src/agents/common.js +13 -0
- package/src/ai-vm.js +9 -1
- package/src/api/activity.js +5 -0
- package/src/api/memory-store.js +21 -10
- package/src/api/notify.js +134 -0
- package/src/api/routes/browsers.js +29 -0
- package/src/api/routes/github.js +0 -13
- package/src/api/routes/relay.js +26 -3
- package/src/api/routes/seam-connect.js +103 -0
- package/src/api/routes/session-migrate.js +39 -0
- package/src/api/routes/sessions.js +73 -1
- package/src/api/server.js +46 -12
- package/src/api/supabase-store.js +53 -4
- package/src/autowork.js +182 -16
- package/src/connection.js +39 -9
- package/src/control.js +139 -13
- package/src/force-date.js +27 -0
- package/src/fsutil.js +13 -3
- package/src/gitstate.js +38 -2
- package/src/history.js +25 -1
- package/src/mcp.js +93 -7
- package/src/merge3.js +11 -3
- package/src/now-summary.js +113 -0
- package/src/pathrules.js +12 -0
- package/src/protocol.js +10 -2
- package/src/quilt-default.js +84 -0
- package/src/quilt-history.js +106 -0
- package/src/quilt-remote.js +78 -0
- package/src/relay-commit.js +5 -0
- package/src/relay-lease.js +6 -2
- package/src/relay-mcp.js +172 -8
- package/src/relay-outbound.js +1 -1
- package/src/relay-prune.js +4 -1
- package/src/release-words.js +2 -2
- package/src/runner.js +20 -7
- package/src/seam-access.js +97 -0
- package/src/seam-claims.js +53 -0
- package/src/seam-url.js +15 -0
- package/src/server.js +1767 -33
- package/src/session-follow.js +515 -0
- package/src/session-layout3.js +466 -0
- package/src/session-migrate.js +123 -0
- package/src/session-quilt.js +145 -0
- package/src/session-seam.js +284 -0
- package/src/session.js +899 -46
- package/src/source-live/bring-in.js +283 -0
- package/src/source-live/live-block.js +507 -35
- package/src/source-live/live-square.js +17 -6
- package/src/status.js +5 -0
- package/src/ui/agent-guide.js +1 -1
- package/src/ui/app.css +85 -20
- package/src/ui/app.js +4 -1
- package/src/ui/chat.js +9 -3
- package/src/ui/cloud-ai-model.js +13 -0
- package/src/ui/cloud-ai.js +10 -2
- package/src/ui/common.js +6 -2
- package/src/ui/fileview.js +20 -7
- package/src/ui/force-words.js +19 -0
- package/src/ui/git-rule.js +9 -0
- package/src/ui/home.js +0 -147
- package/src/ui/loom-model.js +15 -1
- package/src/ui/quiltview.js +213 -0
- package/src/ui/seam-model.js +251 -0
- package/src/ui/session.js +413 -31
- package/src/ui/voice-call.js +83 -14
- package/src/ui/voice-model.js +39 -0
- package/src/ui/voice.js +167 -17
- package/src/ui-server.js +80 -28
- package/src/update-check.js +25 -1
- package/src/voice.js +13 -6
- package/src/git.js +0 -147
- package/src/quilts/AGENTS.md +0 -1
- package/src/quilts/CLAUDE.md +0 -1
- package/src/quilts/archiver.js +0 -175
- package/src/quilts/autorevert.js +0 -49
- package/src/quilts/bisect-run.js +0 -31
- package/src/quilts/blame.js +0 -103
- package/src/quilts/block.js +0 -347
- package/src/quilts/bucket.js +0 -25
- package/src/quilts/cbor.js +0 -92
- package/src/quilts/claims.js +0 -144
- package/src/quilts/cli.js +0 -449
- package/src/quilts/client.js +0 -128
- package/src/quilts/conflict-format.js +0 -190
- package/src/quilts/describe.js +0 -266
- package/src/quilts/dogfood.js +0 -82
- package/src/quilts/errors.js +0 -8
- package/src/quilts/events.js +0 -160
- package/src/quilts/export-git.js +0 -130
- package/src/quilts/export.js +0 -81
- package/src/quilts/fastmerge.js +0 -41
- package/src/quilts/hash.js +0 -44
- package/src/quilts/history.js +0 -266
- package/src/quilts/ids.js +0 -12
- package/src/quilts/import-git.js +0 -291
- package/src/quilts/import.js +0 -134
- package/src/quilts/index.js +0 -72
- package/src/quilts/limits.js +0 -67
- package/src/quilts/lines.js +0 -49
- package/src/quilts/live.js +0 -50
- package/src/quilts/merge-smart.js +0 -114
- package/src/quilts/merge.js +0 -486
- package/src/quilts/objects.js +0 -144
- package/src/quilts/pack.js +0 -118
- package/src/quilts/records.js +0 -211
- package/src/quilts/resolutions.js +0 -69
- package/src/quilts/reviews.js +0 -195
- package/src/quilts/routes.js +0 -126
- package/src/quilts/service.js +0 -923
- package/src/quilts/setup.js +0 -34
- package/src/quilts/signing.js +0 -149
- package/src/quilts/squares.js +0 -239
- package/src/quilts/store-memory.js +0 -683
- package/src/quilts/store-rpc.js +0 -79
- package/src/quilts/store-supabase.js +0 -13
- package/src/quilts/tags.js +0 -64
- package/src/quilts/textdiff.js +0 -96
- package/src/quilts/tokens.js +0 -66
- package/src/quilts/tools.js +0 -375
- package/src/quilts/tree.js +0 -351
- package/src/quilts/webhook.js +0 -116
- package/src/quilts/window.js +0 -119
- package/src/quilts/xdiff.js +0 -435
- 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
|
-
}
|