@prometheus-ags/entity-graph-core 3.0.0-alpha.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.
- package/dist/index.d.cts +2913 -0
- package/dist/index.d.ts +2913 -0
- package/dist/index.js +3879 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3758 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2913 @@
|
|
|
1
|
+
import * as zustand from 'zustand';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed errors used throughout 2.0 to drive retry policy and consumer
|
|
6
|
+
* UI decisions.
|
|
7
|
+
*
|
|
8
|
+
* Why two classes (not a discriminator field):
|
|
9
|
+
* - `instanceof` checks are zero-cost at runtime and survive bundler
|
|
10
|
+
* minification of property names.
|
|
11
|
+
* - Consumers can write `if (error instanceof TerminalError) renderInline()`
|
|
12
|
+
* without importing a type-narrowing helper.
|
|
13
|
+
* - The engine's retry policy is one `instanceof TransientError` check.
|
|
14
|
+
*
|
|
15
|
+
* 4xx → `TerminalError` (no retry — the request is structurally wrong:
|
|
16
|
+
* unauthorized, not-found, schema mismatch, validation failure).
|
|
17
|
+
*
|
|
18
|
+
* 5xx + network failures → `TransientError` (engine retries with
|
|
19
|
+
* exponential backoff up to `maxRetries`).
|
|
20
|
+
*
|
|
21
|
+
* Custom transports can throw either class. The default REST helper
|
|
22
|
+
* (`makeRestTransport`) maps HTTP status codes to the appropriate
|
|
23
|
+
* class automatically.
|
|
24
|
+
*
|
|
25
|
+
* Both classes preserve the original error via the standard
|
|
26
|
+
* `cause` option so stack traces survive the boundary.
|
|
27
|
+
*/
|
|
28
|
+
interface EntityErrorOptions {
|
|
29
|
+
/** HTTP status if the underlying transport was HTTP. */
|
|
30
|
+
status?: number;
|
|
31
|
+
/** Original thrown value (`Error`, `Response`, anything). */
|
|
32
|
+
cause?: unknown;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Permanent failure — do not retry. The request will not succeed by
|
|
36
|
+
* trying again with the same inputs.
|
|
37
|
+
*
|
|
38
|
+
* Typical sources: HTTP 4xx, missing tables, validation errors,
|
|
39
|
+
* permission denied, schema mismatches.
|
|
40
|
+
*/
|
|
41
|
+
declare class TerminalError extends Error {
|
|
42
|
+
readonly kind: "terminal";
|
|
43
|
+
readonly status?: number;
|
|
44
|
+
constructor(message: string, opts?: EntityErrorOptions);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Temporary failure — safe to retry. The engine will retry with
|
|
48
|
+
* exponential backoff up to the configured `maxRetries`.
|
|
49
|
+
*
|
|
50
|
+
* Typical sources: HTTP 5xx, network timeouts, fetch rejections,
|
|
51
|
+
* dropped sockets.
|
|
52
|
+
*/
|
|
53
|
+
declare class TransientError extends Error {
|
|
54
|
+
readonly kind: "transient";
|
|
55
|
+
readonly status?: number;
|
|
56
|
+
constructor(message: string, opts?: EntityErrorOptions);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Union of every typed error the library raises. Use on consumer-side
|
|
60
|
+
* `error` props when both classes are possible.
|
|
61
|
+
*/
|
|
62
|
+
type EntityError = TerminalError | TransientError;
|
|
63
|
+
/**
|
|
64
|
+
* Convert an unknown thrown value into a typed entity error.
|
|
65
|
+
*
|
|
66
|
+
* - Already-typed errors pass through.
|
|
67
|
+
* - HTTP-status-bearing errors become Terminal (4xx) or Transient (5xx).
|
|
68
|
+
* - `AbortError` / `DOMException` "AbortError" → Terminal (caller
|
|
69
|
+
* cancelled; not worth retrying).
|
|
70
|
+
* - Everything else → Transient (assume retryable; better than
|
|
71
|
+
* accidentally permanently failing on a one-off network blip).
|
|
72
|
+
*/
|
|
73
|
+
declare function toEntityError(err: unknown): EntityError;
|
|
74
|
+
|
|
75
|
+
/** Logical entity kind (e.g. `"Post"`). Used to partition the normalized graph. */
|
|
76
|
+
type EntityType = string;
|
|
77
|
+
/** Primary key for an entity within its `type`. Lists and relations reference this, not row copies. */
|
|
78
|
+
type EntityId = string;
|
|
79
|
+
/** Stable string key for a list query (often `JSON.stringify`-shaped). Lists store IDs only under this key. */
|
|
80
|
+
type QueryKey = string;
|
|
81
|
+
/** Provenance of the latest known entity state. */
|
|
82
|
+
type SyncOrigin = "server" | "client" | "optimistic";
|
|
83
|
+
/** Optional sync-facing metadata kept outside canonical entity payloads. */
|
|
84
|
+
interface EntitySyncMetadata {
|
|
85
|
+
synced: boolean;
|
|
86
|
+
origin: SyncOrigin;
|
|
87
|
+
updatedAt: number | null;
|
|
88
|
+
}
|
|
89
|
+
/** Snapshot shape returned by sync-aware reads and graph-native query helpers. */
|
|
90
|
+
type EntitySnapshot<T extends object> = T & {
|
|
91
|
+
$synced: boolean;
|
|
92
|
+
$origin: SyncOrigin;
|
|
93
|
+
$updatedAt: number | null;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Fetch/cache metadata for a single entity instance (`type:id`).
|
|
97
|
+
* Separates transport concerns from canonical `entities` data so hooks can show spinners and stale-while-revalidate without mutating server-shaped fields.
|
|
98
|
+
*/
|
|
99
|
+
interface EntityState {
|
|
100
|
+
isFetching: boolean;
|
|
101
|
+
lastFetched: number | null;
|
|
102
|
+
error: string | null;
|
|
103
|
+
stale: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Ordered **IDs** for a list query plus pagination metadata — never embedded entity payloads.
|
|
107
|
+
* Cross-view reactivity depends on this: when `entities[type][id]` updates, every list containing that id re-resolves rows from the graph.
|
|
108
|
+
*/
|
|
109
|
+
interface ListState {
|
|
110
|
+
ids: EntityId[];
|
|
111
|
+
total: number | null;
|
|
112
|
+
nextCursor: string | null;
|
|
113
|
+
prevCursor: string | null;
|
|
114
|
+
hasNextPage: boolean;
|
|
115
|
+
hasPrevPage: boolean;
|
|
116
|
+
isFetching: boolean;
|
|
117
|
+
isFetchingMore: boolean;
|
|
118
|
+
error: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Typed error instance for 2.0 hooks (`useEntities`, `useEntityQuery`).
|
|
121
|
+
* Carries `TerminalError` / `TransientError` so consumers can
|
|
122
|
+
* `instanceof`-check the failure mode without parsing the string in
|
|
123
|
+
* `error`. Both fields are written together by `setListError`; the
|
|
124
|
+
* string `error` field remains for back-compat with 1.x consumers.
|
|
125
|
+
*/
|
|
126
|
+
lastError: EntityError | null;
|
|
127
|
+
lastFetched: number | null;
|
|
128
|
+
stale: boolean;
|
|
129
|
+
currentPage: number | null;
|
|
130
|
+
pageSize: number | null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Stable fallbacks for missing `lists[key]` / `entityStates[key]` slots.
|
|
134
|
+
* Inline `?? { ... }` defaults allocate new objects every `getSnapshot` call and trigger React 19
|
|
135
|
+
* “getSnapshot should be cached” / maximum update depth errors with `useSyncExternalStore`.
|
|
136
|
+
*/
|
|
137
|
+
declare const EMPTY_IDS: EntityId[];
|
|
138
|
+
declare const EMPTY_ENTITY_STATE: EntityState;
|
|
139
|
+
declare const EMPTY_SYNC_METADATA: EntitySyncMetadata;
|
|
140
|
+
declare const EMPTY_LIST_STATE: ListState;
|
|
141
|
+
/**
|
|
142
|
+
* Canonical Zustand store: **entities** (server truth), **patches** (UI-only overlay), **lists** (id order + list meta), and **entityStates** (per-entity fetch state).
|
|
143
|
+
* Prefer hooks for React reads; use `useGraphStore.getState()` inside stores/adapters/engine code where React is not available.
|
|
144
|
+
*/
|
|
145
|
+
interface GraphState {
|
|
146
|
+
/** Normalized server-confirmed records. Mutate only via upsert/replace/remove — not from components. */
|
|
147
|
+
entities: Record<EntityType, Record<EntityId, Record<string, unknown>>>;
|
|
148
|
+
/** Local-only fields merged at read time (`readEntity` / hooks). Never send patches to the server. */
|
|
149
|
+
patches: Record<EntityType, Record<EntityId, Record<string, unknown>>>;
|
|
150
|
+
/** Per-entity fetch lifecycle keyed as `${type}:${id}`. */
|
|
151
|
+
entityStates: Record<string, EntityState>;
|
|
152
|
+
/** Optional sync/provenance state layered beside the canonical entity payload. */
|
|
153
|
+
syncMetadata: Record<string, EntitySyncMetadata>;
|
|
154
|
+
/** List slots keyed by serialized query keys. Values hold id arrays and pagination — not entity clones. */
|
|
155
|
+
lists: Record<QueryKey, ListState>;
|
|
156
|
+
/**
|
|
157
|
+
* Shallow-merge `data` into the canonical entity. Use when the API returns partial updates or normalized fragments.
|
|
158
|
+
* @param type - Entity kind
|
|
159
|
+
* @param id - Entity id
|
|
160
|
+
* @param data - Fields to merge into existing canonical data
|
|
161
|
+
*/
|
|
162
|
+
upsertEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Batch upsert for list endpoints — avoids N separate writes when hydrating many rows at once.
|
|
165
|
+
* @param type - Entity kind
|
|
166
|
+
* @param entries - Pairs of id + partial/full payloads to merge
|
|
167
|
+
*/
|
|
168
|
+
upsertEntities: (type: EntityType, entries: Array<{
|
|
169
|
+
id: EntityId;
|
|
170
|
+
data: Record<string, unknown>;
|
|
171
|
+
}>) => void;
|
|
172
|
+
/**
|
|
173
|
+
* Replace the canonical entity entirely (no merge). Use when the server returns a full snapshot and partial merge would leave stale keys behind.
|
|
174
|
+
*/
|
|
175
|
+
replaceEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => void;
|
|
176
|
+
/** Remove canonical entity, its patches, and its entityState. Does not remove the id from lists — use `removeIdFromAllLists` if needed. */
|
|
177
|
+
removeEntity: (type: EntityType, id: EntityId) => void;
|
|
178
|
+
/**
|
|
179
|
+
* Merge UI-only fields into `patches` so every subscriber sees selection, expansion, optimistic toggles, etc., without forking canonical data.
|
|
180
|
+
*/
|
|
181
|
+
patchEntity: (type: EntityType, id: EntityId, patch: Record<string, unknown>) => void;
|
|
182
|
+
/** Remove specific patch keys; other patch keys remain. */
|
|
183
|
+
unpatchEntity: (type: EntityType, id: EntityId, keys: string[]) => void;
|
|
184
|
+
/** Drop all patches for an entity (e.g. after successful mutation when server data is authoritative). */
|
|
185
|
+
clearPatch: (type: EntityType, id: EntityId) => void;
|
|
186
|
+
/** Reflect in-flight GET for a single entity (deduped fetches in the engine still flip this once per logical request). */
|
|
187
|
+
setEntityFetching: (type: EntityType, id: EntityId, fetching: boolean) => void;
|
|
188
|
+
/** Persist terminal fetch failure message and clear fetching — hooks surface `error` while leaving prior data if any. */
|
|
189
|
+
setEntityError: (type: EntityType, id: EntityId, error: string | null) => void;
|
|
190
|
+
/** Mark entity successfully loaded; clears error, fetching, stale, and updates `lastFetched`. */
|
|
191
|
+
setEntityFetched: (type: EntityType, id: EntityId) => void;
|
|
192
|
+
/** Drive background revalidation: when true, hooks refetch while still showing cached `readEntity` data. */
|
|
193
|
+
setEntityStale: (type: EntityType, id: EntityId, stale: boolean) => void;
|
|
194
|
+
/** Merge sync metadata for one entity without polluting canonical server fields. */
|
|
195
|
+
setEntitySyncMetadata: (type: EntityType, id: EntityId, metadata: Partial<EntitySyncMetadata>) => void;
|
|
196
|
+
/** Clear sync metadata for one entity. */
|
|
197
|
+
clearEntitySyncMetadata: (type: EntityType, id: EntityId) => void;
|
|
198
|
+
/**
|
|
199
|
+
* Replace list ids and merge pagination meta after a primary list fetch (not load-more).
|
|
200
|
+
* Resets fetching flags and clears list error on success path (engine calls this after normalize).
|
|
201
|
+
*/
|
|
202
|
+
setListResult: (key: QueryKey, ids: EntityId[], meta: Partial<Omit<ListState, "ids" | "isFetching" | "isFetchingMore" | "error" | "stale">>) => void;
|
|
203
|
+
/** Append unique ids (e.g. infinite scroll) while merging meta; dedupes id order. */
|
|
204
|
+
appendListResult: (key: QueryKey, ids: EntityId[], meta: Partial<Omit<ListState, "ids" | "isFetching" | "isFetchingMore" | "error" | "stale">>) => void;
|
|
205
|
+
/** Prepend ids (e.g. “new item at top”) with optional meta merge. */
|
|
206
|
+
prependListResult: (key: QueryKey, ids: EntityId[], meta?: Partial<ListState>) => void;
|
|
207
|
+
/** After deletes, keep every list consistent by removing an id everywhere it appears and decrementing `total` when tracked. */
|
|
208
|
+
removeIdFromAllLists: (type: EntityType, id: EntityId) => void;
|
|
209
|
+
/**
|
|
210
|
+
* Insert or move an id in one list. If the id already exists, it is removed first then re-inserted at `position`.
|
|
211
|
+
* @param position - `"start"`, `"end"`, or numeric index
|
|
212
|
+
*/
|
|
213
|
+
insertIdInList: (key: QueryKey, id: EntityId, position: "start" | "end" | number) => void;
|
|
214
|
+
/** Initial or full list fetch in progress (not page-2+). */
|
|
215
|
+
setListFetching: (key: QueryKey, fetching: boolean) => void;
|
|
216
|
+
/** Pagination / “load more” in progress for the same list key. */
|
|
217
|
+
setListFetchingMore: (key: QueryKey, fetchingMore: boolean) => void;
|
|
218
|
+
/**
|
|
219
|
+
* Record list fetch failure; clears both fetching flags.
|
|
220
|
+
* The optional `typed` argument is the typed `EntityError` instance
|
|
221
|
+
* (used by 2.0 hooks); when omitted only the string `error` field is
|
|
222
|
+
* populated (back-compat with 1.x callers).
|
|
223
|
+
*/
|
|
224
|
+
setListError: (key: QueryKey, error: string | null, typed?: EntityError | null) => void;
|
|
225
|
+
/** Mark a list query stale so hooks can refetch without discarding current ids (stale-while-revalidate). */
|
|
226
|
+
setListStale: (key: QueryKey, stale: boolean) => void;
|
|
227
|
+
/**
|
|
228
|
+
* Mark one or all entities of a type stale for subscriber-driven refetch. Does not delete data.
|
|
229
|
+
* @param id - Omit to stale every entity of `type`
|
|
230
|
+
*/
|
|
231
|
+
invalidateEntity: (type: EntityType, id?: EntityId) => void;
|
|
232
|
+
/**
|
|
233
|
+
* Mark lists stale by key prefix or custom predicate so the next access triggers background refresh without dropping ids immediately.
|
|
234
|
+
*/
|
|
235
|
+
invalidateLists: (matcher: string | ((key: QueryKey) => boolean)) => void;
|
|
236
|
+
/** Convenience: stale all entities of a type and lists whose keys start with `type`. */
|
|
237
|
+
invalidateType: (type: EntityType) => void;
|
|
238
|
+
/**
|
|
239
|
+
* Single read path: `{ ...entities[type][id], ...patches[type][id] }` or null if no canonical row exists.
|
|
240
|
+
* @returns Merged view suitable for rendering; not a deep clone
|
|
241
|
+
*/
|
|
242
|
+
readEntity: <T = Record<string, unknown>>(type: EntityType, id: EntityId) => T | null;
|
|
243
|
+
/**
|
|
244
|
+
* Sync-aware read path: canonical entity + patches + virtual sync metadata (`$synced`, `$origin`, `$updatedAt`).
|
|
245
|
+
*/
|
|
246
|
+
readEntitySnapshot: <T = Record<string, unknown>>(type: EntityType, id: EntityId) => EntitySnapshot<T & Record<string, unknown>> | null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Global entity graph store (Zustand + Immer). **Components should not subscribe directly** — use hooks so layering stays `Component → hook → store`.
|
|
250
|
+
* `getState()` is intended for non-React code paths (engine, adapters, mutations) that must write or read the graph synchronously.
|
|
251
|
+
*/
|
|
252
|
+
declare const useGraphStore: zustand.UseBoundStore<Omit<Omit<zustand.StoreApi<GraphState>, "subscribe"> & {
|
|
253
|
+
subscribe: {
|
|
254
|
+
(listener: (selectedState: GraphState, previousSelectedState: GraphState) => void): () => void;
|
|
255
|
+
<U>(selector: (state: GraphState) => U, listener: (selectedState: U, previousSelectedState: U) => void, options?: {
|
|
256
|
+
equalityFn?: ((a: U, b: U) => boolean) | undefined;
|
|
257
|
+
fireImmediately?: boolean;
|
|
258
|
+
} | undefined): () => void;
|
|
259
|
+
};
|
|
260
|
+
}, "setState"> & {
|
|
261
|
+
setState(nextStateOrUpdater: GraphState | Partial<GraphState> | ((state: {
|
|
262
|
+
entities: {
|
|
263
|
+
[x: string]: {
|
|
264
|
+
[x: string]: {
|
|
265
|
+
[x: string]: unknown;
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
patches: {
|
|
270
|
+
[x: string]: {
|
|
271
|
+
[x: string]: {
|
|
272
|
+
[x: string]: unknown;
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
entityStates: {
|
|
277
|
+
[x: string]: {
|
|
278
|
+
isFetching: boolean;
|
|
279
|
+
lastFetched: number | null;
|
|
280
|
+
error: string | null;
|
|
281
|
+
stale: boolean;
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
syncMetadata: {
|
|
285
|
+
[x: string]: {
|
|
286
|
+
synced: boolean;
|
|
287
|
+
origin: SyncOrigin;
|
|
288
|
+
updatedAt: number | null;
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
lists: {
|
|
292
|
+
[x: string]: {
|
|
293
|
+
ids: string[];
|
|
294
|
+
total: number | null;
|
|
295
|
+
nextCursor: string | null;
|
|
296
|
+
prevCursor: string | null;
|
|
297
|
+
hasNextPage: boolean;
|
|
298
|
+
hasPrevPage: boolean;
|
|
299
|
+
isFetching: boolean;
|
|
300
|
+
isFetchingMore: boolean;
|
|
301
|
+
error: string | null;
|
|
302
|
+
lastError: {
|
|
303
|
+
kind: "terminal";
|
|
304
|
+
status?: number | undefined;
|
|
305
|
+
name: string;
|
|
306
|
+
message: string;
|
|
307
|
+
stack?: string | undefined;
|
|
308
|
+
cause?: unknown;
|
|
309
|
+
} | {
|
|
310
|
+
kind: "transient";
|
|
311
|
+
status?: number | undefined;
|
|
312
|
+
name: string;
|
|
313
|
+
message: string;
|
|
314
|
+
stack?: string | undefined;
|
|
315
|
+
cause?: unknown;
|
|
316
|
+
} | null;
|
|
317
|
+
lastFetched: number | null;
|
|
318
|
+
stale: boolean;
|
|
319
|
+
currentPage: number | null;
|
|
320
|
+
pageSize: number | null;
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
upsertEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => void;
|
|
324
|
+
upsertEntities: (type: EntityType, entries: Array<{
|
|
325
|
+
id: EntityId;
|
|
326
|
+
data: Record<string, unknown>;
|
|
327
|
+
}>) => void;
|
|
328
|
+
replaceEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => void;
|
|
329
|
+
removeEntity: (type: EntityType, id: EntityId) => void;
|
|
330
|
+
patchEntity: (type: EntityType, id: EntityId, patch: Record<string, unknown>) => void;
|
|
331
|
+
unpatchEntity: (type: EntityType, id: EntityId, keys: string[]) => void;
|
|
332
|
+
clearPatch: (type: EntityType, id: EntityId) => void;
|
|
333
|
+
setEntityFetching: (type: EntityType, id: EntityId, fetching: boolean) => void;
|
|
334
|
+
setEntityError: (type: EntityType, id: EntityId, error: string | null) => void;
|
|
335
|
+
setEntityFetched: (type: EntityType, id: EntityId) => void;
|
|
336
|
+
setEntityStale: (type: EntityType, id: EntityId, stale: boolean) => void;
|
|
337
|
+
setEntitySyncMetadata: (type: EntityType, id: EntityId, metadata: Partial<EntitySyncMetadata>) => void;
|
|
338
|
+
clearEntitySyncMetadata: (type: EntityType, id: EntityId) => void;
|
|
339
|
+
setListResult: (key: QueryKey, ids: EntityId[], meta: Partial<Omit<ListState, "ids" | "isFetching" | "isFetchingMore" | "error" | "stale">>) => void;
|
|
340
|
+
appendListResult: (key: QueryKey, ids: EntityId[], meta: Partial<Omit<ListState, "ids" | "isFetching" | "isFetchingMore" | "error" | "stale">>) => void;
|
|
341
|
+
prependListResult: (key: QueryKey, ids: EntityId[], meta?: Partial<ListState>) => void;
|
|
342
|
+
removeIdFromAllLists: (type: EntityType, id: EntityId) => void;
|
|
343
|
+
insertIdInList: (key: QueryKey, id: EntityId, position: "start" | "end" | number) => void;
|
|
344
|
+
setListFetching: (key: QueryKey, fetching: boolean) => void;
|
|
345
|
+
setListFetchingMore: (key: QueryKey, fetchingMore: boolean) => void;
|
|
346
|
+
setListError: (key: QueryKey, error: string | null, typed?: EntityError | null) => void;
|
|
347
|
+
setListStale: (key: QueryKey, stale: boolean) => void;
|
|
348
|
+
invalidateEntity: (type: EntityType, id?: EntityId) => void;
|
|
349
|
+
invalidateLists: (matcher: string | ((key: QueryKey) => boolean)) => void;
|
|
350
|
+
invalidateType: (type: EntityType) => void;
|
|
351
|
+
readEntity: <T = Record<string, unknown>>(type: EntityType, id: EntityId) => T | null;
|
|
352
|
+
readEntitySnapshot: <T = Record<string, unknown>>(type: EntityType, id: EntityId) => EntitySnapshot<T & Record<string, unknown>> | null;
|
|
353
|
+
}) => void), shouldReplace?: false): void;
|
|
354
|
+
setState(nextStateOrUpdater: GraphState | ((state: {
|
|
355
|
+
entities: {
|
|
356
|
+
[x: string]: {
|
|
357
|
+
[x: string]: {
|
|
358
|
+
[x: string]: unknown;
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
patches: {
|
|
363
|
+
[x: string]: {
|
|
364
|
+
[x: string]: {
|
|
365
|
+
[x: string]: unknown;
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
entityStates: {
|
|
370
|
+
[x: string]: {
|
|
371
|
+
isFetching: boolean;
|
|
372
|
+
lastFetched: number | null;
|
|
373
|
+
error: string | null;
|
|
374
|
+
stale: boolean;
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
syncMetadata: {
|
|
378
|
+
[x: string]: {
|
|
379
|
+
synced: boolean;
|
|
380
|
+
origin: SyncOrigin;
|
|
381
|
+
updatedAt: number | null;
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
lists: {
|
|
385
|
+
[x: string]: {
|
|
386
|
+
ids: string[];
|
|
387
|
+
total: number | null;
|
|
388
|
+
nextCursor: string | null;
|
|
389
|
+
prevCursor: string | null;
|
|
390
|
+
hasNextPage: boolean;
|
|
391
|
+
hasPrevPage: boolean;
|
|
392
|
+
isFetching: boolean;
|
|
393
|
+
isFetchingMore: boolean;
|
|
394
|
+
error: string | null;
|
|
395
|
+
lastError: {
|
|
396
|
+
kind: "terminal";
|
|
397
|
+
status?: number | undefined;
|
|
398
|
+
name: string;
|
|
399
|
+
message: string;
|
|
400
|
+
stack?: string | undefined;
|
|
401
|
+
cause?: unknown;
|
|
402
|
+
} | {
|
|
403
|
+
kind: "transient";
|
|
404
|
+
status?: number | undefined;
|
|
405
|
+
name: string;
|
|
406
|
+
message: string;
|
|
407
|
+
stack?: string | undefined;
|
|
408
|
+
cause?: unknown;
|
|
409
|
+
} | null;
|
|
410
|
+
lastFetched: number | null;
|
|
411
|
+
stale: boolean;
|
|
412
|
+
currentPage: number | null;
|
|
413
|
+
pageSize: number | null;
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
upsertEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => void;
|
|
417
|
+
upsertEntities: (type: EntityType, entries: Array<{
|
|
418
|
+
id: EntityId;
|
|
419
|
+
data: Record<string, unknown>;
|
|
420
|
+
}>) => void;
|
|
421
|
+
replaceEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => void;
|
|
422
|
+
removeEntity: (type: EntityType, id: EntityId) => void;
|
|
423
|
+
patchEntity: (type: EntityType, id: EntityId, patch: Record<string, unknown>) => void;
|
|
424
|
+
unpatchEntity: (type: EntityType, id: EntityId, keys: string[]) => void;
|
|
425
|
+
clearPatch: (type: EntityType, id: EntityId) => void;
|
|
426
|
+
setEntityFetching: (type: EntityType, id: EntityId, fetching: boolean) => void;
|
|
427
|
+
setEntityError: (type: EntityType, id: EntityId, error: string | null) => void;
|
|
428
|
+
setEntityFetched: (type: EntityType, id: EntityId) => void;
|
|
429
|
+
setEntityStale: (type: EntityType, id: EntityId, stale: boolean) => void;
|
|
430
|
+
setEntitySyncMetadata: (type: EntityType, id: EntityId, metadata: Partial<EntitySyncMetadata>) => void;
|
|
431
|
+
clearEntitySyncMetadata: (type: EntityType, id: EntityId) => void;
|
|
432
|
+
setListResult: (key: QueryKey, ids: EntityId[], meta: Partial<Omit<ListState, "ids" | "isFetching" | "isFetchingMore" | "error" | "stale">>) => void;
|
|
433
|
+
appendListResult: (key: QueryKey, ids: EntityId[], meta: Partial<Omit<ListState, "ids" | "isFetching" | "isFetchingMore" | "error" | "stale">>) => void;
|
|
434
|
+
prependListResult: (key: QueryKey, ids: EntityId[], meta?: Partial<ListState>) => void;
|
|
435
|
+
removeIdFromAllLists: (type: EntityType, id: EntityId) => void;
|
|
436
|
+
insertIdInList: (key: QueryKey, id: EntityId, position: "start" | "end" | number) => void;
|
|
437
|
+
setListFetching: (key: QueryKey, fetching: boolean) => void;
|
|
438
|
+
setListFetchingMore: (key: QueryKey, fetchingMore: boolean) => void;
|
|
439
|
+
setListError: (key: QueryKey, error: string | null, typed?: EntityError | null) => void;
|
|
440
|
+
setListStale: (key: QueryKey, stale: boolean) => void;
|
|
441
|
+
invalidateEntity: (type: EntityType, id?: EntityId) => void;
|
|
442
|
+
invalidateLists: (matcher: string | ((key: QueryKey) => boolean)) => void;
|
|
443
|
+
invalidateType: (type: EntityType) => void;
|
|
444
|
+
readEntity: <T = Record<string, unknown>>(type: EntityType, id: EntityId) => T | null;
|
|
445
|
+
readEntitySnapshot: <T = Record<string, unknown>>(type: EntityType, id: EntityId) => EntitySnapshot<T & Record<string, unknown>> | null;
|
|
446
|
+
}) => void), shouldReplace: true): void;
|
|
447
|
+
}>;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Compiles a {@link FilterSpec} into a Prisma `where` object (plain JSON-serializable shape).
|
|
451
|
+
*
|
|
452
|
+
* Operator mapping matches common Prisma filter APIs: `eq` → `equals`, string ops use `mode: "insensitive"`,
|
|
453
|
+
* `nin` → `notIn`, `arrayContains` → `has`, `isNull` uses `null` / `{ not: null }` per `value`, and `isNotNull` → `{ not: null }`.
|
|
454
|
+
* Unsupported ops (`between`, `arrayOverlaps`, `matches`, `custom`) are omitted from the result.
|
|
455
|
+
*
|
|
456
|
+
* Top-level clause arrays are combined with `AND`. {@link FilterGroup} uses `AND` / `OR` to match `group.logic`.
|
|
457
|
+
*/
|
|
458
|
+
declare function toPrismaWhere(filter: FilterSpec): Record<string, unknown>;
|
|
459
|
+
/**
|
|
460
|
+
* Compiles a {@link SortSpec} into Prisma `orderBy` form: `[{ fieldName: "asc" | "desc" }, …]`.
|
|
461
|
+
* `nulls` and `comparator` on {@link SortClause} are ignored (local-only); extend callers if your Prisma version supports null ordering.
|
|
462
|
+
*/
|
|
463
|
+
declare function toPrismaOrderBy(sort: SortSpec): Record<string, string>[];
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Transport-agnostic comparison operators. Same spec can compile to REST, SQL, GraphQL, or local JS (`evaluator`).
|
|
467
|
+
* `custom` opts out of automatic serialization — use for predicates only the client can evaluate.
|
|
468
|
+
*/
|
|
469
|
+
type FilterOperator = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "contains" | "startsWith" | "endsWith" | "isNull" | "isNotNull" | "between" | "arrayContains" | "arrayOverlaps" | "matches" | "custom";
|
|
470
|
+
/** Atomic filter: field path, operator, optional value, and optional JS predicate for `custom`. */
|
|
471
|
+
interface FilterClause {
|
|
472
|
+
field: string;
|
|
473
|
+
op: FilterOperator;
|
|
474
|
+
value?: unknown;
|
|
475
|
+
predicate?: (fieldValue: unknown, entity: Record<string, unknown>) => boolean;
|
|
476
|
+
}
|
|
477
|
+
type FilterLogic = "and" | "or";
|
|
478
|
+
/** Nested boolean group so you can express `(A AND B) OR C` without losing structure when compiling to backends. */
|
|
479
|
+
interface FilterGroup {
|
|
480
|
+
logic: FilterLogic;
|
|
481
|
+
clauses: Array<FilterClause | FilterGroup>;
|
|
482
|
+
}
|
|
483
|
+
/** Top-level filter: flat AND list of clauses, or a recursive `FilterGroup`. */
|
|
484
|
+
type FilterSpec = FilterGroup | FilterClause[];
|
|
485
|
+
type SortDirection = "asc" | "desc";
|
|
486
|
+
/** Single sort key with optional null ordering and custom comparator for local sort parity with remote semantics. */
|
|
487
|
+
interface SortClause {
|
|
488
|
+
field: string;
|
|
489
|
+
direction: SortDirection;
|
|
490
|
+
nulls?: "first" | "last";
|
|
491
|
+
comparator?: (a: unknown, b: unknown) => number;
|
|
492
|
+
}
|
|
493
|
+
/** Ordered multi-key sort (stable application in `compareEntities`). */
|
|
494
|
+
type SortSpec = SortClause[];
|
|
495
|
+
/**
|
|
496
|
+
* Everything `useEntityView` needs to describe a virtualized collection: filters, sorts, and simple multi-field search.
|
|
497
|
+
* One descriptor can drive local evaluation, remote query compilation, or hybrid mode.
|
|
498
|
+
*/
|
|
499
|
+
interface ViewDescriptor {
|
|
500
|
+
filter?: FilterSpec;
|
|
501
|
+
sort?: SortSpec;
|
|
502
|
+
search?: {
|
|
503
|
+
query: string;
|
|
504
|
+
fields: string[];
|
|
505
|
+
minChars?: number;
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* How complete local graph data is relative to the view: **local** (all in memory), **remote** (server must filter/sort), **hybrid** (show local fast + remote reconcile).
|
|
510
|
+
*/
|
|
511
|
+
type CompletenessMode = "local" | "remote" | "hybrid";
|
|
512
|
+
/**
|
|
513
|
+
* Compile a view to flat REST query params (`sort`, `q`, and `field[op]=value` keys). Skips `custom` clauses — those cannot be expressed as strings.
|
|
514
|
+
*/
|
|
515
|
+
declare function toRestParams(view: ViewDescriptor): Record<string, string>;
|
|
516
|
+
/**
|
|
517
|
+
* Compile a view to parameterized SQL fragments for server-side filtering/sorting. Unknown ops become `TRUE` — validate or restrict ops at the edge.
|
|
518
|
+
*/
|
|
519
|
+
declare function toSQLClauses(view: ViewDescriptor): {
|
|
520
|
+
where: string;
|
|
521
|
+
orderBy: string;
|
|
522
|
+
params: unknown[];
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Produce a GraphQL-variable-shaped object from a view (Hasura/Postgraphile-style `_op` maps). Intended as a starting point — wire to your actual schema.
|
|
526
|
+
*/
|
|
527
|
+
declare function toGraphQLVariables(view: ViewDescriptor): {
|
|
528
|
+
where?: Record<string, unknown>;
|
|
529
|
+
orderBy?: Array<Record<string, unknown>>;
|
|
530
|
+
search?: string;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Normalize nested `FilterGroup` trees to a flat clause list for compilers that only understand atomic predicates.
|
|
535
|
+
*/
|
|
536
|
+
declare function flattenClauses(filter: FilterSpec): FilterClause[];
|
|
537
|
+
/** True if any clause requires client-side `predicate` logic — forces local/hybrid evaluation paths that cannot be pushed to generic REST/SQL. */
|
|
538
|
+
declare function hasCustomPredicates(filter: FilterSpec): boolean;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 2.0 transport contract.
|
|
542
|
+
*
|
|
543
|
+
* Why this exists: in 1.x each call site of `useEntityList` /
|
|
544
|
+
* `useEntityView` passed its own `fetch` closure + `normalize`
|
|
545
|
+
* callback. That leaked the transport contract to every consumer and
|
|
546
|
+
* was the structural cause of the Quick Stats trap — each widget could
|
|
547
|
+
* (and did) reinvent the same retry-loop bug.
|
|
548
|
+
*
|
|
549
|
+
* In 2.0 the transport is registered ONCE per entity type at app boot
|
|
550
|
+
* via `registerEntityTransport(type, transport)`. Hooks
|
|
551
|
+
* (`useEntities`, `useEntityQuery`) look up the registered transport
|
|
552
|
+
* by entity type — consumers never pass a fetcher.
|
|
553
|
+
*
|
|
554
|
+
* Transports are pluggable: a built-in `makeRestTransport` covers
|
|
555
|
+
* PostgREST / Supabase; future helpers can cover GraphQL, PGlite-direct,
|
|
556
|
+
* mock-in-memory, etc. The contract is intentionally tiny.
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Transport-agnostic list query passed by `useEntities` / `useEntityQuery`
|
|
561
|
+
* down to the registered transport.
|
|
562
|
+
*
|
|
563
|
+
* The transport is responsible for compiling this into the
|
|
564
|
+
* transport-specific wire format (HTTP query string, GraphQL variables,
|
|
565
|
+
* SQL WHERE, in-memory predicate, etc.).
|
|
566
|
+
*/
|
|
567
|
+
interface ListQuery {
|
|
568
|
+
/** Optional filter; transport-agnostic, see `FilterSpec`. */
|
|
569
|
+
filter?: FilterSpec | null;
|
|
570
|
+
/** Optional sort spec; transport-agnostic. */
|
|
571
|
+
sort?: SortSpec | null;
|
|
572
|
+
/** Free-text search string; transport decides which fields to match. */
|
|
573
|
+
search?: string;
|
|
574
|
+
/** Page size hint. Transport may cap or ignore. */
|
|
575
|
+
limit?: number;
|
|
576
|
+
/** Opaque cursor for pagination (cursor or numeric offset). */
|
|
577
|
+
cursor?: string | number | null;
|
|
578
|
+
/** Aborts the request when fired. Transports MUST honour this. */
|
|
579
|
+
signal?: AbortSignal;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Page of results returned by `EntityTransport.list`.
|
|
583
|
+
*
|
|
584
|
+
* `total` is `null` when the backend doesn't report it cheaply
|
|
585
|
+
* (consumers should treat as "unknown" rather than "zero").
|
|
586
|
+
*
|
|
587
|
+
* `nextCursor` is `null`/`undefined` when there are no more pages.
|
|
588
|
+
*/
|
|
589
|
+
interface ListResult<T> {
|
|
590
|
+
rows: T[];
|
|
591
|
+
total: number | null;
|
|
592
|
+
nextCursor?: string | number | null;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Operation flavour for a realtime change event.
|
|
596
|
+
*/
|
|
597
|
+
type ChangeOp = "insert" | "update" | "delete";
|
|
598
|
+
/**
|
|
599
|
+
* Realtime change event emitted by a transport's `subscribe`.
|
|
600
|
+
*
|
|
601
|
+
* For `delete` only `id` is meaningful; `row` MAY be omitted.
|
|
602
|
+
*/
|
|
603
|
+
interface ChangeEvent<T> {
|
|
604
|
+
op: ChangeOp;
|
|
605
|
+
id: string;
|
|
606
|
+
row?: T;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The full per-entity transport contract.
|
|
610
|
+
*
|
|
611
|
+
* One transport per entity type, registered at app boot. The hooks
|
|
612
|
+
* (`useEntities`, `useEntityQuery`) look it up via `getEntityTransport(type)`.
|
|
613
|
+
*
|
|
614
|
+
* Required: `identify`, `authoritative`, `list`.
|
|
615
|
+
* Optional: `get` (for single-row fetches), `subscribe` (for realtime),
|
|
616
|
+
* `staleTime` (hint for SWR).
|
|
617
|
+
*/
|
|
618
|
+
interface EntityTransport<T extends object> {
|
|
619
|
+
/**
|
|
620
|
+
* Derive the stable string id of a row. Used by the engine to write
|
|
621
|
+
* into the entity graph under the right id key.
|
|
622
|
+
*/
|
|
623
|
+
identify: (row: T) => string;
|
|
624
|
+
/**
|
|
625
|
+
* `true` when the local copy is considered authoritative (e.g. PGlite
|
|
626
|
+
* + Electric Tier-A sync) — consumers can render from cache without
|
|
627
|
+
* a remote round-trip on every mount.
|
|
628
|
+
*
|
|
629
|
+
* `false` when the remote is authoritative (Tier-B REST hybrid) — the
|
|
630
|
+
* cache is a hint, not the truth.
|
|
631
|
+
*/
|
|
632
|
+
authoritative: boolean;
|
|
633
|
+
/**
|
|
634
|
+
* Stale-time hint in ms. The engine SWR logic refetches when
|
|
635
|
+
* `Date.now() - lastFetched > staleTime`. Omit to inherit the engine
|
|
636
|
+
* default (30s).
|
|
637
|
+
*/
|
|
638
|
+
staleTime?: number;
|
|
639
|
+
/** Fetch a list. The signal in `q.signal` MUST be honoured. */
|
|
640
|
+
list: (q: ListQuery) => Promise<ListResult<T>>;
|
|
641
|
+
/** Fetch a single row by id. Optional. */
|
|
642
|
+
get?: (id: string, signal?: AbortSignal) => Promise<T | null>;
|
|
643
|
+
/**
|
|
644
|
+
* Subscribe to realtime change events for this entity type. Optional.
|
|
645
|
+
*
|
|
646
|
+
* Called by `useEntities` / `useEntityQuery` exactly once per
|
|
647
|
+
* subscriber-bearing key. The returned function MUST tear down all
|
|
648
|
+
* underlying resources (sockets, channels, intervals).
|
|
649
|
+
*/
|
|
650
|
+
subscribe?: (onChange: (ev: ChangeEvent<T>) => void) => () => void;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Process-global registry of `EntityTransport` instances by entity type.
|
|
655
|
+
*
|
|
656
|
+
* Why global state: every consumer of `useEntities("Client")` must
|
|
657
|
+
* resolve the SAME transport. Threading a registry through React
|
|
658
|
+
* context would require every test, every isolated story, every
|
|
659
|
+
* non-React caller (engine internals) to plumb the context — a tax
|
|
660
|
+
* that buys nothing. The 1.x library already used module-global state
|
|
661
|
+
* for the `useGraphStore` Zustand store; this follows the same shape.
|
|
662
|
+
*
|
|
663
|
+
* The registry is mutable: re-registering a type silently replaces the
|
|
664
|
+
* prior transport (tests rely on this; production code should only
|
|
665
|
+
* register once at app boot).
|
|
666
|
+
*/
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Register the transport for an entity type. Called once per type at
|
|
670
|
+
* app boot, typically from `src/shared/db/entity-transports.ts` in the
|
|
671
|
+
* consuming application.
|
|
672
|
+
*
|
|
673
|
+
* Re-registering REPLACES the prior transport — useful for tests, a
|
|
674
|
+
* footgun for production code. If you find yourself re-registering at
|
|
675
|
+
* runtime, you almost certainly want a different design.
|
|
676
|
+
*/
|
|
677
|
+
declare function registerEntityTransport<T extends object>(type: EntityType, transport: EntityTransport<T>): void;
|
|
678
|
+
/**
|
|
679
|
+
* Look up the transport for an entity type. Throws if none is
|
|
680
|
+
* registered — there is no "default" transport, by design.
|
|
681
|
+
*
|
|
682
|
+
* The cast to `EntityTransport<T>` is the API ergonomic
|
|
683
|
+
* compromise that lets call sites stay generic; the registry stores
|
|
684
|
+
* `EntityTransport<object>` because TypeScript can't otherwise unify
|
|
685
|
+
* heterogeneous rows in one map.
|
|
686
|
+
*/
|
|
687
|
+
declare function getEntityTransport<T extends object>(type: EntityType): EntityTransport<T>;
|
|
688
|
+
/**
|
|
689
|
+
* Test-only: clear ALL registered transports. Production code should
|
|
690
|
+
* never need this; tests use it to reset between specs.
|
|
691
|
+
*/
|
|
692
|
+
declare function __resetEntityTransports(): void;
|
|
693
|
+
/**
|
|
694
|
+
* Introspection: how many transports are currently registered.
|
|
695
|
+
* Useful for asserting in tests that boot wiring ran exactly once.
|
|
696
|
+
*/
|
|
697
|
+
declare function getRegisteredEntityTypes(): EntityType[];
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* `makeRestTransport` — built-in helper for the most common case: a
|
|
701
|
+
* Supabase / PostgREST backend reached through a `SupabaseClient`-like
|
|
702
|
+
* object.
|
|
703
|
+
*
|
|
704
|
+
* Why a helper (not a class): consumers stay declarative —
|
|
705
|
+
* registerEntityTransport("Client",
|
|
706
|
+
* makeRestTransport<Client>({ supabase, table: "client" }));
|
|
707
|
+
* — and the library never imports `@supabase/supabase-js` directly
|
|
708
|
+
* (peer-dep-free).
|
|
709
|
+
*
|
|
710
|
+
* The transport:
|
|
711
|
+
* - Compiles `ListQuery` (filter / sort / search / cursor / limit)
|
|
712
|
+
* into a PostgREST query via the supabase-js builder.
|
|
713
|
+
* - Maps HTTP status to `TerminalError` (4xx) or `TransientError`
|
|
714
|
+
* (5xx / network).
|
|
715
|
+
* - Honours `AbortSignal` via supabase-js `.abortSignal(signal)`.
|
|
716
|
+
* - Returns `total` from PostgREST `count: 'exact'` when requested.
|
|
717
|
+
*/
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Minimal slice of the `@supabase/supabase-js` `SupabaseClient` we use.
|
|
721
|
+
* Declared structurally so the library does not need to import the
|
|
722
|
+
* package as a dependency (peer-dep-free).
|
|
723
|
+
*/
|
|
724
|
+
interface SupabaseLike {
|
|
725
|
+
from: (table: string) => SupabaseQueryBuilderLike;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Subset of the supabase-js builder we exercise. Each call returns the
|
|
729
|
+
* same builder for chaining; `then`-able at the end resolves to
|
|
730
|
+
* `{ data, error, count, status }`.
|
|
731
|
+
*/
|
|
732
|
+
interface SupabaseQueryBuilderLike {
|
|
733
|
+
select: (columns: string, opts?: {
|
|
734
|
+
count?: "exact" | "planned" | "estimated";
|
|
735
|
+
head?: boolean;
|
|
736
|
+
}) => SupabaseQueryBuilderLike;
|
|
737
|
+
eq: (col: string, val: unknown) => SupabaseQueryBuilderLike;
|
|
738
|
+
neq: (col: string, val: unknown) => SupabaseQueryBuilderLike;
|
|
739
|
+
gt: (col: string, val: unknown) => SupabaseQueryBuilderLike;
|
|
740
|
+
gte: (col: string, val: unknown) => SupabaseQueryBuilderLike;
|
|
741
|
+
lt: (col: string, val: unknown) => SupabaseQueryBuilderLike;
|
|
742
|
+
lte: (col: string, val: unknown) => SupabaseQueryBuilderLike;
|
|
743
|
+
in: (col: string, vals: readonly unknown[]) => SupabaseQueryBuilderLike;
|
|
744
|
+
ilike: (col: string, pattern: string) => SupabaseQueryBuilderLike;
|
|
745
|
+
is: (col: string, val: null | boolean) => SupabaseQueryBuilderLike;
|
|
746
|
+
or: (filter: string) => SupabaseQueryBuilderLike;
|
|
747
|
+
order: (col: string, opts?: {
|
|
748
|
+
ascending?: boolean;
|
|
749
|
+
nullsFirst?: boolean;
|
|
750
|
+
}) => SupabaseQueryBuilderLike;
|
|
751
|
+
range: (from: number, to: number) => SupabaseQueryBuilderLike;
|
|
752
|
+
limit: (n: number) => SupabaseQueryBuilderLike;
|
|
753
|
+
abortSignal?: (signal: AbortSignal) => SupabaseQueryBuilderLike;
|
|
754
|
+
then: <TResult1 = unknown, TResult2 = never>(onfulfilled?: ((value: {
|
|
755
|
+
data: unknown;
|
|
756
|
+
error: unknown;
|
|
757
|
+
count?: number | null;
|
|
758
|
+
status?: number;
|
|
759
|
+
}) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null) => Promise<TResult1 | TResult2>;
|
|
760
|
+
}
|
|
761
|
+
/** Options for `makeRestTransport`. */
|
|
762
|
+
interface MakeRestTransportOptions<T extends object> {
|
|
763
|
+
/** A `@supabase/supabase-js` client (or any structurally-compatible thing). */
|
|
764
|
+
supabase: SupabaseLike;
|
|
765
|
+
/** PostgREST table name. */
|
|
766
|
+
table: string;
|
|
767
|
+
/** Columns to select (default `"*"`). */
|
|
768
|
+
select?: string;
|
|
769
|
+
/** Stable id derivation. Defaults to `(row) => String(row.id)`. */
|
|
770
|
+
identify?: (row: T) => string;
|
|
771
|
+
/** Whether the local cache is treated as authoritative. */
|
|
772
|
+
authoritative?: boolean;
|
|
773
|
+
/** Stale-time hint in ms. */
|
|
774
|
+
staleTime?: number;
|
|
775
|
+
/** Free-text search column list (compiled to `or=col.ilike.*query*,…`). */
|
|
776
|
+
searchColumns?: string[];
|
|
777
|
+
/** Default page size when `ListQuery.limit` is absent. */
|
|
778
|
+
defaultLimit?: number;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Build an `EntityTransport<T>` from a Supabase client + a table name.
|
|
782
|
+
*
|
|
783
|
+
* The returned transport:
|
|
784
|
+
* - Maps `ListQuery.filter` clauses to PostgREST operators (`eq`, `gt`,
|
|
785
|
+
* `in`, `ilike`, `is null`, etc.).
|
|
786
|
+
* - Compiles `ListQuery.search` into an `or=` clause across
|
|
787
|
+
* `searchColumns`.
|
|
788
|
+
* - Uses `count: 'exact'` on first page to surface `total`.
|
|
789
|
+
* - Throws `TerminalError` on 4xx (no retry); `TransientError` on 5xx
|
|
790
|
+
* or network failure.
|
|
791
|
+
* - Honours `signal` via `.abortSignal(signal)` (supabase-js >= 2.39).
|
|
792
|
+
*/
|
|
793
|
+
declare function makeRestTransport<T extends object>(opts: MakeRestTransportOptions<T>): EntityTransport<T>;
|
|
794
|
+
|
|
795
|
+
declare function getValueAtPath<T = unknown>(source: unknown, path: string): T | undefined;
|
|
796
|
+
declare function setValueAtPath<T extends object>(source: T, path: string, value: unknown): T;
|
|
797
|
+
declare function collectDirtyPaths(current: unknown, original: unknown, prefix?: string, acc?: Set<string>): Set<string>;
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* merge/types.ts — Pluggable conflict-resolution seam for the entity graph.
|
|
801
|
+
*
|
|
802
|
+
* Why a port (not a hard-wired engine):
|
|
803
|
+
* - The graph receives concurrent writes from multiple sources — realtime
|
|
804
|
+
* adapters (Flint, Supabase, Surreal), AG-UI agents, and optimistic local
|
|
805
|
+
* mutations. The default resolution is last-write-wins (LWW), matching the
|
|
806
|
+
* historical shallow-merge in `upsertEntity`.
|
|
807
|
+
* - Collaborative / agentic multi-writer scenarios need stronger guarantees
|
|
808
|
+
* (CRDT). But the *engine* (Loro vs Automerge vs Yjs) is a downstream
|
|
809
|
+
* decision — and the Flint fabric's own CRDT choice is still open. Encoding
|
|
810
|
+
* the decision as a swappable `MergeStrategy` keeps that open decision from
|
|
811
|
+
* blocking this layer.
|
|
812
|
+
*
|
|
813
|
+
* One strategy is registered per entity type (or a global default). The graph
|
|
814
|
+
* consults it at write time; absent a registration it falls back to LWW, so
|
|
815
|
+
* existing behavior is byte-for-byte preserved.
|
|
816
|
+
*/
|
|
817
|
+
|
|
818
|
+
/** Context handed to a {@link MergeStrategy} for one write. */
|
|
819
|
+
interface MergeContext {
|
|
820
|
+
/** Entity kind being written. */
|
|
821
|
+
type: EntityType;
|
|
822
|
+
/** Entity id being written. */
|
|
823
|
+
id: EntityId;
|
|
824
|
+
/** Provenance of the incoming write (server / client / optimistic). */
|
|
825
|
+
origin: SyncOrigin;
|
|
826
|
+
/** Wall-clock ms of the incoming write, when the caller knows it. */
|
|
827
|
+
updatedAt: number | null;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Resolves the next canonical entity value from the previous value and an
|
|
831
|
+
* incoming partial/full write.
|
|
832
|
+
*
|
|
833
|
+
* Contract:
|
|
834
|
+
* - MUST be pure (no side effects, no graph access) — the graph applies the
|
|
835
|
+
* returned value inside its Immer producer.
|
|
836
|
+
* - MUST return a NEW object; never mutate `prev` or `next` (immutability rule).
|
|
837
|
+
* - `prev` is `undefined` on first write for an id.
|
|
838
|
+
*
|
|
839
|
+
* @param prev - Current canonical value, or undefined if none exists yet.
|
|
840
|
+
* @param next - Incoming write (partial for upsert, full for replace).
|
|
841
|
+
* @param ctx - Type/id/origin/timestamp metadata for this write.
|
|
842
|
+
* @returns The merged canonical value to store.
|
|
843
|
+
*/
|
|
844
|
+
type MergeStrategy = (prev: Record<string, unknown> | undefined, next: Record<string, unknown>, ctx: MergeContext) => Record<string, unknown>;
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* merge/lww.ts — Last-write-wins default strategy.
|
|
848
|
+
*
|
|
849
|
+
* Semantics match the historical `upsertEntity` behavior exactly: shallow-merge
|
|
850
|
+
* the incoming write over the previous value. This is the fallback when no
|
|
851
|
+
* other strategy is registered, so registering nothing is a no-op change.
|
|
852
|
+
*/
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Shallow-merge `next` over `prev` (incoming keys win). Returns a new object.
|
|
856
|
+
* On first write (`prev` undefined) returns a shallow copy of `next`.
|
|
857
|
+
*/
|
|
858
|
+
declare const lwwStrategy: MergeStrategy;
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* merge/registry.ts — One MergeStrategy per entity type (+ optional global default).
|
|
862
|
+
*
|
|
863
|
+
* Lookup order at write time:
|
|
864
|
+
* 1. strategy registered for the exact `type`
|
|
865
|
+
* 2. global default strategy (if one was set)
|
|
866
|
+
* 3. built-in LWW (`lwwStrategy`)
|
|
867
|
+
*
|
|
868
|
+
* Step 3 guarantees that with zero registrations the graph behaves exactly as
|
|
869
|
+
* it did before this seam existed.
|
|
870
|
+
*/
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Register a {@link MergeStrategy} for one entity type. Overwrites any prior
|
|
874
|
+
* registration for that type.
|
|
875
|
+
*/
|
|
876
|
+
declare function registerMergeStrategy(type: string, strategy: MergeStrategy): void;
|
|
877
|
+
/**
|
|
878
|
+
* Set the fallback strategy used for any type without its own registration.
|
|
879
|
+
* Pass `null` to clear it (reverting the fallback to built-in LWW).
|
|
880
|
+
*/
|
|
881
|
+
declare function setDefaultMergeStrategy(strategy: MergeStrategy | null): void;
|
|
882
|
+
/**
|
|
883
|
+
* Resolve the effective strategy for a type: per-type → global default → LWW.
|
|
884
|
+
* Always returns a usable strategy (never null).
|
|
885
|
+
*/
|
|
886
|
+
declare function getMergeStrategy(type: string): MergeStrategy;
|
|
887
|
+
/** True if `type` (or a global default) has a non-LWW strategy registered. */
|
|
888
|
+
declare function hasMergeStrategy(type: string): boolean;
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* merge/loro.ts — Loro-backed CRDT MergeStrategy (reference implementation).
|
|
892
|
+
*
|
|
893
|
+
* Maps each entity (`type:id`) onto a Loro document whose root is an LWW-Map —
|
|
894
|
+
* a near-1:1 fit for the graph's `type:id:field` model. Concurrent field writes
|
|
895
|
+
* resolve via Loro's CRDT semantics rather than blind last-write-wins, so two
|
|
896
|
+
* writers updating *different* fields never clobber each other, and writers
|
|
897
|
+
* updating the *same* field resolve deterministically across replicas.
|
|
898
|
+
*
|
|
899
|
+
* `loro-crdt` is an OPTIONAL peer dependency. It is imported lazily so the core
|
|
900
|
+
* bundle stays `zustand + immer` only — nothing loads Loro unless an app
|
|
901
|
+
* actually registers `createLoroMergeStrategy()`.
|
|
902
|
+
*
|
|
903
|
+
* NOTE: `createLoroMergeStrategy` is async because it dynamically imports the
|
|
904
|
+
* engine. Await it once at boot, then register the resolved synchronous
|
|
905
|
+
* strategy. The returned strategy itself is pure+sync (the MergeStrategy
|
|
906
|
+
* contract), keeping per-write resolution off the async path.
|
|
907
|
+
*/
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Build a Loro-backed {@link MergeStrategy}. Resolves `loro-crdt` lazily; throws
|
|
911
|
+
* a clear error if the optional peer is not installed.
|
|
912
|
+
*
|
|
913
|
+
* @example
|
|
914
|
+
* ```ts
|
|
915
|
+
* const loro = await createLoroMergeStrategy();
|
|
916
|
+
* registerMergeStrategy("Document", loro);
|
|
917
|
+
* ```
|
|
918
|
+
*/
|
|
919
|
+
declare function createLoroMergeStrategy(): Promise<MergeStrategy>;
|
|
920
|
+
|
|
921
|
+
type GraphStore = ReturnType<typeof useGraphStore.getState>;
|
|
922
|
+
type GraphIncludeMap = Record<string, GraphIncludeRelation>;
|
|
923
|
+
type GraphIncludeRelation = {
|
|
924
|
+
type: EntityType;
|
|
925
|
+
via: {
|
|
926
|
+
kind: "field";
|
|
927
|
+
field: string;
|
|
928
|
+
};
|
|
929
|
+
include?: GraphIncludeMap;
|
|
930
|
+
} | {
|
|
931
|
+
type: EntityType;
|
|
932
|
+
via: {
|
|
933
|
+
kind: "array";
|
|
934
|
+
field: string;
|
|
935
|
+
};
|
|
936
|
+
include?: GraphIncludeMap;
|
|
937
|
+
} | {
|
|
938
|
+
type: EntityType;
|
|
939
|
+
via: {
|
|
940
|
+
kind: "list";
|
|
941
|
+
key: QueryKey | ((entity: Record<string, unknown>) => QueryKey | null | undefined);
|
|
942
|
+
};
|
|
943
|
+
include?: GraphIncludeMap;
|
|
944
|
+
} | {
|
|
945
|
+
type: EntityType;
|
|
946
|
+
via: {
|
|
947
|
+
kind: "resolver";
|
|
948
|
+
resolve: (entity: Record<string, unknown>, store: GraphStore) => EntityId | EntityId[] | null | undefined;
|
|
949
|
+
};
|
|
950
|
+
include?: GraphIncludeMap;
|
|
951
|
+
};
|
|
952
|
+
interface GraphQueryOptions<TEntity extends object> {
|
|
953
|
+
type: EntityType;
|
|
954
|
+
id?: EntityId;
|
|
955
|
+
ids?: EntityId[];
|
|
956
|
+
listKey?: QueryKey;
|
|
957
|
+
where?: (entity: EntitySnapshot<TEntity>) => boolean;
|
|
958
|
+
sort?: (a: EntitySnapshot<TEntity>, b: EntitySnapshot<TEntity>) => number;
|
|
959
|
+
include?: GraphIncludeMap;
|
|
960
|
+
select?: ((entity: Record<string, unknown>) => unknown) | string[];
|
|
961
|
+
}
|
|
962
|
+
type ProjectedRow = Record<string, unknown>;
|
|
963
|
+
declare function queryOnce<TEntity extends object>(opts: GraphQueryOptions<TEntity> & {
|
|
964
|
+
id: EntityId;
|
|
965
|
+
}): ProjectedRow | null;
|
|
966
|
+
declare function queryOnce<TEntity extends object>(opts: GraphQueryOptions<TEntity>): ProjectedRow[];
|
|
967
|
+
declare const selectGraph: typeof queryOnce;
|
|
968
|
+
|
|
969
|
+
interface GraphDataSnapshot {
|
|
970
|
+
entities: ReturnType<typeof useGraphStore.getState>["entities"];
|
|
971
|
+
patches: ReturnType<typeof useGraphStore.getState>["patches"];
|
|
972
|
+
entityStates: ReturnType<typeof useGraphStore.getState>["entityStates"];
|
|
973
|
+
syncMetadata: ReturnType<typeof useGraphStore.getState>["syncMetadata"];
|
|
974
|
+
lists: ReturnType<typeof useGraphStore.getState>["lists"];
|
|
975
|
+
}
|
|
976
|
+
interface GraphTransaction {
|
|
977
|
+
upsertEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => GraphTransaction;
|
|
978
|
+
replaceEntity: (type: EntityType, id: EntityId, data: Record<string, unknown>) => GraphTransaction;
|
|
979
|
+
removeEntity: (type: EntityType, id: EntityId) => GraphTransaction;
|
|
980
|
+
patchEntity: (type: EntityType, id: EntityId, patch: Record<string, unknown>) => GraphTransaction;
|
|
981
|
+
clearPatch: (type: EntityType, id: EntityId) => GraphTransaction;
|
|
982
|
+
insertIdInList: (key: QueryKey, id: EntityId, position: "start" | "end" | number) => GraphTransaction;
|
|
983
|
+
removeIdFromAllLists: (type: EntityType, id: EntityId) => GraphTransaction;
|
|
984
|
+
setEntitySyncMetadata: (type: EntityType, id: EntityId, metadata: Partial<EntitySyncMetadata>) => GraphTransaction;
|
|
985
|
+
markEntityPending: (type: EntityType, id: EntityId, origin?: SyncOrigin) => GraphTransaction;
|
|
986
|
+
markEntitySynced: (type: EntityType, id: EntityId, origin?: SyncOrigin) => GraphTransaction;
|
|
987
|
+
commit: () => void;
|
|
988
|
+
rollback: () => void;
|
|
989
|
+
snapshot: () => GraphDataSnapshot;
|
|
990
|
+
}
|
|
991
|
+
interface GraphActionOptions<TInput, TResult> {
|
|
992
|
+
key?: string;
|
|
993
|
+
optimistic?: (tx: GraphTransaction, input: TInput) => void;
|
|
994
|
+
run: (tx: GraphTransaction, input: TInput) => Promise<TResult> | TResult;
|
|
995
|
+
onSuccess?: (result: TResult, input: TInput, tx: GraphTransaction) => void;
|
|
996
|
+
onError?: (error: Error, input: TInput) => void;
|
|
997
|
+
}
|
|
998
|
+
interface GraphActionRecord$1 {
|
|
999
|
+
id: string;
|
|
1000
|
+
key: string;
|
|
1001
|
+
input: unknown;
|
|
1002
|
+
enqueuedAt: string;
|
|
1003
|
+
}
|
|
1004
|
+
type GraphActionEvent = {
|
|
1005
|
+
type: "enqueued";
|
|
1006
|
+
record: GraphActionRecord$1;
|
|
1007
|
+
} | {
|
|
1008
|
+
type: "settled";
|
|
1009
|
+
record: GraphActionRecord$1;
|
|
1010
|
+
};
|
|
1011
|
+
declare function createGraphTransaction(): GraphTransaction;
|
|
1012
|
+
declare function createGraphAction<TInput, TResult>(opts: GraphActionOptions<TInput, TResult>): (input: TInput) => Promise<TResult>;
|
|
1013
|
+
|
|
1014
|
+
interface GraphEffectEvent<T> {
|
|
1015
|
+
key: string;
|
|
1016
|
+
value: T;
|
|
1017
|
+
previousValue: T;
|
|
1018
|
+
}
|
|
1019
|
+
interface GraphEffectOptions<T> {
|
|
1020
|
+
query: () => T[] | T | null;
|
|
1021
|
+
getKey?: (value: T, index: number) => string;
|
|
1022
|
+
skipInitial?: boolean;
|
|
1023
|
+
isEqual?: (previousValue: T, nextValue: T) => boolean;
|
|
1024
|
+
onEnter?: (event: {
|
|
1025
|
+
key: string;
|
|
1026
|
+
value: T;
|
|
1027
|
+
}) => void;
|
|
1028
|
+
onUpdate?: (event: GraphEffectEvent<T>) => void;
|
|
1029
|
+
onExit?: (event: {
|
|
1030
|
+
key: string;
|
|
1031
|
+
previousValue: T;
|
|
1032
|
+
}) => void;
|
|
1033
|
+
}
|
|
1034
|
+
interface GraphEffectHandle {
|
|
1035
|
+
dispose: () => void;
|
|
1036
|
+
}
|
|
1037
|
+
declare function createGraphEffect<T>(opts: GraphEffectOptions<T>): GraphEffectHandle;
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* schema.ts (core) — framework-agnostic JSON-schema registry + snapshot/markdown
|
|
1041
|
+
* helpers. The React field renderers/editors (MarkdownFieldRenderer,
|
|
1042
|
+
* MarkdownFieldEditor, useSchemaEntityFields, buildEntityFieldsFromSchema which
|
|
1043
|
+
* depends on the UI FieldDescriptor type) live in the react binding's schema.tsx,
|
|
1044
|
+
* built on these primitives.
|
|
1045
|
+
*/
|
|
1046
|
+
interface JsonSchemaObject {
|
|
1047
|
+
$id?: string;
|
|
1048
|
+
title?: string;
|
|
1049
|
+
description?: string;
|
|
1050
|
+
type?: string | string[];
|
|
1051
|
+
format?: string;
|
|
1052
|
+
enum?: readonly unknown[];
|
|
1053
|
+
default?: unknown;
|
|
1054
|
+
properties?: Record<string, JsonSchemaObject>;
|
|
1055
|
+
items?: JsonSchemaObject;
|
|
1056
|
+
required?: string[];
|
|
1057
|
+
additionalProperties?: boolean | JsonSchemaObject;
|
|
1058
|
+
["x-a2ui-component"]?: string;
|
|
1059
|
+
["x-display-order"]?: number;
|
|
1060
|
+
["x-field-type"]?: string;
|
|
1061
|
+
["x-hidden"]?: boolean;
|
|
1062
|
+
}
|
|
1063
|
+
interface EntityJsonSchemaConfig {
|
|
1064
|
+
entityType: string;
|
|
1065
|
+
schemaId?: string;
|
|
1066
|
+
field?: string;
|
|
1067
|
+
version?: string;
|
|
1068
|
+
source?: "static" | "runtime" | "ai";
|
|
1069
|
+
schema: JsonSchemaObject;
|
|
1070
|
+
}
|
|
1071
|
+
interface GetEntityJsonSchemaOptions {
|
|
1072
|
+
entityType: string;
|
|
1073
|
+
schemaId?: string;
|
|
1074
|
+
field?: string;
|
|
1075
|
+
}
|
|
1076
|
+
interface GraphSnapshotWithSchemasOptions {
|
|
1077
|
+
scope: string;
|
|
1078
|
+
data: unknown;
|
|
1079
|
+
schemas: Array<EntityJsonSchemaConfig | null | undefined>;
|
|
1080
|
+
pretty?: boolean;
|
|
1081
|
+
}
|
|
1082
|
+
declare function registerEntityJsonSchema(config: EntityJsonSchemaConfig): void;
|
|
1083
|
+
declare function registerRuntimeSchema(config: EntityJsonSchemaConfig): void;
|
|
1084
|
+
declare function getEntityJsonSchema(opts: GetEntityJsonSchemaOptions): EntityJsonSchemaConfig | null;
|
|
1085
|
+
declare function exportGraphSnapshotWithSchemas(opts: GraphSnapshotWithSchemasOptions): string;
|
|
1086
|
+
declare function escapeHtml(input: string): string;
|
|
1087
|
+
declare function renderMarkdownToHtml(value: string): string;
|
|
1088
|
+
/** Infer a coarse field-type string from a JSON-schema node (UI-agnostic). */
|
|
1089
|
+
declare function inferSchemaFieldType(schema: JsonSchemaObject): string;
|
|
1090
|
+
declare function registryKey(entityType: string, field?: string, schemaId?: string): string;
|
|
1091
|
+
declare function humanizeFieldName(value: string): string;
|
|
1092
|
+
|
|
1093
|
+
interface GraphSnapshotExportOptions {
|
|
1094
|
+
scope: string;
|
|
1095
|
+
data: unknown;
|
|
1096
|
+
pretty?: boolean;
|
|
1097
|
+
}
|
|
1098
|
+
interface GraphToolContext {
|
|
1099
|
+
store: ReturnType<typeof useGraphStore.getState>;
|
|
1100
|
+
queryOnce: typeof queryOnce;
|
|
1101
|
+
exportGraphSnapshot: typeof exportGraphSnapshot;
|
|
1102
|
+
}
|
|
1103
|
+
interface SchemaGraphToolContext extends GraphToolContext {
|
|
1104
|
+
getEntityJsonSchema: typeof getEntityJsonSchema;
|
|
1105
|
+
exportGraphSnapshotWithSchemas: typeof exportGraphSnapshotWithSchemas;
|
|
1106
|
+
}
|
|
1107
|
+
declare function exportGraphSnapshot(opts: GraphSnapshotExportOptions): string;
|
|
1108
|
+
declare function createGraphTool<TInput, TResult>(handler: (input: TInput, ctx: GraphToolContext) => Promise<TResult> | TResult): (input: TInput) => TResult | Promise<TResult>;
|
|
1109
|
+
declare function createSchemaGraphTool<TInput, TResult>(handler: (input: TInput, ctx: SchemaGraphToolContext) => Promise<TResult> | TResult): (input: TInput) => TResult | Promise<TResult>;
|
|
1110
|
+
|
|
1111
|
+
interface GraphPersistenceAdapter {
|
|
1112
|
+
get: (key: string) => Promise<string | null> | string | null;
|
|
1113
|
+
set: (key: string, value: string) => Promise<void> | void;
|
|
1114
|
+
remove?: (key: string) => Promise<void> | void;
|
|
1115
|
+
}
|
|
1116
|
+
interface GraphActionRecord {
|
|
1117
|
+
id: string;
|
|
1118
|
+
key: string;
|
|
1119
|
+
input: unknown;
|
|
1120
|
+
enqueuedAt: string;
|
|
1121
|
+
}
|
|
1122
|
+
interface GraphSyncStatus {
|
|
1123
|
+
phase: "idle" | "hydrating" | "syncing" | "ready" | "offline" | "error";
|
|
1124
|
+
isOnline: boolean;
|
|
1125
|
+
isSynced: boolean;
|
|
1126
|
+
pendingActions: number;
|
|
1127
|
+
lastHydratedAt: string | null;
|
|
1128
|
+
lastPersistedAt: string | null;
|
|
1129
|
+
storageKey: string | null;
|
|
1130
|
+
error: string | null;
|
|
1131
|
+
}
|
|
1132
|
+
interface GraphSnapshotPayload {
|
|
1133
|
+
version: 1;
|
|
1134
|
+
snapshot: {
|
|
1135
|
+
entities: ReturnType<typeof useGraphStore.getState>["entities"];
|
|
1136
|
+
patches: ReturnType<typeof useGraphStore.getState>["patches"];
|
|
1137
|
+
entityStates: ReturnType<typeof useGraphStore.getState>["entityStates"];
|
|
1138
|
+
syncMetadata: ReturnType<typeof useGraphStore.getState>["syncMetadata"];
|
|
1139
|
+
lists: ReturnType<typeof useGraphStore.getState>["lists"];
|
|
1140
|
+
};
|
|
1141
|
+
pendingActions: GraphActionRecord[];
|
|
1142
|
+
}
|
|
1143
|
+
interface PersistGraphToStorageOptions {
|
|
1144
|
+
storage: GraphPersistenceAdapter;
|
|
1145
|
+
key: string;
|
|
1146
|
+
pendingActions?: GraphActionRecord[];
|
|
1147
|
+
}
|
|
1148
|
+
interface HydrateGraphFromStorageOptions {
|
|
1149
|
+
storage: GraphPersistenceAdapter;
|
|
1150
|
+
key: string;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Retry-with-backoff policy for replaying pending offline actions.
|
|
1154
|
+
*
|
|
1155
|
+
* The replay loop tracks per-action attempt counts. After `maxAttempts`
|
|
1156
|
+
* failed attempts the action is considered "poisoned" — it is removed from
|
|
1157
|
+
* the in-memory pending queue (so it won't block other actions) and the
|
|
1158
|
+
* optional `poisonHandler` is invoked. Consumers typically log the action,
|
|
1159
|
+
* surface a UI prompt, or persist it to a dead-letter store.
|
|
1160
|
+
*
|
|
1161
|
+
* Defaults: 5 attempts, starting at 500ms, doubling up to 30s, with
|
|
1162
|
+
* "equal" jitter (random in `[delay/2, delay]`).
|
|
1163
|
+
*/
|
|
1164
|
+
interface ReplayRetryPolicy {
|
|
1165
|
+
maxAttempts?: number;
|
|
1166
|
+
initialDelayMs?: number;
|
|
1167
|
+
maxDelayMs?: number;
|
|
1168
|
+
backoffFactor?: number;
|
|
1169
|
+
jitter?: "full" | "equal" | "none";
|
|
1170
|
+
poisonHandler?: (action: GraphActionRecord, error: unknown) => void | Promise<void>;
|
|
1171
|
+
}
|
|
1172
|
+
interface StartLocalFirstGraphOptions {
|
|
1173
|
+
storage: GraphPersistenceAdapter;
|
|
1174
|
+
key?: string;
|
|
1175
|
+
replayPendingActions?: boolean;
|
|
1176
|
+
onlineSource?: {
|
|
1177
|
+
getIsOnline: () => boolean;
|
|
1178
|
+
subscribe: (listener: (online: boolean) => void) => () => void;
|
|
1179
|
+
};
|
|
1180
|
+
persistDebounceMs?: number;
|
|
1181
|
+
/** Retry-with-backoff policy for offline action replay. */
|
|
1182
|
+
retryPolicy?: ReplayRetryPolicy;
|
|
1183
|
+
}
|
|
1184
|
+
interface LocalFirstGraphRuntime {
|
|
1185
|
+
ready: Promise<void>;
|
|
1186
|
+
dispose: () => void;
|
|
1187
|
+
persistNow: () => Promise<void>;
|
|
1188
|
+
hydrate: () => Promise<Awaited<ReturnType<typeof hydrateGraphFromStorage>>>;
|
|
1189
|
+
getStatus: () => GraphSyncStatus;
|
|
1190
|
+
}
|
|
1191
|
+
declare function useGraphSyncStatus(): GraphSyncStatus;
|
|
1192
|
+
declare function persistGraphToStorage(opts: PersistGraphToStorageOptions): Promise<{
|
|
1193
|
+
ok: true;
|
|
1194
|
+
key: string;
|
|
1195
|
+
bytes: number;
|
|
1196
|
+
persistedAt: string;
|
|
1197
|
+
}>;
|
|
1198
|
+
declare function hydrateGraphFromStorage(opts: HydrateGraphFromStorageOptions): Promise<{
|
|
1199
|
+
ok: false;
|
|
1200
|
+
key: string;
|
|
1201
|
+
hydratedAt: null;
|
|
1202
|
+
entityCounts: {};
|
|
1203
|
+
error: string;
|
|
1204
|
+
pendingActions?: undefined;
|
|
1205
|
+
} | {
|
|
1206
|
+
ok: true;
|
|
1207
|
+
key: string;
|
|
1208
|
+
hydratedAt: string;
|
|
1209
|
+
entityCounts: {
|
|
1210
|
+
[k: string]: number;
|
|
1211
|
+
};
|
|
1212
|
+
pendingActions: GraphActionRecord[];
|
|
1213
|
+
error?: undefined;
|
|
1214
|
+
}>;
|
|
1215
|
+
declare function startLocalFirstGraph(opts: StartLocalFirstGraphOptions): LocalFirstGraphRuntime;
|
|
1216
|
+
interface ResolvedRetryPolicy {
|
|
1217
|
+
maxAttempts: number;
|
|
1218
|
+
initialDelayMs: number;
|
|
1219
|
+
maxDelayMs: number;
|
|
1220
|
+
backoffFactor: number;
|
|
1221
|
+
jitter: "full" | "equal" | "none";
|
|
1222
|
+
poisonHandler?: (action: GraphActionRecord, error: unknown) => void | Promise<void>;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Attempt to replay an action up to `maxAttempts` times. On exhaustion the
|
|
1226
|
+
* action goes to "poison" — the optional handler is invoked and the function
|
|
1227
|
+
* resolves. Exposed for unit testing.
|
|
1228
|
+
*/
|
|
1229
|
+
declare function replayActionWithRetry(action: GraphActionRecord, policy: ResolvedRetryPolicy): Promise<{
|
|
1230
|
+
ok: true;
|
|
1231
|
+
} | {
|
|
1232
|
+
ok: false;
|
|
1233
|
+
poisoned: true;
|
|
1234
|
+
error: unknown;
|
|
1235
|
+
}>;
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Process-wide defaults for stale times, retries, and background revalidation.
|
|
1239
|
+
* Keeps hook signatures small: `useEntity` / `useEntityList` merge these with per-query overrides.
|
|
1240
|
+
*/
|
|
1241
|
+
interface EngineOptions {
|
|
1242
|
+
defaultStaleTime?: number;
|
|
1243
|
+
/** Max age (`Date.now() - lastFetched`) for evicting entities with zero subscribers during GC. */
|
|
1244
|
+
defaultGcTime?: number;
|
|
1245
|
+
/** Interval between GC passes when the collector is active (default 60s). */
|
|
1246
|
+
gcInterval?: number;
|
|
1247
|
+
maxRetries?: number;
|
|
1248
|
+
retryBaseDelay?: number;
|
|
1249
|
+
revalidateOnFocus?: boolean;
|
|
1250
|
+
revalidateOnReconnect?: boolean;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Declarative **instruction** for loading one entity: wire transport (`fetch`), normalization, and graph writes.
|
|
1254
|
+
* Hooks pass this to `fetchEntity`; the graph remains source of truth after success.
|
|
1255
|
+
*/
|
|
1256
|
+
interface EntityQueryOptions<TRaw, TEntity extends object> {
|
|
1257
|
+
type: EntityType;
|
|
1258
|
+
id: EntityId | null | undefined;
|
|
1259
|
+
fetch: (id: EntityId) => Promise<TRaw>;
|
|
1260
|
+
normalize: (raw: TRaw) => TEntity;
|
|
1261
|
+
idField?: string;
|
|
1262
|
+
sideEffects?: (raw: TRaw, store: typeof useGraphStore) => void;
|
|
1263
|
+
staleTime?: number;
|
|
1264
|
+
enabled?: boolean;
|
|
1265
|
+
onSuccess?: (entity: TEntity) => void;
|
|
1266
|
+
onError?: (error: Error) => void;
|
|
1267
|
+
}
|
|
1268
|
+
/** Cursor/page knobs passed through to list `fetch` implementations (REST, GraphQL, etc.). */
|
|
1269
|
+
interface ListFetchParams {
|
|
1270
|
+
cursor?: string;
|
|
1271
|
+
page?: number;
|
|
1272
|
+
pageSize?: number;
|
|
1273
|
+
params?: Record<string, unknown>;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Normalized list page shape from any backend. Items are mapped through `normalize` into `{ id, data }` upserts — the list stores ids only.
|
|
1277
|
+
*/
|
|
1278
|
+
interface ListResponse<T> {
|
|
1279
|
+
items: T[];
|
|
1280
|
+
total?: number | null;
|
|
1281
|
+
nextCursor?: string | null;
|
|
1282
|
+
prevCursor?: string;
|
|
1283
|
+
hasNextPage?: boolean;
|
|
1284
|
+
hasPrevPage?: boolean;
|
|
1285
|
+
page?: number;
|
|
1286
|
+
pageSize?: number;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Declarative **instruction** for a collection query: stable `queryKey`, fetcher, and per-row normalization into the graph.
|
|
1290
|
+
* `mode` controls whether a fetch replaces ids or appends (infinite scroll) when used with load-more.
|
|
1291
|
+
*/
|
|
1292
|
+
interface ListQueryOptions<TRaw, TEntity extends object> {
|
|
1293
|
+
type: EntityType;
|
|
1294
|
+
queryKey: unknown[];
|
|
1295
|
+
/**
|
|
1296
|
+
* Inline fetcher (deprecated 1.x REST form). Optional in 2.0: pure graph
|
|
1297
|
+
* subscriptions for Tier-A PGlite/Electric entities pass no fetcher — the
|
|
1298
|
+
* graph is hydrated out-of-band and the hook only joins ids → entities.
|
|
1299
|
+
* When omitted, `fetchList` is a no-op and the hook is read-only.
|
|
1300
|
+
*/
|
|
1301
|
+
fetch?: (params: ListFetchParams) => Promise<ListResponse<TRaw>>;
|
|
1302
|
+
/** Row normalizer. Optional alongside `fetch` (graph-only callers omit both). */
|
|
1303
|
+
normalize?: (raw: TRaw) => {
|
|
1304
|
+
id: EntityId;
|
|
1305
|
+
data: TEntity;
|
|
1306
|
+
};
|
|
1307
|
+
sideEffects?: (items: TRaw[], store: typeof useGraphStore) => void;
|
|
1308
|
+
mode?: "replace" | "append";
|
|
1309
|
+
staleTime?: number;
|
|
1310
|
+
enabled?: boolean;
|
|
1311
|
+
onSuccess?: (result: ListResponse<TRaw>) => void;
|
|
1312
|
+
onError?: (error: Error) => void;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Deterministic string key for list queries so object order in nested keys does not create duplicate cache entries.
|
|
1316
|
+
* @param key - Hook-provided query key array (serializable values)
|
|
1317
|
+
*/
|
|
1318
|
+
declare function serializeKey(key: unknown[]): string;
|
|
1319
|
+
/**
|
|
1320
|
+
* Collapse concurrent identical requests into one Promise (prevents stampedes when many components mount the same entity/list).
|
|
1321
|
+
* @param key - Logical dedupe key (e.g. `type:id` or serialized list key)
|
|
1322
|
+
* @param fn - Async work that performs the fetch + graph writes
|
|
1323
|
+
*/
|
|
1324
|
+
declare function dedupe<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
1325
|
+
/**
|
|
1326
|
+
* Ref-count graph interest for a subscriber key (`${type}:${id}`). Background revalidation skips keys with zero subscribers.
|
|
1327
|
+
* @returns Opaque token required for `unregisterSubscriber`
|
|
1328
|
+
*/
|
|
1329
|
+
declare function registerSubscriber(key: string): symbol;
|
|
1330
|
+
/** Pair with `registerSubscriber` on unmount so idle entities stop refetching. */
|
|
1331
|
+
declare function unregisterSubscriber(key: string, token: symbol): void;
|
|
1332
|
+
/**
|
|
1333
|
+
* Subscribe to ref-count changes for `registerSubscriber` / `unregisterSubscriber`.
|
|
1334
|
+
* Used by `useGraphDevTools` so subscriber totals update without graph mutations.
|
|
1335
|
+
*/
|
|
1336
|
+
declare function subscribeSubscriberStats(onChange: () => void): () => boolean;
|
|
1337
|
+
/** Total active entity subscriber tokens across all keys (sum of ref-counts). */
|
|
1338
|
+
declare function getActiveSubscriberCount(): number;
|
|
1339
|
+
/**
|
|
1340
|
+
* Discriminated union of devtools events.
|
|
1341
|
+
*
|
|
1342
|
+
* - `upsert`, `patch`, `clearPatch` are emitted from `graph-actions.ts` ops.
|
|
1343
|
+
* - `unpatch` and `list` are forward-compatible kinds reserved for future
|
|
1344
|
+
* instrumentation (W5+ event bus / W6+ panel). Consumers MUST handle them
|
|
1345
|
+
* in exhaustive `switch`/`if` chains today, even though no op-site emits
|
|
1346
|
+
* them in this change.
|
|
1347
|
+
*/
|
|
1348
|
+
type DevtoolsEvent = {
|
|
1349
|
+
kind: "upsert";
|
|
1350
|
+
type: EntityType;
|
|
1351
|
+
id: EntityId;
|
|
1352
|
+
data: Record<string, unknown>;
|
|
1353
|
+
at: string;
|
|
1354
|
+
} | {
|
|
1355
|
+
kind: "patch";
|
|
1356
|
+
type: EntityType;
|
|
1357
|
+
id: EntityId;
|
|
1358
|
+
patch: Record<string, unknown>;
|
|
1359
|
+
at: string;
|
|
1360
|
+
} | {
|
|
1361
|
+
kind: "unpatch";
|
|
1362
|
+
type: EntityType;
|
|
1363
|
+
id: EntityId;
|
|
1364
|
+
keys: string[];
|
|
1365
|
+
at: string;
|
|
1366
|
+
} | {
|
|
1367
|
+
kind: "clearPatch";
|
|
1368
|
+
type: EntityType;
|
|
1369
|
+
id: EntityId;
|
|
1370
|
+
at: string;
|
|
1371
|
+
} | {
|
|
1372
|
+
kind: "list";
|
|
1373
|
+
key: string;
|
|
1374
|
+
idCount: number;
|
|
1375
|
+
at: string;
|
|
1376
|
+
};
|
|
1377
|
+
type DevtoolsListener = (event: DevtoolsEvent) => void;
|
|
1378
|
+
/**
|
|
1379
|
+
* Subscribe to the devtools event stream.
|
|
1380
|
+
*
|
|
1381
|
+
* - Returns an `UnsubscribeFn` that removes this listener.
|
|
1382
|
+
* - Idempotent: subscribing the same function reference twice tracks it once
|
|
1383
|
+
* (Set semantics).
|
|
1384
|
+
* - Throwing listeners do NOT block delivery to other listeners; the error is
|
|
1385
|
+
* logged via `console.warn` and dispatch continues.
|
|
1386
|
+
* - Re-entrancy safe: listeners that subscribe/unsubscribe during dispatch
|
|
1387
|
+
* affect future events only, not the in-flight one.
|
|
1388
|
+
*/
|
|
1389
|
+
declare function subscribeDevtoolsEvent(cb: DevtoolsListener): () => void;
|
|
1390
|
+
/**
|
|
1391
|
+
* Stops the periodic garbage-collection timer started by `startGarbageCollector` / `configureEngine`.
|
|
1392
|
+
*/
|
|
1393
|
+
declare function stopGarbageCollector(): void;
|
|
1394
|
+
/**
|
|
1395
|
+
* Starts periodic garbage collection using current `getEngineOptions().gcInterval`.
|
|
1396
|
+
* Stops any previous interval first. No-ops during SSR (`window` is undefined) or without `setInterval`.
|
|
1397
|
+
* @returns Disposer that stops this collector (equivalent to `stopGarbageCollector`).
|
|
1398
|
+
*/
|
|
1399
|
+
declare function startGarbageCollector(): () => void;
|
|
1400
|
+
/** Override global engine behavior (typically once at app bootstrap). Restarts GC with merged options. */
|
|
1401
|
+
declare function configureEngine(opts: EngineOptions): void;
|
|
1402
|
+
/** Read merged engine options; hooks use this for default `staleTime` and retry behavior. */
|
|
1403
|
+
declare function getEngineOptions(): Required<EngineOptions>;
|
|
1404
|
+
/**
|
|
1405
|
+
* Run a single-entity fetch with dedupe, retries, normalization, and graph updates.
|
|
1406
|
+
* Call from hooks/adapters — not from presentational components.
|
|
1407
|
+
*/
|
|
1408
|
+
declare function fetchEntity<TRaw, TEntity extends object>(opts: EntityQueryOptions<TRaw, TEntity>, engineOpts: Required<EngineOptions>): Promise<void>;
|
|
1409
|
+
/**
|
|
1410
|
+
* Fetch a list page: upserts all rows, writes ids to the list key, supports append mode for pagination.
|
|
1411
|
+
* @param isLoadMore - When true, uses a separate dedupe key and `appendListResult` / `setListFetchingMore`.
|
|
1412
|
+
*/
|
|
1413
|
+
declare function fetchList<TRaw, TEntity extends object>(opts: ListQueryOptions<TRaw, TEntity>, params: ListFetchParams, engineOpts: Required<EngineOptions>, isLoadMore?: boolean): Promise<void>;
|
|
1414
|
+
/**
|
|
1415
|
+
* Opt-in window listeners that mark **subscribed** entities stale on focus/reconnect so hooks can SWR-refetch.
|
|
1416
|
+
* Idempotent; safe for SSR (no-ops without `window`). Pair with `configureEngine` flags.
|
|
1417
|
+
*/
|
|
1418
|
+
declare function attachGlobalListeners(): void;
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* devtools-event-bus — W5 (seim-em-explorer-event-bus-registry)
|
|
1422
|
+
*
|
|
1423
|
+
* Ring-buffered, fan-out event bus on top of the W3 `subscribeDevtoolsEvent` tap.
|
|
1424
|
+
* Adds: replay-on-subscribe, microtask-level burst coalescing, multi-store registry.
|
|
1425
|
+
*/
|
|
1426
|
+
|
|
1427
|
+
interface DevtoolsEventBusOptions {
|
|
1428
|
+
/** Maximum events retained in the ring buffer. Default: 500. */
|
|
1429
|
+
bufferSize?: number;
|
|
1430
|
+
/**
|
|
1431
|
+
* Events 1..N (N = threshold) are dispatched individually as they arrive.
|
|
1432
|
+
* Events N+1.. accumulate until the end of the microtask, then are coalesced
|
|
1433
|
+
* into one synthetic `kind:"list"` summary event.
|
|
1434
|
+
* Default: 10. Set to 0 to disable coalescing entirely.
|
|
1435
|
+
*/
|
|
1436
|
+
coalesceBurstThreshold?: number;
|
|
1437
|
+
}
|
|
1438
|
+
interface DevtoolsEventBus {
|
|
1439
|
+
/** Subscribe to live events. Replays current buffer synchronously before returning. */
|
|
1440
|
+
subscribe(cb: (event: DevtoolsEvent) => void): () => void;
|
|
1441
|
+
/** Snapshot of buffered events in chronological order (oldest first). */
|
|
1442
|
+
getBuffer(): readonly DevtoolsEvent[];
|
|
1443
|
+
/** Force-dispatch any pending coalesced burst synchronously. */
|
|
1444
|
+
flush(): void;
|
|
1445
|
+
/** Release engine-tap, clear all subscribers, cascade inactive to registry. */
|
|
1446
|
+
destroy(): void;
|
|
1447
|
+
/**
|
|
1448
|
+
* Inject a synthetic `DevtoolsEvent` directly into the bus, bypassing the
|
|
1449
|
+
* engine tap. Used by the Chrome MV3 extension panel to feed events arriving
|
|
1450
|
+
* over the port-messaging bridge into the local bus instance.
|
|
1451
|
+
*/
|
|
1452
|
+
inject(event: DevtoolsEvent): void;
|
|
1453
|
+
}
|
|
1454
|
+
/** Matches the signature of `subscribeDevtoolsEvent` — used for external sources. */
|
|
1455
|
+
type DevtoolsSourceFn = (cb: (event: DevtoolsEvent) => void) => () => void;
|
|
1456
|
+
interface RegisteredStore {
|
|
1457
|
+
name: string;
|
|
1458
|
+
active: boolean;
|
|
1459
|
+
}
|
|
1460
|
+
/** @internal Test-only. NOT re-exported from index.ts. */
|
|
1461
|
+
declare function __resetStoreRegistry(): void;
|
|
1462
|
+
declare function createDevtoolsEventBus(opts?: DevtoolsEventBusOptions): DevtoolsEventBus;
|
|
1463
|
+
/**
|
|
1464
|
+
* Wire an additional event source into an existing `DevtoolsEventBus`.
|
|
1465
|
+
* Events from `source` flow through the bus's ring buffer, coalescing, and fan-out.
|
|
1466
|
+
* Throws if `name` is already registered and active.
|
|
1467
|
+
*/
|
|
1468
|
+
declare function registerStore(bus: DevtoolsEventBus, source: DevtoolsSourceFn, name: string): () => void;
|
|
1469
|
+
/** Returns a snapshot of all registered stores (active + inactive) in registration order. */
|
|
1470
|
+
declare function getRegisteredStores(): ReadonlyArray<RegisteredStore>;
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* devtools-time-travel.ts — G4 parity: true time-travel for the entity graph.
|
|
1474
|
+
*
|
|
1475
|
+
* The Timeline tab (C5) inspected a read-only history log. This adds the
|
|
1476
|
+
* defining Redux-DevTools capability the gap analysis called out: **rewind the
|
|
1477
|
+
* LIVE graph to a prior recorded state and replay forward.**
|
|
1478
|
+
*
|
|
1479
|
+
* Model:
|
|
1480
|
+
* - `recordGraphSnapshot()` captures the canonical graph data (entities, patches,
|
|
1481
|
+
* entityStates, syncMetadata, lists) into a bounded ring (default 50).
|
|
1482
|
+
* - `restoreGraphSnapshot(index)` writes a captured snapshot back into the live
|
|
1483
|
+
* Zustand store — the graph (and every subscriber/view) reverts to that state.
|
|
1484
|
+
* - `getTimeTravelState()` exposes the ring + current cursor for the devtools UI.
|
|
1485
|
+
*
|
|
1486
|
+
* Snapshots are deep-cloned on capture and on restore so restoring never aliases
|
|
1487
|
+
* ring contents into the live store (a later mutation must not corrupt history).
|
|
1488
|
+
*
|
|
1489
|
+
* This is debug-time machinery: it is intended to run behind the EntityExplorer,
|
|
1490
|
+
* not in production data paths.
|
|
1491
|
+
*/
|
|
1492
|
+
|
|
1493
|
+
type GraphSlice = Pick<ReturnType<typeof useGraphStore.getState>, "entities" | "patches" | "entityStates" | "syncMetadata" | "lists">;
|
|
1494
|
+
interface TimeTravelSnapshot {
|
|
1495
|
+
/** Monotonic id (capture order). */
|
|
1496
|
+
seq: number;
|
|
1497
|
+
/** Capture timestamp (ms). */
|
|
1498
|
+
at: number;
|
|
1499
|
+
/** Optional label (e.g. the action that produced this state). */
|
|
1500
|
+
label?: string;
|
|
1501
|
+
data: GraphSlice;
|
|
1502
|
+
}
|
|
1503
|
+
interface TimeTravelState {
|
|
1504
|
+
snapshots: ReadonlyArray<Omit<TimeTravelSnapshot, "data">>;
|
|
1505
|
+
/** Index currently restored, or null if live (at head). */
|
|
1506
|
+
cursor: number | null;
|
|
1507
|
+
capacity: number;
|
|
1508
|
+
}
|
|
1509
|
+
/** Configure ring capacity (number of retained snapshots). */
|
|
1510
|
+
declare function configureTimeTravel(opts: {
|
|
1511
|
+
capacity?: number;
|
|
1512
|
+
}): void;
|
|
1513
|
+
/** Capture the current live graph state into the ring. Returns the snapshot seq. */
|
|
1514
|
+
declare function recordGraphSnapshot(label?: string): number;
|
|
1515
|
+
/**
|
|
1516
|
+
* Restore the live graph to the snapshot at ring `index` (0-based into the
|
|
1517
|
+
* current ring). The Zustand store is overwritten with a deep clone of the
|
|
1518
|
+
* captured slice, so all subscribers re-render at that historical state.
|
|
1519
|
+
* Returns true if restored.
|
|
1520
|
+
*/
|
|
1521
|
+
declare function restoreGraphSnapshot(index: number): boolean;
|
|
1522
|
+
/** Restore by capture seq (stable across ring eviction within capacity). */
|
|
1523
|
+
declare function restoreGraphSnapshotBySeq(seq: number): boolean;
|
|
1524
|
+
/** Step the cursor by `delta` (negative = back in time) and restore. */
|
|
1525
|
+
declare function stepTimeTravel(delta: number): boolean;
|
|
1526
|
+
/** Snapshot metadata + cursor for the devtools UI (no heavy data payloads). */
|
|
1527
|
+
declare function getTimeTravelState(): TimeTravelState;
|
|
1528
|
+
/** Subscribe to ring/cursor changes (for useSyncExternalStore in the UI). */
|
|
1529
|
+
declare function subscribeTimeTravel(cb: () => void): () => void;
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Evaluate `FilterSpec` against one in-memory entity — mirrors remote semantics as closely as plain JS allows.
|
|
1533
|
+
* Use for **local** and **hybrid** `useEntityView` paths so UI filtering matches what users expect from the declarative spec.
|
|
1534
|
+
*/
|
|
1535
|
+
declare function matchesFilter(entity: Record<string, unknown>, filter: FilterSpec): boolean;
|
|
1536
|
+
/**
|
|
1537
|
+
* Case-insensitive substring match across configured string fields; empty query is a no-op pass.
|
|
1538
|
+
* Keeps quick search consistent between client-only and debounced remote `q` params.
|
|
1539
|
+
*/
|
|
1540
|
+
declare function matchesSearch(entity: Record<string, unknown>, query: string, fields: string[]): boolean;
|
|
1541
|
+
/**
|
|
1542
|
+
* Multi-key comparator implementing `SortSpec` (null ordering, optional custom comparators, locale-aware string fallback).
|
|
1543
|
+
* Shared by local sorting and binary insertion for realtime updates.
|
|
1544
|
+
*/
|
|
1545
|
+
declare function compareEntities(a: Record<string, unknown>, b: Record<string, unknown>, sort: SortSpec): number;
|
|
1546
|
+
/**
|
|
1547
|
+
* Binary-search insertion index so realtime inserts land in sorted order without resorting the full id list (O(log n) per insert).
|
|
1548
|
+
* Skips missing `getEntity` rows by advancing low — callers should ensure entities exist or tolerate append-like behavior.
|
|
1549
|
+
*/
|
|
1550
|
+
declare function findInsertionIndex(entity: Record<string, unknown>, sortedIds: string[], getEntity: (id: string) => Record<string, unknown> | null, sort: SortSpec): number;
|
|
1551
|
+
/**
|
|
1552
|
+
* Pure list projection: map ids → entities, drop missing, filter/sort/search, return **ids** in display order.
|
|
1553
|
+
* Bridges stored id lists with on-the-fly view descriptors without duplicating entity payloads.
|
|
1554
|
+
*/
|
|
1555
|
+
declare function applyView(ids: string[], getEntity: (id: string) => Record<string, unknown> | null, filter?: FilterSpec | null, sort?: SortSpec | null, search?: {
|
|
1556
|
+
query: string;
|
|
1557
|
+
fields: string[];
|
|
1558
|
+
} | null): string[];
|
|
1559
|
+
/**
|
|
1560
|
+
* Heuristic for whether the graph likely holds **all** rows for a list key (enables local-only filtering/sorting in `useEntityView`).
|
|
1561
|
+
* `total` and `hasNextPage` come from list metadata written by fetchers.
|
|
1562
|
+
*/
|
|
1563
|
+
declare function checkCompleteness(loadedCount: number, total: number | null, hasNextPage: boolean): {
|
|
1564
|
+
isComplete: boolean;
|
|
1565
|
+
reason: string;
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* view/incremental.ts — G3 parity: incremental view maintenance.
|
|
1570
|
+
*
|
|
1571
|
+
* The baseline `applyView` re-filters + re-sorts the entire id list on every
|
|
1572
|
+
* change — O(n) per update. For large lists this is the gap vs TanStack DB's
|
|
1573
|
+
* differential-dataflow live queries.
|
|
1574
|
+
*
|
|
1575
|
+
* This maintains a derived, sorted result and updates it for a SINGLE entity
|
|
1576
|
+
* change in O(log n): binary-search the entity's current position, decide
|
|
1577
|
+
* membership via the filter, and insert / remove / reposition. No full
|
|
1578
|
+
* re-derivation. Result is byte-identical to `applyView` for the same inputs
|
|
1579
|
+
* (correctness parity is asserted in tests).
|
|
1580
|
+
*
|
|
1581
|
+
* Design choice: a hand-rolled dirty-tracking index rather than adopting d2ts
|
|
1582
|
+
* (0.1.x, pre-production) — same acceptance bar (sub-linear single-row update),
|
|
1583
|
+
* far lower dependency risk. The differential engine can replace the internals
|
|
1584
|
+
* later behind this same class without an API change.
|
|
1585
|
+
*/
|
|
1586
|
+
|
|
1587
|
+
interface IncrementalViewOptions {
|
|
1588
|
+
filter?: FilterSpec | null;
|
|
1589
|
+
sort?: SortSpec | null;
|
|
1590
|
+
search?: {
|
|
1591
|
+
query: string;
|
|
1592
|
+
fields: string[];
|
|
1593
|
+
} | null;
|
|
1594
|
+
/** Resolve an entity by id from the graph (or null if absent). */
|
|
1595
|
+
getEntity: (id: string) => Record<string, unknown> | null;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* A live, sorted view over a set of entity ids that updates incrementally.
|
|
1599
|
+
*
|
|
1600
|
+
* - `seed(ids)` builds the initial result (one O(n log n) pass).
|
|
1601
|
+
* - `applyChange(id)` updates the result for one changed/added/removed entity
|
|
1602
|
+
* in O(log n) comparisons (+ array splice).
|
|
1603
|
+
* - `result()` returns the current ordered id list (stable reference until a
|
|
1604
|
+
* change mutates it).
|
|
1605
|
+
*/
|
|
1606
|
+
declare class IncrementalView {
|
|
1607
|
+
private readonly opts;
|
|
1608
|
+
private ids;
|
|
1609
|
+
private readonly members;
|
|
1610
|
+
constructor(opts: IncrementalViewOptions);
|
|
1611
|
+
/** Whether an entity qualifies for this view (filter + search). */
|
|
1612
|
+
private qualifies;
|
|
1613
|
+
/** Comparator over ids using the configured sort (stable, id tiebreak). */
|
|
1614
|
+
private cmp;
|
|
1615
|
+
/** Binary-search the insertion index for `id` among current `ids`. */
|
|
1616
|
+
private insertionIndex;
|
|
1617
|
+
/** Build the initial sorted result from a full id set. */
|
|
1618
|
+
seed(ids: string[]): void;
|
|
1619
|
+
/**
|
|
1620
|
+
* Update the view for ONE entity that changed/was added/removed.
|
|
1621
|
+
* Returns true if the result order changed.
|
|
1622
|
+
*
|
|
1623
|
+
* O(log n) to locate; the array splice is O(n) memory-move but performs no
|
|
1624
|
+
* filter/sort over the whole set — the differentiator from full re-derivation.
|
|
1625
|
+
*/
|
|
1626
|
+
applyChange(id: string): boolean;
|
|
1627
|
+
/** Current ordered id list. */
|
|
1628
|
+
result(): string[];
|
|
1629
|
+
/** Current member count. */
|
|
1630
|
+
size(): number;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* FK edge: this entity points at one parent row. Used to invalidate parent aggregates and optional list keys when the FK changes.
|
|
1635
|
+
*/
|
|
1636
|
+
interface BelongsToRelation {
|
|
1637
|
+
cardinality: "belongsTo";
|
|
1638
|
+
foreignKey: string;
|
|
1639
|
+
targetType: EntityType;
|
|
1640
|
+
invalidateTargetLists?: string[];
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Inverse collection: children carry `foreignKey` pointing here; `listKeyPrefix` builds the child list query key for a given parent id.
|
|
1644
|
+
*/
|
|
1645
|
+
interface HasManyRelation {
|
|
1646
|
+
cardinality: "hasMany";
|
|
1647
|
+
targetType: EntityType;
|
|
1648
|
+
foreignKey: string;
|
|
1649
|
+
listKeyPrefix: (parentId: EntityId) => unknown[];
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Join-style relation stored as an id array on this entity; invalidates partner lists derived via `listKeyPrefix` for each touched id.
|
|
1653
|
+
*/
|
|
1654
|
+
interface ManyToManyRelation {
|
|
1655
|
+
cardinality: "manyToMany";
|
|
1656
|
+
targetType: EntityType;
|
|
1657
|
+
localArrayField?: string;
|
|
1658
|
+
listKeyPrefix: (thisId: EntityId) => unknown[];
|
|
1659
|
+
}
|
|
1660
|
+
type RelationDescriptor = BelongsToRelation | HasManyRelation | ManyToManyRelation;
|
|
1661
|
+
/**
|
|
1662
|
+
* Declarative relation metadata for one `EntityType`: optional named relations and list key prefixes to invalidate on any mutation.
|
|
1663
|
+
*/
|
|
1664
|
+
interface EntitySchema {
|
|
1665
|
+
type: EntityType;
|
|
1666
|
+
relations?: Record<string, RelationDescriptor>;
|
|
1667
|
+
globalListKeys?: string[];
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Snapshot diff passed to cascade rules after CRUD: compare `previous` vs `next` to find FK moves, array membership changes, etc.
|
|
1671
|
+
*/
|
|
1672
|
+
interface CascadeContext {
|
|
1673
|
+
type: EntityType;
|
|
1674
|
+
id: EntityId;
|
|
1675
|
+
previous: Record<string, unknown> | null;
|
|
1676
|
+
next: Record<string, unknown> | null;
|
|
1677
|
+
op: "create" | "update" | "delete";
|
|
1678
|
+
}
|
|
1679
|
+
/** Register or replace schema for `schema.type` (typically at app init). */
|
|
1680
|
+
declare function registerSchema(schema: EntitySchema): void;
|
|
1681
|
+
/** Lookup schema for cascade/join reads; returns null if unregistered. */
|
|
1682
|
+
declare function getSchema(type: EntityType): EntitySchema | null;
|
|
1683
|
+
/**
|
|
1684
|
+
* After a successful mutation, mark related entities/lists stale so hooks refetch without manually hunting query keys.
|
|
1685
|
+
* Traverses registered schemas (including reverse `hasMany`) so denormalized UIs stay eventually consistent with the graph.
|
|
1686
|
+
*/
|
|
1687
|
+
declare function cascadeInvalidation(ctx: CascadeContext): void;
|
|
1688
|
+
/**
|
|
1689
|
+
* Resolve relation **placeholders** for detail panels: joins graph reads for belongs-to targets, has-many id lists, or many-to-many id arrays.
|
|
1690
|
+
* Returns plain objects suitable for rendering; does not mutate the graph.
|
|
1691
|
+
*/
|
|
1692
|
+
declare function readRelations(type: EntityType, entity: Record<string, unknown>): Record<string, unknown>;
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* agent/json-patch.ts — Minimal RFC-6902 JSON Patch applier.
|
|
1696
|
+
*
|
|
1697
|
+
* AG-UI `STATE_DELTA` events carry RFC-6902 operations. Rather than take a hard
|
|
1698
|
+
* dependency on `fast-json-patch` (last released 2022) or require consumers to
|
|
1699
|
+
* have `@ag-ui/client`'s applier, this implements the standard's six operations
|
|
1700
|
+
* directly. It is small, dependency-free, and fully unit-tested.
|
|
1701
|
+
*
|
|
1702
|
+
* Immutability: every op returns a NEW document; the input is never mutated.
|
|
1703
|
+
*
|
|
1704
|
+
* Supported ops: add, replace, remove, move, copy, test.
|
|
1705
|
+
* Pointers follow RFC-6901 (`/a/b/0`), including the `-` array-append token and
|
|
1706
|
+
* `~1`/`~0` escapes for `/` and `~`.
|
|
1707
|
+
*/
|
|
1708
|
+
interface JsonPatchOp {
|
|
1709
|
+
op: "add" | "remove" | "replace" | "move" | "copy" | "test";
|
|
1710
|
+
path: string;
|
|
1711
|
+
/** For add/replace/test. */
|
|
1712
|
+
value?: unknown;
|
|
1713
|
+
/** For move/copy. */
|
|
1714
|
+
from?: string;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Apply a sequence of RFC-6902 operations to `doc`, returning a new document.
|
|
1718
|
+
* A failing `test` op throws (per spec); structural ops are applied in order.
|
|
1719
|
+
*/
|
|
1720
|
+
declare function applyJsonPatch<T = unknown>(doc: T, ops: readonly JsonPatchOp[]): T;
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* agent/ag-ui-bridge.ts — Ingest AG-UI agent state into the entity graph.
|
|
1724
|
+
*
|
|
1725
|
+
* The headline agentic capability (change C2): an agent emits AG-UI state, it
|
|
1726
|
+
* lands in the entity graph, and every view reading those entities updates.
|
|
1727
|
+
*
|
|
1728
|
+
* AG-UI (https://docs.ag-ui.com) streams two state events:
|
|
1729
|
+
* - STATE_SNAPSHOT — the complete agent state object
|
|
1730
|
+
* - STATE_DELTA — a partial update as an RFC-6902 JSON Patch
|
|
1731
|
+
*
|
|
1732
|
+
* Our `patchEntity`/`upsertEntity` already align with this model: STATE_DELTA's
|
|
1733
|
+
* JSON Patch maps onto the graph, and writes route through the C1 MergeStrategy
|
|
1734
|
+
* so agent edits resolve against concurrent realtime/local writes.
|
|
1735
|
+
*
|
|
1736
|
+
* Mapping: the agent's state tree is projected onto entities via an
|
|
1737
|
+
* {@link AgUiStateMapping}. Each entry says "the object at this JSON-Pointer
|
|
1738
|
+
* prefix is an entity of `type`, keyed by `id` (a field of that object or a
|
|
1739
|
+
* constant)". This keeps the bridge transport-agnostic — it does not depend on
|
|
1740
|
+
* `@ag-ui/client` and takes plain event objects, so `@ag-ui/*` stays an OPTIONAL
|
|
1741
|
+
* peer (use it for typed events on the consumer side if desired).
|
|
1742
|
+
*/
|
|
1743
|
+
|
|
1744
|
+
/** AG-UI STATE_SNAPSHOT event (minimal shape — no @ag-ui dependency). */
|
|
1745
|
+
interface AgUiStateSnapshotEvent {
|
|
1746
|
+
type?: "STATE_SNAPSHOT" | string;
|
|
1747
|
+
snapshot: Record<string, unknown>;
|
|
1748
|
+
}
|
|
1749
|
+
/** AG-UI STATE_DELTA event (minimal shape — RFC-6902 patch). */
|
|
1750
|
+
interface AgUiStateDeltaEvent {
|
|
1751
|
+
type?: "STATE_DELTA" | string;
|
|
1752
|
+
delta: JsonPatchOp[];
|
|
1753
|
+
}
|
|
1754
|
+
/** Maps one slice of agent state onto an entity type. */
|
|
1755
|
+
interface AgUiStateMapping {
|
|
1756
|
+
/** Entity type the mapped object(s) belong to. */
|
|
1757
|
+
entityType: string;
|
|
1758
|
+
/**
|
|
1759
|
+
* JSON-Pointer prefix into the agent state where this entity (or collection)
|
|
1760
|
+
* lives. `"/order"` → a single entity; `"/orders"` → a collection keyed by id.
|
|
1761
|
+
*/
|
|
1762
|
+
pointer: string;
|
|
1763
|
+
/** Whether `pointer` addresses one object or a collection (array/record). */
|
|
1764
|
+
kind: "single" | "collection";
|
|
1765
|
+
/**
|
|
1766
|
+
* Field on each object that holds its entity id. For `single` you may instead
|
|
1767
|
+
* pass a constant id via `constantId`.
|
|
1768
|
+
*/
|
|
1769
|
+
idField?: string;
|
|
1770
|
+
/** Constant id for `single` mappings whose object has no id field. */
|
|
1771
|
+
constantId?: string;
|
|
1772
|
+
/**
|
|
1773
|
+
* How to write: `upsert` (merge via MergeStrategy — default) or `replace`
|
|
1774
|
+
* (full overwrite). Snapshots typically replace; deltas typically upsert.
|
|
1775
|
+
*/
|
|
1776
|
+
write?: "upsert" | "replace";
|
|
1777
|
+
}
|
|
1778
|
+
interface ApplyAgUiOptions {
|
|
1779
|
+
mappings: AgUiStateMapping[];
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Apply an AG-UI STATE_SNAPSHOT to the graph. Each mapped slice is written as an
|
|
1783
|
+
* entity. Snapshots default to `replace` semantics when the mapping does not
|
|
1784
|
+
* specify otherwise is left to the mapping's `write` field (default upsert).
|
|
1785
|
+
*
|
|
1786
|
+
* @example
|
|
1787
|
+
* ```ts
|
|
1788
|
+
* applyAgUiSnapshot(event, { mappings: [
|
|
1789
|
+
* { entityType: "Order", pointer: "/order", kind: "single", write: "replace" },
|
|
1790
|
+
* { entityType: "LineItem", pointer: "/order/items", kind: "collection" },
|
|
1791
|
+
* ]});
|
|
1792
|
+
* ```
|
|
1793
|
+
*/
|
|
1794
|
+
declare function applyAgUiSnapshot(event: AgUiStateSnapshotEvent, options: ApplyAgUiOptions): void;
|
|
1795
|
+
/**
|
|
1796
|
+
* Apply an AG-UI STATE_DELTA (RFC-6902 JSON Patch) to the graph.
|
|
1797
|
+
*
|
|
1798
|
+
* The delta is applied against a reconstructed agent-state view assembled from
|
|
1799
|
+
* the current graph entities (per the mappings), then the resulting slices are
|
|
1800
|
+
* re-projected back into the graph. This lets path-based deltas target nested
|
|
1801
|
+
* entity fields without the caller tracking the full agent state themselves.
|
|
1802
|
+
*/
|
|
1803
|
+
declare function applyAgUiDelta(event: AgUiStateDeltaEvent, options: ApplyAgUiOptions,
|
|
1804
|
+
/** Current agent-state view; if omitted, reconstructed from the graph mappings. */
|
|
1805
|
+
currentState?: Record<string, unknown>): void;
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* adapters/types.ts
|
|
1809
|
+
*
|
|
1810
|
+
* Common contract every data-source adapter implements.
|
|
1811
|
+
* The entity graph doesn't care whether data comes from REST, GraphQL,
|
|
1812
|
+
* WebSocket, Supabase, Convex, PGlite shape sync — they all speak this
|
|
1813
|
+
* interface and write into the same graph.
|
|
1814
|
+
*/
|
|
1815
|
+
|
|
1816
|
+
type ChangeOperation = "insert" | "update" | "delete" | "upsert";
|
|
1817
|
+
interface EntityChange<T = Record<string, unknown>> {
|
|
1818
|
+
op: ChangeOperation;
|
|
1819
|
+
type: EntityType;
|
|
1820
|
+
id: EntityId;
|
|
1821
|
+
data?: T;
|
|
1822
|
+
patch?: Partial<T>;
|
|
1823
|
+
}
|
|
1824
|
+
interface ChangeSet<T = Record<string, unknown>> {
|
|
1825
|
+
changes: EntityChange<T>[];
|
|
1826
|
+
affectedListKeys?: string[];
|
|
1827
|
+
timestamp?: string;
|
|
1828
|
+
}
|
|
1829
|
+
type UnsubscribeFn = () => void;
|
|
1830
|
+
interface SubscriptionConfig {
|
|
1831
|
+
label?: string;
|
|
1832
|
+
replayOnConnect?: boolean;
|
|
1833
|
+
}
|
|
1834
|
+
interface RealtimeAdapter {
|
|
1835
|
+
readonly name: string;
|
|
1836
|
+
subscribe(config: SubscriptionConfig, handler: (changeset: ChangeSet) => void): UnsubscribeFn;
|
|
1837
|
+
onStatusChange?: (cb: (status: AdapterStatus) => void) => UnsubscribeFn;
|
|
1838
|
+
}
|
|
1839
|
+
type AdapterStatus = "connecting" | "connected" | "disconnected" | "error";
|
|
1840
|
+
interface SyncQueryResult<T> {
|
|
1841
|
+
rows: T[];
|
|
1842
|
+
total?: number;
|
|
1843
|
+
}
|
|
1844
|
+
interface SyncAdapter extends RealtimeAdapter {
|
|
1845
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<SyncQueryResult<T>>;
|
|
1846
|
+
execute(sql: string, params?: unknown[]): Promise<void>;
|
|
1847
|
+
isSynced(): boolean;
|
|
1848
|
+
onSyncComplete(cb: () => void): UnsubscribeFn;
|
|
1849
|
+
}
|
|
1850
|
+
interface ChannelConfig {
|
|
1851
|
+
type: EntityType;
|
|
1852
|
+
filter?: Record<string, unknown>;
|
|
1853
|
+
id?: EntityId;
|
|
1854
|
+
operations?: ChangeOperation[];
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
interface ManagerOptions {
|
|
1858
|
+
flushInterval?: number;
|
|
1859
|
+
onStatusChange?: (adapter: string, status: AdapterStatus) => void;
|
|
1860
|
+
onChangeReceived?: (adapter: string, change: EntityChange) => void;
|
|
1861
|
+
}
|
|
1862
|
+
declare class RealtimeManager {
|
|
1863
|
+
private adapters;
|
|
1864
|
+
private pendingChanges;
|
|
1865
|
+
private pendingListKeys;
|
|
1866
|
+
private flushTimer;
|
|
1867
|
+
private opts;
|
|
1868
|
+
constructor(opts?: ManagerOptions);
|
|
1869
|
+
register(adapter: RealtimeAdapter, channels: ChannelConfig[], normalize?: (raw: unknown) => EntityChange | null): UnsubscribeFn;
|
|
1870
|
+
unregister(name: string): void;
|
|
1871
|
+
unregisterAll(): void;
|
|
1872
|
+
private handleChangeset;
|
|
1873
|
+
private scheduleFlush;
|
|
1874
|
+
flush(): void;
|
|
1875
|
+
forceFlush(): void;
|
|
1876
|
+
}
|
|
1877
|
+
declare function getRealtimeManager(opts?: ManagerOptions): RealtimeManager;
|
|
1878
|
+
declare function resetRealtimeManager(): void;
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* adapters/realtime-adapters.ts
|
|
1882
|
+
*
|
|
1883
|
+
* WebSocket, Supabase Realtime, Convex, and GraphQL subscription adapters.
|
|
1884
|
+
* All implement RealtimeAdapter and route through RealtimeManager → entity graph.
|
|
1885
|
+
*/
|
|
1886
|
+
|
|
1887
|
+
interface WebSocketAdapterOptions {
|
|
1888
|
+
url: string | (() => string);
|
|
1889
|
+
parseMessage?: (data: unknown) => EntityChange[] | null;
|
|
1890
|
+
protocols?: string | string[];
|
|
1891
|
+
reconnectBaseDelay?: number;
|
|
1892
|
+
maxReconnectAttempts?: number;
|
|
1893
|
+
pingInterval?: number;
|
|
1894
|
+
pingMessage?: string;
|
|
1895
|
+
}
|
|
1896
|
+
declare function createWebSocketAdapter(opts: WebSocketAdapterOptions): RealtimeAdapter;
|
|
1897
|
+
interface SupabaseClient {
|
|
1898
|
+
channel(name: string): SupabaseChannel;
|
|
1899
|
+
}
|
|
1900
|
+
interface SupabaseChannel {
|
|
1901
|
+
on(event: "postgres_changes", config: {
|
|
1902
|
+
event: "*" | "INSERT" | "UPDATE" | "DELETE";
|
|
1903
|
+
schema: string;
|
|
1904
|
+
table: string;
|
|
1905
|
+
filter?: string;
|
|
1906
|
+
}, handler: (payload: SupabasePayload) => void): SupabaseChannel;
|
|
1907
|
+
subscribe(cb?: (status: string) => void): SupabaseChannel;
|
|
1908
|
+
unsubscribe(): Promise<void>;
|
|
1909
|
+
}
|
|
1910
|
+
interface SupabasePayload {
|
|
1911
|
+
eventType: "INSERT" | "UPDATE" | "DELETE";
|
|
1912
|
+
new: Record<string, unknown>;
|
|
1913
|
+
old: Record<string, unknown>;
|
|
1914
|
+
table: string;
|
|
1915
|
+
}
|
|
1916
|
+
interface SupabaseAdapterOptions {
|
|
1917
|
+
tableTypeMap?: Record<string, string>;
|
|
1918
|
+
extractId?: (record: Record<string, unknown>) => string;
|
|
1919
|
+
schema?: string;
|
|
1920
|
+
}
|
|
1921
|
+
declare function createSupabaseRealtimeAdapter(client: SupabaseClient, opts?: SupabaseAdapterOptions): RealtimeAdapter & {
|
|
1922
|
+
subscribeChannel: (config: ChannelConfig & {
|
|
1923
|
+
_handler?: (cs: ChangeSet) => void;
|
|
1924
|
+
}) => UnsubscribeFn;
|
|
1925
|
+
};
|
|
1926
|
+
interface ConvexClient {
|
|
1927
|
+
onUpdate<T>(query: unknown, args: Record<string, unknown>, handler: (result: T) => void): UnsubscribeFn;
|
|
1928
|
+
}
|
|
1929
|
+
interface ConvexChannelConfig<T extends object> {
|
|
1930
|
+
type: string;
|
|
1931
|
+
query: unknown;
|
|
1932
|
+
args?: Record<string, unknown>;
|
|
1933
|
+
extractId?: (record: T) => string;
|
|
1934
|
+
normalize?: (record: T) => Record<string, unknown>;
|
|
1935
|
+
}
|
|
1936
|
+
declare function createConvexAdapter<T extends object>(opts: {
|
|
1937
|
+
client: ConvexClient;
|
|
1938
|
+
channels: ConvexChannelConfig<T>[];
|
|
1939
|
+
}): RealtimeAdapter;
|
|
1940
|
+
interface GQLWsClient {
|
|
1941
|
+
subscribe<T>(payload: {
|
|
1942
|
+
query: string;
|
|
1943
|
+
variables?: Record<string, unknown>;
|
|
1944
|
+
}, sink: {
|
|
1945
|
+
next: (value: {
|
|
1946
|
+
data: T;
|
|
1947
|
+
}) => void;
|
|
1948
|
+
error: (err: unknown) => void;
|
|
1949
|
+
complete: () => void;
|
|
1950
|
+
}): UnsubscribeFn;
|
|
1951
|
+
}
|
|
1952
|
+
interface GQLSubscriptionConfig<T extends object> {
|
|
1953
|
+
type: string;
|
|
1954
|
+
document: string;
|
|
1955
|
+
variables?: Record<string, unknown>;
|
|
1956
|
+
getPayload: (data: T) => GQLPayload | GQLPayload[] | null;
|
|
1957
|
+
}
|
|
1958
|
+
interface GQLPayload {
|
|
1959
|
+
type?: string;
|
|
1960
|
+
node?: Record<string, unknown>;
|
|
1961
|
+
id?: string;
|
|
1962
|
+
}
|
|
1963
|
+
declare function createGraphQLSubscriptionAdapter<T extends object>(opts: {
|
|
1964
|
+
client: GQLWsClient;
|
|
1965
|
+
subscriptions: GQLSubscriptionConfig<T>[];
|
|
1966
|
+
extractId?: (node: Record<string, unknown>, type: string) => string;
|
|
1967
|
+
normalize?: (node: Record<string, unknown>, type: string) => Record<string, unknown>;
|
|
1968
|
+
}): RealtimeAdapter;
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* adapters/surreal-live.ts
|
|
1972
|
+
*
|
|
1973
|
+
* SurrealDB LIVE SELECT realtime adapter.
|
|
1974
|
+
*
|
|
1975
|
+
* SurrealDB live stream → per-channel state machine → RealtimeAdapter contract
|
|
1976
|
+
* one ChannelConfig = one LIVE SELECT = one independent reconnect/replay state
|
|
1977
|
+
*
|
|
1978
|
+
* Implements the RealtimeAdapter contract (NOT SyncAdapter) per the corrected
|
|
1979
|
+
* spec at openspec/specs/entity-surreal-live-adapter/spec.md. The consumer
|
|
1980
|
+
* owns the SurrealDB client; the adapter only orchestrates live queries.
|
|
1981
|
+
*/
|
|
1982
|
+
|
|
1983
|
+
interface SurrealLike {
|
|
1984
|
+
query<T = unknown>(sql: string, vars?: Record<string, unknown>): Promise<T[]>;
|
|
1985
|
+
live<T = unknown>(table: string, callback: (action: SurrealLiveAction<T>) => void, diff?: boolean): Promise<string>;
|
|
1986
|
+
kill(uuid: string): Promise<void>;
|
|
1987
|
+
}
|
|
1988
|
+
interface SurrealLiveAction<T = unknown> {
|
|
1989
|
+
action: "CREATE" | "UPDATE" | "DELETE" | "CLOSE" | string;
|
|
1990
|
+
result: T;
|
|
1991
|
+
}
|
|
1992
|
+
interface SurrealCheckpointStore {
|
|
1993
|
+
get(channelKey: string): Promise<string | undefined>;
|
|
1994
|
+
set(channelKey: string, value: string): Promise<void>;
|
|
1995
|
+
}
|
|
1996
|
+
interface SurrealTableConfig {
|
|
1997
|
+
type: EntityType;
|
|
1998
|
+
/** SurrealDB table name; defaults to `type`. */
|
|
1999
|
+
table?: string;
|
|
2000
|
+
/** Field in the row that carries the entity id; defaults to "id". */
|
|
2001
|
+
idField?: string;
|
|
2002
|
+
}
|
|
2003
|
+
interface SurrealLiveAdapterOptions {
|
|
2004
|
+
name?: string;
|
|
2005
|
+
surreal: SurrealLike;
|
|
2006
|
+
tables: SurrealTableConfig[];
|
|
2007
|
+
initialQueryStrategy?: "select-then-live" | "live-only";
|
|
2008
|
+
initialDelayMs?: number;
|
|
2009
|
+
maxDelayMs?: number;
|
|
2010
|
+
connectedSettleMs?: number;
|
|
2011
|
+
checkpointStore?: SurrealCheckpointStore;
|
|
2012
|
+
checkpointField?: string;
|
|
2013
|
+
listKeyResolver?: (change: EntityChange) => string[] | undefined;
|
|
2014
|
+
permanentErrorPatterns?: RegExp[];
|
|
2015
|
+
}
|
|
2016
|
+
declare function createSurrealLiveAdapter(opts: SurrealLiveAdapterOptions): RealtimeAdapter;
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* adapters/flint.ts — Flint Realtime Fabric adapter.
|
|
2020
|
+
*
|
|
2021
|
+
* Flint frf-entity-management `watchEntities()` AsyncIterable
|
|
2022
|
+
* → our ChangeSet stream → RealtimeManager → entity graph
|
|
2023
|
+
*
|
|
2024
|
+
* GAP-1 is a BRIDGE, not a from-scratch build: the Flint repo already ships
|
|
2025
|
+
* `@prometheusags/frf-entity-management` whose `RealtimeAdapter.watchEntities()`
|
|
2026
|
+
* yields a plain `EntityEvent` (it JSON-decodes the spine envelope — no proto
|
|
2027
|
+
* types leak). We consume that entity-shaped facade behind a minimal-surface
|
|
2028
|
+
* type, so:
|
|
2029
|
+
* - The unfrozen `proto-v1` risk is contained BELOW this seam.
|
|
2030
|
+
* - `@prometheusags/frf-sdk` stays an OPTIONAL peer; this module imports
|
|
2031
|
+
* nothing from it. The consumer constructs the Flint client and passes it in.
|
|
2032
|
+
*
|
|
2033
|
+
* `EntityEvent.offset` (bigint) is forwarded to an optional checkpoint store so
|
|
2034
|
+
* reconnects resume from the last seen offset (mirrors the surreal-live adapter).
|
|
2035
|
+
*/
|
|
2036
|
+
|
|
2037
|
+
/** A decoded entity change event received from the Flint spine. */
|
|
2038
|
+
interface FlintEntityEvent {
|
|
2039
|
+
entityType: string;
|
|
2040
|
+
entityId: string;
|
|
2041
|
+
tenantId: string;
|
|
2042
|
+
channelId: string;
|
|
2043
|
+
data: Record<string, unknown>;
|
|
2044
|
+
offset: bigint;
|
|
2045
|
+
correlationId?: string;
|
|
2046
|
+
}
|
|
2047
|
+
/** Filter criteria for a Flint entity subscription. */
|
|
2048
|
+
interface FlintEntityQuery {
|
|
2049
|
+
channelId: string;
|
|
2050
|
+
consumerId: string;
|
|
2051
|
+
entityType?: string;
|
|
2052
|
+
fromOffset?: bigint;
|
|
2053
|
+
}
|
|
2054
|
+
/** An entity mutation to publish on the Flint spine. */
|
|
2055
|
+
interface FlintEntityRecord {
|
|
2056
|
+
entityType: string;
|
|
2057
|
+
entityId: string;
|
|
2058
|
+
tenantId: string;
|
|
2059
|
+
channelId: string;
|
|
2060
|
+
data: Record<string, unknown>;
|
|
2061
|
+
correlationId?: string;
|
|
2062
|
+
}
|
|
2063
|
+
/** Minimal surface of the Flint `RealtimeAdapter` we consume. */
|
|
2064
|
+
interface FlintClientLike {
|
|
2065
|
+
watchEntities(query: FlintEntityQuery): AsyncIterable<FlintEntityEvent>;
|
|
2066
|
+
mutateEntity(record: FlintEntityRecord): Promise<void>;
|
|
2067
|
+
}
|
|
2068
|
+
/** Optional resume-offset persistence (per channel+consumer). */
|
|
2069
|
+
interface FlintCheckpointStore {
|
|
2070
|
+
get(key: string): Promise<bigint | undefined> | bigint | undefined;
|
|
2071
|
+
set(key: string, offset: bigint): Promise<void> | void;
|
|
2072
|
+
}
|
|
2073
|
+
interface CreateFlintAdapterOptions {
|
|
2074
|
+
name?: string;
|
|
2075
|
+
/** The Flint entity-management client (frf-entity-management RealtimeAdapter). */
|
|
2076
|
+
client: FlintClientLike;
|
|
2077
|
+
/** Channel to subscribe/publish on. */
|
|
2078
|
+
channelId: string;
|
|
2079
|
+
/** Consumer id for this subscription. */
|
|
2080
|
+
consumerId: string;
|
|
2081
|
+
/** Restrict to one entity type (optional). */
|
|
2082
|
+
entityType?: string;
|
|
2083
|
+
/** Resume-offset store; reconnects start `fromOffset = lastSeen + 1`. */
|
|
2084
|
+
checkpoints?: FlintCheckpointStore;
|
|
2085
|
+
/** Map a Flint event to a change operation. Defaults to "upsert". */
|
|
2086
|
+
resolveOp?: (event: FlintEntityEvent) => ChangeOperation;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Build a {@link RealtimeAdapter} backed by the Flint Realtime Fabric.
|
|
2090
|
+
*
|
|
2091
|
+
* The consumer owns the Flint client (constructed from `@prometheusags/frf-sdk`
|
|
2092
|
+
* / `frf-entity-management`); this adapter only orchestrates the stream.
|
|
2093
|
+
*
|
|
2094
|
+
* @example
|
|
2095
|
+
* ```ts
|
|
2096
|
+
* import { RealtimeAdapter as FrfAdapter } from "@prometheusags/frf-entity-management";
|
|
2097
|
+
* import { getRealtimeManager, createFlintAdapter } from "@prometheus-ags/prometheus-entity-management";
|
|
2098
|
+
*
|
|
2099
|
+
* const flint = new FrfAdapter(spineClient);
|
|
2100
|
+
* const adapter = createFlintAdapter({ client: flint, channelId: "tenant-1", consumerId: "web-1" });
|
|
2101
|
+
* getRealtimeManager().register(adapter);
|
|
2102
|
+
* ```
|
|
2103
|
+
*/
|
|
2104
|
+
declare function createFlintAdapter(options: CreateFlintAdapterOptions): RealtimeAdapter;
|
|
2105
|
+
/**
|
|
2106
|
+
* Publish an entity mutation to the Flint spine via the client.
|
|
2107
|
+
* Thin pass-through kept here so consumers import one Flint surface from us.
|
|
2108
|
+
*/
|
|
2109
|
+
declare function publishFlintMutation(client: FlintClientLike, record: FlintEntityRecord): Promise<void>;
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* adapters/electricsql.ts (core)
|
|
2113
|
+
*
|
|
2114
|
+
* ElectricSQL + PGlite local-first adapter — the React-free base.
|
|
2115
|
+
*
|
|
2116
|
+
* Server Postgres → ElectricSQL shape sync (SSE) → PGlite (local WASM Postgres)
|
|
2117
|
+
* ↓ NOTIFY
|
|
2118
|
+
* Entity Graph (Zustand)
|
|
2119
|
+
*
|
|
2120
|
+
* React hooks (`useLocalFirst`, `usePGliteQuery`) live in the binding package
|
|
2121
|
+
* (`@prometheus-ags/entity-graph-react`), built on this adapter.
|
|
2122
|
+
*/
|
|
2123
|
+
|
|
2124
|
+
interface PGlite$1 {
|
|
2125
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<{
|
|
2126
|
+
rows: T[];
|
|
2127
|
+
}>;
|
|
2128
|
+
exec(sql: string): Promise<unknown>;
|
|
2129
|
+
listen(channel: string, handler: (payload: string) => void): Promise<() => void>;
|
|
2130
|
+
}
|
|
2131
|
+
interface ShapeMessage$1<T = Record<string, unknown>> {
|
|
2132
|
+
headers: {
|
|
2133
|
+
operation: "insert" | "update" | "delete";
|
|
2134
|
+
};
|
|
2135
|
+
offset: string;
|
|
2136
|
+
value: T;
|
|
2137
|
+
key: string;
|
|
2138
|
+
}
|
|
2139
|
+
interface ShapeStream$1<T = Record<string, unknown>> {
|
|
2140
|
+
subscribe(onMsg: (msgs: ShapeMessage$1<T>[]) => void, onErr?: (e: Error) => void): () => void;
|
|
2141
|
+
isUpToDate: boolean;
|
|
2142
|
+
lastOffset: string;
|
|
2143
|
+
}
|
|
2144
|
+
interface ElectricTableConfig<T extends object> {
|
|
2145
|
+
type: EntityType;
|
|
2146
|
+
table: string;
|
|
2147
|
+
where?: string;
|
|
2148
|
+
idColumn?: string;
|
|
2149
|
+
normalize?: (row: T) => Record<string, unknown>;
|
|
2150
|
+
shapeStream: ShapeStream$1<T>;
|
|
2151
|
+
}
|
|
2152
|
+
interface ElectricAdapterOptions {
|
|
2153
|
+
pglite: PGlite$1;
|
|
2154
|
+
tables: ElectricTableConfig<Record<string, unknown>>[];
|
|
2155
|
+
onSynced?: () => void;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
declare function createElectricAdapter(opts: ElectricAdapterOptions): SyncAdapter;
|
|
2159
|
+
|
|
2160
|
+
/**
|
|
2161
|
+
* adapters/electricsql-tenant.ts
|
|
2162
|
+
*
|
|
2163
|
+
* Tenant-scoped Electric adapter — the safety primitive that enforces
|
|
2164
|
+
* **RULE 5 — Shape predicates ⊆ RLS** (see hotseaters constraints).
|
|
2165
|
+
*
|
|
2166
|
+
* PGlite has no row-level security. The only way to guarantee a client
|
|
2167
|
+
* never sees cross-tenant data through Electric is to refuse to attach
|
|
2168
|
+
* any shape that lacks a tenant predicate. This wrapper does exactly that:
|
|
2169
|
+
*
|
|
2170
|
+
* - Validates the tenant claim (currently `{ companyId }`) is a UUID.
|
|
2171
|
+
* - Refuses to attach a shape whose `tenantColumn` is `undefined`. We
|
|
2172
|
+
* accept explicit `null` for the company root itself (filtered by `id`).
|
|
2173
|
+
* - Builds the `where` clause from `tenantColumn` and the validated
|
|
2174
|
+
* companyId so individual shape factories cannot drift from RLS.
|
|
2175
|
+
* - Delegates to the existing `createElectricAdapter` for the actual
|
|
2176
|
+
* sync wiring.
|
|
2177
|
+
*
|
|
2178
|
+
* This also fulfils Change 13 item 11 (auth-claim-aware shape registration):
|
|
2179
|
+
* the `tenantClaim` is the typed seam where authn meets shape registration.
|
|
2180
|
+
*
|
|
2181
|
+
* Self-hosted Supabase only. Local stack and `https://electricsql.prometheusags.ai`
|
|
2182
|
+
* are the only acceptable Electric endpoints — this module does not
|
|
2183
|
+
* hard-code either; it only governs the predicate.
|
|
2184
|
+
*/
|
|
2185
|
+
|
|
2186
|
+
interface PGlite {
|
|
2187
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<{
|
|
2188
|
+
rows: T[];
|
|
2189
|
+
}>;
|
|
2190
|
+
exec(sql: string): Promise<unknown>;
|
|
2191
|
+
listen(channel: string, handler: (payload: string) => void): Promise<() => void>;
|
|
2192
|
+
}
|
|
2193
|
+
interface ShapeMessage<T = Record<string, unknown>> {
|
|
2194
|
+
headers: {
|
|
2195
|
+
operation: "insert" | "update" | "delete";
|
|
2196
|
+
};
|
|
2197
|
+
offset: string;
|
|
2198
|
+
value: T;
|
|
2199
|
+
key: string;
|
|
2200
|
+
}
|
|
2201
|
+
interface ShapeStream<T = Record<string, unknown>> {
|
|
2202
|
+
subscribe(onMsg: (msgs: ShapeMessage<T>[]) => void, onErr?: (e: Error) => void): () => void;
|
|
2203
|
+
isUpToDate: boolean;
|
|
2204
|
+
lastOffset: string;
|
|
2205
|
+
}
|
|
2206
|
+
/** Auth claim required to register tenant-scoped shapes. */
|
|
2207
|
+
interface TenantClaim {
|
|
2208
|
+
companyId: string;
|
|
2209
|
+
}
|
|
2210
|
+
interface TenantScopedTableConfig<T extends object = Record<string, unknown>> {
|
|
2211
|
+
type: EntityType;
|
|
2212
|
+
table: string;
|
|
2213
|
+
/**
|
|
2214
|
+
* Column used for tenant filtering.
|
|
2215
|
+
*
|
|
2216
|
+
* - A string (`"company_id"`) yields `WHERE company_id = '<companyId>'`.
|
|
2217
|
+
* - Explicit `null` means the table IS the tenant root and is filtered by
|
|
2218
|
+
* `id = '<companyId>'`. Only the `company` table qualifies.
|
|
2219
|
+
* - `undefined` is **rejected** — this is the safety gate.
|
|
2220
|
+
*/
|
|
2221
|
+
tenantColumn: string | null;
|
|
2222
|
+
primaryKey?: string[];
|
|
2223
|
+
/**
|
|
2224
|
+
* Factory invoked with the constructed `where` clause and the tenant claim.
|
|
2225
|
+
* Consumers wire their preferred Electric `ShapeStream` builder here. The
|
|
2226
|
+
* factory must use the supplied `where` verbatim (no widening).
|
|
2227
|
+
*/
|
|
2228
|
+
shapeStreamFactory: (params: {
|
|
2229
|
+
table: string;
|
|
2230
|
+
where: string;
|
|
2231
|
+
tenantClaim: TenantClaim;
|
|
2232
|
+
}) => ShapeStream<T>;
|
|
2233
|
+
normalize?: (row: T) => Record<string, unknown>;
|
|
2234
|
+
}
|
|
2235
|
+
interface TenantScopedAdapterOptions {
|
|
2236
|
+
pglite: PGlite;
|
|
2237
|
+
tenantClaim: TenantClaim;
|
|
2238
|
+
tables: TenantScopedTableConfig[];
|
|
2239
|
+
onSynced?: () => void;
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Build the `where` clause for a single shape. Exposed for tests.
|
|
2243
|
+
*
|
|
2244
|
+
* - `tenantColumn === null` → `id = '<companyId>'` (company-root case)
|
|
2245
|
+
* - `tenantColumn === string` → `<tenantColumn> = '<companyId>'`
|
|
2246
|
+
*
|
|
2247
|
+
* Throws if `tenantColumn` is `undefined` or `companyId` is not a UUID.
|
|
2248
|
+
*/
|
|
2249
|
+
declare function buildTenantWhere(tenantColumn: string | null | undefined, companyId: string, tableLabel: string): string;
|
|
2250
|
+
/**
|
|
2251
|
+
* Build a tenant-scoped {@link SyncAdapter}.
|
|
2252
|
+
*
|
|
2253
|
+
* For each table in `tables`:
|
|
2254
|
+
* 1. Validates `tenantColumn` is defined (string or explicit null).
|
|
2255
|
+
* 2. Validates `companyId` is a UUID.
|
|
2256
|
+
* 3. Builds the predicate and calls `shapeStreamFactory(...)` to get the
|
|
2257
|
+
* actual `ShapeStream`.
|
|
2258
|
+
* 4. Hands the resulting list off to `createElectricAdapter` for wiring.
|
|
2259
|
+
*
|
|
2260
|
+
* @throws if any table lacks `tenantColumn`, or the company id is not a UUID.
|
|
2261
|
+
*/
|
|
2262
|
+
declare function createTenantScopedElectricAdapter(opts: TenantScopedAdapterOptions): SyncAdapter;
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* adapters/pglite-persistence.ts
|
|
2266
|
+
*
|
|
2267
|
+
* PGlite-backed implementation of {@link GraphPersistenceAdapter}.
|
|
2268
|
+
*
|
|
2269
|
+
* Stores the local-first runtime's graph snapshot inside a PGlite table
|
|
2270
|
+
* (`_graph_snapshot` by default) instead of `localStorage`/`IndexedDB`.
|
|
2271
|
+
*
|
|
2272
|
+
* Why this exists:
|
|
2273
|
+
* - The app already has a PGlite handle for ElectricSQL sync.
|
|
2274
|
+
* - Putting the graph snapshot alongside synced data means one storage
|
|
2275
|
+
* surface to back up, clear, and reason about.
|
|
2276
|
+
* - Works in Tauri WebView + Node (PGlite is a WASM Postgres) without
|
|
2277
|
+
* pulling in browser-only storage.
|
|
2278
|
+
*
|
|
2279
|
+
* Contract:
|
|
2280
|
+
* - `get(key)` -> returns the stored string or null
|
|
2281
|
+
* - `set(key, v)` -> upserts (insert-or-replace), stamping `updated_at`
|
|
2282
|
+
* - `remove(key)` -> deletes the row (no-op if absent)
|
|
2283
|
+
*
|
|
2284
|
+
* The function lazily ensures the storage table exists on first call.
|
|
2285
|
+
* That `CREATE TABLE IF NOT EXISTS` is idempotent so it's safe to retry.
|
|
2286
|
+
*
|
|
2287
|
+
* Self-hosted Supabase only. HotSeatersMVP is the bible. This module does
|
|
2288
|
+
* not import `@electric-sql/pglite` — consumers pass a PGlite-shaped handle.
|
|
2289
|
+
*/
|
|
2290
|
+
|
|
2291
|
+
interface PGlitePersistenceClient {
|
|
2292
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<{
|
|
2293
|
+
rows: T[];
|
|
2294
|
+
}>;
|
|
2295
|
+
exec(sql: string): Promise<unknown>;
|
|
2296
|
+
}
|
|
2297
|
+
interface CreatePGlitePersistenceAdapterOptions {
|
|
2298
|
+
/** Name of the snapshot table. Defaults to `_graph_snapshot`. */
|
|
2299
|
+
tableName?: string;
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Returns a {@link GraphPersistenceAdapter} that reads/writes the graph
|
|
2303
|
+
* snapshot through a PGlite-shaped client.
|
|
2304
|
+
*
|
|
2305
|
+
* @example
|
|
2306
|
+
* ```ts
|
|
2307
|
+
* import { PGlite } from "@electric-sql/pglite";
|
|
2308
|
+
* import { startLocalFirstGraph, createPGlitePersistenceAdapter } from "@prometheus-ags/prometheus-entity-management";
|
|
2309
|
+
*
|
|
2310
|
+
* const pglite = await PGlite.create("idb://hotseaters");
|
|
2311
|
+
* const storage = await createPGlitePersistenceAdapter(pglite);
|
|
2312
|
+
* const runtime = startLocalFirstGraph({ storage, key: "hotseaters:graph" });
|
|
2313
|
+
* ```
|
|
2314
|
+
*/
|
|
2315
|
+
declare function createPGlitePersistenceAdapter(pglite: PGlitePersistenceClient, options?: CreatePGlitePersistenceAdapterOptions): Promise<GraphPersistenceAdapter>;
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* adapters/tauri-sql-persistence.ts
|
|
2319
|
+
*
|
|
2320
|
+
* SQLite-backed implementation of {@link GraphPersistenceAdapter} for Tauri
|
|
2321
|
+
* desktop, via the official `@tauri-apps/plugin-sql` `Database` handle.
|
|
2322
|
+
*
|
|
2323
|
+
* Why this exists:
|
|
2324
|
+
* - On Tauri desktop the natural on-device store is SQLite, not PGlite-wasm.
|
|
2325
|
+
* - This pairs with `createPGlitePersistenceAdapter` (browser/wasm) so the
|
|
2326
|
+
* same local-first runtime persists across web and desktop unchanged.
|
|
2327
|
+
*
|
|
2328
|
+
* The consumer owns the Tauri `Database` handle (from `Database.load(...)`).
|
|
2329
|
+
* This module does NOT import `@tauri-apps/plugin-sql` — it takes a
|
|
2330
|
+
* minimal-surface client, mirroring the PGlite/Electric adapter pattern, so it
|
|
2331
|
+
* adds no hard dependency. `@tauri-apps/plugin-sql` is an optional peer.
|
|
2332
|
+
*
|
|
2333
|
+
* Contract (same as every GraphPersistenceAdapter):
|
|
2334
|
+
* - `get(key)` -> stored string or null
|
|
2335
|
+
* - `set(key, v)` -> upsert (INSERT ... ON CONFLICT)
|
|
2336
|
+
* - `remove(key)` -> delete (no-op if absent)
|
|
2337
|
+
*
|
|
2338
|
+
* Tauri SQL uses SQLite `?`-positional bind params (not `$1`).
|
|
2339
|
+
*/
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Minimal surface of a `@tauri-apps/plugin-sql` `Database` handle.
|
|
2343
|
+
* Avoids a hard dep; matches the loaded-Database shape (`execute` + `select`).
|
|
2344
|
+
*/
|
|
2345
|
+
interface TauriSqlClient {
|
|
2346
|
+
execute(query: string, bindValues?: unknown[]): Promise<unknown>;
|
|
2347
|
+
select<T = Record<string, unknown>>(query: string, bindValues?: unknown[]): Promise<T[]>;
|
|
2348
|
+
}
|
|
2349
|
+
interface CreateTauriSqlPersistenceAdapterOptions {
|
|
2350
|
+
/** Name of the snapshot table. Defaults to `_graph_snapshot`. */
|
|
2351
|
+
tableName?: string;
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Returns a {@link GraphPersistenceAdapter} backed by a Tauri SQLite `Database`.
|
|
2355
|
+
*
|
|
2356
|
+
* @example
|
|
2357
|
+
* ```ts
|
|
2358
|
+
* import Database from "@tauri-apps/plugin-sql";
|
|
2359
|
+
* import { startLocalFirstGraph, createTauriSqlPersistenceAdapter } from "@prometheus-ags/prometheus-entity-management";
|
|
2360
|
+
*
|
|
2361
|
+
* const db = await Database.load("sqlite:app.db");
|
|
2362
|
+
* const storage = await createTauriSqlPersistenceAdapter(db);
|
|
2363
|
+
* const runtime = startLocalFirstGraph({ storage, key: "app:graph" });
|
|
2364
|
+
* ```
|
|
2365
|
+
*/
|
|
2366
|
+
declare function createTauriSqlPersistenceAdapter(db: TauriSqlClient, options?: CreateTauriSqlPersistenceAdapterOptions): Promise<GraphPersistenceAdapter>;
|
|
2367
|
+
|
|
2368
|
+
/**
|
|
2369
|
+
* schema-from-sql.ts
|
|
2370
|
+
*
|
|
2371
|
+
* Generates a JSON Schema (and registers it via `registerEntityJsonSchema`)
|
|
2372
|
+
* directly from a Postgres `CREATE TABLE` statement. Lets a consumer keep
|
|
2373
|
+
* `latest-data/supabase/migrations/*.sql` as the single source of truth for
|
|
2374
|
+
* column shapes — no hand-maintained TypeScript schema duplicates.
|
|
2375
|
+
*
|
|
2376
|
+
* Deliberately regex-based: a real SQL parser is overkill for the slice of
|
|
2377
|
+
* DDL we care about, and would add a non-trivial runtime dep.
|
|
2378
|
+
*
|
|
2379
|
+
* Type mapping
|
|
2380
|
+
* ------------
|
|
2381
|
+
* UUID, TEXT, VARCHAR(*) -> { type: "string" }
|
|
2382
|
+
* INTEGER, INT, BIGINT, SMALLINT -> { type: "integer" }
|
|
2383
|
+
* NUMERIC(*), DECIMAL(*) -> { type: "number" }
|
|
2384
|
+
* BOOLEAN, BOOL -> { type: "boolean" }
|
|
2385
|
+
* TIMESTAMPTZ, TIMESTAMP -> { type: "string", format: "date-time" }
|
|
2386
|
+
* DATE -> { type: "string", format: "date" }
|
|
2387
|
+
* JSONB, JSON -> { type: "object" }
|
|
2388
|
+
* TEXT[] -> { type: "array", items: { type: "string" } }
|
|
2389
|
+
* anything else -> { type: "string" } + "x-warning"
|
|
2390
|
+
*
|
|
2391
|
+
* `required` is populated from `NOT NULL` columns that lack a `DEFAULT`.
|
|
2392
|
+
* Callers can pass `overrides` to deep-merge a partial config (typically to
|
|
2393
|
+
* narrow a JSONB column to an array, or to add `enum`/`format` hints).
|
|
2394
|
+
*/
|
|
2395
|
+
|
|
2396
|
+
interface RegisterEntityFromSqlOptions {
|
|
2397
|
+
entityType: string;
|
|
2398
|
+
/** A single `CREATE TABLE` statement (extra DDL around it is ignored). */
|
|
2399
|
+
createTableSql: string;
|
|
2400
|
+
/** Optional deep-merge overrides (overrides win). */
|
|
2401
|
+
overrides?: Partial<EntityJsonSchemaConfig>;
|
|
2402
|
+
}
|
|
2403
|
+
interface ParsedColumn {
|
|
2404
|
+
name: string;
|
|
2405
|
+
sqlType: string;
|
|
2406
|
+
notNull: boolean;
|
|
2407
|
+
hasDefault: boolean;
|
|
2408
|
+
}
|
|
2409
|
+
interface ParsedTable {
|
|
2410
|
+
tableName: string;
|
|
2411
|
+
columns: ParsedColumn[];
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Parse a `CREATE TABLE` block. Exposed for tests and for callers who need
|
|
2415
|
+
* the intermediate representation.
|
|
2416
|
+
*/
|
|
2417
|
+
declare function parseCreateTable(sql: string): ParsedTable;
|
|
2418
|
+
/**
|
|
2419
|
+
* Map a normalized (uppercased, whitespace-stripped) SQL type to a JSON
|
|
2420
|
+
* schema fragment.
|
|
2421
|
+
*
|
|
2422
|
+
* Returns `{ schema, warning }` so the caller can surface "unmapped type"
|
|
2423
|
+
* notes via `x-warning` on the property.
|
|
2424
|
+
*/
|
|
2425
|
+
declare function sqlTypeToJsonSchema(sqlType: string): {
|
|
2426
|
+
schema: JsonSchemaObject;
|
|
2427
|
+
warning?: string;
|
|
2428
|
+
};
|
|
2429
|
+
/**
|
|
2430
|
+
* Build (and register) an `EntityJsonSchemaConfig` from a CREATE TABLE
|
|
2431
|
+
* statement. Returns the registered config so callers can introspect it.
|
|
2432
|
+
*/
|
|
2433
|
+
declare function registerEntityFromSql(opts: RegisterEntityFromSqlOptions): EntityJsonSchemaConfig;
|
|
2434
|
+
|
|
2435
|
+
/**
|
|
2436
|
+
* table/types.ts
|
|
2437
|
+
*
|
|
2438
|
+
* Pure table engine type definitions — zero external dependencies.
|
|
2439
|
+
* Structurally compatible with TanStack Table v8 for easy migration,
|
|
2440
|
+
* but fully self-contained within the prometheus-entity-management library.
|
|
2441
|
+
*/
|
|
2442
|
+
|
|
2443
|
+
type ViewMode = "table" | "gallery" | "list";
|
|
2444
|
+
type Updater<T> = T | ((prev: T) => T);
|
|
2445
|
+
type AccessorFn<TData, TValue = unknown> = (row: TData, index: number) => TValue;
|
|
2446
|
+
type HeaderContext<TData> = {
|
|
2447
|
+
table: TableInstance<TData>;
|
|
2448
|
+
header: Header<TData>;
|
|
2449
|
+
column: Column<TData>;
|
|
2450
|
+
};
|
|
2451
|
+
type CellContext<TData> = {
|
|
2452
|
+
table: TableInstance<TData>;
|
|
2453
|
+
row: Row<TData>;
|
|
2454
|
+
cell: Cell<TData>;
|
|
2455
|
+
column: Column<TData>;
|
|
2456
|
+
getValue: <T = unknown>() => T;
|
|
2457
|
+
renderValue: <T = unknown>() => T | null;
|
|
2458
|
+
};
|
|
2459
|
+
type HeaderRenderer<TData> = string | ((context: HeaderContext<TData>) => React.ReactNode);
|
|
2460
|
+
type CellRenderer<TData> = string | ((context: CellContext<TData>) => React.ReactNode);
|
|
2461
|
+
type FilterFn<TData> = (row: Row<TData>, columnId: string, filterValue: unknown) => boolean;
|
|
2462
|
+
type SortingFn<TData> = (rowA: Row<TData>, rowB: Row<TData>, columnId: string) => number;
|
|
2463
|
+
type AggregationFn<TData> = (columnId: string, leafRows: Row<TData>[], childRows: Row<TData>[]) => unknown;
|
|
2464
|
+
interface ColumnMeta<TData = unknown> {
|
|
2465
|
+
entityMeta?: {
|
|
2466
|
+
field: keyof TData;
|
|
2467
|
+
filterType: "text" | "number" | "date" | "dateRange" | "boolean" | "enum" | "relation" | "none";
|
|
2468
|
+
enumOptions?: Array<{
|
|
2469
|
+
label: string;
|
|
2470
|
+
value: string;
|
|
2471
|
+
color?: string;
|
|
2472
|
+
}>;
|
|
2473
|
+
relationEntityType?: string;
|
|
2474
|
+
editable?: boolean;
|
|
2475
|
+
hideable?: boolean;
|
|
2476
|
+
};
|
|
2477
|
+
[key: string]: unknown;
|
|
2478
|
+
}
|
|
2479
|
+
interface ColumnDef<TData, TValue = unknown> {
|
|
2480
|
+
id?: string;
|
|
2481
|
+
accessorKey?: keyof TData & string;
|
|
2482
|
+
accessorFn?: AccessorFn<TData, TValue>;
|
|
2483
|
+
header?: HeaderRenderer<TData>;
|
|
2484
|
+
cell?: CellRenderer<TData>;
|
|
2485
|
+
footer?: HeaderRenderer<TData>;
|
|
2486
|
+
size?: number;
|
|
2487
|
+
minSize?: number;
|
|
2488
|
+
maxSize?: number;
|
|
2489
|
+
enableSorting?: boolean;
|
|
2490
|
+
enableFiltering?: boolean;
|
|
2491
|
+
enableHiding?: boolean;
|
|
2492
|
+
enablePinning?: boolean;
|
|
2493
|
+
enableGrouping?: boolean;
|
|
2494
|
+
enableResizing?: boolean;
|
|
2495
|
+
filterFn?: FilterFn<TData> | "auto";
|
|
2496
|
+
sortingFn?: SortingFn<TData> | "auto";
|
|
2497
|
+
aggregationFn?: AggregationFn<TData> | "auto" | "sum" | "min" | "max" | "count" | "mean" | "median" | "unique";
|
|
2498
|
+
sortDescFirst?: boolean;
|
|
2499
|
+
sortUndefined?: "first" | "last" | false;
|
|
2500
|
+
invertSorting?: boolean;
|
|
2501
|
+
meta?: ColumnMeta<TData>;
|
|
2502
|
+
columns?: ColumnDef<TData, unknown>[];
|
|
2503
|
+
}
|
|
2504
|
+
interface SortingColumn {
|
|
2505
|
+
id: string;
|
|
2506
|
+
desc: boolean;
|
|
2507
|
+
}
|
|
2508
|
+
type SortingState = SortingColumn[];
|
|
2509
|
+
interface ColumnFilter {
|
|
2510
|
+
id: string;
|
|
2511
|
+
value: unknown;
|
|
2512
|
+
}
|
|
2513
|
+
type ColumnFiltersState = ColumnFilter[];
|
|
2514
|
+
type RowSelectionState = Record<string, boolean>;
|
|
2515
|
+
type ColumnVisibilityState = Record<string, boolean>;
|
|
2516
|
+
type ColumnOrderState = string[];
|
|
2517
|
+
interface ColumnPinningState {
|
|
2518
|
+
left?: string[];
|
|
2519
|
+
right?: string[];
|
|
2520
|
+
}
|
|
2521
|
+
type ColumnSizingState = Record<string, number>;
|
|
2522
|
+
type ColumnSizingInfoState = {
|
|
2523
|
+
startOffset: number | null;
|
|
2524
|
+
startSize: number | null;
|
|
2525
|
+
deltaOffset: number | null;
|
|
2526
|
+
deltaPercentage: number | null;
|
|
2527
|
+
isResizingColumn: string | false;
|
|
2528
|
+
columnSizingStart: [string, number][];
|
|
2529
|
+
};
|
|
2530
|
+
type ExpandedState = Record<string, boolean> | true;
|
|
2531
|
+
type GroupingState = string[];
|
|
2532
|
+
interface PaginationState {
|
|
2533
|
+
pageIndex: number;
|
|
2534
|
+
pageSize: number;
|
|
2535
|
+
}
|
|
2536
|
+
interface TableState {
|
|
2537
|
+
sorting: SortingState;
|
|
2538
|
+
columnFilters: ColumnFiltersState;
|
|
2539
|
+
globalFilter: unknown;
|
|
2540
|
+
rowSelection: RowSelectionState;
|
|
2541
|
+
columnVisibility: ColumnVisibilityState;
|
|
2542
|
+
columnOrder: ColumnOrderState;
|
|
2543
|
+
columnPinning: ColumnPinningState;
|
|
2544
|
+
columnSizing: ColumnSizingState;
|
|
2545
|
+
columnSizingInfo: ColumnSizingInfoState;
|
|
2546
|
+
expanded: ExpandedState;
|
|
2547
|
+
grouping: GroupingState;
|
|
2548
|
+
pagination: PaginationState;
|
|
2549
|
+
}
|
|
2550
|
+
interface Row<TData> {
|
|
2551
|
+
id: string;
|
|
2552
|
+
index: number;
|
|
2553
|
+
original: TData;
|
|
2554
|
+
depth: number;
|
|
2555
|
+
parentId?: string;
|
|
2556
|
+
subRows: Row<TData>[];
|
|
2557
|
+
getValue: <T = unknown>(columnId: string) => T;
|
|
2558
|
+
renderValue: <T = unknown>(columnId: string) => T | null;
|
|
2559
|
+
getIsSelected: () => boolean;
|
|
2560
|
+
getCanSelect: () => boolean;
|
|
2561
|
+
getIsAllSubRowsSelected: () => boolean;
|
|
2562
|
+
getIsSomeSelected: () => boolean;
|
|
2563
|
+
toggleSelected: (value?: boolean) => void;
|
|
2564
|
+
getToggleSelectedHandler: () => (e: unknown) => void;
|
|
2565
|
+
getIsExpanded: () => boolean;
|
|
2566
|
+
getCanExpand: () => boolean;
|
|
2567
|
+
toggleExpanded: (value?: boolean) => void;
|
|
2568
|
+
getToggleExpandedHandler: () => () => void;
|
|
2569
|
+
getIsGrouped: () => boolean;
|
|
2570
|
+
groupingColumnId?: string;
|
|
2571
|
+
groupingValue?: unknown;
|
|
2572
|
+
getVisibleCells: () => Cell<TData>[];
|
|
2573
|
+
getAllCells: () => Cell<TData>[];
|
|
2574
|
+
getIsPinned: () => "top" | "bottom" | false;
|
|
2575
|
+
pin: (position: "top" | "bottom" | false) => void;
|
|
2576
|
+
}
|
|
2577
|
+
interface Cell<TData> {
|
|
2578
|
+
id: string;
|
|
2579
|
+
row: Row<TData>;
|
|
2580
|
+
column: Column<TData>;
|
|
2581
|
+
getValue: <T = unknown>() => T;
|
|
2582
|
+
renderValue: <T = unknown>() => T | null;
|
|
2583
|
+
getIsGrouped: () => boolean;
|
|
2584
|
+
getIsPlaceholder: () => boolean;
|
|
2585
|
+
getIsAggregated: () => boolean;
|
|
2586
|
+
getContext: () => CellContext<TData>;
|
|
2587
|
+
}
|
|
2588
|
+
interface Header<TData> {
|
|
2589
|
+
id: string;
|
|
2590
|
+
index: number;
|
|
2591
|
+
depth: number;
|
|
2592
|
+
column: Column<TData>;
|
|
2593
|
+
isPlaceholder: boolean;
|
|
2594
|
+
placeholderId?: string;
|
|
2595
|
+
subHeaders: Header<TData>[];
|
|
2596
|
+
colSpan: number;
|
|
2597
|
+
rowSpan: number;
|
|
2598
|
+
getSize: () => number;
|
|
2599
|
+
getStart: () => number;
|
|
2600
|
+
getContext: () => HeaderContext<TData>;
|
|
2601
|
+
getResizeHandler: () => (event: unknown) => void;
|
|
2602
|
+
getLeafHeaders: () => Header<TData>[];
|
|
2603
|
+
}
|
|
2604
|
+
interface HeaderGroup<TData> {
|
|
2605
|
+
id: string;
|
|
2606
|
+
depth: number;
|
|
2607
|
+
headers: Header<TData>[];
|
|
2608
|
+
}
|
|
2609
|
+
interface Column<TData> {
|
|
2610
|
+
id: string;
|
|
2611
|
+
depth: number;
|
|
2612
|
+
columnDef: ColumnDef<TData>;
|
|
2613
|
+
columns: Column<TData>[];
|
|
2614
|
+
parent?: Column<TData>;
|
|
2615
|
+
getFlatColumns: () => Column<TData>[];
|
|
2616
|
+
getLeafColumns: () => Column<TData>[];
|
|
2617
|
+
getIsSorted: () => false | "asc" | "desc";
|
|
2618
|
+
getNextSortingOrder: () => "asc" | "desc" | false;
|
|
2619
|
+
getCanSort: () => boolean;
|
|
2620
|
+
toggleSorting: (desc?: boolean, isMulti?: boolean) => void;
|
|
2621
|
+
clearSorting: () => void;
|
|
2622
|
+
getSortIndex: () => number;
|
|
2623
|
+
getAutoSortingFn: () => SortingFn<TData>;
|
|
2624
|
+
getAutoSortDir: () => "asc" | "desc";
|
|
2625
|
+
getIsFiltered: () => boolean;
|
|
2626
|
+
getFilterValue: () => unknown;
|
|
2627
|
+
setFilterValue: (value: unknown) => void;
|
|
2628
|
+
getCanFilter: () => boolean;
|
|
2629
|
+
getAutoFilterFn: () => FilterFn<TData> | undefined;
|
|
2630
|
+
getIsVisible: () => boolean;
|
|
2631
|
+
toggleVisibility: (value?: boolean) => void;
|
|
2632
|
+
getCanHide: () => boolean;
|
|
2633
|
+
getIsPinned: () => "left" | "right" | false;
|
|
2634
|
+
pin: (position: "left" | "right" | false) => void;
|
|
2635
|
+
getCanPin: () => boolean;
|
|
2636
|
+
getIsGrouped: () => boolean;
|
|
2637
|
+
toggleGrouping: () => void;
|
|
2638
|
+
getCanGroup: () => boolean;
|
|
2639
|
+
getGroupedIndex: () => number;
|
|
2640
|
+
getSize: () => number;
|
|
2641
|
+
getStart: (position?: "left" | "center" | "right") => number;
|
|
2642
|
+
getCanResize: () => boolean;
|
|
2643
|
+
resetSize: () => void;
|
|
2644
|
+
getIndex: (position?: "left" | "center" | "right") => number;
|
|
2645
|
+
}
|
|
2646
|
+
interface RowModel<TData> {
|
|
2647
|
+
rows: Row<TData>[];
|
|
2648
|
+
flatRows: Row<TData>[];
|
|
2649
|
+
rowsById: Record<string, Row<TData>>;
|
|
2650
|
+
}
|
|
2651
|
+
interface TableOptions<TData> {
|
|
2652
|
+
data: TData[];
|
|
2653
|
+
columns: ColumnDef<TData>[];
|
|
2654
|
+
getRowId?: (row: TData, index: number, parent?: Row<TData>) => string;
|
|
2655
|
+
defaultColumn?: Partial<ColumnDef<TData>>;
|
|
2656
|
+
/** Seed the initial value for any internal state slice without making it controlled. */
|
|
2657
|
+
initialState?: Partial<TableState>;
|
|
2658
|
+
/** Fully controlled state — every key provided here locks that slice and must be updated externally. */
|
|
2659
|
+
state?: Partial<TableState>;
|
|
2660
|
+
onStateChange?: (updater: Updater<TableState>) => void;
|
|
2661
|
+
manualSorting?: boolean;
|
|
2662
|
+
enableSorting?: boolean;
|
|
2663
|
+
enableMultiSort?: boolean;
|
|
2664
|
+
enableSortingRemoval?: boolean;
|
|
2665
|
+
enableMultiRemove?: boolean;
|
|
2666
|
+
maxMultiSortColCount?: number;
|
|
2667
|
+
sortDescFirst?: boolean;
|
|
2668
|
+
onSortingChange?: (updater: Updater<SortingState>) => void;
|
|
2669
|
+
manualFiltering?: boolean;
|
|
2670
|
+
enableFiltering?: boolean;
|
|
2671
|
+
enableColumnFilters?: boolean;
|
|
2672
|
+
enableGlobalFilter?: boolean;
|
|
2673
|
+
globalFilterFn?: FilterFn<TData>;
|
|
2674
|
+
onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void;
|
|
2675
|
+
onGlobalFilterChange?: (updater: Updater<unknown>) => void;
|
|
2676
|
+
manualPagination?: boolean;
|
|
2677
|
+
pageCount?: number;
|
|
2678
|
+
autoResetPageIndex?: boolean;
|
|
2679
|
+
onPaginationChange?: (updater: Updater<PaginationState>) => void;
|
|
2680
|
+
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
|
2681
|
+
enableMultiRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
|
2682
|
+
enableSubRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
|
2683
|
+
onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void;
|
|
2684
|
+
enableHiding?: boolean;
|
|
2685
|
+
onColumnVisibilityChange?: (updater: Updater<ColumnVisibilityState>) => void;
|
|
2686
|
+
onColumnOrderChange?: (updater: Updater<ColumnOrderState>) => void;
|
|
2687
|
+
enablePinning?: boolean;
|
|
2688
|
+
onColumnPinningChange?: (updater: Updater<ColumnPinningState>) => void;
|
|
2689
|
+
enableColumnResizing?: boolean;
|
|
2690
|
+
columnResizeMode?: "onChange" | "onEnd";
|
|
2691
|
+
columnResizeDirection?: "ltr" | "rtl";
|
|
2692
|
+
onColumnSizingChange?: (updater: Updater<ColumnSizingState>) => void;
|
|
2693
|
+
onColumnSizingInfoChange?: (updater: Updater<ColumnSizingInfoState>) => void;
|
|
2694
|
+
manualGrouping?: boolean;
|
|
2695
|
+
enableGrouping?: boolean;
|
|
2696
|
+
onGroupingChange?: (updater: Updater<GroupingState>) => void;
|
|
2697
|
+
manualExpanding?: boolean;
|
|
2698
|
+
enableExpanding?: boolean;
|
|
2699
|
+
getSubRows?: (row: TData, index: number) => TData[] | undefined;
|
|
2700
|
+
getIsRowExpanded?: (row: Row<TData>) => boolean;
|
|
2701
|
+
onExpandedChange?: (updater: Updater<ExpandedState>) => void;
|
|
2702
|
+
paginateExpandedRows?: boolean;
|
|
2703
|
+
enableRowPinning?: boolean | ((row: Row<TData>) => boolean);
|
|
2704
|
+
keepPinnedRows?: boolean;
|
|
2705
|
+
onRowPinningChange?: (updater: Updater<Record<string, "top" | "bottom">>) => void;
|
|
2706
|
+
}
|
|
2707
|
+
interface TableInstance<TData> {
|
|
2708
|
+
options: TableOptions<TData>;
|
|
2709
|
+
getState: () => TableState;
|
|
2710
|
+
setState: (updater: Updater<TableState>) => void;
|
|
2711
|
+
reset: () => void;
|
|
2712
|
+
getAllColumns: () => Column<TData>[];
|
|
2713
|
+
getAllFlatColumns: () => Column<TData>[];
|
|
2714
|
+
getAllLeafColumns: () => Column<TData>[];
|
|
2715
|
+
getColumn: (id: string) => Column<TData> | undefined;
|
|
2716
|
+
getHeaderGroups: () => HeaderGroup<TData>[];
|
|
2717
|
+
getLeftHeaderGroups: () => HeaderGroup<TData>[];
|
|
2718
|
+
getCenterHeaderGroups: () => HeaderGroup<TData>[];
|
|
2719
|
+
getRightHeaderGroups: () => HeaderGroup<TData>[];
|
|
2720
|
+
getFooterGroups: () => HeaderGroup<TData>[];
|
|
2721
|
+
getCoreRowModel: () => RowModel<TData>;
|
|
2722
|
+
getRowModel: () => RowModel<TData>;
|
|
2723
|
+
getPreFilteredRowModel: () => RowModel<TData>;
|
|
2724
|
+
getFilteredRowModel: () => RowModel<TData>;
|
|
2725
|
+
getPreSortedRowModel: () => RowModel<TData>;
|
|
2726
|
+
getSortedRowModel: () => RowModel<TData>;
|
|
2727
|
+
getGroupedRowModel: () => RowModel<TData>;
|
|
2728
|
+
getExpandedRowModel: () => RowModel<TData>;
|
|
2729
|
+
getPrePaginationRowModel: () => RowModel<TData>;
|
|
2730
|
+
getPaginationRowModel: () => RowModel<TData>;
|
|
2731
|
+
getSelectedRowModel: () => RowModel<TData>;
|
|
2732
|
+
getRow: (id: string) => Row<TData>;
|
|
2733
|
+
setSorting: (updater: Updater<SortingState>) => void;
|
|
2734
|
+
resetSorting: (defaultState?: boolean) => void;
|
|
2735
|
+
setColumnFilters: (updater: Updater<ColumnFiltersState>) => void;
|
|
2736
|
+
resetColumnFilters: (defaultState?: boolean) => void;
|
|
2737
|
+
setGlobalFilter: (value: unknown) => void;
|
|
2738
|
+
resetGlobalFilter: (defaultState?: boolean) => void;
|
|
2739
|
+
setPageIndex: (updater: Updater<number>) => void;
|
|
2740
|
+
resetPageIndex: (defaultState?: boolean) => void;
|
|
2741
|
+
setPageSize: (updater: Updater<number>) => void;
|
|
2742
|
+
resetPageSize: (defaultState?: boolean) => void;
|
|
2743
|
+
getPageCount: () => number;
|
|
2744
|
+
getCanPreviousPage: () => boolean;
|
|
2745
|
+
getCanNextPage: () => boolean;
|
|
2746
|
+
previousPage: () => void;
|
|
2747
|
+
nextPage: () => void;
|
|
2748
|
+
firstPage: () => void;
|
|
2749
|
+
lastPage: () => void;
|
|
2750
|
+
setRowSelection: (updater: Updater<RowSelectionState>) => void;
|
|
2751
|
+
resetRowSelection: (defaultState?: boolean) => void;
|
|
2752
|
+
toggleAllRowsSelected: (value?: boolean) => void;
|
|
2753
|
+
toggleAllPageRowsSelected: (value?: boolean) => void;
|
|
2754
|
+
getIsAllRowsSelected: () => boolean;
|
|
2755
|
+
getIsAllPageRowsSelected: () => boolean;
|
|
2756
|
+
getIsSomeRowsSelected: () => boolean;
|
|
2757
|
+
getIsSomePageRowsSelected: () => boolean;
|
|
2758
|
+
getToggleAllRowsSelectedHandler: () => (e: unknown) => void;
|
|
2759
|
+
getToggleAllPageRowsSelectedHandler: () => (e: unknown) => void;
|
|
2760
|
+
setColumnVisibility: (updater: Updater<ColumnVisibilityState>) => void;
|
|
2761
|
+
resetColumnVisibility: (defaultState?: boolean) => void;
|
|
2762
|
+
toggleAllColumnsVisible: (value?: boolean) => void;
|
|
2763
|
+
getIsAllColumnsVisible: () => boolean;
|
|
2764
|
+
getIsSomeColumnsVisible: () => boolean;
|
|
2765
|
+
getToggleAllColumnsVisibilityHandler: () => (e: unknown) => void;
|
|
2766
|
+
getVisibleFlatColumns: () => Column<TData>[];
|
|
2767
|
+
getVisibleLeafColumns: () => Column<TData>[];
|
|
2768
|
+
setColumnOrder: (updater: Updater<ColumnOrderState>) => void;
|
|
2769
|
+
resetColumnOrder: (defaultState?: boolean) => void;
|
|
2770
|
+
setColumnPinning: (updater: Updater<ColumnPinningState>) => void;
|
|
2771
|
+
resetColumnPinning: (defaultState?: boolean) => void;
|
|
2772
|
+
getLeftFlatColumns: () => Column<TData>[];
|
|
2773
|
+
getRightFlatColumns: () => Column<TData>[];
|
|
2774
|
+
getCenterFlatColumns: () => Column<TData>[];
|
|
2775
|
+
getLeftLeafColumns: () => Column<TData>[];
|
|
2776
|
+
getRightLeafColumns: () => Column<TData>[];
|
|
2777
|
+
getCenterLeafColumns: () => Column<TData>[];
|
|
2778
|
+
setColumnSizing: (updater: Updater<ColumnSizingState>) => void;
|
|
2779
|
+
setColumnSizingInfo: (updater: Updater<ColumnSizingInfoState>) => void;
|
|
2780
|
+
resetColumnSizing: (defaultState?: boolean) => void;
|
|
2781
|
+
setGrouping: (updater: Updater<GroupingState>) => void;
|
|
2782
|
+
resetGrouping: (defaultState?: boolean) => void;
|
|
2783
|
+
setExpanded: (updater: Updater<ExpandedState>) => void;
|
|
2784
|
+
resetExpanded: (defaultState?: boolean) => void;
|
|
2785
|
+
toggleAllRowsExpanded: (expanded?: boolean) => void;
|
|
2786
|
+
getIsAllRowsExpanded: () => boolean;
|
|
2787
|
+
getIsSomeRowsExpanded: () => boolean;
|
|
2788
|
+
getCanSomeRowsExpand: () => boolean;
|
|
2789
|
+
getExpandedDepth: () => number;
|
|
2790
|
+
}
|
|
2791
|
+
interface ActionDef<TData> {
|
|
2792
|
+
id: string;
|
|
2793
|
+
label: string;
|
|
2794
|
+
icon?: React.ComponentType<{
|
|
2795
|
+
className?: string;
|
|
2796
|
+
}>;
|
|
2797
|
+
onClick: (item: TData) => void;
|
|
2798
|
+
destructive?: boolean;
|
|
2799
|
+
hidden?: (item: TData) => boolean;
|
|
2800
|
+
disabled?: (item: TData) => boolean;
|
|
2801
|
+
confirm?: string | ((item: TData) => string);
|
|
2802
|
+
variant?: "primary" | "default" | "ghost" | "destructive";
|
|
2803
|
+
}
|
|
2804
|
+
interface ItemDescriptorBadge<TData> {
|
|
2805
|
+
field: keyof TData & string;
|
|
2806
|
+
options?: Array<{
|
|
2807
|
+
value: string;
|
|
2808
|
+
label: string;
|
|
2809
|
+
className?: string;
|
|
2810
|
+
}>;
|
|
2811
|
+
}
|
|
2812
|
+
interface ItemDescriptorMeta<TData> {
|
|
2813
|
+
field: keyof TData & string;
|
|
2814
|
+
label: string;
|
|
2815
|
+
format?: (value: unknown) => string;
|
|
2816
|
+
}
|
|
2817
|
+
interface ItemDescriptor<TData> {
|
|
2818
|
+
title: keyof TData & string;
|
|
2819
|
+
subtitle?: keyof TData & string;
|
|
2820
|
+
image?: keyof TData & string;
|
|
2821
|
+
icon?: (keyof TData & string) | React.ComponentType<{
|
|
2822
|
+
className?: string;
|
|
2823
|
+
}>;
|
|
2824
|
+
avatar?: keyof TData & string;
|
|
2825
|
+
badges?: ItemDescriptorBadge<TData>[];
|
|
2826
|
+
metadata?: ItemDescriptorMeta<TData>[];
|
|
2827
|
+
description?: keyof TData & string;
|
|
2828
|
+
}
|
|
2829
|
+
interface ItemRenderContext<TData> {
|
|
2830
|
+
isSelected: boolean;
|
|
2831
|
+
isEditing: boolean;
|
|
2832
|
+
isMultiSelectMode: boolean;
|
|
2833
|
+
onToggleSelect: () => void;
|
|
2834
|
+
onEdit: () => void;
|
|
2835
|
+
onSave: (changes: Partial<TData>) => void;
|
|
2836
|
+
onCancel: () => void;
|
|
2837
|
+
actions: ActionDef<TData>[];
|
|
2838
|
+
row: Row<TData>;
|
|
2839
|
+
}
|
|
2840
|
+
interface EmptyStateConfig {
|
|
2841
|
+
icon?: React.ComponentType<{
|
|
2842
|
+
className?: string;
|
|
2843
|
+
}>;
|
|
2844
|
+
title?: string;
|
|
2845
|
+
description?: string;
|
|
2846
|
+
action?: {
|
|
2847
|
+
label: string;
|
|
2848
|
+
onClick: () => void;
|
|
2849
|
+
};
|
|
2850
|
+
filteredTitle?: string;
|
|
2851
|
+
filteredDescription?: string;
|
|
2852
|
+
filteredAction?: {
|
|
2853
|
+
label: string;
|
|
2854
|
+
onClick: () => void;
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
interface BatchActionDef {
|
|
2858
|
+
id: string;
|
|
2859
|
+
label: string;
|
|
2860
|
+
icon?: React.ComponentType<{
|
|
2861
|
+
className?: string;
|
|
2862
|
+
}>;
|
|
2863
|
+
destructive?: boolean;
|
|
2864
|
+
}
|
|
2865
|
+
interface GalleryColumns {
|
|
2866
|
+
sm?: number;
|
|
2867
|
+
md?: number;
|
|
2868
|
+
lg?: number;
|
|
2869
|
+
xl?: number;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
/**
|
|
2873
|
+
* table/row-models.ts
|
|
2874
|
+
*
|
|
2875
|
+
* Pure row model pipeline functions. Each stage transforms data
|
|
2876
|
+
* into a RowModel — no side effects, no external dependencies.
|
|
2877
|
+
*/
|
|
2878
|
+
|
|
2879
|
+
declare function createRow<TData>(original: TData, index: number, columns: Column<TData>[], table: TableInstance<TData>, depth?: number, parentId?: string, subRows?: Row<TData>[]): Row<TData>;
|
|
2880
|
+
declare function getCoreRowModel<TData>(data: TData[], columns: Column<TData>[], table: TableInstance<TData>): RowModel<TData>;
|
|
2881
|
+
declare function getFilteredRowModel<TData>(rowModel: RowModel<TData>, columnFilters: ColumnFiltersState, globalFilter: unknown, columns: Column<TData>[], globalFilterFn?: FilterFn<TData>): RowModel<TData>;
|
|
2882
|
+
declare function getSortedRowModel<TData>(rowModel: RowModel<TData>, sorting: SortingState, columns: Column<TData>[]): RowModel<TData>;
|
|
2883
|
+
declare function getGroupedRowModel<TData>(rowModel: RowModel<TData>, grouping: GroupingState, columns: Column<TData>[], table: TableInstance<TData>): RowModel<TData>;
|
|
2884
|
+
declare function getExpandedRowModel<TData>(rowModel: RowModel<TData>, expanded: ExpandedState): RowModel<TData>;
|
|
2885
|
+
declare function getPaginatedRowModel<TData>(rowModel: RowModel<TData>, pagination: PaginationState): RowModel<TData>;
|
|
2886
|
+
declare function getSelectedRowModel<TData>(rowModel: RowModel<TData>, selection: Record<string, boolean>): RowModel<TData>;
|
|
2887
|
+
|
|
2888
|
+
/**
|
|
2889
|
+
* table/faceting.ts
|
|
2890
|
+
*
|
|
2891
|
+
* Column faceting utilities — compute unique values, counts,
|
|
2892
|
+
* and min/max for filter UI controls.
|
|
2893
|
+
*/
|
|
2894
|
+
|
|
2895
|
+
/**
|
|
2896
|
+
* Compute unique values and their counts for a given column.
|
|
2897
|
+
* Useful for rendering filter dropdown options with hit counts.
|
|
2898
|
+
*/
|
|
2899
|
+
declare function getFacetedUniqueValues<TData>(rowModel: RowModel<TData>, columnId: string): Map<unknown, number>;
|
|
2900
|
+
/**
|
|
2901
|
+
* Compute min and max numeric values for a given column.
|
|
2902
|
+
* Returns [min, max] or undefined if no numeric values exist.
|
|
2903
|
+
*/
|
|
2904
|
+
declare function getFacetedMinMaxValues<TData>(rowModel: RowModel<TData>, columnId: string): [number, number] | undefined;
|
|
2905
|
+
/**
|
|
2906
|
+
* Get a filtered row model scoped to a single column's facet.
|
|
2907
|
+
* Returns a row model that excludes the given column's own filter,
|
|
2908
|
+
* so the facet counts reflect what would be available if that
|
|
2909
|
+
* specific filter were removed.
|
|
2910
|
+
*/
|
|
2911
|
+
declare function getFacetedRowModel<TData>(preFilteredRowModel: RowModel<TData>, columnId: string, allFilteredRowModel: RowModel<TData>): RowModel<TData>;
|
|
2912
|
+
|
|
2913
|
+
export { type AccessorFn, type ActionDef, type AdapterStatus, type AgUiStateDeltaEvent, type AgUiStateMapping, type AgUiStateSnapshotEvent, type AggregationFn, type ApplyAgUiOptions, type BatchActionDef, type BelongsToRelation, type CascadeContext, type Cell, type CellContext, type CellRenderer, type ChangeEvent, type ChangeOp, type ChangeOperation, type ChangeSet, type ChannelConfig, type Column, type ColumnDef, type ColumnFiltersState, type ColumnMeta, type ColumnOrderState, type ColumnPinningState, type ColumnSizingInfoState, type ColumnSizingState, type ColumnVisibilityState, type CompletenessMode, type CreateFlintAdapterOptions, type CreatePGlitePersistenceAdapterOptions, type CreateTauriSqlPersistenceAdapterOptions, type DevtoolsEvent, type DevtoolsEventBus, type DevtoolsEventBusOptions, type DevtoolsSourceFn, EMPTY_ENTITY_STATE, EMPTY_IDS, EMPTY_LIST_STATE, EMPTY_SYNC_METADATA, type ElectricAdapterOptions, type ElectricTableConfig, type EmptyStateConfig, type EngineOptions, type EntityChange, type EntityError, type EntityErrorOptions, type EntityId, type EntityJsonSchemaConfig, type EntityQueryOptions, type EntitySchema, type EntitySnapshot, type EntityState, type EntitySyncMetadata, type EntityTransport, type EntityType, type ExpandedState, type FilterClause, type FilterFn, type FilterGroup, type FilterOperator, type FilterSpec, type FlintCheckpointStore, type FlintClientLike, type FlintEntityEvent, type FlintEntityQuery, type FlintEntityRecord, type GalleryColumns, type GetEntityJsonSchemaOptions, type GraphActionEvent, type GraphActionOptions, type GraphActionRecord$1 as GraphActionRecord, type GraphEffectEvent, type GraphEffectHandle, type GraphEffectOptions, type GraphIncludeMap, type GraphIncludeRelation, type GraphPersistenceAdapter, type GraphQueryOptions, type GraphSnapshotExportOptions, type GraphSnapshotPayload, type GraphSnapshotWithSchemasOptions, type GraphState, type GraphSyncStatus, type GraphToolContext, type GraphTransaction, type GroupingState, type HasManyRelation, type Header, type HeaderContext, type HeaderGroup, type HeaderRenderer, IncrementalView, type IncrementalViewOptions, type ItemDescriptor, type ItemDescriptorBadge, type ItemDescriptorMeta, type ItemRenderContext, type JsonPatchOp, type JsonSchemaObject, type ListFetchParams, type ListQuery, type ListQueryOptions, type ListResponse, type ListResult, type ListState, type LocalFirstGraphRuntime, type MakeRestTransportOptions, type ManagerOptions, type ManyToManyRelation, type MergeContext, type MergeStrategy, type PGlitePersistenceClient, type PaginationState, type ColumnDef as PureColumnDef, type ColumnMeta as PureColumnMeta, type QueryKey, type RealtimeAdapter, RealtimeManager, type RegisterEntityFromSqlOptions, type RegisteredStore, type RelationDescriptor, type ReplayRetryPolicy, type Row, type RowModel, type RowSelectionState, type SchemaGraphToolContext, type SortClause, type SortDirection, type SortSpec, type SortingFn, type SortingState, type StartLocalFirstGraphOptions, type SubscriptionConfig, type SupabaseAdapterOptions, type SupabaseLike, type SupabaseQueryBuilderLike, type SurrealCheckpointStore, type SurrealLike, type SurrealLiveAction, type SurrealLiveAdapterOptions, type SurrealTableConfig, type SyncAdapter, type SyncOrigin, type TableInstance, type TableOptions, type TableState, type TauriSqlClient, type TenantClaim, type TenantScopedAdapterOptions, type TenantScopedTableConfig, TerminalError, type TimeTravelSnapshot, type TimeTravelState, TransientError, type UnsubscribeFn, type Updater, type ViewDescriptor, type ViewMode, type WebSocketAdapterOptions, __resetEntityTransports, __resetStoreRegistry, applyAgUiDelta, applyAgUiSnapshot, applyJsonPatch, applyView, attachGlobalListeners, buildTenantWhere, cascadeInvalidation, checkCompleteness, collectDirtyPaths, compareEntities, configureEngine, configureTimeTravel, createConvexAdapter, createDevtoolsEventBus, createElectricAdapter, createFlintAdapter, createGraphAction, createGraphEffect, createGraphQLSubscriptionAdapter, createGraphTool, createGraphTransaction, createLoroMergeStrategy, createPGlitePersistenceAdapter, createRow, createSchemaGraphTool, createSupabaseRealtimeAdapter, createSurrealLiveAdapter, createTauriSqlPersistenceAdapter, createTenantScopedElectricAdapter, createWebSocketAdapter, dedupe, escapeHtml, exportGraphSnapshot, exportGraphSnapshotWithSchemas, fetchEntity, fetchList, findInsertionIndex, flattenClauses, getActiveSubscriberCount, getCoreRowModel, getRealtimeManager as getElectricRealtimeManager, getEngineOptions, getEntityJsonSchema, getEntityTransport, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getMergeStrategy, getPaginatedRowModel, getRealtimeManager, getRegisteredEntityTypes, getRegisteredStores, getSchema, getSelectedRowModel, getSortedRowModel, getTimeTravelState, getValueAtPath, hasCustomPredicates, hasMergeStrategy, humanizeFieldName, hydrateGraphFromStorage, inferSchemaFieldType, lwwStrategy, makeRestTransport, matchesFilter, matchesSearch, parseCreateTable, persistGraphToStorage, publishFlintMutation, queryOnce, readRelations, recordGraphSnapshot, registerEntityFromSql, registerEntityJsonSchema, registerEntityTransport, registerMergeStrategy, registerRuntimeSchema, registerSchema, registerStore, registerSubscriber, registryKey, renderMarkdownToHtml, replayActionWithRetry, resetRealtimeManager, restoreGraphSnapshot, restoreGraphSnapshotBySeq, selectGraph, serializeKey, setDefaultMergeStrategy, setValueAtPath, sqlTypeToJsonSchema, startGarbageCollector, startLocalFirstGraph, stepTimeTravel, stopGarbageCollector, subscribeDevtoolsEvent, subscribeSubscriberStats, subscribeTimeTravel, toEntityError, toGraphQLVariables, toPrismaOrderBy, toPrismaWhere, toRestParams, toSQLClauses, unregisterSubscriber, useGraphStore, useGraphSyncStatus };
|