@volter/world-core 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -0
  3. package/app-route.cjs +154 -0
  4. package/app-route.d.cts +7 -0
  5. package/attach.cjs +80 -0
  6. package/dist/app-route.cjs +154 -0
  7. package/dist/app-route.d.cts +7 -0
  8. package/dist/attach.cjs +80 -0
  9. package/dist/generated/pack-facts.json +4306 -0
  10. package/dist/inject.cjs +1097 -0
  11. package/dist/network-policy.cjs +92 -0
  12. package/dist/network-policy.d.cts +10 -0
  13. package/dist/src/actions.d.ts +276 -0
  14. package/dist/src/actions.js +436 -0
  15. package/dist/src/ancestry.d.ts +22 -0
  16. package/dist/src/ancestry.js +238 -0
  17. package/dist/src/args.d.ts +3 -0
  18. package/dist/src/args.js +12 -0
  19. package/dist/src/blob-store.d.ts +55 -0
  20. package/dist/src/blob-store.js +186 -0
  21. package/dist/src/brand-tokens.d.ts +2 -0
  22. package/dist/src/brand-tokens.js +17 -0
  23. package/dist/src/changeset.d.ts +431 -0
  24. package/dist/src/changeset.js +0 -0
  25. package/dist/src/client-bundle.d.ts +1 -0
  26. package/dist/src/client-bundle.js +28 -0
  27. package/dist/src/credential.d.ts +38 -0
  28. package/dist/src/credential.js +114 -0
  29. package/dist/src/derived-core.d.ts +452 -0
  30. package/dist/src/derived-core.js +782 -0
  31. package/dist/src/derived.d.ts +84 -0
  32. package/dist/src/derived.js +122 -0
  33. package/dist/src/emit.d.ts +106 -0
  34. package/dist/src/emit.js +157 -0
  35. package/dist/src/executor.d.ts +120 -0
  36. package/dist/src/executor.js +387 -0
  37. package/dist/src/file-response.d.ts +3 -0
  38. package/dist/src/file-response.js +22 -0
  39. package/dist/src/fork.d.ts +26 -0
  40. package/dist/src/fork.js +68 -0
  41. package/dist/src/git/history.d.ts +36 -0
  42. package/dist/src/git/history.js +298 -0
  43. package/dist/src/git/index.d.ts +6 -0
  44. package/dist/src/git/index.js +6 -0
  45. package/dist/src/git/inflate.d.ts +11 -0
  46. package/dist/src/git/inflate.js +194 -0
  47. package/dist/src/git/objects.d.ts +64 -0
  48. package/dist/src/git/objects.js +161 -0
  49. package/dist/src/git/pack.d.ts +14 -0
  50. package/dist/src/git/pack.js +199 -0
  51. package/dist/src/git/refs.d.ts +19 -0
  52. package/dist/src/git/refs.js +35 -0
  53. package/dist/src/git/smart-http.d.ts +45 -0
  54. package/dist/src/git/smart-http.js +223 -0
  55. package/dist/src/hash.d.ts +38 -0
  56. package/dist/src/hash.js +48 -0
  57. package/dist/src/head.d.ts +140 -0
  58. package/dist/src/head.js +313 -0
  59. package/dist/src/history.d.ts +76 -0
  60. package/dist/src/history.js +322 -0
  61. package/dist/src/index.d.ts +73 -0
  62. package/dist/src/index.js +98 -0
  63. package/dist/src/lifecycle.d.ts +1 -0
  64. package/dist/src/lifecycle.js +8 -0
  65. package/dist/src/log.d.ts +254 -0
  66. package/dist/src/log.js +801 -0
  67. package/dist/src/mirror-shell.d.ts +2 -0
  68. package/dist/src/mirror-shell.js +13 -0
  69. package/dist/src/observe.d.ts +49 -0
  70. package/dist/src/observe.js +148 -0
  71. package/dist/src/pack-assets.d.ts +30 -0
  72. package/dist/src/pack-assets.js +88 -0
  73. package/dist/src/packRegistry.d.ts +374 -0
  74. package/dist/src/packRegistry.js +142 -0
  75. package/dist/src/placeholder-remote.d.ts +22 -0
  76. package/dist/src/placeholder-remote.js +86 -0
  77. package/dist/src/proxy.d.ts +25 -0
  78. package/dist/src/proxy.js +155 -0
  79. package/dist/src/rateBudget.d.ts +367 -0
  80. package/dist/src/rateBudget.js +925 -0
  81. package/dist/src/references.d.ts +18 -0
  82. package/dist/src/references.js +27 -0
  83. package/dist/src/remote-execute.d.ts +22 -0
  84. package/dist/src/remote-execute.js +1 -0
  85. package/dist/src/resource-blob.d.ts +10 -0
  86. package/dist/src/resource-blob.js +56 -0
  87. package/dist/src/scenario.d.ts +197 -0
  88. package/dist/src/scenario.js +425 -0
  89. package/dist/src/schemas.d.ts +78 -0
  90. package/dist/src/schemas.js +50 -0
  91. package/dist/src/serve-http.d.ts +48 -0
  92. package/dist/src/serve-http.js +340 -0
  93. package/dist/src/serve.d.ts +147 -0
  94. package/dist/src/serve.js +507 -0
  95. package/dist/src/shared-blob-index.d.ts +4 -0
  96. package/dist/src/shared-blob-index.js +126 -0
  97. package/dist/src/state-system.d.ts +70 -0
  98. package/dist/src/state-system.js +90 -0
  99. package/dist/src/storage.d.ts +101 -0
  100. package/dist/src/storage.js +337 -0
  101. package/dist/src/twin-fetch.d.ts +64 -0
  102. package/dist/src/twin-fetch.js +91 -0
  103. package/dist/src/types.d.ts +40 -0
  104. package/dist/src/types.js +1 -0
  105. package/dist/src/v1-removed.d.ts +159 -0
  106. package/dist/src/v1-removed.js +124 -0
  107. package/dist/src/volter-home.d.ts +5 -0
  108. package/dist/src/volter-home.js +10 -0
  109. package/dist/src/world-clock.d.ts +4 -0
  110. package/dist/src/world-clock.js +32 -0
  111. package/dist/src/world-env.d.ts +3 -0
  112. package/dist/src/world-env.js +22 -0
  113. package/dist/src/world-store-sql.d.ts +27 -0
  114. package/dist/src/world-store-sql.js +86 -0
  115. package/dist/src/world-store.d.ts +168 -0
  116. package/dist/src/world-store.js +475 -0
  117. package/dist/src/worldConfig.d.ts +9 -0
  118. package/dist/src/worldConfig.js +17 -0
  119. package/dist/stream-bridge.cjs +80 -0
  120. package/dist/vendor-hosts.cjs +200 -0
  121. package/generated/pack-facts.json +4306 -0
  122. package/inject.cjs +1097 -0
  123. package/network-policy.cjs +92 -0
  124. package/network-policy.d.cts +10 -0
  125. package/package.json +103 -0
  126. package/src/actions.ts +564 -0
  127. package/src/ancestry.ts +213 -0
  128. package/src/args.ts +14 -0
  129. package/src/blob-store.ts +185 -0
  130. package/src/brand-tokens.ts +17 -0
  131. package/src/changeset.ts +1032 -0
  132. package/src/client-bundle.ts +29 -0
  133. package/src/credential.ts +140 -0
  134. package/src/derived-core.ts +1004 -0
  135. package/src/derived.ts +176 -0
  136. package/src/emit.ts +242 -0
  137. package/src/executor.ts +431 -0
  138. package/src/file-response.ts +22 -0
  139. package/src/fork.ts +89 -0
  140. package/src/git/history.ts +177 -0
  141. package/src/git/index.ts +6 -0
  142. package/src/git/inflate.ts +125 -0
  143. package/src/git/objects.ts +110 -0
  144. package/src/git/pack.ts +105 -0
  145. package/src/git/refs.ts +25 -0
  146. package/src/git/smart-http.ts +149 -0
  147. package/src/hash.ts +66 -0
  148. package/src/head.ts +318 -0
  149. package/src/history.ts +246 -0
  150. package/src/index.ts +323 -0
  151. package/src/lifecycle.ts +8 -0
  152. package/src/log.ts +793 -0
  153. package/src/mirror-shell.ts +15 -0
  154. package/src/observe.ts +130 -0
  155. package/src/pack-assets.ts +81 -0
  156. package/src/packRegistry.ts +408 -0
  157. package/src/placeholder-remote.ts +81 -0
  158. package/src/proxy.ts +183 -0
  159. package/src/rateBudget.ts +1115 -0
  160. package/src/references.ts +46 -0
  161. package/src/remote-execute.ts +26 -0
  162. package/src/resource-blob.ts +57 -0
  163. package/src/scenario.ts +479 -0
  164. package/src/schemas.ts +56 -0
  165. package/src/serve-http.ts +299 -0
  166. package/src/serve.ts +618 -0
  167. package/src/shared-blob-index.ts +108 -0
  168. package/src/state-system.ts +115 -0
  169. package/src/storage.ts +407 -0
  170. package/src/twin-fetch.ts +147 -0
  171. package/src/types.ts +50 -0
  172. package/src/v1-removed.ts +172 -0
  173. package/src/volter-home.ts +11 -0
  174. package/src/world-clock.ts +33 -0
  175. package/src/world-env.ts +18 -0
  176. package/src/world-store-sql.ts +118 -0
  177. package/src/world-store.ts +572 -0
  178. package/src/worldConfig.ts +27 -0
  179. package/stream-bridge.cjs +80 -0
  180. package/vendor-hosts.cjs +200 -0
@@ -0,0 +1,177 @@
1
+ // HISTORY AND DIFF: what a hosting twin's REST plane asks of the object store beyond raw reads —
2
+ // the merge base of two commits, the changed-file list between two commits with GitHub's
3
+ // three-dot semantics (merge-base(base, head)..head), per-file addition/deletion counts and a
4
+ // unified patch from a Myers line diff. Pure functions of the store; the `git` binary is the
5
+ // oracle these were proven against, never a dependency.
6
+ import { GitObjectStore, decodeCommit, decodeTree, type TreeEntry } from './objects.ts';
7
+
8
+ async function parentsOf(store: GitObjectStore, sha: string): Promise<string[]> { const o = await store.read(sha); return o?.type === 'commit' ? decodeCommit(o.payload).parents : []; }
9
+
10
+ /** Ancestors of `sha` (inclusive), bounded. */
11
+ async function ancestors(store: GitObjectStore, sha: string, limit: number): Promise<Set<string>> {
12
+ const seen = new Set<string>(); const stack = [sha];
13
+ while (stack.length && seen.size < limit) { const s = stack.pop()!; if (seen.has(s)) continue; seen.add(s); stack.push(...(await parentsOf(store, s))); }
14
+ return seen;
15
+ }
16
+ /** A best common ancestor of `a` and `b` (one that is not an ancestor of another common ancestor), or null. */
17
+ export async function mergeBase(store: GitObjectStore, a: string, b: string, limit = 20000): Promise<string | null> {
18
+ if (!(await store.has(a)) || !(await store.has(b))) return null;
19
+ const ofA = await ancestors(store, a, limit);
20
+ // BFS from b in parent order: the first commits reached that lie in A's ancestry are candidates; a
21
+ // candidate reached through another candidate is dominated and dropped.
22
+ const candidates: string[] = []; const seen = new Set<string>(); const queue = [b];
23
+ while (queue.length && seen.size < limit) {
24
+ const s = queue.shift()!; if (seen.has(s)) continue; seen.add(s);
25
+ if (ofA.has(s)) { candidates.push(s); continue; }
26
+ queue.push(...(await parentsOf(store, s)));
27
+ }
28
+ if (candidates.length === 0) return null;
29
+ if (candidates.length === 1) return candidates[0]!;
30
+ const dominated = new Set<string>();
31
+ for (const c of candidates) { const anc = await ancestors(store, c, limit); for (const d of candidates) if (d !== c && anc.has(d)) dominated.add(d); }
32
+ return candidates.find((c) => !dominated.has(c)) ?? candidates[0]!;
33
+ }
34
+
35
+ export type TreeChange = { path: string; status: 'added' | 'removed' | 'modified'; oldSha?: string; newSha?: string; oldMode?: string; newMode?: string };
36
+ async function treeOf(store: GitObjectStore, sha: string): Promise<TreeEntry[]> {
37
+ const o = await store.read(sha); if (!o) return [];
38
+ if (o.type === 'commit') return treeOf(store, decodeCommit(o.payload).tree);
39
+ return o.type === 'tree' ? decodeTree(o.payload) : [];
40
+ }
41
+ const isDir = (m: string) => m === '40000' || m === '040000';
42
+ /** Recursive tree compare between two commits or trees (paths in the post-image order, no rename pairing — like `diff-tree -r` without -M). */
43
+ export async function diffTrees(store: GitObjectStore, from: string | null, to: string, prefix = ''): Promise<TreeChange[]> {
44
+ const a = from ? await treeOf(store, from) : []; const b = await treeOf(store, to);
45
+ const byName = new Map(a.map((e) => [e.name, e])); const out: TreeChange[] = []; const seenNames = new Set<string>();
46
+ const all = [...a.map((e) => e.name), ...b.map((e) => e.name)].filter((n) => (seenNames.has(n) ? false : (seenNames.add(n), true))).sort();
47
+ const bByName = new Map(b.map((e) => [e.name, e]));
48
+ for (const name of all) {
49
+ const ax = byName.get(name), by = bByName.get(name); const path = prefix + name;
50
+ if (ax && by && ax.sha === by.sha && ax.mode === by.mode) continue;
51
+ const xd = ax ? isDir(ax.mode) : false, yd = by ? isDir(by.mode) : false;
52
+ if (xd || yd) {
53
+ if (xd && yd) out.push(...(await diffTrees(store, ax!.sha, by!.sha, `${path}/`)));
54
+ else { if (xd) out.push(...(await diffTrees(store, ax!.sha, '4b825dc642cb6eb9a060e54bf8d69288fbee4904', `${path}/`)).map((c) => ({ ...c }))); else if (ax) out.push({ path, status: 'removed', oldSha: ax.sha, oldMode: ax.mode }); if (yd) out.push(...(await diffTrees(store, null, by!.sha, `${path}/`))); else if (by) out.push({ path, status: 'added', newSha: by.sha, newMode: by.mode }); }
55
+ continue;
56
+ }
57
+ if (ax && by) out.push({ path, status: 'modified', oldSha: ax.sha, newSha: by.sha, oldMode: ax.mode, newMode: by.mode });
58
+ else if (ax) out.push({ path, status: 'removed', oldSha: ax.sha, oldMode: ax.mode });
59
+ else if (by) out.push({ path, status: 'added', newSha: by.sha, newMode: by.mode });
60
+ }
61
+ return out;
62
+ }
63
+
64
+ // ── Myers line diff ─────────────────────────────────────────────────────────────────────
65
+ export type Edit = { kind: ' ' | '-' | '+'; line: string };
66
+ function splitLines(text: string): string[] { if (text === '') return []; const lines = text.split('\n'); if (lines[lines.length - 1] === '') lines.pop(); return lines; }
67
+ /** A shortest edit script between two texts (Myers, O((N+M)D)). */
68
+ export function lineDiff(oldText: string, newText: string): Edit[] {
69
+ const a = splitLines(oldText), b = splitLines(newText); const n = a.length, m = b.length; const max = n + m;
70
+ const v = new Map<number, number>([[1, 0]]); const trace: Map<number, number>[] = [];
71
+ outer: for (let d = 0; d <= max; d++) {
72
+ trace.push(new Map(v));
73
+ for (let k = -d; k <= d; k += 2) {
74
+ let ax = k === -d || (k !== d && (v.get(k - 1) ?? 0) < (v.get(k + 1) ?? 0)) ? (v.get(k + 1) ?? 0) : (v.get(k - 1) ?? 0) + 1;
75
+ let by = ax - k;
76
+ while (ax < n && by < m && a[ax] === b[by]) { ax++; by++; }
77
+ v.set(k, ax);
78
+ if (ax >= n && by >= m) break outer;
79
+ }
80
+ }
81
+ const edits: Edit[] = []; let ax = n, by = m;
82
+ for (let d = trace.length - 1; d >= 0; d--) {
83
+ const vv = trace[d]!; const k = ax - by;
84
+ const prevK = k === -d || (k !== d && (vv.get(k - 1) ?? 0) < (vv.get(k + 1) ?? 0)) ? k + 1 : k - 1;
85
+ const prevX = vv.get(prevK) ?? 0; const prevY = prevX - prevK;
86
+ while (ax > prevX && by > prevY) { edits.push({ kind: ' ', line: a[ax - 1]! }); ax--; by--; }
87
+ if (d > 0) { if (ax === prevX) { edits.push({ kind: '+', line: b[by - 1]! }); by--; } else { edits.push({ kind: '-', line: a[ax - 1]! }); ax--; } }
88
+ }
89
+ return edits.reverse();
90
+ }
91
+ /** The unified-diff hunks (the body GitHub serves as a file's `patch`), 3 lines of context. */
92
+ export function unifiedPatch(oldText: string, newText: string, context = 3): string {
93
+ const edits = lineDiff(oldText, newText); if (!edits.some((e) => e.kind !== ' ')) return '';
94
+ const oldNoNl = oldText !== '' && !oldText.endsWith('\n'), newNoNl = newText !== '' && !newText.endsWith('\n');
95
+ const oldLines = splitLines(oldText).length, newLines = splitLines(newText).length;
96
+ // group edits into hunks: a run of changes with up to `context` unchanged lines around it
97
+ const hunks: Array<{ start: number; end: number }> = []; let i = 0;
98
+ while (i < edits.length) {
99
+ if (edits[i]!.kind === ' ') { i++; continue; }
100
+ const start = Math.max(0, i - context); let end = i;
101
+ while (end < edits.length) { if (edits[end]!.kind !== ' ') { end++; continue; } let run = end; while (run < edits.length && edits[run]!.kind === ' ') run++; if (run - end > context * 2 || run >= edits.length) { end = Math.min(edits.length, end + context); break; } end = run; }
102
+ hunks.push({ start, end }); i = end;
103
+ }
104
+ let oldPos = 0, newPos = 0, out = ''; let cursor = 0;
105
+ for (const h of hunks) {
106
+ for (; cursor < h.start; cursor++) { const e = edits[cursor]!; if (e.kind !== '+') oldPos++; if (e.kind !== '-') newPos++; }
107
+ let oc = 0, nc = 0, body = '';
108
+ for (let j = h.start; j < h.end; j++) {
109
+ const e = edits[j]!; body += `${e.kind}${e.line}\n`;
110
+ const lastOld = e.kind !== '+' && oldPos + oc === oldLines - 1, lastNew = e.kind !== '-' && newPos + nc === newLines - 1;
111
+ if (e.kind !== '+') oc++; if (e.kind !== '-') nc++;
112
+ if ((e.kind === '-' && lastOld && oldNoNl) || (e.kind === '+' && lastNew && newNoNl) || (e.kind === ' ' && lastOld && oldNoNl && lastNew && newNoNl)) body += '\\n';
113
+ }
114
+ const range = (p: number, c: number) => (c === 1 ? `${p + 1}` : `${c === 0 ? p : p + 1},${c}`);
115
+ out += `@@ -${range(oldPos, oc)} +${range(newPos, nc)} @@\n${body}`;
116
+ oldPos += oc; newPos += nc; cursor = h.end;
117
+ }
118
+ return out.replace(/\n$/, '');
119
+ }
120
+
121
+ export type DiffFile = { filename: string; status: 'added' | 'removed' | 'modified'; additions: number; deletions: number; sha?: string; patch?: string };
122
+ const DEC = new TextDecoder();
123
+ function looksBinary(bytes: Uint8Array): boolean { const n = Math.min(bytes.length, 8000); for (let i = 0; i < n; i++) if (bytes[i] === 0) return true; return false; }
124
+ /** GitHub's file list between `base` and `head` (three-dot: from merge-base(base, head), or base when histories are unrelated), with counts and patches. */
125
+ export async function diffFiles(store: GitObjectStore, baseSha: string, headSha: string, opts: { patch?: boolean } = {}): Promise<DiffFile[] | null> {
126
+ if (!(await store.has(baseSha)) || !(await store.has(headSha))) return null;
127
+ const from = (await mergeBase(store, baseSha, headSha)) ?? baseSha;
128
+ const out: DiffFile[] = [];
129
+ for (const c of await diffTrees(store, from, headSha)) {
130
+ const oldB = c.oldSha ? (await store.read(c.oldSha))?.payload ?? new Uint8Array() : new Uint8Array();
131
+ const newB = c.newSha ? (await store.read(c.newSha))?.payload ?? new Uint8Array() : new Uint8Array();
132
+ const file: DiffFile = { filename: c.path, status: c.status, additions: 0, deletions: 0, ...(c.newSha ? { sha: c.newSha } : {}) };
133
+ if (!looksBinary(oldB) && !looksBinary(newB)) {
134
+ const oldT = DEC.decode(oldB), newT = DEC.decode(newB); const edits = lineDiff(oldT, newT);
135
+ file.additions = edits.filter((e) => e.kind === '+').length; file.deletions = edits.filter((e) => e.kind === '-').length;
136
+ if (opts.patch) { const p = unifiedPatch(oldT, newT); if (p) file.patch = p; }
137
+ }
138
+ out.push(file);
139
+ }
140
+ return out;
141
+ }
142
+
143
+ /** Three-way line merge. Return null for overlapping edits; preserve exact line endings.
144
+ * Binary payloads belong to the caller's conflict path, never a text decoder's replacement. */
145
+ export function mergeText(base: string, ours: string, theirs: string): string | null {
146
+ if (ours === theirs || theirs === base) return ours;
147
+ if (ours === base) return theirs;
148
+ type Hunk = { start: number; end: number; lines: string[] };
149
+ // Encode whole lines, including their terminators, before diffing. Thus changing the final
150
+ // newline is a real edit and CRLF bytes survive without a separate post-merge guess.
151
+ const lines = (text: string): string[] => (text.match(/[^\n]*\n|[^\n]+$/g) ?? []).map(s => JSON.stringify(s));
152
+ const original = lines(base);
153
+ const changes = (text: string): Hunk[] => {
154
+ const out: Hunk[] = []; let cursor = 0; let hunk: Hunk | undefined;
155
+ for (const edit of lineDiff(original.join('\n'), lines(text).join('\n'))) {
156
+ if (edit.kind === ' ') { hunk = undefined; cursor++; continue; }
157
+ if (!hunk) { hunk = { start: cursor, end: cursor, lines: [] }; out.push(hunk); }
158
+ if (edit.kind === '-') { cursor++; hunk.end++; } else hunk.lines.push(edit.line);
159
+ }
160
+ return out;
161
+ };
162
+ const left = changes(ours), right = changes(theirs);
163
+ const identical = (a: Hunk, b: Hunk) => a.start === b.start && a.end === b.end && JSON.stringify(a.lines) === JSON.stringify(b.lines);
164
+ const edits = [...left];
165
+ for (const b of right) {
166
+ if (left.some(a => identical(a, b))) continue;
167
+ for (const a of left) {
168
+ // Two insertions at one point or touching changed intervals have ambiguous order.
169
+ if (a.start <= b.end && b.start <= a.end) return null;
170
+ }
171
+ edits.push(b);
172
+ }
173
+ edits.sort((a, b) => b.start - a.start);
174
+ const result = [...original];
175
+ for (const h of edits) result.splice(h.start, h.end - h.start, ...h.lines);
176
+ return result.map(line => JSON.parse(line) as string).join('');
177
+ }
@@ -0,0 +1,6 @@
1
+ export * from './inflate.ts';
2
+ export * from './objects.ts';
3
+ export * from './pack.ts';
4
+ export * from './refs.ts';
5
+ export * from './smart-http.ts';
6
+ export * from './history.ts';
@@ -0,0 +1,125 @@
1
+ // A pure-JS zlib INFLATE that reports the bytes it consumed — the one thing the standard
2
+ // DecompressionStream cannot say, and the one thing a packfile reader must know (each object's
3
+ // zlib stream is followed by the next object's header with no length in between). Stored,
4
+ // fixed-Huffman and dynamic-Huffman blocks (RFC 1951) inside a zlib wrapper (RFC 1950: a 2-byte
5
+ // header, an Adler-32 trailer). No dependency; runs on Bun and workerd alike.
6
+
7
+ const LENGTH_BASE = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258];
8
+ const LENGTH_EXTRA = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0];
9
+ const DIST_BASE = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577];
10
+ const DIST_EXTRA = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13];
11
+ const CL_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
12
+
13
+ type Huffman = { counts: Uint16Array; symbols: Uint16Array };
14
+
15
+ function buildHuffman(lengths: Uint8Array | number[], n: number): Huffman {
16
+ const counts = new Uint16Array(16); const symbols = new Uint16Array(n);
17
+ for (let i = 0; i < n; i++) counts[lengths[i]!]!++;
18
+ counts[0] = 0;
19
+ const offs = new Uint16Array(16);
20
+ for (let i = 1; i < 16; i++) offs[i] = offs[i - 1]! + counts[i - 1]!;
21
+ for (let i = 0; i < n; i++) if (lengths[i]) symbols[offs[lengths[i]!]!++] = i;
22
+ return { counts, symbols };
23
+ }
24
+
25
+ class BitReader {
26
+ pos: number; bitBuf = 0; bitCount = 0;
27
+ constructor(readonly input: Uint8Array, offset: number) { this.pos = offset; }
28
+ bit(): number {
29
+ if (this.bitCount === 0) { if (this.pos >= this.input.length) throw new Error('inflate: unexpected end of input'); this.bitBuf = this.input[this.pos++]!; this.bitCount = 8; }
30
+ const b = this.bitBuf & 1; this.bitBuf >>>= 1; this.bitCount--; return b;
31
+ }
32
+ bits(n: number): number { let v = 0; for (let i = 0; i < n; i++) v |= this.bit() << i; return v; }
33
+ symbol(h: Huffman): number {
34
+ let code = 0, first = 0, index = 0;
35
+ for (let len = 1; len < 16; len++) {
36
+ code |= this.bit(); const count = h.counts[len]!;
37
+ if (code - count < first) return h.symbols[index + (code - first)]!;
38
+ index += count; first += count; first <<= 1; code <<= 1;
39
+ }
40
+ throw new Error('inflate: bad huffman code');
41
+ }
42
+ alignByte(): void { this.bitBuf = 0; this.bitCount = 0; }
43
+ }
44
+
45
+ class Out {
46
+ buf = new Uint8Array(1 << 16); len = 0;
47
+ push(b: number): void { if (this.len === this.buf.length) { const n = new Uint8Array(this.buf.length * 2); n.set(this.buf); this.buf = n; } this.buf[this.len++] = b; }
48
+ copy(dist: number, length: number): void { if (dist > this.len) throw new Error('inflate: distance too far back'); for (let i = 0; i < length; i++) this.push(this.buf[this.len - dist]!); }
49
+ // A right-sized copy: a view would pin the whole (>= 64 KiB, doubling) scratch buffer behind
50
+ // every inflated object, which is what a 105k-object push cost the github twin (~7 GB).
51
+ bytes(): Uint8Array { return this.buf.slice(0, this.len); }
52
+ }
53
+
54
+ let FIXED: { lit: Huffman; dist: Huffman } | undefined;
55
+ function fixedTables(): { lit: Huffman; dist: Huffman } {
56
+ if (FIXED) return FIXED;
57
+ const l = new Uint8Array(288); for (let i = 0; i < 144; i++) l[i] = 8; for (let i = 144; i < 256; i++) l[i] = 9; for (let i = 256; i < 280; i++) l[i] = 7; for (let i = 280; i < 288; i++) l[i] = 8;
58
+ const d = new Uint8Array(30).fill(5);
59
+ FIXED = { lit: buildHuffman(l, 288), dist: buildHuffman(d, 30) }; return FIXED;
60
+ }
61
+
62
+ function inflateBlockData(r: BitReader, out: Out, lit: Huffman, dist: Huffman): void {
63
+ for (;;) {
64
+ const sym = r.symbol(lit);
65
+ if (sym < 256) out.push(sym);
66
+ else if (sym === 256) return;
67
+ else {
68
+ const li = sym - 257; if (li >= 29) throw new Error('inflate: bad length symbol');
69
+ const length = LENGTH_BASE[li]! + r.bits(LENGTH_EXTRA[li]!);
70
+ const di = r.symbol(dist); if (di >= 30) throw new Error('inflate: bad distance symbol');
71
+ out.copy(DIST_BASE[di]! + r.bits(DIST_EXTRA[di]!), length);
72
+ }
73
+ }
74
+ }
75
+
76
+ function inflateDynamic(r: BitReader, out: Out): void {
77
+ const hlit = r.bits(5) + 257, hdist = r.bits(5) + 1, hclen = r.bits(4) + 4;
78
+ const cl = new Uint8Array(19); for (let i = 0; i < hclen; i++) cl[CL_ORDER[i]!] = r.bits(3);
79
+ const clh = buildHuffman(cl, 19);
80
+ const lengths = new Uint8Array(hlit + hdist);
81
+ for (let i = 0; i < hlit + hdist;) {
82
+ const sym = r.symbol(clh);
83
+ if (sym < 16) lengths[i++] = sym;
84
+ else if (sym === 16) { if (i === 0) throw new Error('inflate: repeat with no previous length'); const prev = lengths[i - 1]!; for (let n = 3 + r.bits(2); n > 0; n--) lengths[i++] = prev; }
85
+ else if (sym === 17) { for (let n = 3 + r.bits(3); n > 0; n--) lengths[i++] = 0; }
86
+ else { for (let n = 11 + r.bits(7); n > 0; n--) lengths[i++] = 0; }
87
+ }
88
+ inflateBlockData(r, out, buildHuffman(lengths.subarray(0, hlit), hlit), buildHuffman(lengths.subarray(hlit), hdist));
89
+ }
90
+
91
+ /** Raw DEFLATE (RFC 1951) starting at `offset`: the bytes out and the input consumed. */
92
+ export function inflateRaw(input: Uint8Array, offset = 0): { out: Uint8Array; consumed: number } {
93
+ const r = new BitReader(input, offset); const out = new Out();
94
+ let final = 0;
95
+ do {
96
+ final = r.bit(); const type = r.bits(2);
97
+ if (type === 0) {
98
+ r.alignByte();
99
+ const len = input[r.pos]! | (input[r.pos + 1]! << 8); const nlen = input[r.pos + 2]! | (input[r.pos + 3]! << 8);
100
+ if ((len ^ 0xffff) !== nlen) throw new Error('inflate: stored block length mismatch');
101
+ r.pos += 4; for (let i = 0; i < len; i++) out.push(input[r.pos++]!);
102
+ } else if (type === 1) { const t = fixedTables(); inflateBlockData(r, out, t.lit, t.dist); }
103
+ else if (type === 2) inflateDynamic(r, out);
104
+ else throw new Error('inflate: reserved block type');
105
+ } while (!final);
106
+ return { out: out.bytes(), consumed: r.pos - offset };
107
+ }
108
+
109
+ export function adler32(bytes: Uint8Array): number {
110
+ let a = 1, b = 0;
111
+ for (let i = 0; i < bytes.length; i++) { a = (a + bytes[i]!) % 65521; b = (b + a) % 65521; }
112
+ return ((b << 16) | a) >>> 0;
113
+ }
114
+
115
+ /** zlib (RFC 1950) starting at `offset`: header checked, Adler-32 verified, consumed includes both. */
116
+ export function inflateZlib(input: Uint8Array, offset = 0): { out: Uint8Array; consumed: number } {
117
+ const cmf = input[offset]!, flg = input[offset + 1]!;
118
+ if ((cmf & 0x0f) !== 8 || ((cmf << 8) | flg) % 31 !== 0) throw new Error('inflate: not a zlib stream');
119
+ if (flg & 0x20) throw new Error('inflate: preset dictionary is not supported');
120
+ const { out, consumed } = inflateRaw(input, offset + 2);
121
+ const t = offset + 2 + consumed;
122
+ const expected = ((input[t]! << 24) | (input[t + 1]! << 16) | (input[t + 2]! << 8) | input[t + 3]!) >>> 0;
123
+ if (expected !== adler32(out)) throw new Error('inflate: adler32 mismatch');
124
+ return { out, consumed: consumed + 6 };
125
+ }
@@ -0,0 +1,110 @@
1
+ // GIT OBJECTS OVER THE BLOB SEAM (docs/contributing/architecture.md#the-engine-slot): loose objects — `<type>
2
+ // <size>\0<payload>`, zlib-deflated, addressed by the SHA-1 of the uncompressed form — written
3
+ // and read through the active BlobStore under `<prefix>/objects/<ab>/<rest>`, the exact layout
4
+ // a bare repository keeps. Pure JS: SHA-1 through WebCrypto, loose objects stored uncompressed in zlib framing (storeZlib),
5
+ // a pack deflated through the standard CompressionStream, inflate through ./inflate.ts (it reports consumed bytes, which a packfile
6
+ // reader needs). Bun and workerd alike; no `git` binary anywhere.
7
+ import { inflateZlib } from './inflate.ts';
8
+ import type { BlobStore } from '../blob-store.ts';
9
+
10
+ export type GitObjectType = 'commit' | 'tree' | 'blob' | 'tag';
11
+ const ENC = new TextEncoder(); const DEC = new TextDecoder();
12
+
13
+ export async function sha1Hex(bytes: Uint8Array): Promise<string> {
14
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-1', bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer));
15
+ return [...digest].map((b) => b.toString(16).padStart(2, '0')).join('');
16
+ }
17
+ export function hexToBytes(hex: string): Uint8Array { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return out; }
18
+ export function bytesToHex(b: Uint8Array): string { return [...b].map((byte) => byte.toString(16).padStart(2, '0')).join(''); }
19
+ export function concat(...parts: Uint8Array[]): Uint8Array { const n = parts.reduce((s, p) => s + p.length, 0); const out = new Uint8Array(n); let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out; }
20
+
21
+ /** The loose-object form: header + payload (what the sha is computed over). */
22
+ export function encodeLoose(type: GitObjectType, payload: Uint8Array): Uint8Array { return concat(ENC.encode(`${type} ${payload.length}\0`), payload); }
23
+ export function decodeLoose(bytes: Uint8Array): { type: GitObjectType; payload: Uint8Array } {
24
+ const nul = bytes.indexOf(0); if (nul < 0) throw new Error('git object: no header');
25
+ const [type, size] = DEC.decode(bytes.subarray(0, nul)).split(' ');
26
+ const payload = bytes.subarray(nul + 1);
27
+ if (Number(size) !== payload.length) throw new Error(`git object: header says ${size} bytes, payload is ${payload.length}`);
28
+ return { type: type as GitObjectType, payload };
29
+ }
30
+ export async function hashObject(type: GitObjectType, payload: Uint8Array): Promise<string> { return sha1Hex(encodeLoose(type, payload)); }
31
+
32
+ export async function deflateZlib(bytes: Uint8Array): Promise<Uint8Array> {
33
+ const cs = new CompressionStream('deflate'); const w = cs.writable.getWriter(); void w.write(new Uint8Array(bytes) as unknown as BufferSource); void w.close();
34
+ return new Uint8Array(await new Response(cs.readable).arrayBuffer());
35
+ }
36
+
37
+ /** zlib with stored (uncompressed) deflate blocks: what git writes at `core.compression 0`, and what every
38
+ * inflater reads. A loose object is written on every commit a World makes; compressing each one through the
39
+ * asynchronous CompressionStream made a write's cost the stream's round trips, not the object's size. */
40
+ export function storeZlib(bytes: Uint8Array): Uint8Array {
41
+ const blocks = Math.max(1, Math.ceil(bytes.length / 0xffff));
42
+ const out = new Uint8Array(2 + bytes.length + blocks * 5 + 4);
43
+ out[0] = 0x78; out[1] = 0x01; let o = 2;
44
+ for (let b = 0; b < blocks; b++) {
45
+ const start = b * 0xffff; const len = Math.min(0xffff, bytes.length - start);
46
+ out[o++] = b === blocks - 1 ? 1 : 0; out[o++] = len & 0xff; out[o++] = len >>> 8; out[o++] = ~len & 0xff; out[o++] = (~len >>> 8) & 0xff;
47
+ out.set(bytes.subarray(start, start + len), o); o += len;
48
+ }
49
+ let a = 1; let d = 0;
50
+ for (let i = 0; i < bytes.length; i++) { a = (a + bytes[i]!) % 65521; d = (d + a) % 65521; }
51
+ const adler = ((d << 16) | a) >>> 0;
52
+ out[o++] = adler >>> 24; out[o++] = (adler >>> 16) & 0xff; out[o++] = (adler >>> 8) & 0xff; out[o++] = adler & 0xff;
53
+ return out;
54
+ }
55
+
56
+ /** The object store: one bare repo's objects under a key prefix in the blob seam. */
57
+ export class GitObjectStore {
58
+ constructor(private readonly blobs: BlobStore, private readonly prefix: string) {}
59
+ key(sha: string): string { return `${this.prefix}/objects/${sha.slice(0, 2)}/${sha.slice(2)}`; }
60
+ async has(sha: string): Promise<boolean> { return this.blobs.exists(this.key(sha)); }
61
+ async write(type: GitObjectType, payload: Uint8Array): Promise<string> {
62
+ const loose = encodeLoose(type, payload); const sha = await sha1Hex(loose);
63
+ if (!(await this.blobs.exists(this.key(sha)))) await this.blobs.put(this.key(sha), storeZlib(loose));
64
+ return sha;
65
+ }
66
+ async read(sha: string): Promise<{ type: GitObjectType; payload: Uint8Array } | null> {
67
+ const z = await this.blobs.get(this.key(sha)); if (z === null) return null;
68
+ return decodeLoose(inflateZlib(z).out);
69
+ }
70
+ }
71
+
72
+ // ── codecs ──────────────────────────────────────────────────────────────────────────────
73
+ export type TreeEntry = { mode: string; name: string; sha: string };
74
+ /** git sorts tree entries by name, with a directory's name compared as if it ended in '/'. */
75
+ export function sortTreeEntries(entries: TreeEntry[]): TreeEntry[] {
76
+ const k = (e: TreeEntry) => (e.mode === '40000' || e.mode === '040000' ? `${e.name}/` : e.name);
77
+ return [...entries].sort((a, b) => (k(a) < k(b) ? -1 : k(a) > k(b) ? 1 : 0));
78
+ }
79
+ export function encodeTree(entries: TreeEntry[]): Uint8Array {
80
+ return concat(...sortTreeEntries(entries).map((e) => concat(ENC.encode(`${e.mode.replace(/^0+(?=\d)/, '')} ${e.name}\0`), hexToBytes(e.sha))));
81
+ }
82
+ export function decodeTree(payload: Uint8Array): TreeEntry[] {
83
+ const out: TreeEntry[] = []; let i = 0;
84
+ while (i < payload.length) {
85
+ const sp = payload.indexOf(0x20, i); const nul = payload.indexOf(0, sp);
86
+ out.push({ mode: DEC.decode(payload.subarray(i, sp)), name: DEC.decode(payload.subarray(sp + 1, nul)), sha: bytesToHex(payload.subarray(nul + 1, nul + 21)) });
87
+ i = nul + 21;
88
+ }
89
+ return out;
90
+ }
91
+ export type Ident = { name: string; email: string; time: number; tz: string };
92
+ export type Commit = { tree: string; parents: string[]; author: Ident; committer: Ident; message: string };
93
+ const ident = (i: Ident) => `${i.name} <${i.email}> ${i.time} ${i.tz}`;
94
+ export function encodeCommit(c: Commit): Uint8Array {
95
+ return ENC.encode(`tree ${c.tree}\n${c.parents.map((p) => `parent ${p}\n`).join('')}author ${ident(c.author)}\ncommitter ${ident(c.committer)}\n\n${c.message}`);
96
+ }
97
+ function parseIdent(s: string): Ident { const m = /^(.*) <([^>]*)> (\d+) ([+-]\d{4})$/.exec(s); if (!m) throw new Error(`git ident: ${s}`); return { name: m[1]!, email: m[2]!, time: Number(m[3]), tz: m[4]! }; }
98
+ export function decodeCommit(payload: Uint8Array): Commit {
99
+ const text = DEC.decode(payload); const sep = text.indexOf('\n\n'); const head = text.slice(0, sep); const message = text.slice(sep + 2);
100
+ const c: Commit = { tree: '', parents: [], author: undefined as never, committer: undefined as never, message };
101
+ for (const line of head.split('\n')) { const sp = line.indexOf(' '); const k = line.slice(0, sp), v = line.slice(sp + 1); if (k === 'tree') c.tree = v; else if (k === 'parent') c.parents.push(v); else if (k === 'author') c.author = parseIdent(v); else if (k === 'committer') c.committer = parseIdent(v); }
102
+ return c;
103
+ }
104
+ export type Tag = { object: string; type: GitObjectType; tag: string; tagger?: Ident; message: string };
105
+ export function encodeTag(t: Tag): Uint8Array { return ENC.encode(`object ${t.object}\ntype ${t.type}\ntag ${t.tag}\n${t.tagger ? `tagger ${ident(t.tagger)}\n` : ''}\n${t.message}`); }
106
+ export function decodeTag(payload: Uint8Array): Tag {
107
+ const text = DEC.decode(payload); const sep = text.indexOf('\n\n'); const t: Tag = { object: '', type: 'commit', tag: '', message: text.slice(sep + 2) };
108
+ for (const line of text.slice(0, sep).split('\n')) { const sp = line.indexOf(' '); const k = line.slice(0, sp), v = line.slice(sp + 1); if (k === 'object') t.object = v; else if (k === 'type') t.type = v as GitObjectType; else if (k === 'tag') t.tag = v; else if (k === 'tagger') t.tagger = parseIdent(v); }
109
+ return t;
110
+ }
@@ -0,0 +1,105 @@
1
+ // PACKFILES (gitformat-pack): what a client sends on push and what a server sends on fetch.
2
+ // READ: parse a v2 pack — undeltified objects (zlib at an offset, consumed bytes from the kernel
3
+ // inflater), OFS_DELTA and REF_DELTA resolved against earlier objects in the pack or the object
4
+ // store, the trailing SHA-1 verified — and write every resolved object to the store. WRITE: a pack
5
+ // of undeltified objects with the trailer, which every git accepts (deltas are an optimization a
6
+ // twin does not owe a client).
7
+ import { inflateZlib } from './inflate.ts';
8
+ import { GitObjectStore, bytesToHex, concat, deflateZlib, sha1Hex, type GitObjectType } from './objects.ts';
9
+
10
+ const TYPE_BY_CODE: Record<number, GitObjectType> = { 1: 'commit', 2: 'tree', 3: 'blob', 4: 'tag' };
11
+ const CODE_BY_TYPE: Record<GitObjectType, number> = { commit: 1, tree: 2, blob: 3, tag: 4 };
12
+ const OFS_DELTA = 6, REF_DELTA = 7;
13
+
14
+ function readVarSize(input: Uint8Array, at: number): { value: number; type: number; next: number } {
15
+ let c = input[at]!; const type = (c >> 4) & 7; let value = c & 15; let shift = 4; let i = at + 1;
16
+ while (c & 0x80) { c = input[i++]!; value += (c & 0x7f) * 2 ** shift; shift += 7; }
17
+ return { value, type, next: i };
18
+ }
19
+ function readDeltaSize(d: Uint8Array, at: number): { value: number; next: number } {
20
+ let value = 0, shift = 0, i = at, c: number;
21
+ do { c = d[i++]!; value += (c & 0x7f) * 2 ** shift; shift += 7; } while (c & 0x80);
22
+ return { value, next: i };
23
+ }
24
+ /** Apply a git delta (copy / insert ops) to a base payload. */
25
+ export function applyDelta(base: Uint8Array, delta: Uint8Array): Uint8Array {
26
+ let p = 0; const src = readDeltaSize(delta, p); p = src.next; if (src.value !== base.length) throw new Error(`delta: base size ${src.value} != ${base.length}`);
27
+ const dst = readDeltaSize(delta, p); p = dst.next; const out = new Uint8Array(dst.value); let o = 0;
28
+ while (p < delta.length) {
29
+ const op = delta[p++]!;
30
+ if (op & 0x80) {
31
+ let off = 0, len = 0;
32
+ if (op & 0x01) off |= delta[p++]!; if (op & 0x02) off |= delta[p++]! << 8; if (op & 0x04) off |= delta[p++]! << 16; if (op & 0x08) off |= delta[p++]! * 2 ** 24;
33
+ if (op & 0x10) len |= delta[p++]!; if (op & 0x20) len |= delta[p++]! << 8; if (op & 0x40) len |= delta[p++]! << 16;
34
+ if (len === 0) len = 0x10000;
35
+ out.set(base.subarray(off, off + len), o); o += len;
36
+ } else if (op > 0) { out.set(delta.subarray(p, p + op), o); o += op; p += op; }
37
+ else throw new Error('delta: reserved op 0');
38
+ }
39
+ if (o !== dst.value) throw new Error(`delta: produced ${o} bytes, expected ${dst.value}`);
40
+ return out;
41
+ }
42
+
43
+ export type PackedObject = { type: GitObjectType; sha: string };
44
+ // Delta bases are re-read from the store they were just written to; only this many bytes of
45
+ // resolved payloads stay in memory (git's own core.deltaBaseCacheLimit is 96 MiB). Holding every
46
+ // payload until the pack ends made a full-history push cost the repository's whole inflated size.
47
+ const DELTA_BASE_CACHE_BYTES = 96 * 1024 * 1024;
48
+ /** Read a pack, resolve every object, write them to `store`; returns each object's type and sha in pack order. */
49
+ export async function readPack(pack: Uint8Array, store: GitObjectStore): Promise<PackedObject[]> {
50
+ const dec = new TextDecoder();
51
+ if (dec.decode(pack.subarray(0, 4)) !== 'PACK') throw new Error('pack: bad signature');
52
+ const version = (pack[4]! << 24) | (pack[5]! << 16) | (pack[6]! << 8) | pack[7]!; if (version !== 2 && version !== 3) throw new Error(`pack: version ${version}`);
53
+ const count = ((pack[8]! << 24) | (pack[9]! << 16) | (pack[10]! << 8) | pack[11]!) >>> 0;
54
+ const trailer = bytesToHex(pack.subarray(pack.length - 20)); const expected = await sha1Hex(pack.subarray(0, pack.length - 20));
55
+ if (trailer !== expected) throw new Error('pack: checksum mismatch');
56
+ type Resolved = { type: GitObjectType; payload: Uint8Array };
57
+ const cache = new Map<string, Resolved>(); let cached = 0;
58
+ const remember = (sha: string, obj: Resolved) => {
59
+ if (obj.payload.length > DELTA_BASE_CACHE_BYTES || cache.has(sha)) return;
60
+ cache.set(sha, obj); cached += obj.payload.length;
61
+ for (const [old, { payload }] of cache) { if (cached <= DELTA_BASE_CACHE_BYTES) break; cache.delete(old); cached -= payload.length; }
62
+ };
63
+ const payloadOf = async (sha: string): Promise<Resolved | null> => {
64
+ const hit = cache.get(sha); if (!hit) return store.read(sha);
65
+ cache.delete(sha); cache.set(sha, hit); return hit;
66
+ };
67
+ const byOffset = new Map<number, PackedObject>(); const out: PackedObject[] = []; let at = 12;
68
+ for (let n = 0; n < count; n++) {
69
+ const start = at; const h = readVarSize(pack, at); at = h.next;
70
+ let type: GitObjectType; let payload: Uint8Array;
71
+ if (h.type === OFS_DELTA || h.type === REF_DELTA) {
72
+ let baseSha: string;
73
+ if (h.type === OFS_DELTA) {
74
+ let c = pack[at++]!; let off = c & 0x7f; while (c & 0x80) { c = pack[at++]!; off = ((off + 1) * 128) + (c & 0x7f); }
75
+ const entry = byOffset.get(start - off); if (!entry) throw new Error(`pack: ofs-delta base at ${start - off} not found`);
76
+ baseSha = entry.sha;
77
+ } else { baseSha = bytesToHex(pack.subarray(at, at + 20)); at += 20; }
78
+ const base = await payloadOf(baseSha); if (!base) throw new Error(`pack: ${h.type === OFS_DELTA ? 'ofs' : 'ref'}-delta base ${baseSha} not found`);
79
+ const z = inflateZlib(pack, at); at += z.consumed;
80
+ type = base.type; payload = applyDelta(base.payload, z.out);
81
+ } else {
82
+ const known = TYPE_BY_CODE[h.type]; if (!known) throw new Error(`pack: object type ${h.type}`);
83
+ const z = inflateZlib(pack, at); at += z.consumed; if (z.out.length !== h.value) throw new Error(`pack: object size ${z.out.length} != ${h.value}`);
84
+ type = known; payload = z.out;
85
+ }
86
+ const obj = { type, sha: await store.write(type, payload) };
87
+ remember(obj.sha, { type, payload }); byOffset.set(start, obj); out.push(obj);
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function encodeVarSize(type: number, size: number): Uint8Array {
93
+ const bytes: number[] = []; let c = (type << 4) | (size & 15); size = Math.floor(size / 16);
94
+ while (size > 0) { bytes.push(c | 0x80); c = size & 0x7f; size = Math.floor(size / 128); }
95
+ bytes.push(c); return new Uint8Array(bytes);
96
+ }
97
+ /** Write a pack of undeltified objects, trailer included. */
98
+ export async function writePack(objects: Array<{ type: GitObjectType; payload: Uint8Array }>): Promise<Uint8Array> {
99
+ const header = new Uint8Array(12); header.set(new TextEncoder().encode('PACK'), 0); header[7] = 2;
100
+ header[8] = (objects.length >>> 24) & 255; header[9] = (objects.length >>> 16) & 255; header[10] = (objects.length >>> 8) & 255; header[11] = objects.length & 255;
101
+ const parts: Uint8Array[] = [header];
102
+ for (const o of objects) parts.push(encodeVarSize(CODE_BY_TYPE[o.type], o.payload.length), await deflateZlib(o.payload));
103
+ const body = concat(...parts); const trailer = await sha1Hex(body);
104
+ return concat(body, new Uint8Array(trailer.match(/../g)!.map((h) => parseInt(h, 16))));
105
+ }
@@ -0,0 +1,25 @@
1
+ // REFS AS WORLD STATE: a repository's refs live in the string world store under
2
+ // `<prefix>/refs/<name>` (one sha per file) and `<prefix>/HEAD` (a symref), the bare layout —
3
+ // state a snapshot carries, a projection can read, and (through the action log the plane keeps
4
+ // beside it) a push policy can gate. No packed-refs: the store lists directories.
5
+ import type { WorldStore } from '../world-store.ts';
6
+
7
+ export class GitRefs {
8
+ constructor(private readonly store: WorldStore, private readonly prefix: string) {}
9
+ private path(name: string): string { return `${this.prefix}/${name}`; }
10
+ get(name: string): string | null { const v = this.store.read(this.path(name)); return v === null ? null : v.trim(); }
11
+ set(name: string, sha: string): void { this.store.write(this.path(name), `${sha}\n`); }
12
+ delete(name: string): void { if (this.store.exists(this.path(name))) this.store.remove(this.path(name)); }
13
+ head(): string { const v = this.store.read(this.path('HEAD')); return v === null ? 'refs/heads/main' : v.trim().replace(/^ref: /, ''); }
14
+ setHead(target: string): void { this.store.write(this.path('HEAD'), `ref: ${target}\n`); }
15
+ /** Run `fn` holding the repository's ref lock (the world store's exclusive lock). */
16
+ withLock<T>(fn: () => T): T { return this.store.withLock(this.path('refs.lock'), fn); }
17
+ /** Every `refs/...` name with its sha, sorted by name. */
18
+ list(): Array<{ name: string; sha: string }> {
19
+ const out: Array<{ name: string; sha: string }> = [];
20
+ // stat before read: on the fs store a directory read throws (EISDIR); the memory store answers null
21
+ const walk = (dir: string, rel: string): void => { for (const child of this.store.list(dir)) { const p = `${dir}/${child}`; const r = `${rel}/${child}`; if (this.store.stat(p)?.isDirectory) { walk(p, r); continue; } const sha = this.store.read(p); if (sha !== null) out.push({ name: r, sha: sha.trim() }); } };
22
+ walk(this.path('refs'), 'refs');
23
+ return out.sort((a, b) => (a.name < b.name ? -1 : 1));
24
+ }
25
+ }