snapback2 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 (132) hide show
  1. package/README.md +127 -0
  2. package/bin/snapback2.mjs +36 -0
  3. package/dist/api.d.ts +20 -0
  4. package/dist/assets.d.ts +34 -0
  5. package/dist/client-assets.d.ts +26 -0
  6. package/dist/client-offline.d.ts +94 -0
  7. package/dist/client-outbox.d.ts +58 -0
  8. package/dist/client-upload.d.ts +41 -0
  9. package/dist/client-wire.d.ts +41 -0
  10. package/dist/client.d.ts +12 -0
  11. package/dist/client.mjs +5 -0
  12. package/dist/client.mjs.map +7 -0
  13. package/dist/compiler-core.mjs +60 -0
  14. package/dist/compiler-core.mjs.map +7 -0
  15. package/dist/compiler-lib.d.ts +71 -0
  16. package/dist/compiler.mjs +5 -0
  17. package/dist/compiler.mjs.map +7 -0
  18. package/dist/drain.d.ts +33 -0
  19. package/dist/expo/files.d.ts +3 -0
  20. package/dist/expo/index.d.ts +16 -0
  21. package/dist/expo/token-store.d.ts +7 -0
  22. package/dist/expo/witness.d.ts +28 -0
  23. package/dist/expo/witness.mjs +2 -0
  24. package/dist/expo/witness.mjs.map +7 -0
  25. package/dist/expo.d.ts +4 -0
  26. package/dist/expo.mjs +9 -0
  27. package/dist/expo.mjs.map +7 -0
  28. package/dist/guide/auth.md +89 -0
  29. package/dist/guide/effects.md +52 -0
  30. package/dist/guide/families.json +446 -0
  31. package/dist/guide/grammar.md +52 -0
  32. package/dist/guide/live-query.md +27 -0
  33. package/dist/guide/offline.md +75 -0
  34. package/dist/guide/personas.md +15 -0
  35. package/dist/guide/quarry.md +297 -0
  36. package/dist/guide/testing.md +57 -0
  37. package/dist/index.d.ts +362 -0
  38. package/dist/index.mjs +2 -0
  39. package/dist/index.mjs.map +7 -0
  40. package/dist/offline-protocol.d.ts +180 -0
  41. package/dist/offline-schema.d.ts +1 -0
  42. package/dist/offline.d.ts +177 -0
  43. package/dist/react-core.d.ts +91 -0
  44. package/dist/react-core.mjs +2 -0
  45. package/dist/react-core.mjs.map +7 -0
  46. package/dist/react-native/components.d.ts +23 -0
  47. package/dist/react-native/files.d.ts +43 -0
  48. package/dist/react-native/source.d.ts +16 -0
  49. package/dist/react-native.d.ts +4 -0
  50. package/dist/react-native.mjs +2 -0
  51. package/dist/react-native.mjs.map +7 -0
  52. package/dist/react.d.ts +19 -0
  53. package/dist/react.mjs +2 -0
  54. package/dist/react.mjs.map +7 -0
  55. package/dist/sqlite-test.mjs +1720 -0
  56. package/dist/sqlite-test.mjs.map +7 -0
  57. package/dist/sse.d.ts +44 -0
  58. package/dist/store/byte-cache.d.ts +162 -0
  59. package/dist/store/canonical.d.ts +2 -0
  60. package/dist/store/indexeddb.d.ts +23 -0
  61. package/dist/store/locks.d.ts +11 -0
  62. package/dist/store/outbox.d.ts +113 -0
  63. package/dist/store/overlay-retirement.d.ts +17 -0
  64. package/dist/store/projection.d.ts +11 -0
  65. package/dist/store/range-store.d.ts +388 -0
  66. package/dist/store/sqlite-driver.conformance.d.ts +7 -0
  67. package/dist/store/sqlite-driver.d.ts +16 -0
  68. package/dist/store/sqlite-expo.d.ts +3 -0
  69. package/dist/store/sqlite.d.ts +31 -0
  70. package/dist/templates/chat/expo/App.tsx +29 -0
  71. package/dist/templates/chat/expo/app.json +11 -0
  72. package/dist/templates/chat/expo/index.js +3 -0
  73. package/dist/templates/chat/expo/shared/log.js +92 -0
  74. package/dist/templates/chat/expo/shared/log.ts +130 -0
  75. package/dist/templates/chat/expo/shared/offline.js +8 -0
  76. package/dist/templates/chat/expo/shared/offline.ts +9 -0
  77. package/dist/templates/chat/expo/src/Chat.tsx +23 -0
  78. package/dist/templates/chat/expo/src/Composer.tsx +23 -0
  79. package/dist/templates/chat/expo/src/MediaView.tsx +29 -0
  80. package/dist/templates/chat/expo/src/Witness.tsx +131 -0
  81. package/dist/templates/chat/expo/src/screens/Inbox.tsx +9 -0
  82. package/dist/templates/chat/expo/src/screens/Search.tsx +10 -0
  83. package/dist/templates/chat/expo/src/screens/Thread.tsx +22 -0
  84. package/dist/templates/chat/expo/src/witness-state.ts +68 -0
  85. package/dist/templates/chat/expo/tsconfig.json +5 -0
  86. package/dist/templates/chat/react/index.html +5 -0
  87. package/dist/templates/chat/react/shared/log.js +92 -0
  88. package/dist/templates/chat/react/shared/log.ts +130 -0
  89. package/dist/templates/chat/react/shared/offline.js +8 -0
  90. package/dist/templates/chat/react/shared/offline.ts +9 -0
  91. package/dist/templates/chat/react/src/App.tsx +36 -0
  92. package/dist/templates/chat/react/src/Composer.tsx +45 -0
  93. package/dist/templates/chat/react/src/Inbox.tsx +24 -0
  94. package/dist/templates/chat/react/src/Search.tsx +26 -0
  95. package/dist/templates/chat/react/src/Thread.tsx +126 -0
  96. package/dist/templates/chat/react/src/env.d.ts +1 -0
  97. package/dist/templates/chat/react/src/main.tsx +20 -0
  98. package/dist/templates/chat/shared/log.js +92 -0
  99. package/dist/templates/chat/shared/log.ts +130 -0
  100. package/dist/templates/chat/shared/offline.js +8 -0
  101. package/dist/templates/chat/shared/offline.ts +9 -0
  102. package/dist/templates/chat/snapback/deliveries.q +33 -0
  103. package/dist/templates/chat/snapback/delivery.ts +30 -0
  104. package/dist/templates/chat/snapback/feeds.q +14 -0
  105. package/dist/templates/chat/snapback/follows.q +11 -0
  106. package/dist/templates/chat/snapback/groups.q +40 -0
  107. package/dist/templates/chat/snapback/messages.q +32 -0
  108. package/dist/templates/chat/snapback/notifications.q +7 -0
  109. package/dist/templates/chat/snapback/posts.q +4 -0
  110. package/dist/templates/chat/snapback/profiles.q +17 -0
  111. package/dist/templates/chat/snapback/schema.q +136 -0
  112. package/dist/templates/chat/snapback/seed.ts +45 -0
  113. package/dist/templates/chat/snapback/tests/chat.test.ts +759 -0
  114. package/dist/templates/react/index.html +5 -0
  115. package/dist/templates/react/src/App.tsx +5 -0
  116. package/dist/templates/react/src/main.tsx +18 -0
  117. package/dist/templates/todos/snapback/schema.q +11 -0
  118. package/dist/templates/todos/snapback/seed.ts +10 -0
  119. package/dist/templates/todos/snapback/tests/todos.test.ts +13 -0
  120. package/dist/templates/todos/snapback/todos.q +6 -0
  121. package/dist/test-runner.mjs +8541 -0
  122. package/dist/test-runner.mjs.map +7 -0
  123. package/dist/test.d.ts +28 -0
  124. package/dist/test.mjs +8541 -0
  125. package/dist/test.mjs.map +7 -0
  126. package/dist/token-store.d.ts +3 -0
  127. package/dist/twin-hydrate.d.ts +43 -0
  128. package/dist/twin.d.ts +70 -0
  129. package/dist/types.d.ts +410 -0
  130. package/dist/witness-test.mjs +2 -0
  131. package/dist/witness-test.mjs.map +7 -0
  132. package/package.json +117 -0
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # snapback2
2
+
3
+ Snapback 2's package contains the Quarry compiler, TypeScript lowering descriptors,
4
+ headless client, React web arm, and CLI shim. It requires Node 20 or newer for Node clients.
5
+
6
+ ```sh
7
+ npm install snapback2 react
8
+ ```
9
+
10
+ ```ts
11
+ import { api, createClient } from "snapback2/client";
12
+
13
+ const client = createClient({ url: process.env.SNAPBACK_URL!, as: "alice" });
14
+ const page = await client.query(api.messages.thread, { conversationId: "conversation-1", c: null });
15
+
16
+ const sub = client.subscribe(api.messages.thread, { conversationId: "conversation-1", c: null }, console.log);
17
+ await sub.until((delivery) => delivery.state === "complete" && delivery.live);
18
+ sub.close();
19
+ ```
20
+
21
+ The generated `.snapback/build/api.d.ts` is the only source of `api` members, so
22
+ operation and channel arguments flow through the headless client and React hooks
23
+ without committed generated code. Typos and extra arguments are compile errors.
24
+
25
+ ```tsx
26
+ import { SnapbackProvider, useMutation, useQuery } from "snapback2/react";
27
+ import { api } from "snapback2/client";
28
+
29
+ function Messages() {
30
+ const messages = useQuery(api.messages.thread, { conversationId: "conversation-1", c: null });
31
+ const send = useMutation(api.messages.send);
32
+ if (messages.state === "hydrating") return <>Loading…</>;
33
+ return <button disabled={send.pending} onClick={() => send.run({ conversationId: "conversation-1", recipientId: "profile-bob", body: "Hi" })}>
34
+ {messages.state}
35
+ </button>;
36
+ }
37
+
38
+ export const App = () => <SnapbackProvider url="http://127.0.0.1:3210" as="alice">
39
+ <Messages />
40
+ </SnapbackProvider>;
41
+ ```
42
+
43
+ Reads surface exactly seven states: `hydrating`, `complete`, `capped`, `partial`,
44
+ `denied`, `refused`, and `stale`. A subscription frame retains its server receipt
45
+ state and sets the orthogonal `live: true`; stream loss changes the view to
46
+ `stale` with `live: false` only after a receipt has been delivered. An initial
47
+ connection failure remains `hydrating`. Transport status is separately exposed as
48
+ `connection: "connected" | "reconnecting" | "offline"`. `useQuery` subscribes by
49
+ default; pass `{ live: false }` for a one-shot query, which remains `hydrating` and
50
+ retries while its connection is reconnecting.
51
+
52
+ `useMutation` returns `{ run, pending, last, transport }`. `pending` covers the
53
+ in-flight interval, and `last` is only the most recent server `committed` or
54
+ `rejected` receipt. A retryable HTTP or network failure rejects `run`, leaves
55
+ `last` untouched, and appears separately as `transport` until the next successful
56
+ run. `queued` is reserved for the deferred offline outbox and is not a v1 mutation
57
+ state.
58
+
59
+ ## Assets
60
+
61
+ Generated image and video arguments accept either their closed record or a
62
+ browser `File`. The client reads the operation's current manifest descriptors,
63
+ preflights every file against its byte bound, uploads them, and substitutes the
64
+ returned records before invoking the mutation. An Expo source may instead be
65
+ `{ uri, size?, fileSize?, type?, mimeType? }`, matching Expo ImagePicker's
66
+ `uri`/`fileSize`/`mimeType` fields. The optional `expo-file-system` peer opens
67
+ `file://` and `content://` URIs as a native Blob-compatible `File`; when neither size
68
+ field is supplied, its `File.size` provides the required upload length. The
69
+ web bundle does not include that peer, and URI bytes never pass through a
70
+ custom owner `fetch` supplied to `createClient`. Native File uploads use the
71
+ documented direct File body through `expo/fetch`; progress reports whole-body
72
+ start and successful completion (0, then total) because that transport does
73
+ not expose streaming request progress.
74
+
75
+ Native URI uploads require the optional peers `expo >=54` and
76
+ `expo-file-system >=19`. Version 19.0.0
77
+ [made the modern API the package-root default](https://github.com/expo/expo/blob/main/packages/expo-file-system/CHANGELOG.md#1900---2025-08-13),
78
+ and Expo SDK 54 pairs it with `~19.0.24` and documents both the package-root
79
+ `File` import and direct `expo/fetch` upload body in the
80
+ [SDK 54 FileSystem API](https://docs.expo.dev/versions/v54.0.0/sdk/filesystem/).
81
+
82
+ ```ts
83
+ const sent = await client.mutation(api.messages.sendPhoto, {
84
+ conversationId: "conversation-1",
85
+ photo: file,
86
+ });
87
+
88
+ const record = await client.upload(file, {
89
+ kind: "image",
90
+ onProgress: ({ loaded, total }) => console.log(loaded, total),
91
+ });
92
+ ```
93
+
94
+ `client.upload` is the explicit two-step form. Pass `kind` to get the nominal
95
+ `ImageAsset`/`VideoAsset` back for a later mutation; the server still decides by
96
+ sniffing the bytes and the client refuses `E_ASSET_TYPE` if the two disagree.
97
+ Without `kind` the result is the union. Upload and transport failures
98
+ reject; a mutation refusal still resolves as a `MutationRejected` receipt.
99
+
100
+ React renders closed records and keeps authority out of image URLs:
101
+
102
+ ```tsx
103
+ import { AssetImage, AssetVideo, useAssetUrl } from "snapback2/react";
104
+
105
+ <AssetImage asset={message.photo} alt="attachment" />
106
+ <AssetVideo asset={message.clip} />
107
+
108
+ const download = useAssetUrl(message.clip, { download: true });
109
+ ```
110
+
111
+ `AssetImage` measures its rendered box and header-fetches an appropriate image
112
+ rung before creating a blob URL. `AssetVideo` mints one short-lived session
113
+ carrier for its stream URL. React Native resolves the same import through the
114
+ package's `react-native` condition; install `react-native`, `expo-video`, and
115
+ `expo-file-system` in that application.
116
+
117
+ Build and run the package tests against a real debug owner from this directory with:
118
+
119
+ ```sh
120
+ node build.mjs
121
+ env -u SNAPBACK_URL node --test tests/
122
+ ```
123
+
124
+ With `SNAPBACK_URL` unset, the harness builds the chat fixture with the bundled
125
+ compiler, starts `target/debug/snapback2 dev --foreground` on an ephemeral port,
126
+ and exports its URL to the suite. To use an already-running compatible owner, set
127
+ `SNAPBACK_URL=http://127.0.0.1:<port>` instead.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ // Resolve the platform binary package and exec it. No other logic lives here.
3
+ import { createRequire } from "node:module";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ const name = `snapback2-${process.platform}-${process.arch}`;
7
+ let bin;
8
+ try {
9
+ bin = createRequire(import.meta.url).resolve(`${name}/bin/snapback2`);
10
+ } catch {
11
+ console.error(`snapback2: no binary package for ${process.platform}-${process.arch} (${name} is not installed)`);
12
+ process.exit(2);
13
+ }
14
+ const env = {
15
+ ...process.env,
16
+ SNAPBACK2_COMPILER: fileURLToPath(new URL("../dist/compiler.mjs", import.meta.url)),
17
+ SNAPBACK2_GUIDE: fileURLToPath(new URL("../dist/guide", import.meta.url)),
18
+ SNAPBACK2_TEMPLATES: fileURLToPath(new URL("../dist/templates", import.meta.url)),
19
+ SNAPBACK2_TEST_RUNNER: fileURLToPath(new URL("../dist/test-runner.mjs", import.meta.url)),
20
+ };
21
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit", env });
22
+ if (result.error) {
23
+ console.error(`snapback2: failed to spawn ${bin}: ${result.error.message}`);
24
+ process.exit(1);
25
+ }
26
+ if (result.signal) {
27
+ try {
28
+ process.kill(process.pid, result.signal);
29
+ } catch (error) {
30
+ console.error(`snapback2: ${bin} terminated by ${result.signal}: ${error instanceof Error ? error.message : String(error)}`);
31
+ process.exit(1);
32
+ }
33
+ }
34
+ if (typeof result.status === "number") process.exit(result.status);
35
+ console.error(`snapback2: ${bin} exited without a status or signal`);
36
+ process.exit(1);
package/dist/api.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export declare const referencePath: unique symbol;
2
+ export interface Op<Args, Result> {
3
+ readonly op: string;
4
+ readonly [referencePath]: string;
5
+ readonly __args?: (args: Args) => void;
6
+ readonly __result?: Result;
7
+ }
8
+ export interface Channel<Args, Payload = unknown> {
9
+ readonly [referencePath]: string;
10
+ readonly __channelArgs?: (args: Args) => void;
11
+ readonly __payload?: Payload;
12
+ }
13
+ export interface Api {
14
+ }
15
+ export type AnyOp = Op<any, any>;
16
+ export interface ApiProxy {
17
+ readonly [referencePath]: string;
18
+ }
19
+ export declare function referenceName(ref: ApiProxy): string;
20
+ export declare const api: Api;
@@ -0,0 +1,34 @@
1
+ import type { AssetRecord, AssetUploadOptions, AssetUploadSource, ImageAsset, NativeAssetSource, VideoAsset } from "./types.js";
2
+ export declare class AssetInputError extends Error {
3
+ readonly code: "E_ASSET_TYPE" | "E_ASSET_TOO_LARGE" | "E_ASSET_NOT_READY" | "E_ASSET_CARRIER_IMAGE" | "E_ASSET_NATIVE_FILE_API";
4
+ constructor(code: "E_ASSET_TYPE" | "E_ASSET_TOO_LARGE" | "E_ASSET_NOT_READY" | "E_ASSET_CARRIER_IMAGE" | "E_ASSET_NATIVE_FILE_API", message: string);
5
+ }
6
+ export interface BlobLike {
7
+ readonly size: number;
8
+ arrayBuffer(): Promise<ArrayBuffer>;
9
+ stream(): ReadableStream<Uint8Array>;
10
+ }
11
+ type RuntimeAssetUploadSource = AssetUploadSource | BlobLike;
12
+ export interface PreparedAssetSource {
13
+ readonly source: RuntimeAssetUploadSource;
14
+ readonly bytes: number;
15
+ body?: BlobLike | ReadableStream<Uint8Array>;
16
+ }
17
+ export interface AssetPlacement {
18
+ readonly path: readonly (string | number)[];
19
+ readonly max: number;
20
+ readonly prepared: PreparedAssetSource;
21
+ }
22
+ export declare function isBrowserFile(value: unknown): value is File | Blob;
23
+ export declare function isBlobLike(value: unknown): value is BlobLike;
24
+ export declare function isNativeAssetSource(value: unknown): value is NativeAssetSource;
25
+ export declare function mayNeedAssetPlanning(value: unknown, ancestors?: Set<object>): boolean;
26
+ export declare function expoUploadFetch(): Promise<typeof globalThis.fetch>;
27
+ export declare function prepareAssetSource(source: RuntimeAssetUploadSource): Promise<PreparedAssetSource>;
28
+ export declare function planAssetArguments(args: unknown, descriptor: unknown): Promise<AssetPlacement[]>;
29
+ export declare function preparedBody(prepared: PreparedAssetSource): Promise<BlobLike | ReadableStream<Uint8Array>>;
30
+ export declare function withProgress(body: BlobLike | ReadableStream<Uint8Array>, bytes: number, options: AssetUploadOptions): BlobLike | ReadableStream<Uint8Array>;
31
+ export declare function substituteAssetRecords(args: unknown, placements: readonly AssetPlacement[], records: readonly AssetRecord[]): unknown;
32
+ export declare function assetRecord(value: unknown): ImageAsset | VideoAsset;
33
+ export declare function assetPath(baseUrl: string, asset: AssetRecord, width?: number, carrier?: string): string;
34
+ export {};
@@ -0,0 +1,26 @@
1
+ import { SnapbackRefusal } from "./client-wire.js";
2
+ import { type AssetCache, type AssetBytes, type ByteCache } from "./store/byte-cache.js";
3
+ import type { RefusalEnvelope } from "./types.js";
4
+ export interface AssetBlobTarget {
5
+ url: string;
6
+ assetId: string;
7
+ width: number | null;
8
+ }
9
+ /** The tokenless asset URL this client issued, split into the cache key it names. */
10
+ export declare function assetBlobTarget(baseUrl: string, url: string): AssetBlobTarget;
11
+ export declare function viewedRefusal(assetId: string): SnapbackRefusal;
12
+ /**
13
+ * The one viewed guard every byte path consults after the cache missed and before
14
+ * a URL is minted or a byte moves: the ids whose view-once row was already viewed
15
+ * on this partition. `assetBlob` refuses them; `useAssetUrl` mints no URL for them.
16
+ */
17
+ export declare function viewedAssetIds(cache: AssetCache | undefined, ids: readonly string[]): Promise<Set<string>>;
18
+ /**
19
+ * Cache-first bytes for one asset rendition. Without a byte cache this is the
20
+ * network fetch; with one, a held rendition (or what this instance showed for a
21
+ * view-once row) is answered without the network, a viewed view-once row is refused
22
+ * before any byte moves, and a fetched rendition is kept under its row's lease.
23
+ */
24
+ export declare function cachedAssetBlob(cache: ByteCache | undefined, target: AssetBlobTarget, network: () => Promise<Blob>, record: (refusal: RefusalEnvelope) => void, current: () => void, viewing?: object): Promise<Blob>;
25
+ /** Native byte ingress; the caller fences custody after each awaited read. */
26
+ export declare function cachedAssetBytes(cache: ByteCache | undefined, target: AssetBlobTarget, network: () => Promise<AssetBytes>, record: (refusal: RefusalEnvelope) => void, current: () => void, viewing?: object): Promise<AssetBytes>;
@@ -0,0 +1,94 @@
1
+ import { SnapbackRefusal } from "./client-wire.js";
2
+ import { OfflineRuntime, type AcceptedDelivery, type MountedQuery, type ResumeAdjustment } from "./offline.js";
3
+ import { type OfflineClientOptions, type LiveQueryDelivery, type QueryResult, type QueryRefusedDelivery, type RefusalEnvelope, type SnapbackClient, type Subscription, type SubscriptionDelivery } from "./types.js";
4
+ import type { Op } from "./api.js";
5
+ export declare function validateOfflineOptions(options: OfflineClientOptions | undefined): void;
6
+ export declare function createOfflineRuntime(options: OfflineClientOptions | undefined): OfflineRuntime | undefined;
7
+ export declare function offlineRefusal(error: unknown, carrier: string): SnapbackRefusal;
8
+ export declare function offlineNetworkRefusal(): SnapbackRefusal;
9
+ export declare function acceptOffline<Result>(runtime: OfflineRuntime | undefined, op: string, args: unknown, raw: unknown, shape: (message: string) => Error): Promise<AcceptedDelivery<Result>>;
10
+ export declare function commitOffline(runtime: OfflineRuntime | undefined, op: string, args: unknown, accepted: AcceptedDelivery<unknown>, failure: (error: unknown) => Error, revision?: number, current?: () => boolean): Promise<boolean>;
11
+ export declare function decodeOfflineSubscription<Result>(options: {
12
+ runtime: OfflineRuntime;
13
+ op: string;
14
+ args: unknown;
15
+ data: string;
16
+ shape(message: string): Error;
17
+ failure(error: unknown): Error;
18
+ observe?(): Promise<void>;
19
+ revision?: number;
20
+ current?: () => boolean;
21
+ superseded?(): void;
22
+ }): Promise<LiveQueryDelivery<Result> | QueryRefusedDelivery>;
23
+ export declare function settleSubscription<Result>(value: LiveQueryDelivery<Result> | QueryRefusedDelivery, ensureAuthority: () => void, patch: (value: {
24
+ connection: "connected";
25
+ lastRefusal: RefusalEnvelope | undefined;
26
+ }) => void): {
27
+ seq: number;
28
+ data?: Result;
29
+ } | undefined;
30
+ export declare function answerOffline<Result>(runtime: OfflineRuntime | undefined, op: string, args: unknown, failure: (error: unknown) => Error): Promise<QueryResult<Result>>;
31
+ export declare function createOfflineDrain<Args, Result>(options: {
32
+ ref: Op<Args, Result>;
33
+ op: string;
34
+ args: Args;
35
+ callback: (delivery: SubscriptionDelivery<Result>) => void;
36
+ runtime: OfflineRuntime | undefined;
37
+ /** Settles once the manifest was loaded or could not be: the cursor argument's name is known after it. */
38
+ ready?: Promise<void>;
39
+ mounted: Map<string, MountedQuery>;
40
+ query(pageArgs: Args): Promise<QueryResult<Result>>;
41
+ subscribe(tailArgs: Args, callback: (delivery: SubscriptionDelivery<Result>) => void): Subscription<SubscriptionDelivery<Result>>;
42
+ track(subscription: Subscription<SubscriptionDelivery<Result>>): Subscription<SubscriptionDelivery<Result>>;
43
+ observe?(): Promise<void>;
44
+ /** Performs a resume; a drained log that holds no replay grant asks for one here. */
45
+ resume?(): Promise<void>;
46
+ }): Subscription<SubscriptionDelivery<Result>>;
47
+ export declare function goOffline(options: {
48
+ ready: Promise<void>;
49
+ ensureOpen(): void;
50
+ force(value: boolean): void;
51
+ connection(value: "offline" | "reconnecting"): void;
52
+ reconnect(): void;
53
+ }): Promise<void>;
54
+ export declare function goOnline<Authority>(options: {
55
+ ready: Promise<void>;
56
+ ensureOpen(): void;
57
+ force(value: boolean): void;
58
+ connection(value: "offline" | "reconnecting"): void;
59
+ authority(): Promise<Authority>;
60
+ resume(authority: Authority): Promise<void>;
61
+ setResume(run: Promise<void>): void;
62
+ reconnect(): void;
63
+ }): Promise<void>;
64
+ export declare function restartOffline(options: {
65
+ ready: Promise<void>;
66
+ ensureOpen(): void;
67
+ forced(): boolean;
68
+ shutdown(): Promise<void>;
69
+ create(): SnapbackClient;
70
+ }): Promise<SnapbackClient>;
71
+ export declare function initializeOfflineClient<Principal, Authority>(options: {
72
+ principal?: Principal;
73
+ bearer: boolean;
74
+ authority: Authority;
75
+ open(principal: Principal, authenticated?: boolean): Promise<void>;
76
+ confirm(authority: Authority): Promise<Principal>;
77
+ metadata(authority: Authority): Promise<void>;
78
+ resume(authority: Authority): Promise<void>;
79
+ ignore(error: unknown): boolean;
80
+ }): Promise<void>;
81
+ export declare function performOfflineResume<Authority>(options: {
82
+ signal?: AbortSignal;
83
+ runtime: OfflineRuntime | undefined;
84
+ mounted: Map<string, MountedQuery>;
85
+ authority: Authority;
86
+ bearerConfirmed: boolean;
87
+ json(path: string, init: RequestInit, authority: Authority): Promise<unknown>;
88
+ validated(authority: Authority): void;
89
+ shape(message: string): Error;
90
+ failure?(error: unknown): Error;
91
+ subscriptions: Iterable<Subscription<unknown>>;
92
+ /** Re-fetches `/api/program` for the live generation after `E_OFFLINE_RESET`. */
93
+ reload?(authority: Authority): Promise<void>;
94
+ }, adjustment?: ResumeAdjustment, retry?: boolean): Promise<boolean>;
@@ -0,0 +1,58 @@
1
+ import { type OfflineRuntime } from "./offline.js";
2
+ import type { MutationResult, OutboxApi } from "./types.js";
3
+ /** Records the grant a login/guest response carries; the value passes through untouched. */
4
+ export declare function captureGrant<T>(runtime: OfflineRuntime | undefined, value: T): T;
5
+ /**
6
+ * The replayed mutation's terminal object: today's committed/rejected shapes
7
+ * plus the trace conflict, which carries no diagnostic code of its own on the
8
+ * wire and is given the client-side `E_OFFLINE_CONFLICT` envelope here.
9
+ */
10
+ export declare function wireReplayResponse<Result>(value: unknown): MutationResult<Result> & {
11
+ intent?: string;
12
+ };
13
+ type Transport = (path: string, init: RequestInit) => Promise<unknown>;
14
+ type DrainResult = {
15
+ stopped: boolean;
16
+ reason?: "fence";
17
+ };
18
+ export interface SubmitMutationOptions {
19
+ runtime: OfflineRuntime | undefined;
20
+ /** The client's transport is cut (`client.offline()`); the owner is never tried. */
21
+ offline: boolean;
22
+ op: string;
23
+ args: unknown;
24
+ json: Transport;
25
+ failure(error: unknown): Error;
26
+ drain(): Promise<{
27
+ stopped: boolean;
28
+ }>;
29
+ }
30
+ /**
31
+ * A client with no store (no partition open) sends today's bytes. With a
32
+ * partition open every write carries the replay envelope, online or not: the
33
+ * twin runs it locally for its ids and trace, the intent is durable before the
34
+ * request, and the server's ledger makes the online path exactly-once too — a
35
+ * 503 after a commit can no longer double-send (LLP 1002 §9, LLP 1012 §7). A
36
+ * partition with no custody yet (no grant, or an unknown write bound) refuses
37
+ * `E_OFFLINE_ID_BLOCK` with no request rather than fall back to plain bytes:
38
+ * that window is exactly where a 503 after a commit would double-send.
39
+ * Offline, behind queued intents a drain could not clear, or after a transport
40
+ * failure, the write resolves `queued`.
41
+ */
42
+ export declare function submitMutation<Result>(options: SubmitMutationOptions): Promise<MutationResult<Result>>;
43
+ export interface DrainOptions {
44
+ signal?: AbortSignal;
45
+ runtime: OfflineRuntime;
46
+ json: Transport;
47
+ shape(message: string): Error;
48
+ failure(error: unknown): Error;
49
+ }
50
+ /**
51
+ * Replays queued intents in order. A transport failure leaves the intent
52
+ * queued and stops; a rejection, conflict, or refusal settles only that intent
53
+ * and the drain continues. Past the fence, replay waits for a successful
54
+ * resume; only the owner's `E_OFFLINE_INTENT_EXPIRED` refusal settles expired.
55
+ */
56
+ export declare function drainOutbox(options: DrainOptions): Promise<DrainResult>;
57
+ export declare function createOutboxApi(runtime: OfflineRuntime | undefined, drain: () => Promise<DrainResult>, ready?: () => Promise<void>): OutboxApi;
58
+ export {};
@@ -0,0 +1,41 @@
1
+ import { type PreparedAssetSource } from "./assets.js";
2
+ import { SnapbackRefusal } from "./client-wire.js";
3
+ import type { AssetBytes, AssetFetchOptions, ByteCache } from "./store/byte-cache.js";
4
+ import type { AssetRecord, AssetUploadOptions, AssetUploadSource, AssetUrlOptions, RefusalEnvelope } from "./types.js";
5
+ /** The authority snapshot every request is fenced by (LLP 1002 §9). */
6
+ export interface Authority {
7
+ authGeneration: number;
8
+ custodyGeneration: number;
9
+ fence: number;
10
+ token?: string;
11
+ }
12
+ export interface CarrierSession {
13
+ token: string;
14
+ expiresAt: number;
15
+ authGeneration: number;
16
+ custodyGeneration: number;
17
+ fence: number;
18
+ }
19
+ /** What the client lends the asset arm; the class satisfies it structurally. */
20
+ export interface UploadHost {
21
+ readonly url: string;
22
+ readonly persona?: string;
23
+ readonly assetCache?: ByteCache;
24
+ readonly fetchImpl: typeof globalThis.fetch;
25
+ readonly customFetch: boolean;
26
+ carrier?: CarrierSession;
27
+ headers(authority: Authority, stream?: boolean, hasBody?: boolean): Headers;
28
+ json(path: string, init: RequestInit, authority: Authority): Promise<unknown>;
29
+ ensureAuthority(authority: Authority | undefined): asserts authority is Authority;
30
+ recordRefusal(envelope: RefusalEnvelope): void;
31
+ validated(authority: Authority | undefined): void;
32
+ networkRefusal(message: string): SnapbackRefusal;
33
+ transportRefusal(message: string): SnapbackRefusal;
34
+ }
35
+ export declare function uploadAsset(host: UploadHost, source: AssetUploadSource, options: AssetUploadOptions, authority: Authority): Promise<AssetRecord>;
36
+ export declare function assetUrlFor(host: UploadHost, assetOrList: AssetRecord | readonly AssetRecord[] | null | undefined, options: AssetUrlOptions, authority: Authority): Promise<string | string[] | undefined>;
37
+ export declare function assetHeadersFor(host: UploadHost, authority: Authority): Record<string, string>;
38
+ export declare function assetBlobFor(host: UploadHost, bytes: ByteCache | undefined, offline: () => boolean, url: string, options: AssetFetchOptions, authority: Authority): Promise<Blob>;
39
+ export declare function assetBytesFor(host: UploadHost, cache: ByteCache | undefined, offline: () => boolean, url: string, options: AssetFetchOptions, authority: Authority): Promise<AssetBytes>;
40
+ export declare function uploadPrepared(host: UploadHost, prepared: PreparedAssetSource, options: AssetUploadOptions, authority: Authority): Promise<AssetRecord>;
41
+ export declare function assetInput(host: UploadHost, error: unknown): SnapbackRefusal;
@@ -0,0 +1,41 @@
1
+ import type { AuthMe, AuthSession, ChannelDelivery, ChannelPublishResult, LiveQueryDelivery, Manifest, MutationResult, QueryRefusedDelivery, RefusalEnvelope, ServerQueryResult } from "./types.js";
2
+ export declare function fallbackRefusal(code: string, message: string): RefusalEnvelope;
3
+ declare const snapbackRefusalBrand: unique symbol;
4
+ export declare class SnapbackRefusal extends Error {
5
+ readonly retryable: boolean;
6
+ readonly status?: number | undefined;
7
+ readonly [snapbackRefusalBrand] = true;
8
+ readonly envelope: RefusalEnvelope;
9
+ readonly code: string;
10
+ constructor(envelope: RefusalEnvelope, retryable?: boolean, status?: number | undefined);
11
+ }
12
+ export declare function isSnapbackRefusal(value: unknown): value is SnapbackRefusal;
13
+ export declare function wireShape(message: string): SnapbackRefusal;
14
+ export declare function wireObject(value: unknown, surface: string): Record<string, unknown>;
15
+ export declare function wireRefusalEnvelope(value: unknown, surface: string): RefusalEnvelope;
16
+ export declare function copyRefusal(envelope: RefusalEnvelope): RefusalEnvelope;
17
+ export declare function isTerminalSession(envelope: Pick<RefusalEnvelope, "code" | "family">): boolean;
18
+ export declare function wireQuery<Result>(value: unknown, surface?: string): ServerQueryResult<Result>;
19
+ export declare function wireMutation<Result>(value: unknown): MutationResult<Result>;
20
+ export declare function wireSubscription<Result>(data: string): LiveQueryDelivery<Result> | QueryRefusedDelivery;
21
+ export declare function wireChannelDelivery<Payload>(data: string): ChannelDelivery<Payload>;
22
+ export declare function wireChannelPublish(value: unknown): ChannelPublishResult;
23
+ export declare function terminalSessionEnvelope(value: unknown): RefusalEnvelope | undefined;
24
+ export declare function wireAuthSession(value: unknown, surface: string): AuthSession;
25
+ export declare function wireAuthMe(value: unknown): AuthMe;
26
+ export declare function wireLogout(value: unknown): {
27
+ ok: boolean;
28
+ };
29
+ export declare function wireManifest(value: unknown): Manifest;
30
+ export declare function wireAssetSession(value: unknown): {
31
+ token: string;
32
+ expiresAt: number;
33
+ };
34
+ export declare function responseRefusal(response: Response): Promise<SnapbackRefusal>;
35
+ export declare function sseObject(data: string, surface: string): Record<string, unknown>;
36
+ /**
37
+ * `AbortSignal.prototype.throwIfAborted` does not exist on React Native's AbortSignal
38
+ * (Hermes, RN 0.81 / Expo SDK 54), so every abort check goes through this helper.
39
+ */
40
+ export declare function throwIfAborted(signal?: AbortSignal | null): void;
41
+ export {};
@@ -0,0 +1,12 @@
1
+ import { api, type Api, type ApiProxy, type Channel as ChannelRef, type Op } from "./api.js";
2
+ import { SnapbackRefusal, isSnapbackRefusal } from "./client-wire.js";
3
+ import type { CreateClientOptions, SnapbackClient } from "./types.js";
4
+ export { api };
5
+ export { SnapbackRefusal, isSnapbackRefusal };
6
+ export { browserTokenStore } from "./token-store.js";
7
+ export type { Api, ApiProxy, ChannelRef as Channel, Op };
8
+ export * from "./types.js";
9
+ export type { Coverage, CoverageFacts, HeldAnswer, HeldOperation, HeldProgram, OutboxEntry, OutboxStore, OverlayRow, RangeStore, RangeStoreFactory, Receipt, ReceiptRange, StoreSwap, } from "./store/range-store.js";
10
+ export type { LocalAnswer, Prediction } from "./twin.js";
11
+ export type { PartialReason, PartialSite } from "./twin-hydrate.js";
12
+ export declare function createClient(options: CreateClientOptions): SnapbackClient;