@fungi.computer/agar 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +766 -2
  3. package/dist/agent-message-meta.d.ts +74 -0
  4. package/dist/agent-message-meta.js +63 -0
  5. package/dist/agents.d.ts +170 -0
  6. package/dist/agents.js +218 -0
  7. package/dist/app-build-contract.d.ts +43 -0
  8. package/dist/app-build-contract.js +24 -0
  9. package/dist/apps.d.ts +66 -0
  10. package/dist/apps.js +47 -0
  11. package/dist/backend-declaration.d.ts +6 -0
  12. package/dist/backend-declaration.js +65 -0
  13. package/dist/backend-definition.d.ts +88 -0
  14. package/dist/backend-definition.js +158 -0
  15. package/dist/backend-refusal.d.ts +10 -0
  16. package/dist/backend-refusal.js +10 -0
  17. package/dist/backend.d.ts +30 -0
  18. package/dist/backend.js +28 -0
  19. package/dist/capabilities.d.ts +30 -0
  20. package/dist/capabilities.js +50 -0
  21. package/dist/channels.d.ts +142 -0
  22. package/dist/channels.js +97 -0
  23. package/dist/commands.d.ts +33 -0
  24. package/dist/commands.js +46 -0
  25. package/dist/computer-contracts.d.ts +73 -0
  26. package/dist/computer-contracts.js +57 -0
  27. package/dist/computer.d.ts +294 -0
  28. package/dist/computer.js +319 -0
  29. package/dist/connect.d.ts +11 -0
  30. package/dist/connect.js +211 -0
  31. package/dist/errors.d.ts +34 -0
  32. package/dist/errors.js +106 -0
  33. package/dist/failure.d.ts +2 -0
  34. package/dist/failure.js +9 -0
  35. package/dist/files.d.ts +25 -0
  36. package/dist/files.js +84 -0
  37. package/dist/game-servers.d.ts +530 -0
  38. package/dist/game-servers.js +244 -0
  39. package/dist/gills.d.ts +43 -0
  40. package/dist/gills.js +174 -0
  41. package/dist/git-commit.d.ts +143 -0
  42. package/dist/git-commit.js +251 -0
  43. package/dist/git-rpc.d.ts +310 -0
  44. package/dist/git-rpc.js +166 -0
  45. package/dist/git-steps.d.ts +579 -0
  46. package/dist/git-steps.js +804 -0
  47. package/dist/git-wire.d.ts +33 -0
  48. package/dist/git-wire.js +240 -0
  49. package/dist/git.d.ts +322 -0
  50. package/dist/git.js +98 -0
  51. package/dist/host.d.ts +97 -0
  52. package/dist/host.js +732 -0
  53. package/dist/inbox.d.ts +99 -0
  54. package/dist/inbox.js +184 -0
  55. package/dist/index.d.ts +31 -0
  56. package/dist/index.js +15 -0
  57. package/dist/inference.d.ts +17 -0
  58. package/dist/inference.js +115 -0
  59. package/dist/internal/app-error.d.ts +8 -0
  60. package/dist/internal/app-error.js +36 -0
  61. package/dist/internal/app-session.d.ts +8 -0
  62. package/dist/internal/app-session.js +551 -0
  63. package/dist/internal/back-inputs.d.ts +22 -0
  64. package/dist/internal/back-inputs.js +93 -0
  65. package/dist/internal/browser-bridge.d.ts +30 -0
  66. package/dist/internal/browser-bridge.js +203 -0
  67. package/dist/internal/capabilities.d.ts +13 -0
  68. package/dist/internal/capabilities.js +138 -0
  69. package/dist/internal/commands.d.ts +9 -0
  70. package/dist/internal/commands.js +257 -0
  71. package/dist/internal/disconnect-host.d.ts +7 -0
  72. package/dist/internal/disconnect-host.js +19 -0
  73. package/dist/internal/git-edit-tree.d.ts +6 -0
  74. package/dist/internal/git-edit-tree.js +46 -0
  75. package/dist/internal/memory-fs.d.ts +39 -0
  76. package/dist/internal/memory-fs.js +154 -0
  77. package/dist/internal/protocol.d.ts +116 -0
  78. package/dist/internal/protocol.js +6 -0
  79. package/dist/internal/query-feed.d.ts +23 -0
  80. package/dist/internal/query-feed.js +111 -0
  81. package/dist/internal/result.d.ts +22 -0
  82. package/dist/internal/result.js +61 -0
  83. package/dist/internal/rpc-contract.d.ts +24 -0
  84. package/dist/internal/rpc-contract.js +78 -0
  85. package/dist/internal/schemas.d.ts +307 -0
  86. package/dist/internal/schemas.js +524 -0
  87. package/dist/internal/store.d.ts +8 -0
  88. package/dist/internal/store.js +30 -0
  89. package/dist/internal/terminal-gate.d.ts +17 -0
  90. package/dist/internal/terminal-gate.js +94 -0
  91. package/dist/lifecycle.d.ts +44 -0
  92. package/dist/lifecycle.js +1 -0
  93. package/dist/marketplace.d.ts +159 -0
  94. package/dist/marketplace.js +102 -0
  95. package/dist/media-json.d.ts +181 -0
  96. package/dist/media-json.js +204 -0
  97. package/dist/media-library.d.ts +11 -0
  98. package/dist/media-library.js +15 -0
  99. package/dist/media-request-id.d.ts +7 -0
  100. package/dist/media-request-id.js +16 -0
  101. package/dist/media.d.ts +354 -0
  102. package/dist/media.js +654 -0
  103. package/dist/method-refusal.d.ts +64 -0
  104. package/dist/method-refusal.js +48 -0
  105. package/dist/pending.d.ts +10 -0
  106. package/dist/pending.js +10 -0
  107. package/dist/query.d.ts +76 -0
  108. package/dist/query.js +195 -0
  109. package/dist/react-mount.d.ts +23 -0
  110. package/dist/react-mount.js +108 -0
  111. package/dist/react.d.ts +41 -0
  112. package/dist/react.js +111 -0
  113. package/dist/read-copy.d.ts +103 -0
  114. package/dist/read-copy.js +195 -0
  115. package/dist/roles/files.d.ts +295 -0
  116. package/dist/roles/files.js +184 -0
  117. package/dist/root.d.ts +21 -0
  118. package/dist/root.js +1 -0
  119. package/dist/rpc.d.ts +1 -0
  120. package/dist/rpc.js +1 -0
  121. package/dist/status.d.ts +17 -0
  122. package/dist/status.js +50 -0
  123. package/dist/storage-wire.d.ts +167 -0
  124. package/dist/storage-wire.js +115 -0
  125. package/dist/storage.d.ts +72 -0
  126. package/dist/storage.js +143 -0
  127. package/dist/store.d.ts +11 -0
  128. package/dist/store.js +1 -0
  129. package/dist/test-kit/git.d.ts +31 -0
  130. package/dist/test-kit/git.js +174 -0
  131. package/dist/ui.d.ts +169 -0
  132. package/dist/ui.js +10 -0
  133. package/dist/window.d.ts +41 -0
  134. package/dist/window.js +15 -0
  135. package/dist/world.d.ts +16 -0
  136. package/dist/world.js +17 -0
  137. package/docs/api/README.md +28 -0
  138. package/docs/api/app-build-contract.md +113 -0
  139. package/docs/api/backend-declaration.md +62 -0
  140. package/docs/api/backend-definition.md +215 -0
  141. package/docs/api/backend.md +269 -0
  142. package/docs/api/errors.md +169 -0
  143. package/docs/api/files.md +91 -0
  144. package/docs/api/gills.md +164 -0
  145. package/docs/api/git-commit.md +279 -0
  146. package/docs/api/git-rpc.md +243 -0
  147. package/docs/api/git-steps.md +1010 -0
  148. package/docs/api/git-wire.md +197 -0
  149. package/docs/api/git.md +579 -0
  150. package/docs/api/index.md +2880 -0
  151. package/docs/api/media-json.md +507 -0
  152. package/docs/api/media-request-id.md +32 -0
  153. package/docs/api/modules.md +32 -0
  154. package/docs/api/query.md +253 -0
  155. package/docs/api/react.md +216 -0
  156. package/docs/api/read-copy.md +204 -0
  157. package/docs/api/rpc.md +128 -0
  158. package/docs/api/storage.md +620 -0
  159. package/package.json +197 -3
package/dist/react.js ADDED
@@ -0,0 +1,111 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore, } from "react";
2
+ import { createWhistle, } from "@fungi.computer/whistle";
3
+ import { registerCatalog } from "./commands.js";
4
+ import { AppError } from "./errors.js";
5
+ export { mountApp } from "./react-mount.js";
6
+ /** Read an SDK snapshot and release its subscription when the store changes. */
7
+ export function useStore(store) {
8
+ const subscribe = useCallback((listener) => store?.subscribe(listener) ?? (() => undefined), [store]);
9
+ const snapshot = useCallback(() => store?.get(), [store]);
10
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
11
+ }
12
+ /** Read the App's live context (focus, visibility, Computer selection, any
13
+ * pending open request), or null while there is no connected App. */
14
+ export function useAppContext(app) {
15
+ return useStore(app?.context) ?? null;
16
+ }
17
+ /** Read the id of the Computer currently selected for this App, or null
18
+ * when no App is connected or no Computer is selected yet. */
19
+ export function useComputerId(app) {
20
+ return useAppContext(app)?.computerId ?? null;
21
+ }
22
+ /**
23
+ * Keep this window's status bar items current in the host while mounted, and
24
+ * hear the person use them. Items compare by value; the bar clears on unmount.
25
+ */
26
+ export function useWindowStatus(app, items, onAction,
27
+ /** Hear a report the host refused, so the App can say so. */
28
+ onReportFailure) {
29
+ const latest = useRef(items);
30
+ latest.current = items;
31
+ const handler = useRef(onAction);
32
+ handler.current = onAction;
33
+ const failed = useRef(onReportFailure);
34
+ failed.current = onReportFailure;
35
+ const key = JSON.stringify(items);
36
+ const listening = onAction !== undefined;
37
+ useEffect(() => {
38
+ if (!app)
39
+ return;
40
+ void app.ui.reportStatus(latest.current).catch((cause) => {
41
+ if (failed.current)
42
+ failed.current(cause);
43
+ else
44
+ console.error("Window status report failed", cause);
45
+ });
46
+ }, [app, key]);
47
+ useEffect(() => {
48
+ if (!app || !listening)
49
+ return;
50
+ return app.ui.onStatusAction((action) => handler.current?.(action) ?? false);
51
+ }, [app, listening]);
52
+ useEffect(() => () => {
53
+ void app?.ui.reportStatus([]).catch((cause) => {
54
+ console.error("Window status report failed", cause);
55
+ });
56
+ }, [app]);
57
+ }
58
+ /**
59
+ * Own one catalog's local slash runtime and host registrations. Memoize `build`
60
+ * with useCallback; changing it replaces the catalog and its availability.
61
+ */
62
+ export function useAppCommandCatalog(app, { sourceId, namespace }, build) {
63
+ const runtime = useMemo(() => createWhistle(), []);
64
+ const catalog = useMemo(build, [build]);
65
+ useEffect(() => {
66
+ const lease = runtime.contribute({
67
+ sourceId,
68
+ namespace,
69
+ commands: catalog,
70
+ });
71
+ return () => {
72
+ lease.dispose();
73
+ };
74
+ }, [runtime, sourceId, namespace, catalog]);
75
+ useEffect(() => (app ? registerCatalog(app.commands, catalog) : undefined), [app, catalog]);
76
+ return runtime;
77
+ }
78
+ /**
79
+ * Publish an App's navigation state to its host and own the host's Back
80
+ * requests while mounted. Pass `app?.window.navigation`; with no connected
81
+ * App it does nothing. Unmounting withdraws Back from the host chrome.
82
+ */
83
+ export function useHostNavigation(navigation, { canGoBack, title, onBack }) {
84
+ const latestBack = useRef(onBack);
85
+ useEffect(() => {
86
+ latestBack.current = onBack;
87
+ }, [onBack]);
88
+ useEffect(() => {
89
+ if (!navigation)
90
+ return undefined;
91
+ const release = navigation.onBack(() => latestBack.current());
92
+ return () => {
93
+ release();
94
+ report(navigation, { canGoBack: false });
95
+ };
96
+ }, [navigation]);
97
+ // Each change replaces the last report; only unmount withdraws Back.
98
+ useEffect(() => {
99
+ if (!navigation)
100
+ return;
101
+ report(navigation, title === undefined ? { canGoBack } : { canGoBack, title });
102
+ }, [navigation, canGoBack, title]);
103
+ }
104
+ function report(navigation, state) {
105
+ navigation.publish(state).catch((cause) => {
106
+ // A closing App has no host left to tell; anything else is a real fault.
107
+ if (cause instanceof AppError && cause.code === "DISCONNECTED")
108
+ return;
109
+ console.warn("App navigation report failed", cause);
110
+ });
111
+ }
@@ -0,0 +1,103 @@
1
+ import * as z from "zod";
2
+ import { type GitBinding } from "./git.js";
3
+ /** A facet's synchronous SQLite port, shared by Workers and local proofs. */
4
+ export type ReadCopySql = {
5
+ /** Execute immediately, as ctx.storage.sql does. */
6
+ exec(query: string, ...bindings: (string | number | null | ArrayBuffer)[]): {
7
+ /** Decode the returned SQLite rows at the App boundary. */ toArray(): readonly unknown[];
8
+ };
9
+ };
10
+ /** App-selected shape and retention bounds; changing version discards old representations. */
11
+ export type ReadCopyLimits = {
12
+ /** App copy-shape version, changed by a clean representation break. */
13
+ version: number;
14
+ /** Maximum retained document rows across this facet. */
15
+ maxRows: number;
16
+ /** Maximum UTF-8 JSON bytes per row. */
17
+ maxValueBytes: number;
18
+ /** Maximum bytes of one immutable blob. */
19
+ maxBlobBytes: number;
20
+ /** Maximum total retained blob bytes, oldest objects evicted first. */
21
+ maxBlobTotalBytes: number;
22
+ };
23
+ /** A screen reads this projection without asking Git whether it is current. */
24
+ export type ReadCopyResult<T> = {
25
+ kind: "ready";
26
+ head: string | null;
27
+ value: T;
28
+ } | {
29
+ kind: "missing";
30
+ } | {
31
+ kind: "rebuilding";
32
+ };
33
+ /** A rebuild stops before dispatching the first call beyond its budget. */
34
+ export declare class ReadCopyBudgetExceeded extends Error {
35
+ constructor();
36
+ }
37
+ /** Count binding operations, never authority or results. The signal is the caller's frame lifetime. */
38
+ export declare function countingGit(binding: GitBinding, options: {
39
+ limit: number;
40
+ signal: AbortSignal;
41
+ }): {
42
+ git: Readonly<{
43
+ create(name: string, options?: {
44
+ description?: string;
45
+ defaultBranch?: string;
46
+ }): Promise<import("./git.js").GitRepositoryInfo>;
47
+ fork(source: string, target: string, options?: {
48
+ description?: string;
49
+ }): Promise<import("./git.js").GitRepositoryInfo>;
50
+ cloneUrl(name: string): Promise<string>;
51
+ info(name: string): Promise<import("./git.js").GitRepositoryInfo | null>;
52
+ delete(name: string): Promise<boolean>;
53
+ log(name: string, options: {
54
+ ref: string;
55
+ limit?: number;
56
+ offset?: number;
57
+ }): Promise<import("./git.js").GitCommit[]>;
58
+ readCommit(name: string, hash: string): Promise<import("./git.js").GitCommit | null>;
59
+ readTree(name: string, hash: string): Promise<import("./git.js").GitTreeEntry[] | null>;
60
+ readBlob(name: string, hash: string): Promise<Uint8Array | null>;
61
+ refs(name: string): Promise<{
62
+ name: string;
63
+ hash: string;
64
+ }[]>;
65
+ exchange(name: string, request: import("./git.js").GitExchangeRequest): Promise<import("./git.js").GitExchangeResponse>;
66
+ }>;
67
+ /** Number of dispatched operations. */ total: () => number;
68
+ /** Dispatch counts by maintained Git operation. */ counts: () => {
69
+ [k: string]: number;
70
+ };
71
+ };
72
+ /** Own bounded rows and content-addressed bytes in the App facet, not a host/provider cache. */
73
+ export declare function createReadCopies(sql: ReadCopySql, limits: ReadCopyLimits): {
74
+ /** Bind a namespace to its maintained App decoder. */
75
+ open: <T>(copy: string, schema: z.ZodType<T>) => {
76
+ /** Read the schema-decoded local projection. */
77
+ read: (key: string) => ReadCopyResult<T>;
78
+ /** Publish a successful Git write. */
79
+ write: (key: string, head: string | null, value: T) => void;
80
+ /** Refuse the row until rebuilt. */
81
+ stale: (key: string) => void;
82
+ /** An accepted push chains on before, ignores its own-write echo, and invalidates gaps/deletions. */
83
+ apply(key: string, update: {
84
+ before: string;
85
+ after: string;
86
+ value: T;
87
+ unexplained?: boolean;
88
+ }): "applied" | "echo" | "stale";
89
+ /** A missing key rebuilds only in this frame; budget exhaustion publishes no partial row. */
90
+ load(key: string, options: {
91
+ git: GitBinding;
92
+ signal: AbortSignal;
93
+ rebuild: (git: GitBinding) => Promise<{
94
+ head: string | null;
95
+ value: T;
96
+ }>;
97
+ }): Promise<ReadCopyResult<T>>;
98
+ };
99
+ /** Store immutable provider-identified bytes; enforce per-object and total retention bounds. */
100
+ putBlob(hash: string, bytes: Uint8Array): void;
101
+ /** A blob miss never performs I/O; the App decides which bounded rebuild owns it. */
102
+ readBlob(hash: string): Uint8Array | null;
103
+ };
@@ -0,0 +1,195 @@
1
+ import * as z from "zod";
2
+ import {} from "./git.js";
3
+ import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
4
+ /** A rebuild stops before dispatching the first call beyond its budget. */
5
+ export class ReadCopyBudgetExceeded extends Error {
6
+ constructor() {
7
+ super("Read-copy Git call budget exhausted");
8
+ this.name = "ReadCopyBudgetExceeded";
9
+ }
10
+ }
11
+ /** Count binding operations, never authority or results. The signal is the caller's frame lifetime. */
12
+ export function countingGit(binding, options) {
13
+ z.number().int().positive().parse(options.limit);
14
+ const counts = new Map();
15
+ let total = 0;
16
+ function wrap(operation, call) {
17
+ return async (...args) => {
18
+ options.signal.throwIfAborted();
19
+ if (total >= options.limit)
20
+ throw new ReadCopyBudgetExceeded();
21
+ total++;
22
+ counts.set(operation, (counts.get(operation) ?? 0) + 1);
23
+ const result = await call(...args);
24
+ options.signal.throwIfAborted();
25
+ return result;
26
+ };
27
+ }
28
+ const git = {
29
+ create: wrap("create", (...args) => binding.create(...args)),
30
+ fork: wrap("fork", (...args) => binding.fork(...args)),
31
+ cloneUrl: wrap("cloneUrl", (...args) => binding.cloneUrl(...args)),
32
+ info: wrap("info", (...args) => binding.info(...args)),
33
+ delete: wrap("delete", (...args) => binding.delete(...args)),
34
+ log: wrap("log", (...args) => binding.log(...args)),
35
+ readCommit: wrap("readCommit", (...args) => binding.readCommit(...args)),
36
+ readTree: wrap("readTree", (...args) => binding.readTree(...args)),
37
+ readBlob: wrap("readBlob", (...args) => binding.readBlob(...args)),
38
+ refs: wrap("refs", (...args) => binding.refs(...args)),
39
+ exchange: wrap("exchange", (...args) => binding.exchange(...args)),
40
+ };
41
+ return {
42
+ git,
43
+ /** Number of dispatched operations. */ total: () => total,
44
+ /** Dispatch counts by maintained Git operation. */ counts: () => Object.fromEntries(counts),
45
+ };
46
+ }
47
+ const rowSchema = z.strictObject({
48
+ head: gitSha1Schema.nullable(),
49
+ version: z.number().int(),
50
+ revision: z.number().int(),
51
+ value: z.string().nullable(),
52
+ });
53
+ const name = z.string().min(1).max(1024);
54
+ const zero = "0".repeat(40);
55
+ const blobHash = z.union([
56
+ gitSha1Schema,
57
+ z.string().regex(/^sha256:[0-9a-f]{64}$/),
58
+ ]);
59
+ /** Own bounded rows and content-addressed bytes in the App facet, not a host/provider cache. */
60
+ export function createReadCopies(sql, limits) {
61
+ z.strictObject({
62
+ version: z.number().int().positive(),
63
+ maxRows: z.number().int().positive(),
64
+ maxValueBytes: z.number().int().positive(),
65
+ maxBlobBytes: z.number().int().positive(),
66
+ maxBlobTotalBytes: z.number().int().positive(),
67
+ }).parse(limits);
68
+ sql.exec("CREATE TABLE IF NOT EXISTS app_read_copy (copy TEXT NOT NULL, key TEXT NOT NULL, head TEXT, version INTEGER NOT NULL, revision INTEGER NOT NULL, value TEXT, PRIMARY KEY(copy,key))");
69
+ sql.exec("CREATE TABLE IF NOT EXISTS app_read_blob (hash TEXT PRIMARY KEY, bytes BLOB NOT NULL)");
70
+ function row(copy, key) {
71
+ const raw = sql
72
+ .exec("SELECT head,version,revision,value FROM app_read_copy WHERE copy=? AND key=?", copy, key)
73
+ .toArray()[0];
74
+ return raw === undefined ? null : rowSchema.parse(raw);
75
+ }
76
+ /** Bind an App-owned schema and copy namespace. */
77
+ function open(copy, schema) {
78
+ name.parse(copy);
79
+ /** Decode a local row; missing and stale never probe Git. */
80
+ function read(key) {
81
+ name.parse(key);
82
+ const saved = row(copy, key);
83
+ if (saved?.version !== limits.version || saved.value === null)
84
+ return { kind: "missing" };
85
+ return {
86
+ kind: "ready",
87
+ head: saved.head,
88
+ value: schema.parse(JSON.parse(saved.value)),
89
+ };
90
+ }
91
+ /** Store an accepted write after Git CAS succeeds. */
92
+ function write(key, head, value) {
93
+ name.parse(key);
94
+ gitSha1Schema.nullable().parse(head);
95
+ const encoded = JSON.stringify(z.json().parse(schema.parse(value)));
96
+ if (new TextEncoder().encode(encoded).byteLength > limits.maxValueBytes)
97
+ throw new RangeError("Read-copy row exceeds its byte bound");
98
+ sql.exec("INSERT INTO app_read_copy(copy,key,head,version,revision,value) VALUES(?,?,?,?,1,?) ON CONFLICT(copy,key) DO UPDATE SET head=excluded.head,version=excluded.version,revision=app_read_copy.revision+1,value=excluded.value", copy, key, head, limits.version, encoded);
99
+ sql.exec("DELETE FROM app_read_copy WHERE rowid IN (SELECT rowid FROM app_read_copy ORDER BY rowid DESC LIMIT -1 OFFSET ?)", limits.maxRows);
100
+ }
101
+ /** Invalidate one key and its in-flight rebuild. */
102
+ function stale(key) {
103
+ name.parse(key);
104
+ sql.exec("INSERT INTO app_read_copy(copy,key,head,version,revision,value) VALUES(?,?,NULL,?,1,NULL) ON CONFLICT(copy,key) DO UPDATE SET revision=app_read_copy.revision+1,value=NULL", copy, key, limits.version);
105
+ sql.exec("DELETE FROM app_read_copy WHERE rowid IN (SELECT rowid FROM app_read_copy ORDER BY rowid DESC LIMIT -1 OFFSET ?)", limits.maxRows);
106
+ }
107
+ return {
108
+ /** Read the schema-decoded local projection. */
109
+ read,
110
+ /** Publish a successful Git write. */
111
+ write,
112
+ /** Refuse the row until rebuilt. */
113
+ stale,
114
+ /** An accepted push chains on before, ignores its own-write echo, and invalidates gaps/deletions. */
115
+ apply(key, update) {
116
+ gitSha1Schema.parse(update.before);
117
+ gitSha1Schema.parse(update.after);
118
+ const current = read(key);
119
+ if (update.unexplained || update.after === zero) {
120
+ stale(key);
121
+ return "stale";
122
+ }
123
+ if (current.kind === "ready" && current.head === update.after)
124
+ return "echo";
125
+ if (current.kind !== "ready" ||
126
+ (current.head ?? zero) !== update.before) {
127
+ stale(key);
128
+ return "stale";
129
+ }
130
+ write(key, update.after, update.value);
131
+ return "applied";
132
+ },
133
+ /** A missing key rebuilds only in this frame; budget exhaustion publishes no partial row. */
134
+ async load(key, options) {
135
+ options.signal.throwIfAborted();
136
+ const ready = read(key);
137
+ if (ready.kind === "ready")
138
+ return ready;
139
+ const before = JSON.stringify(row(copy, key));
140
+ const counted = countingGit(options.git, {
141
+ limit: 40,
142
+ signal: options.signal,
143
+ });
144
+ try {
145
+ const rebuilt = await options.rebuild(counted.git);
146
+ options.signal.throwIfAborted();
147
+ if (JSON.stringify(row(copy, key)) !== before)
148
+ return read(key);
149
+ write(key, rebuilt.head, rebuilt.value);
150
+ return read(key);
151
+ }
152
+ catch (error) {
153
+ if (error instanceof ReadCopyBudgetExceeded)
154
+ return { kind: "rebuilding" };
155
+ throw error;
156
+ }
157
+ },
158
+ };
159
+ }
160
+ return {
161
+ /** Bind a namespace to its maintained App decoder. */
162
+ open,
163
+ /** Store immutable provider-identified bytes; enforce per-object and total retention bounds. */
164
+ putBlob(hash, bytes) {
165
+ blobHash.parse(hash);
166
+ if (bytes.byteLength >
167
+ Math.min(limits.maxBlobBytes, limits.maxBlobTotalBytes))
168
+ throw new RangeError("Read-copy blob exceeds its byte bound");
169
+ sql.exec("INSERT INTO app_read_blob(hash,bytes) VALUES(?,?) ON CONFLICT(hash) DO NOTHING", hash, new Uint8Array(bytes).buffer);
170
+ while (z
171
+ .strictObject({ size: z.number() })
172
+ .parse(sql
173
+ .exec("SELECT COALESCE(SUM(length(bytes)),0) AS size FROM app_read_blob")
174
+ .toArray()[0]).size > limits.maxBlobTotalBytes)
175
+ sql.exec("DELETE FROM app_read_blob WHERE rowid=(SELECT MIN(rowid) FROM app_read_blob)");
176
+ },
177
+ /** A blob miss never performs I/O; the App decides which bounded rebuild owns it. */
178
+ readBlob(hash) {
179
+ blobHash.parse(hash);
180
+ const result = sql
181
+ .exec("SELECT bytes FROM app_read_blob WHERE hash=?", hash)
182
+ .toArray()[0];
183
+ return result === undefined
184
+ ? null
185
+ : new Uint8Array(z
186
+ .strictObject({
187
+ bytes: z.union([
188
+ z.instanceof(ArrayBuffer),
189
+ z.instanceof(Uint8Array),
190
+ ]),
191
+ })
192
+ .parse(result).bytes);
193
+ },
194
+ };
195
+ }