@velajs/client 0.1.0 → 0.1.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.
@@ -1,210 +0,0 @@
1
- import { COMMIT_CURSOR_HEADER, COMMIT_EPOCH_HEADER } from "@velajs/live-protocol";
2
- import { RoomConnection } from "./connection.js";
3
- import { toMutationError } from "./errors.js";
4
- import { applyOptimisticLayer } from "./optimistic.js";
5
- import { argsKeyOf, createSubscriptionState, notify, refold, subscriptionKey } from "./subscription.js";
6
- const DEFAULT_WS_PATH = '/rooms/:room/ws';
7
- const DEFAULT_ROOM = 'default';
8
- const DEFAULT_HEARTBEAT_MS = 30_000;
9
- /**
10
- * The framework-neutral Vela live client.
11
- *
12
- * ```ts
13
- * const client = new LiveClient<AppLive>({ url: 'https://api.example.com' });
14
- * const stop = client.subscribe('todos.list', { listId: 'l1' }, (todos) => render(todos));
15
- * await client.mutate('/todos', { text: 'hi' }, {
16
- * optimistic: { query: 'todos.list', args: { listId: 'l1' }, apply: (t = []) => [...t, temp] },
17
- * });
18
- * ```
19
- *
20
- * Identical `(query, args, room)` subscriptions share one wire registration;
21
- * reconnects resubscribe with the last cursor+epoch so untouched
22
- * subscriptions resume without a re-run; optimistic updates are rebaseable
23
- * layers gated on the mutation's `Vela-Commit-Cursor` response header.
24
- */ export class LiveClient {
25
- options;
26
- registry = new Map();
27
- connections = new Map();
28
- statusListeners = new Set();
29
- lastStatus = 'idle';
30
- constructor(options){
31
- this.options = options;
32
- }
33
- subscribe(query, args, callback, subscribeOptions) {
34
- const room = subscribeOptions?.room ?? this.options.defaultRoom ?? DEFAULT_ROOM;
35
- const key = subscriptionKey(query, argsKeyOf(args), room);
36
- let state = this.registry.get(key);
37
- let fresh = false;
38
- if (!state) {
39
- state = createSubscriptionState(query, args, room, subscribeOptions?.key);
40
- this.registry.set(key, state);
41
- fresh = true;
42
- }
43
- const cb = callback;
44
- state.callbacks.add(cb);
45
- if (subscribeOptions?.onError) state.errorCallbacks.add(subscribeOptions.onError);
46
- // Late joiners get the cached value synchronously (lunora parity).
47
- if (state.hasBase || state.layers.length > 0) cb(state.lastValue);
48
- if (fresh) this.connectionFor(room).register(state);
49
- return ()=>{
50
- state.callbacks.delete(cb);
51
- if (subscribeOptions?.onError) state.errorCallbacks.delete(subscribeOptions.onError);
52
- if (state.callbacks.size === 0) {
53
- this.registry.delete(key);
54
- this.connectionFor(room).unregister(state);
55
- }
56
- };
57
- }
58
- /** The current cached (folded) value — referentially stable between notifications. */ peek(query, args, room) {
59
- const state = this.registry.get(subscriptionKey(query, argsKeyOf(args), room ?? this.options.defaultRoom ?? DEFAULT_ROOM));
60
- return state?.lastValue;
61
- }
62
- /**
63
- * Fire an HTTP mutation. Optimistic targets paint immediately; the layer
64
- * drops when a subscription frame's cursor passes the response's
65
- * `Vela-Commit-Cursor` (missing header ⇒ one-shot optimism); a rejection
66
- * rolls back. Resolves with the parsed JSON body (undefined when empty).
67
- */ async mutate(path, body, mutateOptions) {
68
- const handles = [];
69
- const paint = (state, transform)=>{
70
- const handle = applyOptimisticLayer(state, transform);
71
- handles.push({
72
- state,
73
- handle
74
- });
75
- if (refold(state)) notify(state);
76
- };
77
- if (mutateOptions?.optimistic) {
78
- const target = mutateOptions.optimistic;
79
- const state = this.registry.get(subscriptionKey(target.query, argsKeyOf(target.args), target.room ?? mutateOptions.room ?? this.options.defaultRoom ?? DEFAULT_ROOM));
80
- // No live subscription for the target ⇒ nothing displays it: no-op.
81
- if (state) paint(state, target.apply);
82
- }
83
- if (mutateOptions?.optimisticUpdate) {
84
- mutateOptions.optimisticUpdate(this.makeStore(mutateOptions, paint));
85
- }
86
- try {
87
- const doFetch = this.options.fetch ?? fetch;
88
- const token = await this.options.authToken?.();
89
- const headers = {
90
- ...body === undefined ? {} : {
91
- 'content-type': 'application/json'
92
- },
93
- ...token === undefined ? {} : {
94
- authorization: `Bearer ${token}`
95
- },
96
- ...mutateOptions?.headers
97
- };
98
- const response = await doFetch(new URL(path, this.options.url).toString(), {
99
- method: mutateOptions?.method ?? 'POST',
100
- headers,
101
- ...body === undefined ? {} : {
102
- body: JSON.stringify(body)
103
- }
104
- });
105
- if (!response.ok) throw await toMutationError(response);
106
- const stamp = readCommitStamp(response);
107
- for (const { state, handle } of handles){
108
- // confirm() reports true when the covering frame already arrived —
109
- // drop is immediate, re-fold now.
110
- if (handle.confirm(stamp) && refold(state)) notify(state);
111
- }
112
- if (response.status === 204 || response.status === 205) return undefined;
113
- try {
114
- return await response.json();
115
- } catch {
116
- return undefined;
117
- }
118
- } catch (error) {
119
- for (const { state, handle } of handles){
120
- if (handle.rollback() && refold(state)) notify(state);
121
- }
122
- throw error;
123
- }
124
- }
125
- /** Presence heartbeat for a room (see `@velajs/client/presence` for the managed preset). */ presenceBeat(room, meta) {
126
- this.connectionFor(room).sendPresence(room, meta);
127
- }
128
- /** Seed subscription state before connecting (SSR hydration). Subscribes then resume from the seeded cursor. */ hydrate(entries) {
129
- for (const entry of entries){
130
- const room = entry.room ?? this.options.defaultRoom ?? DEFAULT_ROOM;
131
- const key = subscriptionKey(entry.query, argsKeyOf(entry.args), room);
132
- if (this.registry.has(key)) continue;
133
- const state = createSubscriptionState(entry.query, entry.args, room);
134
- state.serverBase = entry.value;
135
- state.hasBase = true;
136
- state.lastValue = entry.value;
137
- state.serverCursor = entry.cursor;
138
- state.serverEpoch = entry.epoch;
139
- this.registry.set(key, state);
140
- }
141
- }
142
- connectionStatus() {
143
- const statuses = [
144
- ...this.connections.values()
145
- ].map((connection)=>connection.status);
146
- if (statuses.includes('connected')) return 'connected';
147
- if (statuses.includes('connecting')) return 'connecting';
148
- if (statuses.includes('offline')) return 'offline';
149
- if (statuses.length > 0 && statuses.every((status)=>status === 'closed')) return 'closed';
150
- return 'idle';
151
- }
152
- onConnectionStatus(listener) {
153
- this.statusListeners.add(listener);
154
- return ()=>this.statusListeners.delete(listener);
155
- }
156
- close() {
157
- for (const connection of this.connections.values())connection.close();
158
- }
159
- makeStore(mutateOptions, paint) {
160
- const resolve = (query, args, room)=>this.registry.get(subscriptionKey(query, argsKeyOf(args), room ?? mutateOptions.room ?? this.options.defaultRoom ?? DEFAULT_ROOM));
161
- return {
162
- get: (query, args, room)=>resolve(query, args, room)?.lastValue,
163
- set: (query, args, next, room)=>{
164
- const state = resolve(query, args, room);
165
- if (!state) return;
166
- const transform = typeof next === 'function' ? next : ()=>next;
167
- paint(state, transform);
168
- }
169
- };
170
- }
171
- connectionFor(room) {
172
- const wsBase = (this.options.wsUrl ?? this.options.url).replace(/^http/, 'ws');
173
- const template = this.options.wsPath ?? DEFAULT_WS_PATH;
174
- const path = template.includes(':room') ? template.replace(':room', encodeURIComponent(room)) : template;
175
- const url = new URL(path, wsBase).toString();
176
- let connection = this.connections.get(url);
177
- if (!connection) {
178
- connection = new RoomConnection(url, {
179
- makeSocket: this.options.WebSocket ?? ((socketUrl)=>{
180
- const WS = globalThis.WebSocket;
181
- if (!WS) {
182
- throw new Error('No WebSocket implementation available — pass one via LiveClientOptions.WebSocket');
183
- }
184
- return new WS(socketUrl);
185
- }),
186
- authToken: this.options.authToken,
187
- heartbeatIntervalMs: this.options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS,
188
- reconnect: this.options.reconnect,
189
- onStatusChange: ()=>{
190
- const status = this.connectionStatus();
191
- if (status === this.lastStatus) return;
192
- this.lastStatus = status;
193
- for (const listener of this.statusListeners)listener(status);
194
- }
195
- });
196
- this.connections.set(url, connection);
197
- }
198
- return connection;
199
- }
200
- }
201
- function readCommitStamp(response) {
202
- const cursor = response.headers.get(COMMIT_CURSOR_HEADER);
203
- const epoch = response.headers.get(COMMIT_EPOCH_HEADER);
204
- if (cursor === null || epoch === null) return undefined;
205
- const parsed = Number(cursor);
206
- return Number.isFinite(parsed) ? {
207
- cursor: parsed,
208
- epoch
209
- } : undefined;
210
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * Rebaseable, cursor-gated optimistic layers (lunora `optimistic-layers.ts`
3
- * port). The displayed value is ALWAYS the authoritative server base folded
4
- * through the pending layers, so:
5
- *
6
- * - an unrelated server frame re-folds the layers onto the new base instead of
7
- * clobbering the optimistic state ("rebasing");
8
- * - a layer drops only when a subscription frame arrives whose cursor passes
9
- * the mutation's commit cursor (same epoch) — NEVER on HTTP-response
10
- * timing, which races the broadcast;
11
- * - confirm without a stamp (a non-live-aware endpoint) degrades to one-shot
12
- * optimism: the layer drops silently and the next authoritative frame
13
- * reconciles;
14
- * - rollback removes the layer and re-folds so the failed write's effect
15
- * disappears immediately.
16
- */
17
- export interface CommitStamp {
18
- cursor: number;
19
- epoch: string;
20
- }
21
- export interface OptimisticLayer {
22
- readonly id: symbol;
23
- readonly transform: (current: unknown) => unknown;
24
- /** Set by confirm(); the layer is dropped by the first frame whose cursor passes it. */
25
- commitCursor?: number;
26
- commitEpoch?: string;
27
- }
28
- /** The slice of subscription state the optimistic engine operates on. */
29
- export interface OptimisticHost {
30
- layers: OptimisticLayer[];
31
- serverBase: unknown;
32
- hasBase: boolean;
33
- serverCursor?: number;
34
- serverEpoch?: string;
35
- }
36
- export declare const foldOptimistic: (host: OptimisticHost) => unknown;
37
- export interface LayerHandle {
38
- /**
39
- * Record the mutation's commit stamp. With a stamp, the layer survives
40
- * until a frame's cursor passes it (dropping immediately when the current
41
- * frame already has). Without one, the layer is removed silently — no
42
- * re-fold, the incoming authoritative frame reconciles.
43
- */
44
- confirm(stamp?: CommitStamp): boolean;
45
- /** Remove the layer and report whether the host must re-fold + notify. */
46
- rollback(): boolean;
47
- }
48
- export declare function applyOptimisticLayer(host: OptimisticHost, transform: (current: unknown) => unknown): LayerHandle;
49
- /**
50
- * Drop every confirmed layer the frame at (cursor, epoch) covers. A layer
51
- * confirmed under a DIFFERENT epoch is also dropped — its gate can never fire
52
- * on this timeline. Returns whether any layer was removed (host re-folds).
53
- */
54
- export declare function dropConfirmedLayers(host: OptimisticHost, cursor?: number, epoch?: string): boolean;
@@ -1,67 +0,0 @@
1
- /**
2
- * Rebaseable, cursor-gated optimistic layers (lunora `optimistic-layers.ts`
3
- * port). The displayed value is ALWAYS the authoritative server base folded
4
- * through the pending layers, so:
5
- *
6
- * - an unrelated server frame re-folds the layers onto the new base instead of
7
- * clobbering the optimistic state ("rebasing");
8
- * - a layer drops only when a subscription frame arrives whose cursor passes
9
- * the mutation's commit cursor (same epoch) — NEVER on HTTP-response
10
- * timing, which races the broadcast;
11
- * - confirm without a stamp (a non-live-aware endpoint) degrades to one-shot
12
- * optimism: the layer drops silently and the next authoritative frame
13
- * reconciles;
14
- * - rollback removes the layer and re-folds so the failed write's effect
15
- * disappears immediately.
16
- */ export const foldOptimistic = (host)=>host.layers.reduce((value, layer)=>layer.transform(value), host.hasBase ? host.serverBase : undefined);
17
- export function applyOptimisticLayer(host, transform) {
18
- const layer = {
19
- id: Symbol('optimistic'),
20
- transform
21
- };
22
- host.layers.push(layer);
23
- const remove = ()=>{
24
- const index = host.layers.indexOf(layer);
25
- if (index === -1) return false;
26
- host.layers.splice(index, 1);
27
- return true;
28
- };
29
- return {
30
- confirm (stamp) {
31
- if (!stamp) {
32
- remove();
33
- return false;
34
- }
35
- // A stamp from a foreign epoch can never be gated — treat as unstamped.
36
- if (host.serverEpoch !== undefined && stamp.epoch !== host.serverEpoch) {
37
- remove();
38
- return false;
39
- }
40
- layer.commitCursor = stamp.cursor;
41
- layer.commitEpoch = stamp.epoch;
42
- // The confirming frame may already have passed while the HTTP response
43
- // was in flight — drop now and tell the caller to re-fold.
44
- if (host.serverCursor !== undefined && host.serverCursor >= stamp.cursor) {
45
- return remove();
46
- }
47
- return false;
48
- },
49
- rollback () {
50
- return remove();
51
- }
52
- };
53
- }
54
- /**
55
- * Drop every confirmed layer the frame at (cursor, epoch) covers. A layer
56
- * confirmed under a DIFFERENT epoch is also dropped — its gate can never fire
57
- * on this timeline. Returns whether any layer was removed (host re-folds).
58
- */ export function dropConfirmedLayers(host, cursor, epoch) {
59
- if (cursor === undefined) return false;
60
- const before = host.layers.length;
61
- host.layers = host.layers.filter((layer)=>{
62
- if (layer.commitCursor === undefined) return true; // still pending
63
- if (epoch !== undefined && layer.commitEpoch !== epoch) return false;
64
- return cursor < layer.commitCursor;
65
- });
66
- return host.layers.length !== before;
67
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Decorrelated exponential jitter (lunora `reconnect.ts` port): each retry
3
- * waits a random duration between the base and 3× the previous wait, capped.
4
- * Decorrelation avoids the reconnect stampede a fixed exponential schedule
5
- * produces when a fleet of clients drops at once.
6
- */
7
- export interface ReconnectState {
8
- previousMs?: number;
9
- }
10
- export declare const DEFAULT_RECONNECT_BASE_MS = 250;
11
- export declare const DEFAULT_RECONNECT_CAP_MS = 30000;
12
- export declare function nextReconnectDelay(state: ReconnectState, baseMs?: number, capMs?: number, random?: () => number): number;
13
- export declare function resetReconnect(state: ReconnectState): void;
package/dist/reconnect.js DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Decorrelated exponential jitter (lunora `reconnect.ts` port): each retry
3
- * waits a random duration between the base and 3× the previous wait, capped.
4
- * Decorrelation avoids the reconnect stampede a fixed exponential schedule
5
- * produces when a fleet of clients drops at once.
6
- */ export const DEFAULT_RECONNECT_BASE_MS = 250;
7
- export const DEFAULT_RECONNECT_CAP_MS = 30_000;
8
- export function nextReconnectDelay(state, baseMs = DEFAULT_RECONNECT_BASE_MS, capMs = DEFAULT_RECONNECT_CAP_MS, random = Math.random) {
9
- const high = Math.max(baseMs, (state.previousMs ?? baseMs) * 3);
10
- const delay = Math.min(capMs, baseMs + random() * (high - baseMs));
11
- state.previousMs = delay;
12
- return delay;
13
- }
14
- export function resetReconnect(state) {
15
- state.previousMs = undefined;
16
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Deterministic JSON encoding for cache/dedup keys: object keys are sorted at
3
- * every depth so `{a,b}` and `{b,a}` produce the same key (lunora
4
- * `shared/stable-key.ts` pattern). NOT a wire format — wire frames use plain
5
- * `JSON.stringify` via @velajs/live-protocol's canonical encoders.
6
- */
7
- export declare function stableStringify(value: unknown): string;
@@ -1,20 +0,0 @@
1
- /**
2
- * Deterministic JSON encoding for cache/dedup keys: object keys are sorted at
3
- * every depth so `{a,b}` and `{b,a}` produce the same key (lunora
4
- * `shared/stable-key.ts` pattern). NOT a wire format — wire frames use plain
5
- * `JSON.stringify` via @velajs/live-protocol's canonical encoders.
6
- */ export function stableStringify(value) {
7
- return JSON.stringify(sortDeep(value));
8
- }
9
- function sortDeep(value) {
10
- if (Array.isArray(value)) return value.map(sortDeep);
11
- if (typeof value === 'object' && value !== null) {
12
- const source = value;
13
- const out = {};
14
- for (const key of Object.keys(source).sort()){
15
- out[key] = sortDeep(source[key]);
16
- }
17
- return out;
18
- }
19
- return value;
20
- }
@@ -1,42 +0,0 @@
1
- import type { OptimisticLayer } from './optimistic';
2
- /**
3
- * One deduped live subscription: every `subscribe()` for the same
4
- * (query, args, room) joins a single state (one wire registration, N
5
- * callbacks), lunora `subscription.ts` style.
6
- */
7
- export interface SubscriptionState {
8
- /** Wire subscription id — client-chosen, unique per client (thus per socket). */
9
- sub: string;
10
- query: string;
11
- args: unknown;
12
- argsKey: string;
13
- room: string;
14
- key?: string;
15
- /** Server acknowledged the registration. */
16
- acked: boolean;
17
- callbacks: Set<(value: unknown) => void>;
18
- errorCallbacks: Set<(error: {
19
- code: string;
20
- message: string;
21
- fatal: boolean;
22
- }) => void>;
23
- layers: OptimisticLayer[];
24
- /** Authoritative value with NO optimistic overlay. */
25
- serverBase: unknown;
26
- /** Distinguishes "no value yet" from an authoritative `undefined`-shaped value. */
27
- hasBase: boolean;
28
- serverCursor?: number;
29
- serverEpoch?: string;
30
- /**
31
- * Displayed value = serverBase folded through layers. Reassigned only when
32
- * something actually changed, so snapshot consumers (React's
33
- * useSyncExternalStore) get referentially stable reads between notifies.
34
- */
35
- lastValue: unknown;
36
- }
37
- export declare const subscriptionKey: (query: string, argsKey: string, room: string) => string;
38
- export declare const argsKeyOf: (args: unknown) => string;
39
- export declare function createSubscriptionState(query: string, args: unknown, room: string, key?: string): SubscriptionState;
40
- /** Re-fold the displayed value; true when it changed (notify). */
41
- export declare function refold(state: SubscriptionState): boolean;
42
- export declare function notify(state: SubscriptionState): void;
Binary file
package/dist/types.d.ts DELETED
@@ -1,126 +0,0 @@
1
- import type { CommitStamp } from './optimistic';
2
- /**
3
- * The app-supplied typing contract: one entry per live query, keyed by the
4
- * server's `@LiveQuery(name)`. Hand-written in v1; shaped so a future
5
- * `vela codegen` emission (from OpenAPI operationIds + live metadata) is a
6
- * drop-in replacement.
7
- *
8
- * ```ts
9
- * interface AppLive {
10
- * 'todos.list': { args: { listId: string }; result: Todo[] };
11
- * }
12
- * const client = new LiveClient<AppLive>({ url });
13
- * ```
14
- */
15
- export interface LiveContract {
16
- [query: string]: {
17
- args: unknown;
18
- result: unknown;
19
- };
20
- }
21
- export type ArgsOf<C, Q extends keyof C> = C[Q] extends {
22
- args: infer A;
23
- } ? A : never;
24
- export type ResultOf<C, Q extends keyof C> = C[Q] extends {
25
- result: infer R;
26
- } ? R : never;
27
- export type ConnectionStatus = 'idle' | 'connecting' | 'connected' | 'offline' | 'closed';
28
- /** The WebSocket surface the client uses — injectable for SSR/tests/non-browser runtimes. */
29
- export interface WebSocketLike {
30
- onopen: ((event?: unknown) => void) | null;
31
- onmessage: ((event: {
32
- data: unknown;
33
- }) => void) | null;
34
- onclose: ((event?: unknown) => void) | null;
35
- onerror: ((event?: unknown) => void) | null;
36
- send(data: string): void;
37
- close(code?: number, reason?: string): void;
38
- readyState: number;
39
- }
40
- export type WebSocketFactory = (url: string) => WebSocketLike;
41
- export interface ReconnectOptions {
42
- /** First-retry floor (default 250 ms). */
43
- baseMs?: number;
44
- /** Backoff ceiling (default 30 000 ms). */
45
- capMs?: number;
46
- }
47
- export interface LiveClientOptions {
48
- /** HTTP(S) base of the Vela app, e.g. `https://api.example.com`. */
49
- url: string;
50
- /** WS(S) base override; derived from `url` (http→ws) when omitted. */
51
- wsUrl?: string;
52
- /**
53
- * Gateway path template the live socket connects to. A `:room` placeholder
54
- * is substituted per subscription room (matching vela's node transport,
55
- * which auto-joins the `:id` route param, and Cloudflare's one-DO≈one-room
56
- * model). Default `/rooms/:room/ws`.
57
- */
58
- wsPath?: string;
59
- /** Room used when a subscribe/mutate call names none. Default `'default'`. */
60
- defaultRoom?: string;
61
- /** Injectables — the core never touches globals directly (SSR/edge safety). */
62
- WebSocket?: WebSocketFactory;
63
- fetch?: typeof fetch;
64
- /**
65
- * Async token provider, re-invoked per socket connect and per mutation.
66
- * Sockets carry it as a `?token=` query param (browsers cannot set WS
67
- * headers — use short-lived rotating tokens); mutations as a Bearer header.
68
- */
69
- authToken?: () => string | undefined | Promise<string | undefined>;
70
- /** App-level `{event:'ping'}` keepalive cadence (default 30 000 ms; CF answers without waking the DO). */
71
- heartbeatIntervalMs?: number;
72
- reconnect?: ReconnectOptions;
73
- }
74
- export interface SubscribeOptions {
75
- room?: string;
76
- /** Delta key-field override forwarded to the server (default `'id'`). */
77
- key?: string;
78
- onError?: (error: {
79
- code: string;
80
- message: string;
81
- fatal: boolean;
82
- }) => void;
83
- }
84
- /** Single-subscription optimistic target for `mutate()`. */
85
- export interface OptimisticTarget<T = unknown> {
86
- query: string;
87
- args?: unknown;
88
- room?: string;
89
- apply: (current: T | undefined) => T;
90
- }
91
- /** Multi-subscription optimistic store (the `optimisticUpdate` callback's argument). */
92
- export interface LiveStore {
93
- get(query: string, args?: unknown, room?: string): unknown;
94
- set(query: string, args: unknown, next: unknown | ((current: unknown) => unknown), room?: string): void;
95
- }
96
- export interface MutateOptions {
97
- method?: string;
98
- headers?: Record<string, string>;
99
- room?: string;
100
- optimistic?: OptimisticTarget;
101
- optimisticUpdate?: (store: LiveStore) => void;
102
- }
103
- export type Unsubscribe = () => void;
104
- /** Seed states before the socket connects (SSR hydration). */
105
- export interface HydrationEntry {
106
- query: string;
107
- args?: unknown;
108
- room?: string;
109
- value: unknown;
110
- cursor?: number;
111
- epoch?: string;
112
- }
113
- /** Durable offline write outbox seam — not implemented in v1 (mutations are plain HTTP). */
114
- export interface OutboxSink {
115
- enqueue(path: string, body: unknown, options?: MutateOptions): Promise<void>;
116
- onCommit?(stamp: CommitStamp): void;
117
- }
118
- /** Durable read-cache seam (IndexedDB, …) — in-memory only in v1. */
119
- export interface ReadCacheAdapter {
120
- load(key: string): Promise<{
121
- value: unknown;
122
- cursor?: number;
123
- epoch?: string;
124
- } | undefined>;
125
- persist(key: string, value: unknown, cursor?: number, epoch?: string): Promise<void>;
126
- }
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- /** Durable read-cache seam (IndexedDB, …) — in-memory only in v1. */ export { };