@prometheus-ags/prometheus-entity-management 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +263 -0
- package/README.md +85 -6
- package/dist/index.d.mts +1004 -163
- package/dist/index.d.ts +1004 -163
- package/dist/index.js +2589 -1583
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2573 -1585
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,1738 +1,2470 @@
|
|
|
1
|
+
import React6, { createContext, useMemo, useRef, useState, useCallback, useEffect, useSyncExternalStore, useContext, useId } from 'react';
|
|
1
2
|
import { create, createStore, useStore } from 'zustand';
|
|
2
3
|
import { subscribeWithSelector, persist } from 'zustand/middleware';
|
|
3
4
|
import { immer } from 'zustand/middleware/immer';
|
|
4
|
-
import React6, { createContext, useMemo, useSyncExternalStore, useRef, useCallback, useEffect, useState, useContext, useId } from 'react';
|
|
5
|
-
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
5
|
import { useShallow } from 'zustand/react/shallow';
|
|
6
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
import { useReactTable, getSortedRowModel, getCoreRowModel, flexRender } from '@tanstack/react-table';
|
|
8
8
|
import { Search, X, Loader2, RefreshCw, ChevronLeft, ChevronRight, Pencil, Trash2 } from 'lucide-react';
|
|
9
9
|
import { clsx } from 'clsx';
|
|
10
10
|
import { twMerge } from 'tailwind-merge';
|
|
11
11
|
|
|
12
|
-
// src/
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
// src/errors.ts
|
|
13
|
+
var TerminalError = class _TerminalError extends Error {
|
|
14
|
+
kind = "terminal";
|
|
15
|
+
status;
|
|
16
|
+
constructor(message, opts = {}) {
|
|
17
|
+
super(message, { cause: opts.cause });
|
|
18
|
+
this.name = "TerminalError";
|
|
19
|
+
this.status = opts.status;
|
|
20
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
21
|
+
Error.captureStackTrace(this, _TerminalError);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
24
|
};
|
|
25
|
-
var
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
stale: false,
|
|
37
|
-
currentPage: null,
|
|
38
|
-
pageSize: null
|
|
25
|
+
var TransientError = class _TransientError extends Error {
|
|
26
|
+
kind = "transient";
|
|
27
|
+
status;
|
|
28
|
+
constructor(message, opts = {}) {
|
|
29
|
+
super(message, { cause: opts.cause });
|
|
30
|
+
this.name = "TransientError";
|
|
31
|
+
this.status = opts.status;
|
|
32
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
33
|
+
Error.captureStackTrace(this, _TransientError);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
39
36
|
};
|
|
40
|
-
function
|
|
41
|
-
|
|
37
|
+
function toEntityError(err) {
|
|
38
|
+
if (err instanceof TerminalError) return err;
|
|
39
|
+
if (err instanceof TransientError) return err;
|
|
40
|
+
if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
|
|
41
|
+
return new TerminalError("Aborted", { cause: err });
|
|
42
|
+
}
|
|
43
|
+
if (err && typeof err === "object" && "status" in err) {
|
|
44
|
+
const status = Number(err.status);
|
|
45
|
+
const message2 = err && typeof err === "object" && "message" in err ? String(err.message ?? `HTTP ${status}`) : `HTTP ${status}`;
|
|
46
|
+
if (Number.isFinite(status) && status >= 400 && status < 500) {
|
|
47
|
+
return new TerminalError(message2, { status, cause: err });
|
|
48
|
+
}
|
|
49
|
+
if (Number.isFinite(status) && status >= 500) {
|
|
50
|
+
return new TransientError(message2, { status, cause: err });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
return new TransientError(message, { cause: err });
|
|
42
55
|
}
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
|
|
57
|
+
// src/transport/registry.ts
|
|
58
|
+
var transports = /* @__PURE__ */ new Map();
|
|
59
|
+
function registerEntityTransport(type, transport) {
|
|
60
|
+
transports.set(type, transport);
|
|
61
|
+
}
|
|
62
|
+
function getEntityTransport(type) {
|
|
63
|
+
const t = transports.get(type);
|
|
64
|
+
if (!t) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[entity-management] No transport registered for entity type "${type}". Call registerEntityTransport("${type}", ...) at app boot.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return t;
|
|
45
70
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
71
|
+
function __resetEntityTransports() {
|
|
72
|
+
transports.clear();
|
|
48
73
|
}
|
|
49
|
-
function
|
|
50
|
-
return
|
|
74
|
+
function getRegisteredEntityTypes() {
|
|
75
|
+
return Array.from(transports.keys());
|
|
51
76
|
}
|
|
52
|
-
var useGraphStore = create()(
|
|
53
|
-
subscribeWithSelector(
|
|
54
|
-
immer((set, get) => ({
|
|
55
|
-
entities: {},
|
|
56
|
-
patches: {},
|
|
57
|
-
entityStates: {},
|
|
58
|
-
syncMetadata: {},
|
|
59
|
-
lists: {},
|
|
60
|
-
upsertEntity: (type, id, data) => set((s) => {
|
|
61
|
-
if (!s.entities[type]) s.entities[type] = {};
|
|
62
|
-
s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
|
|
63
|
-
const key = ek(type, id);
|
|
64
|
-
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
65
|
-
}),
|
|
66
|
-
upsertEntities: (type, entries) => set((s) => {
|
|
67
|
-
if (!s.entities[type]) s.entities[type] = {};
|
|
68
|
-
for (const { id, data } of entries) {
|
|
69
|
-
s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
|
|
70
|
-
const key = ek(type, id);
|
|
71
|
-
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
72
|
-
}
|
|
73
|
-
}),
|
|
74
|
-
replaceEntity: (type, id, data) => set((s) => {
|
|
75
|
-
if (!s.entities[type]) s.entities[type] = {};
|
|
76
|
-
s.entities[type][id] = data;
|
|
77
|
-
const key = ek(type, id);
|
|
78
|
-
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
79
|
-
}),
|
|
80
|
-
removeEntity: (type, id) => set((s) => {
|
|
81
|
-
delete s.entities[type]?.[id];
|
|
82
|
-
delete s.patches[type]?.[id];
|
|
83
|
-
delete s.entityStates[ek(type, id)];
|
|
84
|
-
delete s.syncMetadata[ek(type, id)];
|
|
85
|
-
}),
|
|
86
|
-
patchEntity: (type, id, patch) => set((s) => {
|
|
87
|
-
if (!s.patches[type]) s.patches[type] = {};
|
|
88
|
-
s.patches[type][id] = { ...s.patches[type][id] ?? {}, ...patch };
|
|
89
|
-
}),
|
|
90
|
-
unpatchEntity: (type, id, keys) => set((s) => {
|
|
91
|
-
const p = s.patches[type]?.[id];
|
|
92
|
-
if (!p) return;
|
|
93
|
-
for (const k of keys) delete p[k];
|
|
94
|
-
}),
|
|
95
|
-
clearPatch: (type, id) => set((s) => {
|
|
96
|
-
delete s.patches[type]?.[id];
|
|
97
|
-
}),
|
|
98
|
-
setEntityFetching: (type, id, fetching) => set((s) => {
|
|
99
|
-
const k = ek(type, id);
|
|
100
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
101
|
-
s.entityStates[k].isFetching = fetching;
|
|
102
|
-
}),
|
|
103
|
-
setEntityError: (type, id, error) => set((s) => {
|
|
104
|
-
const k = ek(type, id);
|
|
105
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
106
|
-
s.entityStates[k].error = error;
|
|
107
|
-
s.entityStates[k].isFetching = false;
|
|
108
|
-
}),
|
|
109
|
-
setEntityFetched: (type, id) => set((s) => {
|
|
110
|
-
const k = ek(type, id);
|
|
111
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
112
|
-
s.entityStates[k].lastFetched = Date.now();
|
|
113
|
-
s.entityStates[k].isFetching = false;
|
|
114
|
-
s.entityStates[k].error = null;
|
|
115
|
-
s.entityStates[k].stale = false;
|
|
116
|
-
s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
|
|
117
|
-
}),
|
|
118
|
-
setEntityStale: (type, id, stale) => set((s) => {
|
|
119
|
-
const k = ek(type, id);
|
|
120
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
121
|
-
s.entityStates[k].stale = stale;
|
|
122
|
-
}),
|
|
123
|
-
setEntitySyncMetadata: (type, id, metadata) => set((s) => {
|
|
124
|
-
const k = ek(type, id);
|
|
125
|
-
s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
|
|
126
|
-
}),
|
|
127
|
-
clearEntitySyncMetadata: (type, id) => set((s) => {
|
|
128
|
-
delete s.syncMetadata[ek(type, id)];
|
|
129
|
-
}),
|
|
130
|
-
setListResult: (key, ids, meta) => set((s) => {
|
|
131
|
-
const ex = s.lists[key] ?? defaultListState();
|
|
132
|
-
s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, stale: false, lastFetched: Date.now() };
|
|
133
|
-
}),
|
|
134
|
-
appendListResult: (key, ids, meta) => set((s) => {
|
|
135
|
-
const ex = s.lists[key] ?? defaultListState();
|
|
136
|
-
s.lists[key] = { ...ex, ...meta, ids: Array.from(/* @__PURE__ */ new Set([...ex.ids, ...ids])), isFetching: false, isFetchingMore: false, error: null, stale: false, lastFetched: Date.now() };
|
|
137
|
-
}),
|
|
138
|
-
prependListResult: (key, ids, meta) => set((s) => {
|
|
139
|
-
const ex = s.lists[key] ?? defaultListState();
|
|
140
|
-
s.lists[key] = { ...ex, ...meta ?? {}, ids: Array.from(/* @__PURE__ */ new Set([...ids, ...ex.ids])), isFetching: false, isFetchingMore: false, error: null, stale: false, lastFetched: Date.now() };
|
|
141
|
-
}),
|
|
142
|
-
removeIdFromAllLists: (_type, id) => set((s) => {
|
|
143
|
-
for (const key of Object.keys(s.lists)) {
|
|
144
|
-
const list = s.lists[key];
|
|
145
|
-
const idx = list.ids.indexOf(id);
|
|
146
|
-
if (idx !== -1) {
|
|
147
|
-
list.ids.splice(idx, 1);
|
|
148
|
-
if (list.total !== null) list.total -= 1;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}),
|
|
152
|
-
insertIdInList: (key, id, position) => set((s) => {
|
|
153
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
154
|
-
const ids = s.lists[key].ids;
|
|
155
|
-
const ex = ids.indexOf(id);
|
|
156
|
-
if (ex !== -1) ids.splice(ex, 1);
|
|
157
|
-
if (position === "start") ids.unshift(id);
|
|
158
|
-
else if (position === "end") ids.push(id);
|
|
159
|
-
else ids.splice(position, 0, id);
|
|
160
|
-
}),
|
|
161
|
-
setListFetching: (key, fetching) => set((s) => {
|
|
162
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
163
|
-
s.lists[key].isFetching = fetching;
|
|
164
|
-
}),
|
|
165
|
-
setListFetchingMore: (key, fetchingMore) => set((s) => {
|
|
166
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
167
|
-
s.lists[key].isFetchingMore = fetchingMore;
|
|
168
|
-
}),
|
|
169
|
-
setListError: (key, error) => set((s) => {
|
|
170
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
171
|
-
s.lists[key].error = error;
|
|
172
|
-
s.lists[key].isFetching = false;
|
|
173
|
-
s.lists[key].isFetchingMore = false;
|
|
174
|
-
}),
|
|
175
|
-
setListStale: (key, stale) => set((s) => {
|
|
176
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
177
|
-
s.lists[key].stale = stale;
|
|
178
|
-
}),
|
|
179
|
-
invalidateEntity: (type, id) => set((s) => {
|
|
180
|
-
if (id) {
|
|
181
|
-
const k = ek(type, id);
|
|
182
|
-
if (s.entityStates[k]) s.entityStates[k].stale = true;
|
|
183
|
-
} else {
|
|
184
|
-
for (const k of Object.keys(s.entityStates)) if (k.startsWith(`${type}:`)) s.entityStates[k].stale = true;
|
|
185
|
-
}
|
|
186
|
-
}),
|
|
187
|
-
invalidateLists: (matcher) => set((s) => {
|
|
188
|
-
const pred = typeof matcher === "string" ? (k) => k.startsWith(matcher) : matcher;
|
|
189
|
-
for (const key of Object.keys(s.lists)) if (pred(key)) s.lists[key].stale = true;
|
|
190
|
-
}),
|
|
191
|
-
invalidateType: (type) => {
|
|
192
|
-
get().invalidateEntity(type);
|
|
193
|
-
get().invalidateLists(type);
|
|
194
|
-
},
|
|
195
|
-
readEntity: (type, id) => {
|
|
196
|
-
const s = get();
|
|
197
|
-
const base = s.entities[type]?.[id];
|
|
198
|
-
if (!base) return null;
|
|
199
|
-
const patch = s.patches[type]?.[id];
|
|
200
|
-
return patch ? { ...base, ...patch } : base;
|
|
201
|
-
},
|
|
202
|
-
readEntitySnapshot: (type, id) => {
|
|
203
|
-
const s = get();
|
|
204
|
-
const base = s.entities[type]?.[id];
|
|
205
|
-
if (!base) return null;
|
|
206
|
-
const patch = s.patches[type]?.[id];
|
|
207
|
-
const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
|
|
208
|
-
return {
|
|
209
|
-
...patch ? { ...base, ...patch } : base,
|
|
210
|
-
$synced: metadata.synced,
|
|
211
|
-
$origin: metadata.origin,
|
|
212
|
-
$updatedAt: metadata.updatedAt
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
}))
|
|
216
|
-
)
|
|
217
|
-
);
|
|
218
77
|
|
|
219
|
-
// src/
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (opts.where) rows = rows.filter(opts.where);
|
|
225
|
-
if (opts.sort) rows = [...rows].sort(opts.sort);
|
|
226
|
-
const projected = rows.map((row) => applySelection(projectRow(row, opts.include, store), opts.select));
|
|
227
|
-
if (opts.id) return projected[0] ?? null;
|
|
228
|
-
return projected;
|
|
229
|
-
}
|
|
230
|
-
var selectGraph = queryOnce;
|
|
231
|
-
function resolveCandidateIds(store, opts) {
|
|
232
|
-
if (opts.id) return [opts.id];
|
|
233
|
-
if (opts.ids) return opts.ids;
|
|
234
|
-
if (opts.listKey) return store.lists[opts.listKey]?.ids ?? [];
|
|
235
|
-
return Object.keys(store.entities[opts.type] ?? {});
|
|
236
|
-
}
|
|
237
|
-
function projectRow(row, include, store) {
|
|
238
|
-
if (!include) return row;
|
|
239
|
-
const projected = { ...row };
|
|
240
|
-
for (const [key, relation] of Object.entries(include)) {
|
|
241
|
-
const related = resolveRelation(row, relation, store);
|
|
242
|
-
projected[key] = related;
|
|
243
|
-
}
|
|
244
|
-
return projected;
|
|
78
|
+
// src/view/prisma-compile.ts
|
|
79
|
+
function nestWhereField(parts, leaf) {
|
|
80
|
+
if (parts.length === 0) return {};
|
|
81
|
+
if (parts.length === 1) return { [parts[0]]: leaf };
|
|
82
|
+
return { [parts[0]]: nestWhereField(parts.slice(1), leaf) };
|
|
245
83
|
}
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
case "
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
case "
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
84
|
+
function clauseToPrismaLeaf(c) {
|
|
85
|
+
switch (c.op) {
|
|
86
|
+
case "eq":
|
|
87
|
+
return { equals: c.value };
|
|
88
|
+
case "neq":
|
|
89
|
+
return { not: c.value };
|
|
90
|
+
case "gt":
|
|
91
|
+
return { gt: c.value };
|
|
92
|
+
case "gte":
|
|
93
|
+
return { gte: c.value };
|
|
94
|
+
case "lt":
|
|
95
|
+
return { lt: c.value };
|
|
96
|
+
case "lte":
|
|
97
|
+
return { lte: c.value };
|
|
98
|
+
case "contains":
|
|
99
|
+
return { contains: c.value, mode: "insensitive" };
|
|
100
|
+
case "startsWith":
|
|
101
|
+
return { startsWith: c.value, mode: "insensitive" };
|
|
102
|
+
case "endsWith":
|
|
103
|
+
return { endsWith: c.value, mode: "insensitive" };
|
|
104
|
+
case "in":
|
|
105
|
+
return { in: c.value };
|
|
106
|
+
case "nin":
|
|
107
|
+
return { notIn: c.value };
|
|
108
|
+
case "arrayContains":
|
|
109
|
+
return { has: c.value };
|
|
110
|
+
case "between":
|
|
111
|
+
case "arrayOverlaps":
|
|
112
|
+
case "matches":
|
|
113
|
+
case "custom":
|
|
114
|
+
default:
|
|
115
|
+
return null;
|
|
275
116
|
}
|
|
276
117
|
}
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
118
|
+
function clauseToPrismaEntry(c) {
|
|
119
|
+
const parts = c.field.split(".").filter(Boolean);
|
|
120
|
+
if (parts.length === 0) return null;
|
|
121
|
+
if (c.op === "isNull") {
|
|
122
|
+
const equalsNull = c.value === void 0 || c.value === true;
|
|
123
|
+
return nestWhereField(parts, equalsNull ? null : { not: null });
|
|
282
124
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (key in row) picked[key] = row[key];
|
|
125
|
+
if (c.op === "isNotNull") {
|
|
126
|
+
return nestWhereField(parts, { not: null });
|
|
286
127
|
}
|
|
287
|
-
|
|
128
|
+
const leaf = clauseToPrismaLeaf(c);
|
|
129
|
+
if (leaf === null) return null;
|
|
130
|
+
return nestWhereField(parts, leaf);
|
|
288
131
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
useGraphStore.getState().upsertEntity(type, id, data);
|
|
299
|
-
return tx;
|
|
300
|
-
},
|
|
301
|
-
replaceEntity(type, id, data) {
|
|
302
|
-
useGraphStore.getState().replaceEntity(type, id, data);
|
|
303
|
-
return tx;
|
|
304
|
-
},
|
|
305
|
-
removeEntity(type, id) {
|
|
306
|
-
useGraphStore.getState().removeEntity(type, id);
|
|
307
|
-
return tx;
|
|
308
|
-
},
|
|
309
|
-
patchEntity(type, id, patch) {
|
|
310
|
-
useGraphStore.getState().patchEntity(type, id, patch);
|
|
311
|
-
return tx;
|
|
312
|
-
},
|
|
313
|
-
clearPatch(type, id) {
|
|
314
|
-
useGraphStore.getState().clearPatch(type, id);
|
|
315
|
-
return tx;
|
|
316
|
-
},
|
|
317
|
-
insertIdInList(key, id, position) {
|
|
318
|
-
useGraphStore.getState().insertIdInList(key, id, position);
|
|
319
|
-
return tx;
|
|
320
|
-
},
|
|
321
|
-
removeIdFromAllLists(type, id) {
|
|
322
|
-
useGraphStore.getState().removeIdFromAllLists(type, id);
|
|
323
|
-
return tx;
|
|
324
|
-
},
|
|
325
|
-
setEntitySyncMetadata(type, id, metadata) {
|
|
326
|
-
useGraphStore.getState().setEntitySyncMetadata(type, id, metadata);
|
|
327
|
-
return tx;
|
|
328
|
-
},
|
|
329
|
-
markEntityPending(type, id, origin = "optimistic") {
|
|
330
|
-
useGraphStore.getState().setEntitySyncMetadata(type, id, {
|
|
331
|
-
synced: false,
|
|
332
|
-
origin,
|
|
333
|
-
updatedAt: Date.now()
|
|
334
|
-
});
|
|
335
|
-
return tx;
|
|
336
|
-
},
|
|
337
|
-
markEntitySynced(type, id, origin = "server") {
|
|
338
|
-
useGraphStore.getState().setEntitySyncMetadata(type, id, {
|
|
339
|
-
synced: true,
|
|
340
|
-
origin,
|
|
341
|
-
updatedAt: Date.now()
|
|
342
|
-
});
|
|
343
|
-
return tx;
|
|
344
|
-
},
|
|
345
|
-
commit() {
|
|
346
|
-
closed = true;
|
|
347
|
-
},
|
|
348
|
-
rollback() {
|
|
349
|
-
if (closed) return;
|
|
350
|
-
useGraphStore.setState(cloneGraphData(baseline));
|
|
351
|
-
closed = true;
|
|
352
|
-
},
|
|
353
|
-
snapshot() {
|
|
354
|
-
return cloneGraphData();
|
|
132
|
+
function groupToPrismaWhere(g) {
|
|
133
|
+
const parts = [];
|
|
134
|
+
for (const item of g.clauses) {
|
|
135
|
+
if ("logic" in item) {
|
|
136
|
+
const nested = groupToPrismaWhere(item);
|
|
137
|
+
if (Object.keys(nested).length > 0) parts.push(nested);
|
|
138
|
+
} else {
|
|
139
|
+
const entry = clauseToPrismaEntry(item);
|
|
140
|
+
if (entry) parts.push(entry);
|
|
355
141
|
}
|
|
356
|
-
};
|
|
357
|
-
return tx;
|
|
358
|
-
}
|
|
359
|
-
function createGraphAction(opts) {
|
|
360
|
-
if (opts.key) {
|
|
361
|
-
graphActionReplayers.set(opts.key, async (record) => {
|
|
362
|
-
const tx = createGraphTransaction();
|
|
363
|
-
try {
|
|
364
|
-
const result = await opts.run(tx, record.input);
|
|
365
|
-
tx.commit();
|
|
366
|
-
return result;
|
|
367
|
-
} catch (error) {
|
|
368
|
-
tx.rollback();
|
|
369
|
-
throw error;
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
142
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
id: `${opts.key}:${Date.now()}`,
|
|
377
|
-
key: opts.key,
|
|
378
|
-
input: structuredClone(input),
|
|
379
|
-
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
380
|
-
} : null;
|
|
381
|
-
try {
|
|
382
|
-
if (record) emitGraphActionEvent({ type: "enqueued", record });
|
|
383
|
-
opts.optimistic?.(tx, input);
|
|
384
|
-
const result = await opts.run(tx, input);
|
|
385
|
-
opts.onSuccess?.(result, input, tx);
|
|
386
|
-
tx.commit();
|
|
387
|
-
if (record) emitGraphActionEvent({ type: "settled", record });
|
|
388
|
-
return result;
|
|
389
|
-
} catch (error) {
|
|
390
|
-
tx.rollback();
|
|
391
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
392
|
-
if (record) emitGraphActionEvent({ type: "settled", record });
|
|
393
|
-
opts.onError?.(normalized, input);
|
|
394
|
-
throw normalized;
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function subscribeGraphActionEvents(listener) {
|
|
399
|
-
graphActionListeners.add(listener);
|
|
400
|
-
return () => graphActionListeners.delete(listener);
|
|
401
|
-
}
|
|
402
|
-
async function replayRegisteredGraphAction(record) {
|
|
403
|
-
const replayer = graphActionReplayers.get(record.key);
|
|
404
|
-
if (!replayer) throw new Error(`No graph action registered for key "${record.key}"`);
|
|
405
|
-
return replayer(record);
|
|
143
|
+
if (parts.length === 0) return {};
|
|
144
|
+
if (parts.length === 1) return parts[0];
|
|
145
|
+
return g.logic === "or" ? { OR: parts } : { AND: parts };
|
|
406
146
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
147
|
+
function toPrismaWhere(filter) {
|
|
148
|
+
if (Array.isArray(filter)) {
|
|
149
|
+
const parts = [];
|
|
150
|
+
for (const item of filter) {
|
|
151
|
+
const entry = clauseToPrismaEntry(item);
|
|
152
|
+
if (entry) parts.push(entry);
|
|
153
|
+
}
|
|
154
|
+
if (parts.length === 0) return {};
|
|
155
|
+
if (parts.length === 1) return parts[0];
|
|
156
|
+
return { AND: parts };
|
|
157
|
+
}
|
|
158
|
+
return groupToPrismaWhere(filter);
|
|
415
159
|
}
|
|
416
|
-
function
|
|
417
|
-
|
|
160
|
+
function toPrismaOrderBy(sort) {
|
|
161
|
+
return sort.map((s) => ({ [s.field]: s.direction }));
|
|
418
162
|
}
|
|
419
163
|
|
|
420
|
-
// src/
|
|
421
|
-
function
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
nextValues.forEach((value, index) => {
|
|
430
|
-
next.set(getKey(value, index), value);
|
|
431
|
-
});
|
|
432
|
-
if (!initialized) {
|
|
433
|
-
initialized = true;
|
|
434
|
-
previous = next;
|
|
435
|
-
if (opts.skipInitial) return;
|
|
436
|
-
}
|
|
437
|
-
for (const [key, value] of next.entries()) {
|
|
438
|
-
const previousValue = previous.get(key);
|
|
439
|
-
if (previousValue === void 0) {
|
|
440
|
-
opts.onEnter?.({ key, value });
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (!isEqual(previousValue, value)) {
|
|
444
|
-
opts.onUpdate?.({ key, value, previousValue });
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
for (const [key, previousValue] of previous.entries()) {
|
|
448
|
-
if (!next.has(key)) opts.onExit?.({ key, previousValue });
|
|
164
|
+
// src/view/types.ts
|
|
165
|
+
function toRestParams(view) {
|
|
166
|
+
const params = {};
|
|
167
|
+
if (view.filter) {
|
|
168
|
+
const clauses = flattenClauses(view.filter);
|
|
169
|
+
for (const c of clauses) {
|
|
170
|
+
if (c.op === "custom") continue;
|
|
171
|
+
const key = c.op === "eq" ? c.field : `${c.field}[${c.op}]`;
|
|
172
|
+
params[key] = Array.isArray(c.value) ? c.value.join(",") : String(c.value ?? "");
|
|
449
173
|
}
|
|
450
|
-
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
174
|
+
}
|
|
175
|
+
if (view.sort) params["sort"] = view.sort.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
|
|
176
|
+
if (view.search?.query) params["q"] = view.search.query;
|
|
177
|
+
return params;
|
|
178
|
+
}
|
|
179
|
+
function toSQLClauses(view) {
|
|
180
|
+
const params = [];
|
|
181
|
+
let paramIdx = 1;
|
|
182
|
+
function clauseToSQL(c) {
|
|
183
|
+
const col = `"${c.field}"`;
|
|
184
|
+
switch (c.op) {
|
|
185
|
+
case "eq":
|
|
186
|
+
params.push(c.value);
|
|
187
|
+
return `${col} = $${paramIdx++}`;
|
|
188
|
+
case "neq":
|
|
189
|
+
params.push(c.value);
|
|
190
|
+
return `${col} != $${paramIdx++}`;
|
|
191
|
+
case "gt":
|
|
192
|
+
params.push(c.value);
|
|
193
|
+
return `${col} > $${paramIdx++}`;
|
|
194
|
+
case "gte":
|
|
195
|
+
params.push(c.value);
|
|
196
|
+
return `${col} >= $${paramIdx++}`;
|
|
197
|
+
case "lt":
|
|
198
|
+
params.push(c.value);
|
|
199
|
+
return `${col} < $${paramIdx++}`;
|
|
200
|
+
case "lte":
|
|
201
|
+
params.push(c.value);
|
|
202
|
+
return `${col} <= $${paramIdx++}`;
|
|
203
|
+
case "in":
|
|
204
|
+
params.push(c.value);
|
|
205
|
+
return `${col} = ANY($${paramIdx++})`;
|
|
206
|
+
case "nin":
|
|
207
|
+
params.push(c.value);
|
|
208
|
+
return `${col} != ALL($${paramIdx++})`;
|
|
209
|
+
case "isNull":
|
|
210
|
+
return `${col} IS NULL`;
|
|
211
|
+
case "isNotNull":
|
|
212
|
+
return `${col} IS NOT NULL`;
|
|
213
|
+
case "contains":
|
|
214
|
+
params.push(`%${c.value}%`);
|
|
215
|
+
return `${col} ILIKE $${paramIdx++}`;
|
|
216
|
+
case "startsWith":
|
|
217
|
+
params.push(`${c.value}%`);
|
|
218
|
+
return `${col} ILIKE $${paramIdx++}`;
|
|
219
|
+
case "between": {
|
|
220
|
+
const [lo, hi] = c.value;
|
|
221
|
+
params.push(lo, hi);
|
|
222
|
+
return `${col} BETWEEN $${paramIdx++} AND $${paramIdx++}`;
|
|
223
|
+
}
|
|
224
|
+
case "arrayContains":
|
|
225
|
+
params.push(c.value);
|
|
226
|
+
return `$${paramIdx++} = ANY(${col})`;
|
|
227
|
+
default:
|
|
228
|
+
return "TRUE";
|
|
459
229
|
}
|
|
460
|
-
}
|
|
230
|
+
}
|
|
231
|
+
function groupToSQL(g) {
|
|
232
|
+
const parts = g.clauses.map((c) => "logic" in c ? `(${groupToSQL(c)})` : clauseToSQL(c));
|
|
233
|
+
return parts.join(` ${g.logic.toUpperCase()} `);
|
|
234
|
+
}
|
|
235
|
+
let where = "TRUE";
|
|
236
|
+
if (view.filter) {
|
|
237
|
+
if (Array.isArray(view.filter)) where = view.filter.map(clauseToSQL).join(" AND ") || "TRUE";
|
|
238
|
+
else where = groupToSQL(view.filter) || "TRUE";
|
|
239
|
+
}
|
|
240
|
+
if (view.search?.query) {
|
|
241
|
+
params.push(`%${view.search.query}%`);
|
|
242
|
+
where += ` AND (${view.search.fields.map((f) => `"${f}"`).join(" || ' ' || ")}) ILIKE $${paramIdx++}`;
|
|
243
|
+
}
|
|
244
|
+
const orderBy = view.sort ? view.sort.map((s) => `"${s.field}" ${s.direction.toUpperCase()}${s.nulls ? ` NULLS ${s.nulls.toUpperCase()}` : ""}`).join(", ") : "";
|
|
245
|
+
return { where, orderBy, params };
|
|
461
246
|
}
|
|
462
|
-
function
|
|
463
|
-
|
|
464
|
-
|
|
247
|
+
function toGraphQLVariables(view) {
|
|
248
|
+
const result = {};
|
|
249
|
+
if (view.filter) {
|
|
250
|
+
const clauses = flattenClauses(view.filter);
|
|
251
|
+
const where = {};
|
|
252
|
+
for (const c of clauses) {
|
|
253
|
+
if (c.op === "custom") continue;
|
|
254
|
+
where[c.field] = { [`_${c.op}`]: c.value };
|
|
255
|
+
}
|
|
256
|
+
if (Object.keys(where).length) result.where = where;
|
|
257
|
+
}
|
|
258
|
+
if (view.sort) result.orderBy = view.sort.map((s) => ({ [s.field]: s.direction === "desc" ? "desc_nulls_last" : "asc_nulls_last" }));
|
|
259
|
+
if (view.search?.query) result.search = view.search.query;
|
|
260
|
+
return result;
|
|
465
261
|
}
|
|
466
|
-
function
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (typeof record.$key === "string") return record.$key;
|
|
262
|
+
function flattenClauses(filter) {
|
|
263
|
+
if (Array.isArray(filter)) return filter;
|
|
264
|
+
function walk(g) {
|
|
265
|
+
return g.clauses.flatMap((c) => "logic" in c ? walk(c) : [c]);
|
|
471
266
|
}
|
|
472
|
-
return
|
|
267
|
+
return walk(filter);
|
|
473
268
|
}
|
|
474
|
-
function
|
|
475
|
-
return
|
|
269
|
+
function hasCustomPredicates(filter) {
|
|
270
|
+
return flattenClauses(filter).some((c) => c.op === "custom");
|
|
476
271
|
}
|
|
477
272
|
|
|
478
|
-
// src/
|
|
479
|
-
function
|
|
480
|
-
|
|
273
|
+
// src/transport/rest.ts
|
|
274
|
+
function makeRestTransport(opts) {
|
|
275
|
+
const {
|
|
276
|
+
supabase,
|
|
277
|
+
table,
|
|
278
|
+
select = "*",
|
|
279
|
+
identify = (row) => String(row.id),
|
|
280
|
+
authoritative = false,
|
|
281
|
+
staleTime,
|
|
282
|
+
searchColumns = [],
|
|
283
|
+
defaultLimit = 200
|
|
284
|
+
} = opts;
|
|
285
|
+
return {
|
|
286
|
+
identify,
|
|
287
|
+
authoritative,
|
|
288
|
+
staleTime,
|
|
289
|
+
async list(q) {
|
|
290
|
+
const limit = q.limit ?? defaultLimit;
|
|
291
|
+
const offset = typeof q.cursor === "number" ? q.cursor : 0;
|
|
292
|
+
let builder = supabase.from(table).select(select, { count: offset === 0 ? "exact" : void 0 });
|
|
293
|
+
if (q.filter) builder = applyFilter(builder, q.filter);
|
|
294
|
+
if (q.search && searchColumns.length > 0) {
|
|
295
|
+
const escaped = q.search.replace(/[%,()]/g, "");
|
|
296
|
+
const orClause = searchColumns.map((c) => `${c}.ilike.*${escaped}*`).join(",");
|
|
297
|
+
builder = builder.or(orClause);
|
|
298
|
+
}
|
|
299
|
+
if (q.sort) {
|
|
300
|
+
for (const s of q.sort) {
|
|
301
|
+
builder = builder.order(s.field, {
|
|
302
|
+
ascending: s.direction === "asc",
|
|
303
|
+
nullsFirst: s.nulls === "first"
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
builder = builder.range(offset, offset + limit - 1);
|
|
308
|
+
if (q.signal && builder.abortSignal) builder = builder.abortSignal(q.signal);
|
|
309
|
+
const result = await builder;
|
|
310
|
+
if (result.error) {
|
|
311
|
+
throw mapPostgrestError(result.error, result.status);
|
|
312
|
+
}
|
|
313
|
+
const rows = result.data ?? [];
|
|
314
|
+
const total = typeof result.count === "number" ? result.count : null;
|
|
315
|
+
const nextCursor = rows.length === limit ? offset + limit : null;
|
|
316
|
+
return { rows, total, nextCursor };
|
|
317
|
+
}
|
|
318
|
+
};
|
|
481
319
|
}
|
|
482
|
-
function
|
|
483
|
-
if (!
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (!isObject(current) && !Array.isArray(current)) return void 0;
|
|
488
|
-
current = current[segment];
|
|
320
|
+
function applyFilter(builder, filter) {
|
|
321
|
+
if (!Array.isArray(filter) && filter.logic === "or") {
|
|
322
|
+
const parts = filter.clauses.filter((c) => !("logic" in c)).map((c) => clauseToOrFragment(c)).filter((s) => !!s);
|
|
323
|
+
if (parts.length > 0) return builder.or(parts.join(","));
|
|
324
|
+
return builder;
|
|
489
325
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (segments.length === 0) return source;
|
|
495
|
-
const clone = structuredClone(source);
|
|
496
|
-
let current = clone;
|
|
497
|
-
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
498
|
-
const segment = segments[index];
|
|
499
|
-
const next = current[segment];
|
|
500
|
-
if (!isObject(next)) current[segment] = {};
|
|
501
|
-
current = current[segment];
|
|
326
|
+
const clauses = flattenClauses(filter);
|
|
327
|
+
let b = builder;
|
|
328
|
+
for (const c of clauses) {
|
|
329
|
+
b = applyClause(b, c);
|
|
502
330
|
}
|
|
503
|
-
|
|
504
|
-
return clone;
|
|
331
|
+
return b;
|
|
505
332
|
}
|
|
506
|
-
function
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
333
|
+
function applyClause(builder, c) {
|
|
334
|
+
switch (c.op) {
|
|
335
|
+
case "eq":
|
|
336
|
+
return builder.eq(c.field, c.value);
|
|
337
|
+
case "neq":
|
|
338
|
+
return builder.neq(c.field, c.value);
|
|
339
|
+
case "gt":
|
|
340
|
+
return builder.gt(c.field, c.value);
|
|
341
|
+
case "gte":
|
|
342
|
+
return builder.gte(c.field, c.value);
|
|
343
|
+
case "lt":
|
|
344
|
+
return builder.lt(c.field, c.value);
|
|
345
|
+
case "lte":
|
|
346
|
+
return builder.lte(c.field, c.value);
|
|
347
|
+
case "in":
|
|
348
|
+
return builder.in(c.field, c.value);
|
|
349
|
+
case "isNull":
|
|
350
|
+
return builder.is(c.field, null);
|
|
351
|
+
case "isNotNull":
|
|
352
|
+
return builder.neq(c.field, null);
|
|
353
|
+
case "contains":
|
|
354
|
+
return builder.ilike(c.field, `%${String(c.value)}%`);
|
|
355
|
+
case "startsWith":
|
|
356
|
+
return builder.ilike(c.field, `${String(c.value)}%`);
|
|
357
|
+
case "endsWith":
|
|
358
|
+
return builder.ilike(c.field, `%${String(c.value)}`);
|
|
359
|
+
case "matches":
|
|
360
|
+
return builder.ilike(c.field, String(c.value));
|
|
361
|
+
case "custom":
|
|
362
|
+
return builder;
|
|
363
|
+
// local-only predicate; skip on server
|
|
364
|
+
default:
|
|
365
|
+
return builder;
|
|
514
366
|
}
|
|
515
|
-
if (JSON.stringify(current) !== JSON.stringify(original) && prefix) acc.add(prefix);
|
|
516
|
-
return acc;
|
|
517
|
-
}
|
|
518
|
-
var schemaRegistry = /* @__PURE__ */ new Map();
|
|
519
|
-
function registerEntityJsonSchema(config) {
|
|
520
|
-
const key = registryKey(config.entityType, config.field, config.schemaId);
|
|
521
|
-
schemaRegistry.set(key, config);
|
|
522
|
-
}
|
|
523
|
-
function registerRuntimeSchema(config) {
|
|
524
|
-
registerEntityJsonSchema(config);
|
|
525
367
|
}
|
|
526
|
-
function
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
368
|
+
function clauseToOrFragment(c) {
|
|
369
|
+
const v = encodeURIComponent(String(c.value ?? ""));
|
|
370
|
+
switch (c.op) {
|
|
371
|
+
case "eq":
|
|
372
|
+
return `${c.field}.eq.${v}`;
|
|
373
|
+
case "neq":
|
|
374
|
+
return `${c.field}.neq.${v}`;
|
|
375
|
+
case "gt":
|
|
376
|
+
return `${c.field}.gt.${v}`;
|
|
377
|
+
case "gte":
|
|
378
|
+
return `${c.field}.gte.${v}`;
|
|
379
|
+
case "lt":
|
|
380
|
+
return `${c.field}.lt.${v}`;
|
|
381
|
+
case "lte":
|
|
382
|
+
return `${c.field}.lte.${v}`;
|
|
383
|
+
case "isNull":
|
|
384
|
+
return `${c.field}.is.null`;
|
|
385
|
+
case "isNotNull":
|
|
386
|
+
return `${c.field}.not.is.null`;
|
|
387
|
+
case "contains":
|
|
388
|
+
return `${c.field}.ilike.*${v}*`;
|
|
389
|
+
case "startsWith":
|
|
390
|
+
return `${c.field}.ilike.${v}*`;
|
|
391
|
+
case "endsWith":
|
|
392
|
+
return `${c.field}.ilike.*${v}`;
|
|
393
|
+
default:
|
|
394
|
+
return null;
|
|
532
395
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
396
|
+
}
|
|
397
|
+
function mapPostgrestError(error, status) {
|
|
398
|
+
if (error instanceof TerminalError || error instanceof TransientError) return error;
|
|
399
|
+
const message = error && typeof error === "object" && "message" in error ? String(error.message ?? "Unknown REST error") : String(error);
|
|
400
|
+
if (typeof status === "number") {
|
|
401
|
+
if (status >= 400 && status < 500) {
|
|
402
|
+
return new TerminalError(message, { status, cause: error });
|
|
403
|
+
}
|
|
404
|
+
if (status >= 500) {
|
|
405
|
+
return new TransientError(message, { status, cause: error });
|
|
406
|
+
}
|
|
536
407
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return schema;
|
|
408
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
|
|
409
|
+
if (code === "42P01" || code === "PGRST116" || code === "PGRST106") {
|
|
410
|
+
return new TerminalError(message, { cause: error });
|
|
541
411
|
}
|
|
542
|
-
return
|
|
412
|
+
return toEntityError(error);
|
|
543
413
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
414
|
+
var EMPTY_IDS = [];
|
|
415
|
+
var EMPTY_ENTITY_STATE = {
|
|
416
|
+
isFetching: false,
|
|
417
|
+
lastFetched: null,
|
|
418
|
+
error: null,
|
|
419
|
+
stale: false
|
|
420
|
+
};
|
|
421
|
+
var EMPTY_SYNC_METADATA = {
|
|
422
|
+
synced: true,
|
|
423
|
+
origin: "server",
|
|
424
|
+
updatedAt: null
|
|
425
|
+
};
|
|
426
|
+
var EMPTY_LIST_STATE = {
|
|
427
|
+
ids: EMPTY_IDS,
|
|
428
|
+
total: null,
|
|
429
|
+
nextCursor: null,
|
|
430
|
+
prevCursor: null,
|
|
431
|
+
hasNextPage: false,
|
|
432
|
+
hasPrevPage: false,
|
|
433
|
+
isFetching: false,
|
|
434
|
+
isFetchingMore: false,
|
|
435
|
+
error: null,
|
|
436
|
+
lastError: null,
|
|
437
|
+
lastFetched: null,
|
|
438
|
+
stale: false,
|
|
439
|
+
currentPage: null,
|
|
440
|
+
pageSize: null
|
|
441
|
+
};
|
|
442
|
+
function defaultEntityState() {
|
|
443
|
+
return { ...EMPTY_ENTITY_STATE };
|
|
550
444
|
}
|
|
551
|
-
function
|
|
552
|
-
return
|
|
445
|
+
function defaultSyncMetadata() {
|
|
446
|
+
return { ...EMPTY_SYNC_METADATA };
|
|
553
447
|
}
|
|
554
|
-
function
|
|
555
|
-
return
|
|
556
|
-
{
|
|
557
|
-
scope: opts.scope,
|
|
558
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
559
|
-
data: opts.data,
|
|
560
|
-
schemas: opts.schemas.filter(Boolean)
|
|
561
|
-
},
|
|
562
|
-
null,
|
|
563
|
-
opts.pretty === false ? 0 : 2
|
|
564
|
-
);
|
|
448
|
+
function defaultListState() {
|
|
449
|
+
return { ...EMPTY_LIST_STATE, ids: [] };
|
|
565
450
|
}
|
|
566
|
-
function
|
|
567
|
-
return
|
|
451
|
+
function ek(type, id) {
|
|
452
|
+
return `${type}:${id}`;
|
|
568
453
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
454
|
+
var snapshotCache = /* @__PURE__ */ new Map();
|
|
455
|
+
function readCachedEntitySnapshot(type, id, base, patch, metadata) {
|
|
456
|
+
const key = ek(type, id);
|
|
457
|
+
const cached = snapshotCache.get(key);
|
|
458
|
+
if (cached && cached.base === base && cached.patch === patch && cached.metadata === metadata) {
|
|
459
|
+
return cached.snapshot;
|
|
460
|
+
}
|
|
461
|
+
const snapshot = {
|
|
462
|
+
...patch ? { ...base, ...patch } : base,
|
|
463
|
+
$synced: metadata.synced,
|
|
464
|
+
$origin: metadata.origin,
|
|
465
|
+
$updatedAt: metadata.updatedAt
|
|
466
|
+
};
|
|
467
|
+
snapshotCache.set(key, { base, patch, metadata, snapshot });
|
|
468
|
+
return snapshot;
|
|
573
469
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
470
|
+
var useGraphStore = create()(
|
|
471
|
+
subscribeWithSelector(
|
|
472
|
+
immer((set, get) => ({
|
|
473
|
+
entities: {},
|
|
474
|
+
patches: {},
|
|
475
|
+
entityStates: {},
|
|
476
|
+
syncMetadata: {},
|
|
477
|
+
lists: {},
|
|
478
|
+
upsertEntity: (type, id, data) => set((s) => {
|
|
479
|
+
if (!s.entities[type]) s.entities[type] = {};
|
|
480
|
+
s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
|
|
481
|
+
const key = ek(type, id);
|
|
482
|
+
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
483
|
+
}),
|
|
484
|
+
upsertEntities: (type, entries) => set((s) => {
|
|
485
|
+
if (!s.entities[type]) s.entities[type] = {};
|
|
486
|
+
for (const { id, data } of entries) {
|
|
487
|
+
s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
|
|
488
|
+
const key = ek(type, id);
|
|
489
|
+
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
490
|
+
}
|
|
491
|
+
}),
|
|
492
|
+
replaceEntity: (type, id, data) => set((s) => {
|
|
493
|
+
if (!s.entities[type]) s.entities[type] = {};
|
|
494
|
+
s.entities[type][id] = data;
|
|
495
|
+
const key = ek(type, id);
|
|
496
|
+
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
497
|
+
}),
|
|
498
|
+
removeEntity: (type, id) => set((s) => {
|
|
499
|
+
delete s.entities[type]?.[id];
|
|
500
|
+
delete s.patches[type]?.[id];
|
|
501
|
+
delete s.entityStates[ek(type, id)];
|
|
502
|
+
delete s.syncMetadata[ek(type, id)];
|
|
503
|
+
}),
|
|
504
|
+
patchEntity: (type, id, patch) => set((s) => {
|
|
505
|
+
if (!s.patches[type]) s.patches[type] = {};
|
|
506
|
+
s.patches[type][id] = { ...s.patches[type][id] ?? {}, ...patch };
|
|
507
|
+
}),
|
|
508
|
+
unpatchEntity: (type, id, keys) => set((s) => {
|
|
509
|
+
const p = s.patches[type]?.[id];
|
|
510
|
+
if (!p) return;
|
|
511
|
+
for (const k of keys) delete p[k];
|
|
512
|
+
}),
|
|
513
|
+
clearPatch: (type, id) => set((s) => {
|
|
514
|
+
delete s.patches[type]?.[id];
|
|
515
|
+
}),
|
|
516
|
+
setEntityFetching: (type, id, fetching) => set((s) => {
|
|
517
|
+
const k = ek(type, id);
|
|
518
|
+
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
519
|
+
s.entityStates[k].isFetching = fetching;
|
|
520
|
+
}),
|
|
521
|
+
setEntityError: (type, id, error) => set((s) => {
|
|
522
|
+
const k = ek(type, id);
|
|
523
|
+
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
524
|
+
s.entityStates[k].error = error;
|
|
525
|
+
s.entityStates[k].isFetching = false;
|
|
526
|
+
}),
|
|
527
|
+
setEntityFetched: (type, id) => set((s) => {
|
|
528
|
+
const k = ek(type, id);
|
|
529
|
+
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
530
|
+
s.entityStates[k].lastFetched = Date.now();
|
|
531
|
+
s.entityStates[k].isFetching = false;
|
|
532
|
+
s.entityStates[k].error = null;
|
|
533
|
+
s.entityStates[k].stale = false;
|
|
534
|
+
s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
|
|
535
|
+
}),
|
|
536
|
+
setEntityStale: (type, id, stale) => set((s) => {
|
|
537
|
+
const k = ek(type, id);
|
|
538
|
+
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
539
|
+
s.entityStates[k].stale = stale;
|
|
540
|
+
}),
|
|
541
|
+
setEntitySyncMetadata: (type, id, metadata) => set((s) => {
|
|
542
|
+
const k = ek(type, id);
|
|
543
|
+
s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
|
|
544
|
+
}),
|
|
545
|
+
clearEntitySyncMetadata: (type, id) => set((s) => {
|
|
546
|
+
delete s.syncMetadata[ek(type, id)];
|
|
547
|
+
}),
|
|
548
|
+
setListResult: (key, ids, meta) => set((s) => {
|
|
549
|
+
const ex = s.lists[key] ?? defaultListState();
|
|
550
|
+
s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
|
|
551
|
+
}),
|
|
552
|
+
appendListResult: (key, ids, meta) => set((s) => {
|
|
553
|
+
const ex = s.lists[key] ?? defaultListState();
|
|
554
|
+
s.lists[key] = { ...ex, ...meta, ids: Array.from(/* @__PURE__ */ new Set([...ex.ids, ...ids])), isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
|
|
555
|
+
}),
|
|
556
|
+
prependListResult: (key, ids, meta) => set((s) => {
|
|
557
|
+
const ex = s.lists[key] ?? defaultListState();
|
|
558
|
+
s.lists[key] = { ...ex, ...meta ?? {}, ids: Array.from(/* @__PURE__ */ new Set([...ids, ...ex.ids])), isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
|
|
559
|
+
}),
|
|
560
|
+
removeIdFromAllLists: (_type, id) => set((s) => {
|
|
561
|
+
for (const key of Object.keys(s.lists)) {
|
|
562
|
+
const list = s.lists[key];
|
|
563
|
+
const idx = list.ids.indexOf(id);
|
|
564
|
+
if (idx !== -1) {
|
|
565
|
+
list.ids.splice(idx, 1);
|
|
566
|
+
if (list.total !== null) list.total -= 1;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}),
|
|
570
|
+
insertIdInList: (key, id, position) => set((s) => {
|
|
571
|
+
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
572
|
+
const ids = s.lists[key].ids;
|
|
573
|
+
const ex = ids.indexOf(id);
|
|
574
|
+
if (ex !== -1) ids.splice(ex, 1);
|
|
575
|
+
if (position === "start") ids.unshift(id);
|
|
576
|
+
else if (position === "end") ids.push(id);
|
|
577
|
+
else ids.splice(position, 0, id);
|
|
578
|
+
}),
|
|
579
|
+
setListFetching: (key, fetching) => set((s) => {
|
|
580
|
+
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
581
|
+
s.lists[key].isFetching = fetching;
|
|
582
|
+
}),
|
|
583
|
+
setListFetchingMore: (key, fetchingMore) => set((s) => {
|
|
584
|
+
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
585
|
+
s.lists[key].isFetchingMore = fetchingMore;
|
|
586
|
+
}),
|
|
587
|
+
setListError: (key, error, typed) => set((s) => {
|
|
588
|
+
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
589
|
+
s.lists[key].error = error;
|
|
590
|
+
s.lists[key].lastError = typed ?? (error === null ? null : s.lists[key].lastError);
|
|
591
|
+
s.lists[key].isFetching = false;
|
|
592
|
+
s.lists[key].isFetchingMore = false;
|
|
593
|
+
s.lists[key].lastFetched = Date.now();
|
|
594
|
+
s.lists[key].stale = false;
|
|
595
|
+
}),
|
|
596
|
+
setListStale: (key, stale) => set((s) => {
|
|
597
|
+
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
598
|
+
s.lists[key].stale = stale;
|
|
599
|
+
}),
|
|
600
|
+
invalidateEntity: (type, id) => set((s) => {
|
|
601
|
+
if (id) {
|
|
602
|
+
const k = ek(type, id);
|
|
603
|
+
if (s.entityStates[k]) s.entityStates[k].stale = true;
|
|
604
|
+
} else {
|
|
605
|
+
for (const k of Object.keys(s.entityStates)) if (k.startsWith(`${type}:`)) s.entityStates[k].stale = true;
|
|
606
|
+
}
|
|
607
|
+
}),
|
|
608
|
+
invalidateLists: (matcher) => set((s) => {
|
|
609
|
+
const pred = typeof matcher === "string" ? (k) => k.startsWith(matcher) : matcher;
|
|
610
|
+
for (const key of Object.keys(s.lists)) if (pred(key)) s.lists[key].stale = true;
|
|
611
|
+
}),
|
|
612
|
+
invalidateType: (type) => {
|
|
613
|
+
get().invalidateEntity(type);
|
|
614
|
+
get().invalidateLists(type);
|
|
615
|
+
},
|
|
616
|
+
readEntity: (type, id) => {
|
|
617
|
+
const s = get();
|
|
618
|
+
const base = s.entities[type]?.[id];
|
|
619
|
+
if (!base) return null;
|
|
620
|
+
const patch = s.patches[type]?.[id];
|
|
621
|
+
return patch ? { ...base, ...patch } : base;
|
|
622
|
+
},
|
|
623
|
+
readEntitySnapshot: (type, id) => {
|
|
624
|
+
const s = get();
|
|
625
|
+
const base = s.entities[type]?.[id];
|
|
626
|
+
if (!base) {
|
|
627
|
+
snapshotCache.delete(ek(type, id));
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const patch = s.patches[type]?.[id];
|
|
631
|
+
const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
|
|
632
|
+
return readCachedEntitySnapshot(
|
|
633
|
+
type,
|
|
634
|
+
id,
|
|
635
|
+
base,
|
|
636
|
+
patch,
|
|
637
|
+
metadata
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}))
|
|
641
|
+
)
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
// src/engine.ts
|
|
645
|
+
function serializeKey(key) {
|
|
646
|
+
return JSON.stringify(key, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v);
|
|
647
|
+
}
|
|
648
|
+
function sleep(ms) {
|
|
649
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
650
|
+
}
|
|
651
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
652
|
+
function dedupe(key, fn) {
|
|
653
|
+
if (inflight.has(key)) return inflight.get(key);
|
|
654
|
+
const p = fn().finally(() => inflight.delete(key));
|
|
655
|
+
inflight.set(key, p);
|
|
656
|
+
return p;
|
|
657
|
+
}
|
|
658
|
+
var subscriberStatsListeners = /* @__PURE__ */ new Set();
|
|
659
|
+
function emitSubscriberStatsChange() {
|
|
660
|
+
for (const cb of subscriberStatsListeners) cb();
|
|
661
|
+
}
|
|
662
|
+
var subscribers = /* @__PURE__ */ new Map();
|
|
663
|
+
function registerSubscriber(key) {
|
|
664
|
+
const token = Symbol(key);
|
|
665
|
+
if (!subscribers.has(key)) subscribers.set(key, /* @__PURE__ */ new Set());
|
|
666
|
+
subscribers.get(key).add(token);
|
|
667
|
+
emitSubscriberStatsChange();
|
|
668
|
+
return token;
|
|
669
|
+
}
|
|
670
|
+
function unregisterSubscriber(key, token) {
|
|
671
|
+
const set = subscribers.get(key);
|
|
672
|
+
if (!set) return;
|
|
673
|
+
set.delete(token);
|
|
674
|
+
if (set.size === 0) subscribers.delete(key);
|
|
675
|
+
emitSubscriberStatsChange();
|
|
676
|
+
}
|
|
677
|
+
function hasSubscribers(key) {
|
|
678
|
+
return (subscribers.get(key)?.size ?? 0) > 0;
|
|
679
|
+
}
|
|
680
|
+
var DEFAULT_OPTIONS = {
|
|
681
|
+
defaultStaleTime: 3e4,
|
|
682
|
+
defaultGcTime: 3e5,
|
|
683
|
+
gcInterval: 6e4,
|
|
684
|
+
maxRetries: 3,
|
|
685
|
+
retryBaseDelay: 1e3,
|
|
686
|
+
revalidateOnFocus: true,
|
|
687
|
+
revalidateOnReconnect: true
|
|
688
|
+
};
|
|
689
|
+
var engineOptions = { ...DEFAULT_OPTIONS };
|
|
690
|
+
function subscribeSubscriberStats(onChange) {
|
|
691
|
+
subscriberStatsListeners.add(onChange);
|
|
692
|
+
return () => subscriberStatsListeners.delete(onChange);
|
|
693
|
+
}
|
|
694
|
+
function getActiveSubscriberCount() {
|
|
695
|
+
let n = 0;
|
|
696
|
+
for (const set of subscribers.values()) n += set.size;
|
|
697
|
+
return n;
|
|
698
|
+
}
|
|
699
|
+
var gcIntervalId = null;
|
|
700
|
+
function runGarbageCollection() {
|
|
701
|
+
const store = useGraphStore.getState();
|
|
702
|
+
const { defaultGcTime: gcTime } = getEngineOptions();
|
|
703
|
+
const now = Date.now();
|
|
704
|
+
const toRemove = [];
|
|
705
|
+
for (const type of Object.keys(store.entities)) {
|
|
706
|
+
const bucket = store.entities[type];
|
|
707
|
+
if (!bucket) continue;
|
|
708
|
+
for (const id of Object.keys(bucket)) {
|
|
709
|
+
const key = `${type}:${id}`;
|
|
710
|
+
if (hasSubscribers(key)) continue;
|
|
711
|
+
const patch = store.patches[type]?.[id];
|
|
712
|
+
if (patch !== void 0 && Object.keys(patch).length > 0) continue;
|
|
713
|
+
const entityState = store.entityStates[key];
|
|
714
|
+
if (entityState?.isFetching) continue;
|
|
715
|
+
const lastFetched = entityState?.lastFetched;
|
|
716
|
+
if (lastFetched == null) continue;
|
|
717
|
+
if (now - lastFetched <= gcTime) continue;
|
|
718
|
+
toRemove.push({ type, id });
|
|
580
719
|
}
|
|
581
|
-
|
|
720
|
+
}
|
|
721
|
+
for (const { type, id } of toRemove) {
|
|
722
|
+
store.removeEntity(type, id);
|
|
723
|
+
store.removeIdFromAllLists(type, id);
|
|
724
|
+
}
|
|
582
725
|
}
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
589
|
-
/* @__PURE__ */ jsx(
|
|
590
|
-
"textarea",
|
|
591
|
-
{
|
|
592
|
-
value,
|
|
593
|
-
onChange: (event) => onChange(event.target.value),
|
|
594
|
-
placeholder,
|
|
595
|
-
className: "w-full min-h-[120px] rounded-md border bg-muted/50 px-3 py-2 text-sm resize-y focus:outline-none focus:ring-1 focus:ring-ring transition-colors"
|
|
596
|
-
}
|
|
597
|
-
),
|
|
598
|
-
/* @__PURE__ */ jsx("div", { className: "rounded-md border bg-background px-3 py-2", children: /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value, className: "prose prose-sm max-w-none" }) })
|
|
599
|
-
] });
|
|
726
|
+
function stopGarbageCollector() {
|
|
727
|
+
if (gcIntervalId != null && typeof clearInterval !== "undefined") {
|
|
728
|
+
clearInterval(gcIntervalId);
|
|
729
|
+
gcIntervalId = null;
|
|
730
|
+
}
|
|
600
731
|
}
|
|
601
|
-
function
|
|
602
|
-
|
|
732
|
+
function startGarbageCollector() {
|
|
733
|
+
stopGarbageCollector();
|
|
734
|
+
if (typeof window === "undefined" || typeof setInterval === "undefined") return () => {
|
|
735
|
+
};
|
|
736
|
+
gcIntervalId = setInterval(() => runGarbageCollection(), getEngineOptions().gcInterval);
|
|
737
|
+
return () => stopGarbageCollector();
|
|
603
738
|
}
|
|
604
|
-
function
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
739
|
+
function restartGarbageCollector() {
|
|
740
|
+
startGarbageCollector();
|
|
741
|
+
}
|
|
742
|
+
function configureEngine(opts) {
|
|
743
|
+
engineOptions = { ...DEFAULT_OPTIONS, ...opts };
|
|
744
|
+
restartGarbageCollector();
|
|
745
|
+
}
|
|
746
|
+
function getEngineOptions() {
|
|
747
|
+
return engineOptions;
|
|
748
|
+
}
|
|
749
|
+
async function fetchEntity(opts, engineOpts) {
|
|
750
|
+
const { type, id, fetch: fetch2, normalize, sideEffects, idField = "id" } = opts;
|
|
751
|
+
if (!id) return;
|
|
752
|
+
useGraphStore.getState().setEntityFetching(type, id, true);
|
|
753
|
+
const attempt = async (retries) => {
|
|
754
|
+
try {
|
|
755
|
+
const raw = await fetch2(id);
|
|
756
|
+
const normalized = normalize(raw);
|
|
757
|
+
const resolvedId = normalized[idField] ?? id;
|
|
758
|
+
useGraphStore.getState().upsertEntity(type, resolvedId, normalized);
|
|
759
|
+
useGraphStore.getState().setEntityFetched(type, resolvedId);
|
|
760
|
+
if (sideEffects) sideEffects(raw, useGraphStore);
|
|
761
|
+
opts.onSuccess?.(normalized);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
764
|
+
if (retries < engineOpts.maxRetries) {
|
|
765
|
+
await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
|
|
766
|
+
return attempt(retries + 1);
|
|
617
767
|
}
|
|
618
|
-
|
|
768
|
+
useGraphStore.getState().setEntityError(type, id, error.message);
|
|
769
|
+
opts.onError?.(error);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
await dedupe(`${type}:${id}`, () => attempt(0));
|
|
773
|
+
}
|
|
774
|
+
async function fetchList(opts, params, engineOpts, isLoadMore = false) {
|
|
775
|
+
const { type, queryKey, fetch: fetch2, normalize, sideEffects, mode = "replace" } = opts;
|
|
776
|
+
const key = serializeKey(queryKey);
|
|
777
|
+
const store = useGraphStore.getState();
|
|
778
|
+
if (isLoadMore) store.setListFetchingMore(key, true);
|
|
779
|
+
else store.setListFetching(key, true);
|
|
780
|
+
const attempt = async (retries) => {
|
|
781
|
+
try {
|
|
782
|
+
const response = await fetch2(params);
|
|
783
|
+
const normalized = response.items.map(normalize);
|
|
784
|
+
useGraphStore.getState().upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
|
|
785
|
+
for (const { id } of normalized) useGraphStore.getState().setEntityFetched(type, id);
|
|
786
|
+
const ids = normalized.map(({ id }) => id);
|
|
787
|
+
const meta = { total: response.total ?? null, nextCursor: response.nextCursor ?? null, prevCursor: response.prevCursor ?? null, hasNextPage: response.hasNextPage ?? !!response.nextCursor, hasPrevPage: response.hasPrevPage ?? !!response.prevCursor, currentPage: response.page ?? null, pageSize: response.pageSize ?? null };
|
|
788
|
+
if (mode === "append" && isLoadMore) useGraphStore.getState().appendListResult(key, ids, meta);
|
|
789
|
+
else useGraphStore.getState().setListResult(key, ids, meta);
|
|
790
|
+
if (sideEffects) sideEffects(response.items, useGraphStore);
|
|
791
|
+
opts.onSuccess?.(response);
|
|
792
|
+
} catch (err) {
|
|
793
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
794
|
+
if (retries < engineOpts.maxRetries) {
|
|
795
|
+
await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
|
|
796
|
+
return attempt(retries + 1);
|
|
797
|
+
}
|
|
798
|
+
useGraphStore.getState().setListError(key, error.message);
|
|
799
|
+
opts.onError?.(error);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
await dedupe(isLoadMore ? `${key}:more` : key, () => attempt(0));
|
|
803
|
+
}
|
|
804
|
+
var focusListenerAttached = false;
|
|
805
|
+
function attachGlobalListeners() {
|
|
806
|
+
if (typeof window === "undefined" || focusListenerAttached) return;
|
|
807
|
+
focusListenerAttached = true;
|
|
808
|
+
restartGarbageCollector();
|
|
809
|
+
const revalidateAll = () => {
|
|
810
|
+
const state = useGraphStore.getState();
|
|
811
|
+
for (const key of subscribers.keys()) {
|
|
812
|
+
if (!hasSubscribers(key)) continue;
|
|
813
|
+
const colonIdx = key.indexOf(":");
|
|
814
|
+
if (colonIdx === -1) continue;
|
|
815
|
+
const type = key.slice(0, colonIdx);
|
|
816
|
+
const id = key.slice(colonIdx + 1);
|
|
817
|
+
state.setEntityStale(type, id, true);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
if (engineOptions.revalidateOnFocus) {
|
|
821
|
+
window.addEventListener("visibilitychange", () => {
|
|
822
|
+
if (document.visibilityState === "visible") revalidateAll();
|
|
619
823
|
});
|
|
824
|
+
window.addEventListener("focus", revalidateAll);
|
|
620
825
|
}
|
|
621
|
-
|
|
826
|
+
if (engineOptions.revalidateOnReconnect) window.addEventListener("online", revalidateAll);
|
|
622
827
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
828
|
+
|
|
829
|
+
// src/hooks/use-entities.ts
|
|
830
|
+
function sleep2(ms) {
|
|
831
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
832
|
+
}
|
|
833
|
+
function useEntities(type, options = {}) {
|
|
834
|
+
const {
|
|
835
|
+
filter,
|
|
836
|
+
sort,
|
|
837
|
+
search,
|
|
838
|
+
limit,
|
|
839
|
+
cursor,
|
|
840
|
+
enabled = true
|
|
841
|
+
} = options;
|
|
842
|
+
const queryKey = useMemo(
|
|
843
|
+
() => serializeKey([type, { filter, sort, search, limit, cursor }]),
|
|
844
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
845
|
+
[type, JSON.stringify({ filter, sort, search, limit, cursor })]
|
|
846
|
+
);
|
|
847
|
+
const abortRef = useRef(null);
|
|
848
|
+
const fetchCountRef = useRef(0);
|
|
849
|
+
const [fetchTick, setFetchTick] = useState(0);
|
|
850
|
+
const listState = useStore(
|
|
851
|
+
useGraphStore,
|
|
852
|
+
useCallback((s) => s.lists[queryKey] ?? EMPTY_LIST_STATE, [queryKey])
|
|
853
|
+
);
|
|
854
|
+
const items = useMemo(() => {
|
|
855
|
+
const state = useGraphStore.getState();
|
|
856
|
+
return listState.ids.map((id) => state.readEntity(type, id)).filter((item) => item !== null);
|
|
857
|
+
}, [listState.ids, type]);
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
if (!enabled) return;
|
|
860
|
+
const store = useGraphStore.getState();
|
|
861
|
+
const existing = store.lists[queryKey];
|
|
862
|
+
const engineOpts = getEngineOptions();
|
|
863
|
+
let transport;
|
|
864
|
+
try {
|
|
865
|
+
transport = getEntityTransport(type);
|
|
866
|
+
} catch {
|
|
867
|
+
const err = new TerminalError(`No transport registered for entity type "${type}"`);
|
|
868
|
+
store.setListError(queryKey, err.message, err);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const effectiveStaleTime = transport.staleTime ?? engineOpts.defaultStaleTime;
|
|
872
|
+
if (existing?.lastFetched !== null && existing?.lastFetched !== void 0 && !existing.stale && Date.now() - existing.lastFetched < effectiveStaleTime) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
abortRef.current?.abort();
|
|
876
|
+
const controller = new AbortController();
|
|
877
|
+
abortRef.current = controller;
|
|
878
|
+
const thisCount = ++fetchCountRef.current;
|
|
879
|
+
const query = {
|
|
880
|
+
filter: filter ?? void 0,
|
|
881
|
+
sort: sort ?? void 0,
|
|
882
|
+
search,
|
|
883
|
+
limit,
|
|
884
|
+
cursor: cursor ?? void 0,
|
|
885
|
+
signal: controller.signal
|
|
886
|
+
};
|
|
887
|
+
const maxRetries = engineOpts.maxRetries ?? 3;
|
|
888
|
+
const retryBaseDelay = engineOpts.retryBaseDelay ?? 1e3;
|
|
889
|
+
store.setListFetching(queryKey, true);
|
|
890
|
+
const attempt = async (retries) => {
|
|
891
|
+
try {
|
|
892
|
+
const result = await transport.list(query);
|
|
893
|
+
if (thisCount !== fetchCountRef.current) return;
|
|
894
|
+
if (controller.signal.aborted) return;
|
|
895
|
+
const graphStore = useGraphStore.getState();
|
|
896
|
+
const entries = result.rows.map((row) => ({
|
|
897
|
+
id: transport.identify(row),
|
|
898
|
+
data: row
|
|
899
|
+
}));
|
|
900
|
+
graphStore.upsertEntities(type, entries);
|
|
901
|
+
for (const { id } of entries) graphStore.setEntityFetched(type, id);
|
|
902
|
+
const ids = entries.map(({ id }) => id);
|
|
903
|
+
graphStore.setListResult(queryKey, ids, {
|
|
904
|
+
total: result.total,
|
|
905
|
+
nextCursor: typeof result.nextCursor === "string" ? result.nextCursor : null,
|
|
906
|
+
hasNextPage: result.nextCursor !== null && result.nextCursor !== void 0
|
|
907
|
+
});
|
|
908
|
+
} catch (err) {
|
|
909
|
+
if (thisCount !== fetchCountRef.current) return;
|
|
910
|
+
if (controller.signal.aborted) return;
|
|
911
|
+
const typed = toEntityError(err);
|
|
912
|
+
if (typed instanceof TransientError && retries < maxRetries) {
|
|
913
|
+
await sleep2(retryBaseDelay * Math.pow(2, retries));
|
|
914
|
+
if (controller.signal.aborted) return;
|
|
915
|
+
return attempt(retries + 1);
|
|
916
|
+
}
|
|
917
|
+
useGraphStore.getState().setListError(queryKey, typed.message, typed);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
attempt(0);
|
|
921
|
+
return () => {
|
|
922
|
+
abortRef.current?.abort();
|
|
923
|
+
};
|
|
924
|
+
}, [queryKey, enabled, fetchTick]);
|
|
925
|
+
useEffect(() => {
|
|
926
|
+
let transport;
|
|
927
|
+
try {
|
|
928
|
+
transport = getEntityTransport(type);
|
|
929
|
+
} catch {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (!transport.subscribe || !enabled) return;
|
|
933
|
+
const unsub = transport.subscribe((ev) => {
|
|
934
|
+
const store = useGraphStore.getState();
|
|
935
|
+
if (ev.op === "delete") {
|
|
936
|
+
store.removeIdFromAllLists(type, ev.id);
|
|
937
|
+
store.removeEntity(type, ev.id);
|
|
938
|
+
} else if (ev.row) {
|
|
939
|
+
store.upsertEntity(type, ev.id, ev.row);
|
|
940
|
+
store.setEntityFetched(type, ev.id);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
return () => unsub();
|
|
944
|
+
}, [type, enabled]);
|
|
945
|
+
const refetch = useCallback(() => {
|
|
946
|
+
abortRef.current?.abort();
|
|
947
|
+
useGraphStore.getState().setListStale(queryKey, true);
|
|
948
|
+
setFetchTick((n) => n + 1);
|
|
949
|
+
}, [queryKey]);
|
|
950
|
+
const typedError = listState.lastError ?? null;
|
|
951
|
+
return {
|
|
952
|
+
items,
|
|
953
|
+
// isLoading = first attempt in-flight with zero cached rows
|
|
954
|
+
isLoading: !enabled ? false : listState.lastFetched === null && listState.isFetching,
|
|
955
|
+
isError: typedError !== null,
|
|
956
|
+
error: typedError,
|
|
957
|
+
refetch
|
|
634
958
|
};
|
|
635
|
-
if (schema.enum) {
|
|
636
|
-
descriptor.options = schema.enum.map((value) => ({
|
|
637
|
-
value: String(value),
|
|
638
|
-
label: String(value)
|
|
639
|
-
}));
|
|
640
|
-
}
|
|
641
|
-
if (type === "markdown") {
|
|
642
|
-
descriptor.render = createMarkdownDetailRenderer(field);
|
|
643
|
-
}
|
|
644
|
-
return descriptor;
|
|
645
959
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return
|
|
663
|
-
case "
|
|
664
|
-
|
|
665
|
-
|
|
960
|
+
|
|
961
|
+
// src/view/evaluator.ts
|
|
962
|
+
function matchesFilter(entity, filter) {
|
|
963
|
+
if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
|
|
964
|
+
return matchesGroup(entity, filter);
|
|
965
|
+
}
|
|
966
|
+
function matchesGroup(entity, group) {
|
|
967
|
+
const { logic, clauses } = group;
|
|
968
|
+
if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
969
|
+
return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
970
|
+
}
|
|
971
|
+
function matchesClause(entity, clause) {
|
|
972
|
+
const { field, op, value, predicate } = clause;
|
|
973
|
+
const fv = getNestedValue(entity, field);
|
|
974
|
+
switch (op) {
|
|
975
|
+
case "eq":
|
|
976
|
+
return fv === value;
|
|
977
|
+
case "neq":
|
|
978
|
+
return fv !== value;
|
|
979
|
+
case "gt":
|
|
980
|
+
return fv > value;
|
|
981
|
+
case "gte":
|
|
982
|
+
return fv >= value;
|
|
983
|
+
case "lt":
|
|
984
|
+
return fv < value;
|
|
985
|
+
case "lte":
|
|
986
|
+
return fv <= value;
|
|
987
|
+
case "in":
|
|
988
|
+
return Array.isArray(value) && value.includes(fv);
|
|
989
|
+
case "nin":
|
|
990
|
+
return Array.isArray(value) && !value.includes(fv);
|
|
991
|
+
case "isNull":
|
|
992
|
+
return fv == null;
|
|
993
|
+
case "isNotNull":
|
|
994
|
+
return fv != null;
|
|
995
|
+
case "contains":
|
|
996
|
+
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
|
|
997
|
+
case "startsWith":
|
|
998
|
+
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
|
|
999
|
+
case "endsWith":
|
|
1000
|
+
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
|
|
1001
|
+
case "between": {
|
|
1002
|
+
const [lo, hi] = value;
|
|
1003
|
+
return fv >= lo && fv <= hi;
|
|
1004
|
+
}
|
|
1005
|
+
case "arrayContains":
|
|
1006
|
+
return Array.isArray(fv) && fv.includes(value);
|
|
1007
|
+
case "arrayOverlaps":
|
|
1008
|
+
return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
|
|
1009
|
+
case "matches":
|
|
1010
|
+
return typeof fv === "string" && new RegExp(value).test(fv);
|
|
1011
|
+
case "custom":
|
|
1012
|
+
return predicate ? predicate(fv, entity) : true;
|
|
666
1013
|
default:
|
|
667
|
-
return
|
|
1014
|
+
return true;
|
|
668
1015
|
}
|
|
669
1016
|
}
|
|
670
|
-
function
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1017
|
+
function matchesSearch(entity, query, fields) {
|
|
1018
|
+
if (!query.trim()) return true;
|
|
1019
|
+
const lq = query.toLowerCase();
|
|
1020
|
+
return fields.some((field) => {
|
|
1021
|
+
const v = getNestedValue(entity, field);
|
|
1022
|
+
return typeof v === "string" && v.toLowerCase().includes(lq);
|
|
1023
|
+
});
|
|
675
1024
|
}
|
|
676
|
-
function
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1025
|
+
function compareEntities(a, b, sort) {
|
|
1026
|
+
for (const clause of sort) {
|
|
1027
|
+
const r = compareByClause(a, b, clause);
|
|
1028
|
+
if (r !== 0) return r;
|
|
1029
|
+
}
|
|
1030
|
+
return 0;
|
|
680
1031
|
}
|
|
681
|
-
function
|
|
682
|
-
|
|
1032
|
+
function compareByClause(a, b, clause) {
|
|
1033
|
+
const { field, direction, nulls = "last", comparator } = clause;
|
|
1034
|
+
const av = getNestedValue(a, field);
|
|
1035
|
+
const bv = getNestedValue(b, field);
|
|
1036
|
+
const aNull = av == null;
|
|
1037
|
+
const bNull = bv == null;
|
|
1038
|
+
if (aNull && bNull) return 0;
|
|
1039
|
+
if (aNull) return nulls === "first" ? -1 : 1;
|
|
1040
|
+
if (bNull) return nulls === "first" ? 1 : -1;
|
|
1041
|
+
let cmp;
|
|
1042
|
+
if (comparator) cmp = comparator(av, bv);
|
|
1043
|
+
else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
|
|
1044
|
+
else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
|
|
1045
|
+
else cmp = String(av).localeCompare(String(bv));
|
|
1046
|
+
return direction === "desc" ? -cmp : cmp;
|
|
683
1047
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1048
|
+
function findInsertionIndex(entity, sortedIds, getEntity, sort) {
|
|
1049
|
+
let lo = 0;
|
|
1050
|
+
let hi = sortedIds.length;
|
|
1051
|
+
while (lo < hi) {
|
|
1052
|
+
const mid = lo + hi >>> 1;
|
|
1053
|
+
const me = getEntity(sortedIds[mid]);
|
|
1054
|
+
if (!me) {
|
|
1055
|
+
lo = mid + 1;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (compareEntities(entity, me, sort) <= 0) hi = mid;
|
|
1059
|
+
else lo = mid + 1;
|
|
1060
|
+
}
|
|
1061
|
+
return lo;
|
|
693
1062
|
}
|
|
694
|
-
function
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1063
|
+
function applyView(ids, getEntity, filter, sort, search) {
|
|
1064
|
+
let entries = [];
|
|
1065
|
+
for (const id of ids) {
|
|
1066
|
+
const entity = getEntity(id);
|
|
1067
|
+
if (!entity) continue;
|
|
1068
|
+
entries.push({ id, entity });
|
|
1069
|
+
}
|
|
1070
|
+
if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
|
|
1071
|
+
if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
|
|
1072
|
+
if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
|
|
1073
|
+
return entries.map((e) => e.id);
|
|
700
1074
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
exportGraphSnapshot,
|
|
706
|
-
getEntityJsonSchema,
|
|
707
|
-
exportGraphSnapshotWithSchemas
|
|
708
|
-
});
|
|
1075
|
+
function checkCompleteness(loadedCount, total, hasNextPage) {
|
|
1076
|
+
if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
|
|
1077
|
+
if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
|
|
1078
|
+
return { isComplete: true, reason: "no-more-pages" };
|
|
709
1079
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
lastPersistedAt: null,
|
|
719
|
-
storageKey: null,
|
|
720
|
-
error: null
|
|
721
|
-
},
|
|
722
|
-
setStatus: (status) => set((state) => ({
|
|
723
|
-
status: {
|
|
724
|
-
...state.status,
|
|
725
|
-
...status
|
|
726
|
-
}
|
|
727
|
-
}))
|
|
728
|
-
}));
|
|
729
|
-
var pendingActions = /* @__PURE__ */ new Map();
|
|
730
|
-
function useGraphSyncStatus() {
|
|
731
|
-
return useGraphSyncStatusStore((state) => state.status);
|
|
1080
|
+
function getNestedValue(obj, path) {
|
|
1081
|
+
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
1082
|
+
let current = obj;
|
|
1083
|
+
for (const part of parts) {
|
|
1084
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
1085
|
+
current = current[part];
|
|
1086
|
+
}
|
|
1087
|
+
return current;
|
|
732
1088
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
pendingActions: opts.pendingActions ?? Array.from(pendingActions.values())
|
|
738
|
-
};
|
|
739
|
-
const json = JSON.stringify(payload);
|
|
740
|
-
await opts.storage.set(opts.key, json);
|
|
741
|
-
const persistedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
742
|
-
useGraphSyncStatusStore.getState().setStatus({
|
|
743
|
-
lastPersistedAt: persistedAt,
|
|
744
|
-
storageKey: opts.key,
|
|
745
|
-
pendingActions: payload.pendingActions.length
|
|
746
|
-
});
|
|
747
|
-
return {
|
|
748
|
-
ok: true,
|
|
749
|
-
key: opts.key,
|
|
750
|
-
bytes: json.length,
|
|
751
|
-
persistedAt
|
|
752
|
-
};
|
|
1089
|
+
|
|
1090
|
+
// src/hooks/use-entity-query.ts
|
|
1091
|
+
function sleep3(ms) {
|
|
1092
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
753
1093
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1094
|
+
var EMPTY_ENTITY_BUCKET = {};
|
|
1095
|
+
function useEntityQuery(type, opts = {}) {
|
|
1096
|
+
const {
|
|
1097
|
+
view: initialView = {},
|
|
1098
|
+
mode: forcedMode,
|
|
1099
|
+
enabled = true,
|
|
1100
|
+
initialIds,
|
|
1101
|
+
initialTotal,
|
|
1102
|
+
remoteDebounce = 300
|
|
1103
|
+
} = opts;
|
|
1104
|
+
const engineOpts = getEngineOptions();
|
|
1105
|
+
const optsRef = useRef(opts);
|
|
1106
|
+
optsRef.current = opts;
|
|
1107
|
+
const [liveView, setLiveView] = useState(initialView);
|
|
1108
|
+
const liveViewRef = useRef(liveView);
|
|
1109
|
+
liveViewRef.current = liveView;
|
|
1110
|
+
const abortRef = useRef(null);
|
|
1111
|
+
const fetchCountRef = useRef(0);
|
|
1112
|
+
const [fetchTick, setFetchTick] = useState(0);
|
|
1113
|
+
const [remoteResultKey, setRemoteResultKey] = useState(null);
|
|
1114
|
+
const [isFetchingState, setIsFetchingState] = useState(false);
|
|
1115
|
+
const baseKey = useMemo(
|
|
1116
|
+
() => serializeKey([type, "__base__"]),
|
|
1117
|
+
[type]
|
|
1118
|
+
);
|
|
1119
|
+
const seededRef = useRef(false);
|
|
1120
|
+
if (!seededRef.current && initialIds && initialIds.length > 0) {
|
|
1121
|
+
seededRef.current = true;
|
|
1122
|
+
const store = useGraphStore.getState();
|
|
1123
|
+
if (!store.lists[baseKey]) {
|
|
1124
|
+
store.setListResult(baseKey, initialIds, { total: initialTotal ?? null });
|
|
1125
|
+
}
|
|
764
1126
|
}
|
|
1127
|
+
const listState = useStore(
|
|
1128
|
+
useGraphStore,
|
|
1129
|
+
useCallback((s) => s.lists[baseKey] ?? EMPTY_LIST_STATE, [baseKey])
|
|
1130
|
+
);
|
|
1131
|
+
const remoteListState = useStore(
|
|
1132
|
+
useGraphStore,
|
|
1133
|
+
useCallback(
|
|
1134
|
+
(s) => remoteResultKey ? s.lists[remoteResultKey] ?? null : null,
|
|
1135
|
+
[remoteResultKey]
|
|
1136
|
+
)
|
|
1137
|
+
);
|
|
1138
|
+
const { isComplete } = useMemo(() => {
|
|
1139
|
+
if (!listState) return { isComplete: false };
|
|
1140
|
+
return checkCompleteness(listState.ids.length, listState.total, listState.hasNextPage);
|
|
1141
|
+
}, [listState]);
|
|
1142
|
+
let transport = null;
|
|
765
1143
|
try {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
pendingActions.clear();
|
|
769
|
-
for (const action of parsed.pendingActions ?? []) pendingActions.set(action.id, action);
|
|
770
|
-
const hydratedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
771
|
-
useGraphSyncStatusStore.getState().setStatus({
|
|
772
|
-
lastHydratedAt: hydratedAt,
|
|
773
|
-
storageKey: opts.key,
|
|
774
|
-
pendingActions: pendingActions.size,
|
|
775
|
-
error: null
|
|
776
|
-
});
|
|
777
|
-
return {
|
|
778
|
-
ok: true,
|
|
779
|
-
key: opts.key,
|
|
780
|
-
hydratedAt,
|
|
781
|
-
entityCounts: Object.fromEntries(
|
|
782
|
-
Object.entries(parsed.snapshot.entities).map(([type, entities]) => [type, Object.keys(entities).length])
|
|
783
|
-
),
|
|
784
|
-
pendingActions: Array.from(pendingActions.values())
|
|
785
|
-
};
|
|
786
|
-
} catch (error) {
|
|
787
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
788
|
-
useGraphSyncStatusStore.getState().setStatus({
|
|
789
|
-
phase: "error",
|
|
790
|
-
error: message,
|
|
791
|
-
storageKey: opts.key
|
|
792
|
-
});
|
|
793
|
-
return {
|
|
794
|
-
ok: false,
|
|
795
|
-
key: opts.key,
|
|
796
|
-
hydratedAt: null,
|
|
797
|
-
entityCounts: {},
|
|
798
|
-
error: message
|
|
799
|
-
};
|
|
1144
|
+
transport = getEntityTransport(type);
|
|
1145
|
+
} catch {
|
|
800
1146
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
phase: online ? "ready" : "offline"
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
const ready = (async () => {
|
|
840
|
-
const hydrated = await hydrateGraphFromStorage({ storage: opts.storage, key });
|
|
841
|
-
if (opts.replayPendingActions && hydrated.ok && pendingActions.size > 0) {
|
|
842
|
-
useGraphSyncStatusStore.getState().setStatus({
|
|
843
|
-
phase: "syncing",
|
|
844
|
-
isSynced: false
|
|
845
|
-
});
|
|
846
|
-
for (const action of Array.from(pendingActions.values())) {
|
|
847
|
-
await replayRegisteredGraphAction(action);
|
|
848
|
-
pendingActions.delete(action.id);
|
|
1147
|
+
const completenessMode = useMemo(() => {
|
|
1148
|
+
if (forcedMode) return forcedMode;
|
|
1149
|
+
if (liveView.filter && hasCustomPredicates(liveView.filter)) return "local";
|
|
1150
|
+
if (isComplete) return "local";
|
|
1151
|
+
if (!transport) return "local";
|
|
1152
|
+
return "hybrid";
|
|
1153
|
+
}, [forcedMode, isComplete, liveView.filter, transport?.authoritative]);
|
|
1154
|
+
const localViewIds = useStore(
|
|
1155
|
+
useGraphStore,
|
|
1156
|
+
useShallow((state) => {
|
|
1157
|
+
const list = state.lists[baseKey] ?? EMPTY_LIST_STATE;
|
|
1158
|
+
const sourceIds = completenessMode !== "remote" && remoteResultKey ? state.lists[remoteResultKey]?.ids ?? EMPTY_IDS : list.ids;
|
|
1159
|
+
const getEntity = (id) => state.readEntitySnapshot(type, id);
|
|
1160
|
+
return applyView(
|
|
1161
|
+
sourceIds,
|
|
1162
|
+
getEntity,
|
|
1163
|
+
liveView.filter,
|
|
1164
|
+
liveView.sort,
|
|
1165
|
+
liveView.search?.query ? { query: liveView.search.query, fields: liveView.search.fields } : null
|
|
1166
|
+
);
|
|
1167
|
+
})
|
|
1168
|
+
);
|
|
1169
|
+
const items = useMemo(
|
|
1170
|
+
() => localViewIds.map((id) => useGraphStore.getState().readEntitySnapshot(type, id)).filter((item) => item !== null),
|
|
1171
|
+
[localViewIds, type]
|
|
1172
|
+
);
|
|
1173
|
+
const fireRemoteFetch = useCallback(
|
|
1174
|
+
async (view, cursor) => {
|
|
1175
|
+
let tp;
|
|
1176
|
+
try {
|
|
1177
|
+
tp = getEntityTransport(type);
|
|
1178
|
+
} catch (e) {
|
|
1179
|
+
const err = toEntityError(e);
|
|
1180
|
+
useGraphStore.getState().setListError(baseKey, err.message, err);
|
|
1181
|
+
return;
|
|
849
1182
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1183
|
+
abortRef.current?.abort();
|
|
1184
|
+
const controller = new AbortController();
|
|
1185
|
+
abortRef.current = controller;
|
|
1186
|
+
const thisCount = ++fetchCountRef.current;
|
|
1187
|
+
const rKey = serializeKey([type, "__view__", view, cursor]);
|
|
1188
|
+
setRemoteResultKey(rKey);
|
|
1189
|
+
setIsFetchingState(true);
|
|
1190
|
+
const store = useGraphStore.getState();
|
|
1191
|
+
store.setListFetching(rKey, true);
|
|
1192
|
+
store.setListFetching(baseKey, true);
|
|
1193
|
+
tp.staleTime ?? engineOpts.defaultStaleTime;
|
|
1194
|
+
const maxRetries = engineOpts.maxRetries ?? 3;
|
|
1195
|
+
const retryBaseDelay = engineOpts.retryBaseDelay ?? 1e3;
|
|
1196
|
+
const query = {
|
|
1197
|
+
filter: view.filter,
|
|
1198
|
+
sort: view.sort,
|
|
1199
|
+
search: view.search?.query,
|
|
1200
|
+
cursor,
|
|
1201
|
+
signal: controller.signal
|
|
1202
|
+
};
|
|
1203
|
+
const attempt = async (retries) => {
|
|
1204
|
+
try {
|
|
1205
|
+
const result = await tp.list(query);
|
|
1206
|
+
if (thisCount !== fetchCountRef.current) return;
|
|
1207
|
+
if (controller.signal.aborted) return;
|
|
1208
|
+
const graphStore = useGraphStore.getState();
|
|
1209
|
+
const entries = result.rows.map((row) => ({
|
|
1210
|
+
id: tp.identify(row),
|
|
1211
|
+
data: row
|
|
1212
|
+
}));
|
|
1213
|
+
graphStore.upsertEntities(type, entries);
|
|
1214
|
+
for (const { id } of entries) graphStore.setEntityFetched(type, id);
|
|
1215
|
+
const ids = entries.map(({ id }) => id);
|
|
1216
|
+
if (cursor !== void 0) {
|
|
1217
|
+
graphStore.appendListResult(rKey, ids, {
|
|
1218
|
+
total: result.total,
|
|
1219
|
+
nextCursor: typeof result.nextCursor === "string" ? result.nextCursor : null,
|
|
1220
|
+
hasNextPage: result.nextCursor !== null && result.nextCursor !== void 0
|
|
1221
|
+
});
|
|
1222
|
+
} else {
|
|
1223
|
+
graphStore.setListResult(rKey, ids, {
|
|
1224
|
+
total: result.total,
|
|
1225
|
+
nextCursor: typeof result.nextCursor === "string" ? result.nextCursor : null,
|
|
1226
|
+
hasNextPage: result.nextCursor !== null && result.nextCursor !== void 0
|
|
1227
|
+
});
|
|
1228
|
+
graphStore.setListFetching(baseKey, false);
|
|
1229
|
+
}
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
if (thisCount !== fetchCountRef.current) return;
|
|
1232
|
+
if (controller.signal.aborted) return;
|
|
1233
|
+
const typed = toEntityError(err);
|
|
1234
|
+
if (typed instanceof TransientError && retries < maxRetries) {
|
|
1235
|
+
await sleep3(retryBaseDelay * Math.pow(2, retries));
|
|
1236
|
+
if (controller.signal.aborted) return;
|
|
1237
|
+
return attempt(retries + 1);
|
|
1238
|
+
}
|
|
1239
|
+
const gs = useGraphStore.getState();
|
|
1240
|
+
gs.setListError(rKey, typed.message, typed);
|
|
1241
|
+
gs.setListError(baseKey, typed.message, typed);
|
|
1242
|
+
} finally {
|
|
1243
|
+
if (thisCount === fetchCountRef.current) setIsFetchingState(false);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
void attempt(0);
|
|
873
1247
|
},
|
|
874
|
-
|
|
875
|
-
|
|
1248
|
+
[type, baseKey, engineOpts.defaultStaleTime, engineOpts.maxRetries, engineOpts.retryBaseDelay]
|
|
1249
|
+
);
|
|
1250
|
+
const debounceTimer = useRef(null);
|
|
1251
|
+
useEffect(() => {
|
|
1252
|
+
if (!enabled || completenessMode === "local" || !transport) return;
|
|
1253
|
+
const searchQuery = liveView.search?.query ?? "";
|
|
1254
|
+
const minChars = liveView.search?.minChars ?? 2;
|
|
1255
|
+
if (searchQuery.length > 0 && searchQuery.length < minChars) return;
|
|
1256
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
1257
|
+
debounceTimer.current = setTimeout(
|
|
1258
|
+
() => fireRemoteFetch(liveViewRef.current),
|
|
1259
|
+
remoteDebounce
|
|
1260
|
+
);
|
|
1261
|
+
return () => {
|
|
1262
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
1263
|
+
};
|
|
1264
|
+
}, [liveView, completenessMode, enabled, remoteDebounce]);
|
|
1265
|
+
useEffect(() => {
|
|
1266
|
+
if (!enabled || !transport) return;
|
|
1267
|
+
const store = useGraphStore.getState();
|
|
1268
|
+
const existing = store.lists[baseKey];
|
|
1269
|
+
const effectiveStaleTime = transport.staleTime ?? engineOpts.defaultStaleTime;
|
|
1270
|
+
const isStale = !existing?.lastFetched || existing.stale || Date.now() - (existing.lastFetched ?? 0) > effectiveStaleTime;
|
|
1271
|
+
if (!existing || isStale) {
|
|
1272
|
+
void fireRemoteFetch(liveViewRef.current);
|
|
876
1273
|
}
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1274
|
+
}, [baseKey, enabled, fetchTick]);
|
|
1275
|
+
useEffect(() => {
|
|
1276
|
+
const unsub = useGraphStore.subscribe(
|
|
1277
|
+
(state) => state.entities[type] ?? EMPTY_ENTITY_BUCKET,
|
|
1278
|
+
(newEntities, prevEntities) => {
|
|
1279
|
+
const view = liveViewRef.current;
|
|
1280
|
+
const store = useGraphStore.getState();
|
|
1281
|
+
const list = store.lists[baseKey];
|
|
1282
|
+
if (!list) return;
|
|
1283
|
+
for (const id of /* @__PURE__ */ new Set([
|
|
1284
|
+
...Object.keys(newEntities),
|
|
1285
|
+
...Object.keys(prevEntities)
|
|
1286
|
+
])) {
|
|
1287
|
+
const isPresent = id in newEntities;
|
|
1288
|
+
if (!isPresent) continue;
|
|
1289
|
+
const entity = newEntities[id];
|
|
1290
|
+
const merged = store.readEntitySnapshot(type, id) ?? entity;
|
|
1291
|
+
const matchesCurrentView = (!view.filter || matchesFilter(merged, view.filter)) && (!view.search?.query || matchesSearch(merged, view.search.query, view.search.fields));
|
|
1292
|
+
if (matchesCurrentView && !list.ids.includes(id)) {
|
|
1293
|
+
if (view.sort && view.sort.length > 0) {
|
|
1294
|
+
const idx = findInsertionIndex(
|
|
1295
|
+
merged,
|
|
1296
|
+
list.ids,
|
|
1297
|
+
(eid) => store.readEntitySnapshot(type, eid),
|
|
1298
|
+
view.sort
|
|
1299
|
+
);
|
|
1300
|
+
store.insertIdInList(baseKey, id, idx);
|
|
1301
|
+
} else {
|
|
1302
|
+
store.insertIdInList(baseKey, id, "start");
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
902
1306
|
}
|
|
903
|
-
|
|
904
|
-
|
|
1307
|
+
);
|
|
1308
|
+
return unsub;
|
|
1309
|
+
}, [type, baseKey]);
|
|
1310
|
+
useEffect(() => {
|
|
1311
|
+
let tp;
|
|
1312
|
+
try {
|
|
1313
|
+
tp = getEntityTransport(type);
|
|
1314
|
+
} catch {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (!tp.subscribe || !enabled) return;
|
|
1318
|
+
const unsub = tp.subscribe((ev) => {
|
|
1319
|
+
const store = useGraphStore.getState();
|
|
1320
|
+
if (ev.op === "delete") {
|
|
1321
|
+
store.removeIdFromAllLists(type, ev.id);
|
|
1322
|
+
store.removeEntity(type, ev.id);
|
|
1323
|
+
} else if (ev.row) {
|
|
1324
|
+
store.upsertEntity(type, ev.id, ev.row);
|
|
1325
|
+
store.setEntityFetched(type, ev.id);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
return () => unsub();
|
|
1329
|
+
}, [type, enabled]);
|
|
1330
|
+
const setView = useCallback(
|
|
1331
|
+
(partial) => setLiveView((prev) => ({ ...prev, ...partial })),
|
|
1332
|
+
[]
|
|
1333
|
+
);
|
|
1334
|
+
const setFilter = useCallback(
|
|
1335
|
+
(filter) => setLiveView((prev) => ({ ...prev, filter: filter ?? void 0 })),
|
|
1336
|
+
[]
|
|
1337
|
+
);
|
|
1338
|
+
const setSort = useCallback(
|
|
1339
|
+
(sort) => setLiveView((prev) => ({ ...prev, sort: sort ?? void 0 })),
|
|
1340
|
+
[]
|
|
1341
|
+
);
|
|
1342
|
+
const setSearch = useCallback(
|
|
1343
|
+
(query) => setLiveView((prev) => ({
|
|
1344
|
+
...prev,
|
|
1345
|
+
search: prev.search ? { ...prev.search, query } : { query, fields: [] }
|
|
1346
|
+
})),
|
|
1347
|
+
[]
|
|
1348
|
+
);
|
|
1349
|
+
const clearView = useCallback(() => setLiveView(initialView), [initialView]);
|
|
1350
|
+
const fetchNextPage = useCallback(() => {
|
|
1351
|
+
if (completenessMode === "local" || isFetchingState) return;
|
|
1352
|
+
const cursor = remoteListState?.nextCursor ?? void 0;
|
|
1353
|
+
void fireRemoteFetch(liveViewRef.current, cursor ?? void 0);
|
|
1354
|
+
}, [completenessMode, isFetchingState, remoteListState?.nextCursor, fireRemoteFetch]);
|
|
1355
|
+
const refetch = useCallback(() => {
|
|
1356
|
+
abortRef.current?.abort();
|
|
1357
|
+
useGraphStore.getState().setListStale(baseKey, true);
|
|
1358
|
+
setFetchTick((n) => n + 1);
|
|
1359
|
+
}, [baseKey]);
|
|
1360
|
+
const viewTotal = remoteListState?.total ?? (isComplete ? localViewIds.length : listState?.total ?? null);
|
|
1361
|
+
const baseError = listState?.lastError ?? null;
|
|
1362
|
+
const remoteError = remoteListState?.lastError ?? null;
|
|
1363
|
+
const typedError = remoteError ?? baseError ?? null;
|
|
905
1364
|
return {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1365
|
+
items,
|
|
1366
|
+
viewIds: localViewIds,
|
|
1367
|
+
viewTotal,
|
|
1368
|
+
isLoading: !enabled ? false : items.length === 0 && ((listState?.isFetching ?? false) || isFetchingState),
|
|
1369
|
+
isFetching: (listState?.isFetching ?? false) || isFetchingState,
|
|
1370
|
+
isFetchingMore: remoteListState?.isFetching ?? false,
|
|
1371
|
+
isError: typedError !== null,
|
|
1372
|
+
error: typedError,
|
|
1373
|
+
hasNextPage: completenessMode === "local" ? false : remoteListState?.hasNextPage ?? listState?.hasNextPage ?? false,
|
|
1374
|
+
fetchNextPage,
|
|
1375
|
+
isShowingLocalPending: completenessMode === "hybrid" && isFetchingState && items.length > 0,
|
|
1376
|
+
isLocallyComplete: isComplete,
|
|
1377
|
+
completenessMode,
|
|
1378
|
+
setView,
|
|
1379
|
+
setFilter,
|
|
1380
|
+
setSort,
|
|
1381
|
+
setSearch,
|
|
1382
|
+
clearView,
|
|
1383
|
+
refetch
|
|
909
1384
|
};
|
|
910
1385
|
}
|
|
911
1386
|
|
|
912
|
-
// src/
|
|
913
|
-
function
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const p = fn().finally(() => inflight.delete(key));
|
|
923
|
-
inflight.set(key, p);
|
|
924
|
-
return p;
|
|
925
|
-
}
|
|
926
|
-
var subscriberStatsListeners = /* @__PURE__ */ new Set();
|
|
927
|
-
function emitSubscriberStatsChange() {
|
|
928
|
-
for (const cb of subscriberStatsListeners) cb();
|
|
1387
|
+
// src/graph-query.ts
|
|
1388
|
+
function queryOnce(opts) {
|
|
1389
|
+
const store = useGraphStore.getState();
|
|
1390
|
+
const ids = resolveCandidateIds(store, opts);
|
|
1391
|
+
let rows = ids.map((id) => store.readEntitySnapshot(opts.type, id)).filter((row) => row !== null);
|
|
1392
|
+
if (opts.where) rows = rows.filter(opts.where);
|
|
1393
|
+
if (opts.sort) rows = [...rows].sort(opts.sort);
|
|
1394
|
+
const projected = rows.map((row) => applySelection(projectRow(row, opts.include, store), opts.select));
|
|
1395
|
+
if (opts.id) return projected[0] ?? null;
|
|
1396
|
+
return projected;
|
|
929
1397
|
}
|
|
930
|
-
var
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
return token;
|
|
1398
|
+
var selectGraph = queryOnce;
|
|
1399
|
+
function resolveCandidateIds(store, opts) {
|
|
1400
|
+
if (opts.id) return [opts.id];
|
|
1401
|
+
if (opts.ids) return opts.ids;
|
|
1402
|
+
if (opts.listKey) return store.lists[opts.listKey]?.ids ?? [];
|
|
1403
|
+
return Object.keys(store.entities[opts.type] ?? {});
|
|
937
1404
|
}
|
|
938
|
-
function
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1405
|
+
function projectRow(row, include, store) {
|
|
1406
|
+
if (!include) return row;
|
|
1407
|
+
const projected = { ...row };
|
|
1408
|
+
for (const [key, relation] of Object.entries(include)) {
|
|
1409
|
+
const related = resolveRelation(row, relation, store);
|
|
1410
|
+
projected[key] = related;
|
|
1411
|
+
}
|
|
1412
|
+
return projected;
|
|
944
1413
|
}
|
|
945
|
-
function
|
|
946
|
-
|
|
1414
|
+
function resolveRelation(entity, relation, store) {
|
|
1415
|
+
const include = relation.include;
|
|
1416
|
+
switch (relation.via.kind) {
|
|
1417
|
+
case "field": {
|
|
1418
|
+
const relatedId = entity[relation.via.field];
|
|
1419
|
+
if (typeof relatedId !== "string") return null;
|
|
1420
|
+
const related = store.readEntitySnapshot(relation.type, relatedId);
|
|
1421
|
+
return related ? projectRow(related, include, store) : null;
|
|
1422
|
+
}
|
|
1423
|
+
case "array": {
|
|
1424
|
+
const ids = entity[relation.via.field];
|
|
1425
|
+
if (!Array.isArray(ids)) return [];
|
|
1426
|
+
return ids.map((id) => typeof id === "string" ? store.readEntitySnapshot(relation.type, id) : null).filter((row) => row !== null).map((row) => projectRow(row, include, store));
|
|
1427
|
+
}
|
|
1428
|
+
case "list": {
|
|
1429
|
+
const key = typeof relation.via.key === "function" ? relation.via.key(entity) : relation.via.key;
|
|
1430
|
+
if (!key) return [];
|
|
1431
|
+
const ids = store.lists[key]?.ids ?? [];
|
|
1432
|
+
return ids.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
|
|
1433
|
+
}
|
|
1434
|
+
case "resolver": {
|
|
1435
|
+
const resolved = relation.via.resolve(entity, store);
|
|
1436
|
+
if (Array.isArray(resolved)) {
|
|
1437
|
+
return resolved.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
|
|
1438
|
+
}
|
|
1439
|
+
if (typeof resolved !== "string") return null;
|
|
1440
|
+
const related = store.readEntitySnapshot(relation.type, resolved);
|
|
1441
|
+
return related ? projectRow(related, include, store) : null;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
947
1444
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
subscriberStatsListeners.add(onChange);
|
|
960
|
-
return () => subscriberStatsListeners.delete(onChange);
|
|
1445
|
+
function applySelection(row, select) {
|
|
1446
|
+
if (!select) return row;
|
|
1447
|
+
if (typeof select === "function") {
|
|
1448
|
+
const result = select(row);
|
|
1449
|
+
return result && typeof result === "object" ? result : { value: result };
|
|
1450
|
+
}
|
|
1451
|
+
const picked = {};
|
|
1452
|
+
for (const key of select) {
|
|
1453
|
+
if (key in row) picked[key] = row[key];
|
|
1454
|
+
}
|
|
1455
|
+
return picked;
|
|
961
1456
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1457
|
+
|
|
1458
|
+
// src/graph-actions.ts
|
|
1459
|
+
var graphActionListeners = /* @__PURE__ */ new Set();
|
|
1460
|
+
var graphActionReplayers = /* @__PURE__ */ new Map();
|
|
1461
|
+
function createGraphTransaction() {
|
|
1462
|
+
const baseline = cloneGraphData();
|
|
1463
|
+
let closed = false;
|
|
1464
|
+
const tx = {
|
|
1465
|
+
upsertEntity(type, id, data) {
|
|
1466
|
+
useGraphStore.getState().upsertEntity(type, id, data);
|
|
1467
|
+
return tx;
|
|
1468
|
+
},
|
|
1469
|
+
replaceEntity(type, id, data) {
|
|
1470
|
+
useGraphStore.getState().replaceEntity(type, id, data);
|
|
1471
|
+
return tx;
|
|
1472
|
+
},
|
|
1473
|
+
removeEntity(type, id) {
|
|
1474
|
+
useGraphStore.getState().removeEntity(type, id);
|
|
1475
|
+
return tx;
|
|
1476
|
+
},
|
|
1477
|
+
patchEntity(type, id, patch) {
|
|
1478
|
+
useGraphStore.getState().patchEntity(type, id, patch);
|
|
1479
|
+
return tx;
|
|
1480
|
+
},
|
|
1481
|
+
clearPatch(type, id) {
|
|
1482
|
+
useGraphStore.getState().clearPatch(type, id);
|
|
1483
|
+
return tx;
|
|
1484
|
+
},
|
|
1485
|
+
insertIdInList(key, id, position) {
|
|
1486
|
+
useGraphStore.getState().insertIdInList(key, id, position);
|
|
1487
|
+
return tx;
|
|
1488
|
+
},
|
|
1489
|
+
removeIdFromAllLists(type, id) {
|
|
1490
|
+
useGraphStore.getState().removeIdFromAllLists(type, id);
|
|
1491
|
+
return tx;
|
|
1492
|
+
},
|
|
1493
|
+
setEntitySyncMetadata(type, id, metadata) {
|
|
1494
|
+
useGraphStore.getState().setEntitySyncMetadata(type, id, metadata);
|
|
1495
|
+
return tx;
|
|
1496
|
+
},
|
|
1497
|
+
markEntityPending(type, id, origin = "optimistic") {
|
|
1498
|
+
useGraphStore.getState().setEntitySyncMetadata(type, id, {
|
|
1499
|
+
synced: false,
|
|
1500
|
+
origin,
|
|
1501
|
+
updatedAt: Date.now()
|
|
1502
|
+
});
|
|
1503
|
+
return tx;
|
|
1504
|
+
},
|
|
1505
|
+
markEntitySynced(type, id, origin = "server") {
|
|
1506
|
+
useGraphStore.getState().setEntitySyncMetadata(type, id, {
|
|
1507
|
+
synced: true,
|
|
1508
|
+
origin,
|
|
1509
|
+
updatedAt: Date.now()
|
|
1510
|
+
});
|
|
1511
|
+
return tx;
|
|
1512
|
+
},
|
|
1513
|
+
commit() {
|
|
1514
|
+
closed = true;
|
|
1515
|
+
},
|
|
1516
|
+
rollback() {
|
|
1517
|
+
if (closed) return;
|
|
1518
|
+
useGraphStore.setState(cloneGraphData(baseline));
|
|
1519
|
+
closed = true;
|
|
1520
|
+
},
|
|
1521
|
+
snapshot() {
|
|
1522
|
+
return cloneGraphData();
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
return tx;
|
|
966
1526
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1527
|
+
function createGraphAction(opts) {
|
|
1528
|
+
if (opts.key) {
|
|
1529
|
+
graphActionReplayers.set(opts.key, async (record) => {
|
|
1530
|
+
const tx = createGraphTransaction();
|
|
1531
|
+
try {
|
|
1532
|
+
const result = await opts.run(tx, record.input);
|
|
1533
|
+
tx.commit();
|
|
1534
|
+
return result;
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
tx.rollback();
|
|
1537
|
+
throw error;
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
return async (input) => {
|
|
1542
|
+
const tx = createGraphTransaction();
|
|
1543
|
+
const record = opts.key ? {
|
|
1544
|
+
id: `${opts.key}:${Date.now()}`,
|
|
1545
|
+
key: opts.key,
|
|
1546
|
+
input: structuredClone(input),
|
|
1547
|
+
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1548
|
+
} : null;
|
|
1549
|
+
try {
|
|
1550
|
+
if (record) emitGraphActionEvent({ type: "enqueued", record });
|
|
1551
|
+
opts.optimistic?.(tx, input);
|
|
1552
|
+
const result = await opts.run(tx, input);
|
|
1553
|
+
opts.onSuccess?.(result, input, tx);
|
|
1554
|
+
tx.commit();
|
|
1555
|
+
if (record) emitGraphActionEvent({ type: "settled", record });
|
|
1556
|
+
return result;
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
tx.rollback();
|
|
1559
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1560
|
+
if (record) emitGraphActionEvent({ type: "settled", record });
|
|
1561
|
+
opts.onError?.(normalized, input);
|
|
1562
|
+
throw normalized;
|
|
987
1563
|
}
|
|
988
|
-
}
|
|
989
|
-
for (const { type, id } of toRemove) {
|
|
990
|
-
store.removeEntity(type, id);
|
|
991
|
-
store.removeIdFromAllLists(type, id);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
function stopGarbageCollector() {
|
|
995
|
-
if (gcIntervalId != null && typeof clearInterval !== "undefined") {
|
|
996
|
-
clearInterval(gcIntervalId);
|
|
997
|
-
gcIntervalId = null;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
function startGarbageCollector() {
|
|
1001
|
-
stopGarbageCollector();
|
|
1002
|
-
if (typeof window === "undefined" || typeof setInterval === "undefined") return () => {
|
|
1003
1564
|
};
|
|
1004
|
-
gcIntervalId = setInterval(() => runGarbageCollection(), getEngineOptions().gcInterval);
|
|
1005
|
-
return () => stopGarbageCollector();
|
|
1006
|
-
}
|
|
1007
|
-
function restartGarbageCollector() {
|
|
1008
|
-
startGarbageCollector();
|
|
1009
1565
|
}
|
|
1010
|
-
function
|
|
1011
|
-
|
|
1012
|
-
|
|
1566
|
+
function subscribeGraphActionEvents(listener) {
|
|
1567
|
+
graphActionListeners.add(listener);
|
|
1568
|
+
return () => graphActionListeners.delete(listener);
|
|
1013
1569
|
}
|
|
1014
|
-
function
|
|
1015
|
-
|
|
1570
|
+
async function replayRegisteredGraphAction(record) {
|
|
1571
|
+
const replayer = graphActionReplayers.get(record.key);
|
|
1572
|
+
if (!replayer) throw new Error(`No graph action registered for key "${record.key}"`);
|
|
1573
|
+
return replayer(record);
|
|
1016
1574
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const normalized = normalize(raw);
|
|
1025
|
-
const resolvedId = normalized[idField] ?? id;
|
|
1026
|
-
useGraphStore.getState().upsertEntity(type, resolvedId, normalized);
|
|
1027
|
-
useGraphStore.getState().setEntityFetched(type, resolvedId);
|
|
1028
|
-
if (sideEffects) sideEffects(raw, useGraphStore);
|
|
1029
|
-
opts.onSuccess?.(normalized);
|
|
1030
|
-
} catch (err) {
|
|
1031
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1032
|
-
if (retries < engineOpts.maxRetries) {
|
|
1033
|
-
await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
|
|
1034
|
-
return attempt(retries + 1);
|
|
1035
|
-
}
|
|
1036
|
-
useGraphStore.getState().setEntityError(type, id, error.message);
|
|
1037
|
-
opts.onError?.(error);
|
|
1038
|
-
}
|
|
1575
|
+
function cloneGraphData(source = useGraphStore.getState()) {
|
|
1576
|
+
return {
|
|
1577
|
+
entities: structuredClone(source.entities),
|
|
1578
|
+
patches: structuredClone(source.patches),
|
|
1579
|
+
entityStates: structuredClone(source.entityStates),
|
|
1580
|
+
syncMetadata: structuredClone(source.syncMetadata),
|
|
1581
|
+
lists: structuredClone(source.lists)
|
|
1039
1582
|
};
|
|
1040
|
-
await dedupe(`${type}:${id}`, () => attempt(0));
|
|
1041
1583
|
}
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1584
|
+
function emitGraphActionEvent(event) {
|
|
1585
|
+
for (const listener of graphActionListeners) listener(event);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/graph-effects.ts
|
|
1589
|
+
function createGraphEffect(opts) {
|
|
1590
|
+
const getKey = opts.getKey ?? defaultGetKey;
|
|
1591
|
+
const isEqual = opts.isEqual ?? defaultIsEqual;
|
|
1592
|
+
let initialized = false;
|
|
1593
|
+
let previous = /* @__PURE__ */ new Map();
|
|
1594
|
+
const evaluate = () => {
|
|
1595
|
+
const nextValues = normalizeQueryResult(opts.query());
|
|
1596
|
+
const next = /* @__PURE__ */ new Map();
|
|
1597
|
+
nextValues.forEach((value, index) => {
|
|
1598
|
+
next.set(getKey(value, index), value);
|
|
1599
|
+
});
|
|
1600
|
+
if (!initialized) {
|
|
1601
|
+
initialized = true;
|
|
1602
|
+
previous = next;
|
|
1603
|
+
if (opts.skipInitial) return;
|
|
1604
|
+
}
|
|
1605
|
+
for (const [key, value] of next.entries()) {
|
|
1606
|
+
const previousValue = previous.get(key);
|
|
1607
|
+
if (previousValue === void 0) {
|
|
1608
|
+
opts.onEnter?.({ key, value });
|
|
1609
|
+
continue;
|
|
1065
1610
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1611
|
+
if (!isEqual(previousValue, value)) {
|
|
1612
|
+
opts.onUpdate?.({ key, value, previousValue });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
for (const [key, previousValue] of previous.entries()) {
|
|
1616
|
+
if (!next.has(key)) opts.onExit?.({ key, previousValue });
|
|
1068
1617
|
}
|
|
1618
|
+
previous = next;
|
|
1069
1619
|
};
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const revalidateAll = () => {
|
|
1078
|
-
const state = useGraphStore.getState();
|
|
1079
|
-
for (const key of subscribers.keys()) {
|
|
1080
|
-
if (!hasSubscribers(key)) continue;
|
|
1081
|
-
const colonIdx = key.indexOf(":");
|
|
1082
|
-
if (colonIdx === -1) continue;
|
|
1083
|
-
const type = key.slice(0, colonIdx);
|
|
1084
|
-
const id = key.slice(colonIdx + 1);
|
|
1085
|
-
state.setEntityStale(type, id, true);
|
|
1620
|
+
evaluate();
|
|
1621
|
+
const unsubscribe = useGraphStore.subscribe(() => {
|
|
1622
|
+
evaluate();
|
|
1623
|
+
});
|
|
1624
|
+
return {
|
|
1625
|
+
dispose: () => {
|
|
1626
|
+
unsubscribe();
|
|
1086
1627
|
}
|
|
1087
1628
|
};
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1629
|
+
}
|
|
1630
|
+
function normalizeQueryResult(value) {
|
|
1631
|
+
if (value == null) return [];
|
|
1632
|
+
return Array.isArray(value) ? value : [value];
|
|
1633
|
+
}
|
|
1634
|
+
function defaultGetKey(value, index) {
|
|
1635
|
+
if (value && typeof value === "object") {
|
|
1636
|
+
const record = value;
|
|
1637
|
+
if (typeof record.id === "string") return record.id;
|
|
1638
|
+
if (typeof record.$key === "string") return record.$key;
|
|
1093
1639
|
}
|
|
1094
|
-
|
|
1640
|
+
return String(index);
|
|
1095
1641
|
}
|
|
1096
|
-
function
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1642
|
+
function defaultIsEqual(previousValue, nextValue) {
|
|
1643
|
+
return JSON.stringify(previousValue) === JSON.stringify(nextValue);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/object-path.ts
|
|
1647
|
+
function isObject(value) {
|
|
1648
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1649
|
+
}
|
|
1650
|
+
function getValueAtPath(source, path) {
|
|
1651
|
+
if (!path) return source;
|
|
1652
|
+
const segments = path.split(".").filter(Boolean);
|
|
1653
|
+
let current = source;
|
|
1654
|
+
for (const segment of segments) {
|
|
1655
|
+
if (!isObject(current) && !Array.isArray(current)) return void 0;
|
|
1656
|
+
current = current[segment];
|
|
1105
1657
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1658
|
+
return current;
|
|
1659
|
+
}
|
|
1660
|
+
function setValueAtPath(source, path, value) {
|
|
1661
|
+
const segments = path.split(".").filter(Boolean);
|
|
1662
|
+
if (segments.length === 0) return source;
|
|
1663
|
+
const clone = structuredClone(source);
|
|
1664
|
+
let current = clone;
|
|
1665
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
1666
|
+
const segment = segments[index];
|
|
1667
|
+
const next = current[segment];
|
|
1668
|
+
if (!isObject(next)) current[segment] = {};
|
|
1669
|
+
current = current[segment];
|
|
1670
|
+
}
|
|
1671
|
+
current[segments[segments.length - 1]] = value;
|
|
1672
|
+
return clone;
|
|
1673
|
+
}
|
|
1674
|
+
function collectDirtyPaths(current, original, prefix = "", acc = /* @__PURE__ */ new Set()) {
|
|
1675
|
+
if (isObject(current) && isObject(original)) {
|
|
1676
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(original)]);
|
|
1677
|
+
for (const key of keys) {
|
|
1678
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1679
|
+
collectDirtyPaths(current[key], original[key], nextPrefix, acc);
|
|
1115
1680
|
}
|
|
1681
|
+
return acc;
|
|
1116
1682
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1683
|
+
if (JSON.stringify(current) !== JSON.stringify(original) && prefix) acc.add(prefix);
|
|
1684
|
+
return acc;
|
|
1685
|
+
}
|
|
1686
|
+
var schemaRegistry = /* @__PURE__ */ new Map();
|
|
1687
|
+
function registerEntityJsonSchema(config) {
|
|
1688
|
+
const key = registryKey(config.entityType, config.field, config.schemaId);
|
|
1689
|
+
schemaRegistry.set(key, config);
|
|
1690
|
+
}
|
|
1691
|
+
function registerRuntimeSchema(config) {
|
|
1692
|
+
registerEntityJsonSchema(config);
|
|
1693
|
+
}
|
|
1694
|
+
function getEntityJsonSchema(opts) {
|
|
1695
|
+
const exact = schemaRegistry.get(registryKey(opts.entityType, opts.field, opts.schemaId));
|
|
1696
|
+
if (exact) return exact;
|
|
1697
|
+
if (opts.field) {
|
|
1698
|
+
const byField = schemaRegistry.get(registryKey(opts.entityType, opts.field));
|
|
1699
|
+
if (byField) return byField;
|
|
1700
|
+
}
|
|
1701
|
+
if (opts.schemaId) {
|
|
1702
|
+
const byId = schemaRegistry.get(registryKey(opts.entityType, void 0, opts.schemaId));
|
|
1703
|
+
if (byId) return byId;
|
|
1127
1704
|
}
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return {
|
|
1135
|
-
entityCounts,
|
|
1136
|
-
totalEntities,
|
|
1137
|
-
listCount,
|
|
1138
|
-
patchedEntities,
|
|
1139
|
-
staleEntities,
|
|
1140
|
-
fetchingEntities,
|
|
1141
|
-
lists
|
|
1142
|
-
};
|
|
1705
|
+
for (const schema of schemaRegistry.values()) {
|
|
1706
|
+
if (schema.entityType !== opts.entityType) continue;
|
|
1707
|
+
if (opts.field && schema.field !== opts.field) continue;
|
|
1708
|
+
return schema;
|
|
1709
|
+
}
|
|
1710
|
+
return null;
|
|
1143
1711
|
}
|
|
1144
|
-
function
|
|
1145
|
-
return
|
|
1712
|
+
function useSchemaEntityFields(opts) {
|
|
1713
|
+
return useMemo(() => {
|
|
1714
|
+
const schema = opts.schema ?? getEntityJsonSchema(opts)?.schema;
|
|
1715
|
+
if (!schema) return [];
|
|
1716
|
+
return buildEntityFieldsFromSchema({ schema, rootField: opts.rootField ?? opts.field });
|
|
1717
|
+
}, [opts.entityType, opts.field, opts.rootField, opts.schemaId, opts.schema]);
|
|
1146
1718
|
}
|
|
1147
|
-
function
|
|
1148
|
-
|
|
1149
|
-
subscribeSubscriberStats,
|
|
1150
|
-
getActiveSubscriberCount,
|
|
1151
|
-
subscriberCountServerSnapshot
|
|
1152
|
-
);
|
|
1153
|
-
const entities = useStore(useGraphStore, (state) => state.entities);
|
|
1154
|
-
const patches = useStore(useGraphStore, (state) => state.patches);
|
|
1155
|
-
const entityStates = useStore(useGraphStore, (state) => state.entityStates);
|
|
1156
|
-
const listsState = useStore(useGraphStore, (state) => state.lists);
|
|
1157
|
-
const graphPart = useMemo(
|
|
1158
|
-
() => collectGraphDevStats(entities, patches, entityStates, listsState),
|
|
1159
|
-
[entities, patches, entityStates, listsState]
|
|
1160
|
-
);
|
|
1161
|
-
return { ...graphPart, subscriberCount };
|
|
1719
|
+
function buildEntityFieldsFromSchema(opts) {
|
|
1720
|
+
return buildSchemaFields(opts.schema, opts.rootField ?? "", "");
|
|
1162
1721
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1722
|
+
function exportGraphSnapshotWithSchemas(opts) {
|
|
1723
|
+
return JSON.stringify(
|
|
1724
|
+
{
|
|
1725
|
+
scope: opts.scope,
|
|
1726
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1727
|
+
data: opts.data,
|
|
1728
|
+
schemas: opts.schemas.filter(Boolean)
|
|
1729
|
+
},
|
|
1730
|
+
null,
|
|
1731
|
+
opts.pretty === false ? 0 : 2
|
|
1732
|
+
);
|
|
1169
1733
|
}
|
|
1170
|
-
function
|
|
1171
|
-
|
|
1172
|
-
ensureListeners();
|
|
1173
|
-
const fetchRef = useRef(opts.fetch);
|
|
1174
|
-
fetchRef.current = opts.fetch;
|
|
1175
|
-
const normalizeRef = useRef(opts.normalize);
|
|
1176
|
-
normalizeRef.current = opts.normalize;
|
|
1177
|
-
const dataSelector = useCallback((state) => {
|
|
1178
|
-
if (!id) return null;
|
|
1179
|
-
return state.readEntitySnapshot(type, id);
|
|
1180
|
-
}, [id, type]);
|
|
1181
|
-
const data = useStore(useGraphStore, useShallow(dataSelector));
|
|
1182
|
-
const entityState = useStore(useGraphStore, useCallback(
|
|
1183
|
-
(state) => state.entityStates[`${type}:${id}`] ?? EMPTY_ENTITY_STATE,
|
|
1184
|
-
[type, id]
|
|
1185
|
-
));
|
|
1186
|
-
const doFetch = useCallback(() => {
|
|
1187
|
-
if (!id || !enabled) return;
|
|
1188
|
-
fetchEntity({ type, id, fetch: fetchRef.current, normalize: normalizeRef.current }, getEngineOptions());
|
|
1189
|
-
}, [id, enabled, type]);
|
|
1190
|
-
useEffect(() => {
|
|
1191
|
-
if (!id || !enabled) return;
|
|
1192
|
-
const token = registerSubscriber(`${type}:${id}`);
|
|
1193
|
-
const state = useGraphStore.getState();
|
|
1194
|
-
const existingState = state.entityStates[`${type}:${id}`];
|
|
1195
|
-
const hasData = !!state.entities[type]?.[id];
|
|
1196
|
-
const isStale = !existingState?.lastFetched || existingState.stale || Date.now() - (existingState.lastFetched ?? 0) > staleTime;
|
|
1197
|
-
if (!hasData || isStale) doFetch();
|
|
1198
|
-
return () => unregisterSubscriber(`${type}:${id}`, token);
|
|
1199
|
-
}, [id, type, enabled, staleTime, doFetch]);
|
|
1200
|
-
useEffect(() => {
|
|
1201
|
-
if (entityState.stale && id && enabled && !entityState.isFetching) doFetch();
|
|
1202
|
-
}, [entityState.stale, id, enabled, entityState.isFetching, doFetch]);
|
|
1203
|
-
return { data, isLoading: !data && entityState.isFetching, isFetching: entityState.isFetching, error: entityState.error, isStale: entityState.stale, refetch: doFetch };
|
|
1734
|
+
function escapeHtml(input) {
|
|
1735
|
+
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1204
1736
|
}
|
|
1205
|
-
function
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const fetchRef = useRef(opts.fetch);
|
|
1210
|
-
fetchRef.current = opts.fetch;
|
|
1211
|
-
const normalizeRef = useRef(opts.normalize);
|
|
1212
|
-
normalizeRef.current = opts.normalize;
|
|
1213
|
-
const listState = useStore(useGraphStore, useCallback((state) => state.lists[key] ?? EMPTY_LIST_STATE, [key]));
|
|
1214
|
-
const itemsSelector = useCallback((state) => {
|
|
1215
|
-
const ids = state.lists[key]?.ids ?? EMPTY_IDS;
|
|
1216
|
-
return ids.map((id) => state.readEntitySnapshot(type, id)).filter((x) => x !== null);
|
|
1217
|
-
}, [key, type]);
|
|
1218
|
-
const items = useStore(useGraphStore, useShallow(itemsSelector));
|
|
1219
|
-
const doFetch = useCallback((params = {}) => {
|
|
1220
|
-
if (!enabled) return;
|
|
1221
|
-
fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, params, getEngineOptions(), false);
|
|
1222
|
-
}, [enabled, type, queryKey, mode]);
|
|
1223
|
-
const fetchNextPage = useCallback(() => {
|
|
1224
|
-
if (!listState.hasNextPage || listState.isFetchingMore || !enabled) return;
|
|
1225
|
-
fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, { cursor: listState.nextCursor ?? void 0, page: (listState.currentPage ?? 0) + 1, pageSize: listState.pageSize ?? void 0 }, getEngineOptions(), true);
|
|
1226
|
-
}, [listState.hasNextPage, listState.isFetchingMore, listState.nextCursor, listState.currentPage, listState.pageSize, enabled, type, queryKey, mode]);
|
|
1227
|
-
useEffect(() => {
|
|
1228
|
-
if (!enabled) return;
|
|
1229
|
-
const state = useGraphStore.getState();
|
|
1230
|
-
const existing = state.lists[key];
|
|
1231
|
-
const isStale = !existing?.lastFetched || existing.stale || Date.now() - (existing.lastFetched ?? 0) > staleTime;
|
|
1232
|
-
if (!existing || isStale) doFetch({ page: 1, pageSize: listState.pageSize ?? void 0 });
|
|
1233
|
-
}, [key, enabled, staleTime, doFetch, listState.pageSize]);
|
|
1234
|
-
useEffect(() => {
|
|
1235
|
-
if (listState.stale && enabled && !listState.isFetching) doFetch();
|
|
1236
|
-
}, [listState.stale, enabled, listState.isFetching, doFetch]);
|
|
1237
|
-
return { items, ids: listState.ids, isLoading: listState.ids.length === 0 && listState.isFetching, isFetching: listState.isFetching, isFetchingMore: listState.isFetchingMore, error: listState.error, hasNextPage: listState.hasNextPage, hasPrevPage: listState.hasPrevPage, total: listState.total, currentPage: listState.currentPage, fetchNextPage, refetch: doFetch };
|
|
1737
|
+
function renderMarkdownToHtml(value) {
|
|
1738
|
+
const escaped = escapeHtml(value);
|
|
1739
|
+
const blocks = escaped.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
|
|
1740
|
+
return blocks.map((block) => renderMarkdownBlock(block)).join("");
|
|
1238
1741
|
}
|
|
1239
|
-
function
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
setState({ isPending: true, isSuccess: false, isError: false, error: null });
|
|
1246
|
-
let rollback = null;
|
|
1247
|
-
if (optimistic) {
|
|
1248
|
-
const opt = optimistic(input);
|
|
1249
|
-
if (opt) {
|
|
1250
|
-
const { id, patch } = opt;
|
|
1251
|
-
const store = useGraphStore.getState();
|
|
1252
|
-
const previous = { ...store.patches[type]?.[id] };
|
|
1253
|
-
const previousSync = store.syncMetadata[`${type}:${id}`];
|
|
1254
|
-
store.patchEntity(type, id, patch);
|
|
1255
|
-
store.setEntitySyncMetadata(type, id, { synced: false, origin: "optimistic", updatedAt: Date.now() });
|
|
1256
|
-
rollback = () => {
|
|
1257
|
-
const currentStore = useGraphStore.getState();
|
|
1258
|
-
if (Object.keys(previous).length > 0) currentStore.patchEntity(type, id, previous);
|
|
1259
|
-
else currentStore.clearPatch(type, id);
|
|
1260
|
-
if (previousSync) currentStore.setEntitySyncMetadata(type, id, previousSync);
|
|
1261
|
-
else currentStore.clearEntitySyncMetadata(type, id);
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
try {
|
|
1266
|
-
const result = await apiFn(input);
|
|
1267
|
-
if (normalize) {
|
|
1268
|
-
const { id, data } = normalize(result, input);
|
|
1269
|
-
const store = useGraphStore.getState();
|
|
1270
|
-
store.upsertEntity(type, id, data);
|
|
1271
|
-
store.setEntitySyncMetadata(type, id, { synced: true, origin: "server", updatedAt: Date.now() });
|
|
1272
|
-
if (optimistic) {
|
|
1273
|
-
const opt = optimistic(input);
|
|
1274
|
-
if (opt) store.clearPatch(type, opt.id);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
if (invalidateLists) for (const k of invalidateLists) useGraphStore.getState().invalidateLists(k);
|
|
1278
|
-
if (invalidateEntities) for (const { type: t, id } of invalidateEntities) useGraphStore.getState().invalidateEntity(t, id);
|
|
1279
|
-
setState({ isPending: false, isSuccess: true, isError: false, error: null });
|
|
1280
|
-
onSuccess?.(result, input);
|
|
1281
|
-
return result;
|
|
1282
|
-
} catch (err) {
|
|
1283
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1284
|
-
rollback?.();
|
|
1285
|
-
setState({ isPending: false, isSuccess: false, isError: true, error: error.message });
|
|
1286
|
-
onError?.(error, input);
|
|
1287
|
-
return null;
|
|
1742
|
+
function MarkdownFieldRenderer({ value, className }) {
|
|
1743
|
+
return /* @__PURE__ */ jsx(
|
|
1744
|
+
"div",
|
|
1745
|
+
{
|
|
1746
|
+
className,
|
|
1747
|
+
dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(value ?? "") }
|
|
1288
1748
|
}
|
|
1289
|
-
|
|
1290
|
-
const trigger = useCallback((input) => {
|
|
1291
|
-
void mutate(input);
|
|
1292
|
-
}, [mutate]);
|
|
1293
|
-
const reset = useCallback(() => setState({ isPending: false, isSuccess: false, isError: false, error: null }), []);
|
|
1294
|
-
return { mutate, trigger, reset, state };
|
|
1295
|
-
}
|
|
1296
|
-
function useEntityAugment(type, id) {
|
|
1297
|
-
const patch = useStore(useGraphStore, useCallback((state) => id ? state.patches[type]?.[id] ?? null : null, [type, id]));
|
|
1298
|
-
const augment = useCallback((fields) => {
|
|
1299
|
-
if (!id) return;
|
|
1300
|
-
useGraphStore.getState().patchEntity(type, id, fields);
|
|
1301
|
-
}, [type, id]);
|
|
1302
|
-
const unaugment = useCallback((keys) => {
|
|
1303
|
-
if (!id) return;
|
|
1304
|
-
useGraphStore.getState().unpatchEntity(type, id, keys);
|
|
1305
|
-
}, [type, id]);
|
|
1306
|
-
const clear = useCallback(() => {
|
|
1307
|
-
if (!id) return;
|
|
1308
|
-
useGraphStore.getState().clearPatch(type, id);
|
|
1309
|
-
}, [type, id]);
|
|
1310
|
-
return { patch, augment, unaugment, clear };
|
|
1749
|
+
);
|
|
1311
1750
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
if (settled) return;
|
|
1330
|
-
const hasData = !!state.entities[type]?.[id];
|
|
1331
|
-
const es = state.entityStates[key];
|
|
1332
|
-
if (hasData) settle(() => resolve());
|
|
1333
|
-
else if (es != null && es.error != null && !es.isFetching) {
|
|
1334
|
-
const msg = es.error;
|
|
1335
|
-
settle(() => reject(new Error(msg)));
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
inspect(useGraphStore.getState());
|
|
1339
|
-
if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
|
|
1340
|
-
});
|
|
1341
|
-
const tracked = promise.finally(() => {
|
|
1342
|
-
suspenseEntityPromises.delete(key);
|
|
1343
|
-
});
|
|
1344
|
-
suspenseEntityPromises.set(key, tracked);
|
|
1345
|
-
return tracked;
|
|
1751
|
+
function MarkdownFieldEditor({
|
|
1752
|
+
value,
|
|
1753
|
+
onChange,
|
|
1754
|
+
placeholder
|
|
1755
|
+
}) {
|
|
1756
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
1757
|
+
/* @__PURE__ */ jsx(
|
|
1758
|
+
"textarea",
|
|
1759
|
+
{
|
|
1760
|
+
value,
|
|
1761
|
+
onChange: (event) => onChange(event.target.value),
|
|
1762
|
+
placeholder,
|
|
1763
|
+
className: "w-full min-h-[120px] rounded-md border bg-muted/50 px-3 py-2 text-sm resize-y focus:outline-none focus:ring-1 focus:ring-ring transition-colors"
|
|
1764
|
+
}
|
|
1765
|
+
),
|
|
1766
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-md border bg-background px-3 py-2", children: /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value, className: "prose prose-sm max-w-none" }) })
|
|
1767
|
+
] });
|
|
1346
1768
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1349
|
-
if (existing) return existing;
|
|
1350
|
-
let unsub = null;
|
|
1351
|
-
let settled = false;
|
|
1352
|
-
const promise = new Promise((resolve, reject) => {
|
|
1353
|
-
const settle = (fn) => {
|
|
1354
|
-
if (settled) return;
|
|
1355
|
-
settled = true;
|
|
1356
|
-
unsub?.();
|
|
1357
|
-
unsub = null;
|
|
1358
|
-
fn();
|
|
1359
|
-
};
|
|
1360
|
-
const inspect = (state) => {
|
|
1361
|
-
if (settled) return;
|
|
1362
|
-
const list = state.lists[listKey] ?? EMPTY_LIST_STATE;
|
|
1363
|
-
if (list.ids.length > 0) settle(() => resolve());
|
|
1364
|
-
else if (list.error != null && !list.isFetching) {
|
|
1365
|
-
const msg = list.error;
|
|
1366
|
-
settle(() => reject(new Error(msg)));
|
|
1367
|
-
} else if (list.ids.length === 0 && !list.isFetching && list.lastFetched != null) settle(() => resolve());
|
|
1368
|
-
};
|
|
1369
|
-
inspect(useGraphStore.getState());
|
|
1370
|
-
if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
|
|
1371
|
-
});
|
|
1372
|
-
const tracked = promise.finally(() => {
|
|
1373
|
-
suspenseListPromises.delete(listKey);
|
|
1374
|
-
});
|
|
1375
|
-
suspenseListPromises.set(listKey, tracked);
|
|
1376
|
-
return tracked;
|
|
1769
|
+
function createMarkdownDetailRenderer(field) {
|
|
1770
|
+
return (value, entity) => /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value: String(value ?? getValueAtPath(entity, field) ?? ""), className: "prose prose-sm max-w-none" });
|
|
1377
1771
|
}
|
|
1378
|
-
function
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1772
|
+
function buildSchemaFields(schema, pathPrefix, schemaPathPrefix) {
|
|
1773
|
+
if (schema.type === "object" && schema.properties) {
|
|
1774
|
+
const entries = Object.entries(schema.properties).sort(([, left], [, right]) => {
|
|
1775
|
+
const l = left["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
|
|
1776
|
+
const r = right["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
|
|
1777
|
+
return l - r;
|
|
1778
|
+
});
|
|
1779
|
+
return entries.flatMap(([key, childSchema]) => {
|
|
1780
|
+
if (childSchema["x-hidden"]) return [];
|
|
1781
|
+
const field = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
1782
|
+
const schemaPath = schemaPathPrefix ? `${schemaPathPrefix}.${key}` : key;
|
|
1783
|
+
if (childSchema.type === "object" && childSchema.properties) {
|
|
1784
|
+
return buildSchemaFields(childSchema, field, schemaPath);
|
|
1785
|
+
}
|
|
1786
|
+
return [schemaField(field, schemaPath, childSchema, schema.required?.includes(key) ?? false)];
|
|
1787
|
+
});
|
|
1384
1788
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1789
|
+
return [];
|
|
1790
|
+
}
|
|
1791
|
+
function schemaField(field, schemaPath, schema, required) {
|
|
1792
|
+
const type = inferFieldType(schema);
|
|
1793
|
+
const descriptor = {
|
|
1794
|
+
field,
|
|
1795
|
+
label: schema.title ?? humanize(field.split(".").pop() ?? field),
|
|
1796
|
+
type,
|
|
1797
|
+
required,
|
|
1798
|
+
hint: schema.description,
|
|
1799
|
+
schemaPath,
|
|
1800
|
+
schema,
|
|
1801
|
+
componentHint: schema["x-a2ui-component"]
|
|
1802
|
+
};
|
|
1803
|
+
if (schema.enum) {
|
|
1804
|
+
descriptor.options = schema.enum.map((value) => ({
|
|
1805
|
+
value: String(value),
|
|
1806
|
+
label: String(value)
|
|
1807
|
+
}));
|
|
1387
1808
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1809
|
+
if (type === "markdown") {
|
|
1810
|
+
descriptor.render = createMarkdownDetailRenderer(field);
|
|
1390
1811
|
}
|
|
1391
|
-
return
|
|
1392
|
-
data: result.data,
|
|
1393
|
-
isFetching: result.isFetching,
|
|
1394
|
-
isStale: result.isStale,
|
|
1395
|
-
refetch: result.refetch
|
|
1396
|
-
};
|
|
1812
|
+
return descriptor;
|
|
1397
1813
|
}
|
|
1398
|
-
function
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
if (
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1814
|
+
function inferFieldType(schema) {
|
|
1815
|
+
const forced = schema["x-field-type"];
|
|
1816
|
+
if (forced === "markdown") return "markdown";
|
|
1817
|
+
if (schema.format === "markdown") return "markdown";
|
|
1818
|
+
if (schema.enum) return "enum";
|
|
1819
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
1820
|
+
switch (type) {
|
|
1821
|
+
case "boolean":
|
|
1822
|
+
return "boolean";
|
|
1823
|
+
case "integer":
|
|
1824
|
+
case "number":
|
|
1825
|
+
return "number";
|
|
1826
|
+
case "string":
|
|
1827
|
+
if (schema.format === "email") return "email";
|
|
1828
|
+
if (schema.format === "uri" || schema.format === "url") return "url";
|
|
1829
|
+
if (schema.format === "date" || schema.format === "date-time") return "date";
|
|
1830
|
+
return "text";
|
|
1831
|
+
case "array":
|
|
1832
|
+
case "object":
|
|
1833
|
+
return "json";
|
|
1834
|
+
default:
|
|
1835
|
+
return "text";
|
|
1404
1836
|
}
|
|
1405
|
-
const { isLoading: _isLoading, ...rest } = result;
|
|
1406
|
-
return rest;
|
|
1407
1837
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
function matchesFilter(entity, filter) {
|
|
1411
|
-
if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
|
|
1412
|
-
return matchesGroup(entity, filter);
|
|
1838
|
+
function registryKey(entityType, field, schemaId) {
|
|
1839
|
+
return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
|
|
1413
1840
|
}
|
|
1414
|
-
function
|
|
1415
|
-
|
|
1416
|
-
if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
1417
|
-
return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
1841
|
+
function humanize(value) {
|
|
1842
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1418
1843
|
}
|
|
1419
|
-
function
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
case "eq":
|
|
1424
|
-
return fv === value;
|
|
1425
|
-
case "neq":
|
|
1426
|
-
return fv !== value;
|
|
1427
|
-
case "gt":
|
|
1428
|
-
return fv > value;
|
|
1429
|
-
case "gte":
|
|
1430
|
-
return fv >= value;
|
|
1431
|
-
case "lt":
|
|
1432
|
-
return fv < value;
|
|
1433
|
-
case "lte":
|
|
1434
|
-
return fv <= value;
|
|
1435
|
-
case "in":
|
|
1436
|
-
return Array.isArray(value) && value.includes(fv);
|
|
1437
|
-
case "nin":
|
|
1438
|
-
return Array.isArray(value) && !value.includes(fv);
|
|
1439
|
-
case "isNull":
|
|
1440
|
-
return fv == null;
|
|
1441
|
-
case "isNotNull":
|
|
1442
|
-
return fv != null;
|
|
1443
|
-
case "contains":
|
|
1444
|
-
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
|
|
1445
|
-
case "startsWith":
|
|
1446
|
-
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
|
|
1447
|
-
case "endsWith":
|
|
1448
|
-
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
|
|
1449
|
-
case "between": {
|
|
1450
|
-
const [lo, hi] = value;
|
|
1451
|
-
return fv >= lo && fv <= hi;
|
|
1452
|
-
}
|
|
1453
|
-
case "arrayContains":
|
|
1454
|
-
return Array.isArray(fv) && fv.includes(value);
|
|
1455
|
-
case "arrayOverlaps":
|
|
1456
|
-
return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
|
|
1457
|
-
case "matches":
|
|
1458
|
-
return typeof fv === "string" && new RegExp(value).test(fv);
|
|
1459
|
-
case "custom":
|
|
1460
|
-
return predicate ? predicate(fv, entity) : true;
|
|
1461
|
-
default:
|
|
1462
|
-
return true;
|
|
1463
|
-
}
|
|
1844
|
+
function renderMarkdownBlock(block) {
|
|
1845
|
+
if (block.startsWith("# ")) return `<h1>${renderInlineMarkdown(block.slice(2))}</h1>`;
|
|
1846
|
+
if (block.startsWith("## ")) return `<h2>${renderInlineMarkdown(block.slice(3))}</h2>`;
|
|
1847
|
+
return `<p>${renderInlineMarkdown(block).replaceAll("\n", "<br/>")}</p>`;
|
|
1464
1848
|
}
|
|
1465
|
-
function
|
|
1466
|
-
|
|
1467
|
-
const lq = query.toLowerCase();
|
|
1468
|
-
return fields.some((field) => {
|
|
1469
|
-
const v = getNestedValue(entity, field);
|
|
1470
|
-
return typeof v === "string" && v.toLowerCase().includes(lq);
|
|
1471
|
-
});
|
|
1849
|
+
function renderInlineMarkdown(block) {
|
|
1850
|
+
return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
1472
1851
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1852
|
+
|
|
1853
|
+
// src/ai-interop.ts
|
|
1854
|
+
function exportGraphSnapshot(opts) {
|
|
1855
|
+
const payload = {
|
|
1856
|
+
scope: opts.scope,
|
|
1857
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1858
|
+
data: opts.data
|
|
1859
|
+
};
|
|
1860
|
+
return JSON.stringify(payload, null, opts.pretty === false ? 0 : 2);
|
|
1479
1861
|
}
|
|
1480
|
-
function
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
if (aNull && bNull) return 0;
|
|
1487
|
-
if (aNull) return nulls === "first" ? -1 : 1;
|
|
1488
|
-
if (bNull) return nulls === "first" ? 1 : -1;
|
|
1489
|
-
let cmp;
|
|
1490
|
-
if (comparator) cmp = comparator(av, bv);
|
|
1491
|
-
else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
|
|
1492
|
-
else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
|
|
1493
|
-
else cmp = String(av).localeCompare(String(bv));
|
|
1494
|
-
return direction === "desc" ? -cmp : cmp;
|
|
1862
|
+
function createGraphTool(handler) {
|
|
1863
|
+
return (input) => handler(input, {
|
|
1864
|
+
store: useGraphStore.getState(),
|
|
1865
|
+
queryOnce,
|
|
1866
|
+
exportGraphSnapshot
|
|
1867
|
+
});
|
|
1495
1868
|
}
|
|
1496
|
-
function
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1869
|
+
function createSchemaGraphTool(handler) {
|
|
1870
|
+
return (input) => handler(input, {
|
|
1871
|
+
store: useGraphStore.getState(),
|
|
1872
|
+
queryOnce,
|
|
1873
|
+
exportGraphSnapshot,
|
|
1874
|
+
getEntityJsonSchema,
|
|
1875
|
+
exportGraphSnapshotWithSchemas
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
var DEFAULT_STORAGE_KEY = "prometheus:graph";
|
|
1879
|
+
var useGraphSyncStatusStore = create((set) => ({
|
|
1880
|
+
status: {
|
|
1881
|
+
phase: "idle",
|
|
1882
|
+
isOnline: true,
|
|
1883
|
+
isSynced: true,
|
|
1884
|
+
pendingActions: 0,
|
|
1885
|
+
lastHydratedAt: null,
|
|
1886
|
+
lastPersistedAt: null,
|
|
1887
|
+
storageKey: null,
|
|
1888
|
+
error: null
|
|
1889
|
+
},
|
|
1890
|
+
setStatus: (status) => set((state) => ({
|
|
1891
|
+
status: {
|
|
1892
|
+
...state.status,
|
|
1893
|
+
...status
|
|
1505
1894
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1895
|
+
}))
|
|
1896
|
+
}));
|
|
1897
|
+
var pendingActions = /* @__PURE__ */ new Map();
|
|
1898
|
+
function useGraphSyncStatus() {
|
|
1899
|
+
return useGraphSyncStatusStore((state) => state.status);
|
|
1510
1900
|
}
|
|
1511
|
-
function
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1901
|
+
async function persistGraphToStorage(opts) {
|
|
1902
|
+
const payload = {
|
|
1903
|
+
version: 1,
|
|
1904
|
+
snapshot: cloneGraphSnapshot(),
|
|
1905
|
+
pendingActions: opts.pendingActions ?? Array.from(pendingActions.values())
|
|
1906
|
+
};
|
|
1907
|
+
const json = JSON.stringify(payload);
|
|
1908
|
+
await opts.storage.set(opts.key, json);
|
|
1909
|
+
const persistedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1910
|
+
useGraphSyncStatusStore.getState().setStatus({
|
|
1911
|
+
lastPersistedAt: persistedAt,
|
|
1912
|
+
storageKey: opts.key,
|
|
1913
|
+
pendingActions: payload.pendingActions.length
|
|
1914
|
+
});
|
|
1915
|
+
return {
|
|
1916
|
+
ok: true,
|
|
1917
|
+
key: opts.key,
|
|
1918
|
+
bytes: json.length,
|
|
1919
|
+
persistedAt
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
async function hydrateGraphFromStorage(opts) {
|
|
1923
|
+
const raw = await opts.storage.get(opts.key);
|
|
1924
|
+
if (!raw) {
|
|
1925
|
+
return {
|
|
1926
|
+
ok: false,
|
|
1927
|
+
key: opts.key,
|
|
1928
|
+
hydratedAt: null,
|
|
1929
|
+
entityCounts: {},
|
|
1930
|
+
error: "No persisted graph snapshot found"
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
const parsed = JSON.parse(raw);
|
|
1935
|
+
useGraphStore.setState(parsed.snapshot);
|
|
1936
|
+
pendingActions.clear();
|
|
1937
|
+
for (const action of parsed.pendingActions ?? []) pendingActions.set(action.id, action);
|
|
1938
|
+
const hydratedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1939
|
+
useGraphSyncStatusStore.getState().setStatus({
|
|
1940
|
+
lastHydratedAt: hydratedAt,
|
|
1941
|
+
storageKey: opts.key,
|
|
1942
|
+
pendingActions: pendingActions.size,
|
|
1943
|
+
error: null
|
|
1944
|
+
});
|
|
1945
|
+
return {
|
|
1946
|
+
ok: true,
|
|
1947
|
+
key: opts.key,
|
|
1948
|
+
hydratedAt,
|
|
1949
|
+
entityCounts: Object.fromEntries(
|
|
1950
|
+
Object.entries(parsed.snapshot.entities).map(([type, entities]) => [type, Object.keys(entities).length])
|
|
1951
|
+
),
|
|
1952
|
+
pendingActions: Array.from(pendingActions.values())
|
|
1953
|
+
};
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1956
|
+
useGraphSyncStatusStore.getState().setStatus({
|
|
1957
|
+
phase: "error",
|
|
1958
|
+
error: message,
|
|
1959
|
+
storageKey: opts.key
|
|
1960
|
+
});
|
|
1961
|
+
return {
|
|
1962
|
+
ok: false,
|
|
1963
|
+
key: opts.key,
|
|
1964
|
+
hydratedAt: null,
|
|
1965
|
+
entityCounts: {},
|
|
1966
|
+
error: message
|
|
1967
|
+
};
|
|
1517
1968
|
}
|
|
1518
|
-
if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
|
|
1519
|
-
if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
|
|
1520
|
-
if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
|
|
1521
|
-
return entries.map((e) => e.id);
|
|
1522
1969
|
}
|
|
1523
|
-
function
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1970
|
+
function startLocalFirstGraph(opts) {
|
|
1971
|
+
const key = opts.key ?? DEFAULT_STORAGE_KEY;
|
|
1972
|
+
const persistDebounceMs = opts.persistDebounceMs ?? 50;
|
|
1973
|
+
const statusStore = useGraphSyncStatusStore.getState();
|
|
1974
|
+
statusStore.setStatus({
|
|
1975
|
+
phase: "hydrating",
|
|
1976
|
+
storageKey: key,
|
|
1977
|
+
isOnline: opts.onlineSource?.getIsOnline() ?? getDefaultOnlineSource().getIsOnline(),
|
|
1978
|
+
isSynced: pendingActions.size === 0,
|
|
1979
|
+
error: null
|
|
1980
|
+
});
|
|
1981
|
+
let persistTimer = null;
|
|
1982
|
+
const schedulePersist = () => {
|
|
1983
|
+
if (persistTimer) clearTimeout(persistTimer);
|
|
1984
|
+
persistTimer = setTimeout(() => {
|
|
1985
|
+
void persistGraphToStorage({ storage: opts.storage, key });
|
|
1986
|
+
}, persistDebounceMs);
|
|
1987
|
+
};
|
|
1988
|
+
const graphUnsub = useGraphStore.subscribe(() => {
|
|
1989
|
+
schedulePersist();
|
|
1990
|
+
});
|
|
1991
|
+
const actionUnsub = subscribeGraphActionEvents((event) => {
|
|
1992
|
+
if (event.type === "enqueued") pendingActions.set(event.record.id, event.record);
|
|
1993
|
+
if (event.type === "settled") pendingActions.delete(event.record.id);
|
|
1994
|
+
useGraphSyncStatusStore.getState().setStatus({
|
|
1995
|
+
pendingActions: pendingActions.size,
|
|
1996
|
+
isSynced: pendingActions.size === 0
|
|
1997
|
+
});
|
|
1998
|
+
schedulePersist();
|
|
1999
|
+
});
|
|
2000
|
+
const onlineSource = opts.onlineSource ?? getDefaultOnlineSource();
|
|
2001
|
+
const onlineUnsub = onlineSource.subscribe((online) => {
|
|
2002
|
+
useGraphSyncStatusStore.getState().setStatus({
|
|
2003
|
+
isOnline: online,
|
|
2004
|
+
phase: online ? "ready" : "offline"
|
|
2005
|
+
});
|
|
2006
|
+
});
|
|
2007
|
+
const ready = (async () => {
|
|
2008
|
+
const hydrated = await hydrateGraphFromStorage({ storage: opts.storage, key });
|
|
2009
|
+
if (opts.replayPendingActions && hydrated.ok && pendingActions.size > 0) {
|
|
2010
|
+
useGraphSyncStatusStore.getState().setStatus({
|
|
2011
|
+
phase: "syncing",
|
|
2012
|
+
isSynced: false
|
|
2013
|
+
});
|
|
2014
|
+
const policy = resolveRetryPolicy(opts.retryPolicy);
|
|
2015
|
+
for (const action of Array.from(pendingActions.values())) {
|
|
2016
|
+
await replayActionWithRetry(action, policy);
|
|
2017
|
+
pendingActions.delete(action.id);
|
|
2018
|
+
}
|
|
2019
|
+
await persistGraphToStorage({ storage: opts.storage, key });
|
|
2020
|
+
}
|
|
2021
|
+
const online = onlineSource.getIsOnline();
|
|
2022
|
+
useGraphSyncStatusStore.getState().setStatus({
|
|
2023
|
+
phase: online ? "ready" : "offline",
|
|
2024
|
+
isOnline: online,
|
|
2025
|
+
isSynced: pendingActions.size === 0,
|
|
2026
|
+
pendingActions: pendingActions.size
|
|
2027
|
+
});
|
|
2028
|
+
})();
|
|
2029
|
+
return {
|
|
2030
|
+
ready,
|
|
2031
|
+
dispose() {
|
|
2032
|
+
graphUnsub();
|
|
2033
|
+
actionUnsub();
|
|
2034
|
+
onlineUnsub();
|
|
2035
|
+
if (persistTimer) clearTimeout(persistTimer);
|
|
2036
|
+
},
|
|
2037
|
+
async persistNow() {
|
|
2038
|
+
await persistGraphToStorage({ storage: opts.storage, key });
|
|
2039
|
+
},
|
|
2040
|
+
hydrate() {
|
|
2041
|
+
return hydrateGraphFromStorage({ storage: opts.storage, key });
|
|
2042
|
+
},
|
|
2043
|
+
getStatus() {
|
|
2044
|
+
return useGraphSyncStatusStore.getState().status;
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
1527
2047
|
}
|
|
1528
|
-
function
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2048
|
+
function cloneGraphSnapshot() {
|
|
2049
|
+
const state = useGraphStore.getState();
|
|
2050
|
+
return {
|
|
2051
|
+
entities: structuredClone(state.entities),
|
|
2052
|
+
patches: structuredClone(state.patches),
|
|
2053
|
+
entityStates: structuredClone(state.entityStates),
|
|
2054
|
+
syncMetadata: structuredClone(state.syncMetadata),
|
|
2055
|
+
lists: structuredClone(state.lists)
|
|
2056
|
+
};
|
|
1536
2057
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2058
|
+
function resolveRetryPolicy(policy) {
|
|
2059
|
+
return {
|
|
2060
|
+
maxAttempts: policy?.maxAttempts ?? 5,
|
|
2061
|
+
initialDelayMs: policy?.initialDelayMs ?? 500,
|
|
2062
|
+
maxDelayMs: policy?.maxDelayMs ?? 3e4,
|
|
2063
|
+
backoffFactor: policy?.backoffFactor ?? 2,
|
|
2064
|
+
jitter: policy?.jitter ?? "equal",
|
|
2065
|
+
poisonHandler: policy?.poisonHandler
|
|
2066
|
+
};
|
|
1543
2067
|
}
|
|
1544
|
-
function
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
case "
|
|
1551
|
-
return
|
|
1552
|
-
case "
|
|
1553
|
-
return
|
|
1554
|
-
case "
|
|
1555
|
-
return { lt: c.value };
|
|
1556
|
-
case "lte":
|
|
1557
|
-
return { lte: c.value };
|
|
1558
|
-
case "contains":
|
|
1559
|
-
return { contains: c.value, mode: "insensitive" };
|
|
1560
|
-
case "startsWith":
|
|
1561
|
-
return { startsWith: c.value, mode: "insensitive" };
|
|
1562
|
-
case "endsWith":
|
|
1563
|
-
return { endsWith: c.value, mode: "insensitive" };
|
|
1564
|
-
case "in":
|
|
1565
|
-
return { in: c.value };
|
|
1566
|
-
case "nin":
|
|
1567
|
-
return { notIn: c.value };
|
|
1568
|
-
case "arrayContains":
|
|
1569
|
-
return { has: c.value };
|
|
1570
|
-
case "between":
|
|
1571
|
-
case "arrayOverlaps":
|
|
1572
|
-
case "matches":
|
|
1573
|
-
case "custom":
|
|
2068
|
+
function computeDelay(policy, attempt) {
|
|
2069
|
+
const base = Math.min(
|
|
2070
|
+
policy.initialDelayMs * Math.pow(policy.backoffFactor, Math.max(0, attempt - 1)),
|
|
2071
|
+
policy.maxDelayMs
|
|
2072
|
+
);
|
|
2073
|
+
switch (policy.jitter) {
|
|
2074
|
+
case "none":
|
|
2075
|
+
return base;
|
|
2076
|
+
case "full":
|
|
2077
|
+
return Math.random() * base;
|
|
2078
|
+
case "equal":
|
|
1574
2079
|
default:
|
|
1575
|
-
return
|
|
2080
|
+
return base / 2 + Math.random() * (base / 2);
|
|
1576
2081
|
}
|
|
1577
2082
|
}
|
|
1578
|
-
function
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
if (c.op === "isNull") {
|
|
1582
|
-
const equalsNull = c.value === void 0 || c.value === true;
|
|
1583
|
-
return nestWhereField(parts, equalsNull ? null : { not: null });
|
|
1584
|
-
}
|
|
1585
|
-
if (c.op === "isNotNull") {
|
|
1586
|
-
return nestWhereField(parts, { not: null });
|
|
1587
|
-
}
|
|
1588
|
-
const leaf = clauseToPrismaLeaf(c);
|
|
1589
|
-
if (leaf === null) return null;
|
|
1590
|
-
return nestWhereField(parts, leaf);
|
|
2083
|
+
function sleep4(ms) {
|
|
2084
|
+
if (ms <= 0) return Promise.resolve();
|
|
2085
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1591
2086
|
}
|
|
1592
|
-
function
|
|
1593
|
-
|
|
1594
|
-
for (
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
if (
|
|
2087
|
+
async function replayActionWithRetry(action, policy) {
|
|
2088
|
+
let lastError = null;
|
|
2089
|
+
for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
|
|
2090
|
+
try {
|
|
2091
|
+
await replayRegisteredGraphAction(action);
|
|
2092
|
+
return { ok: true };
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
lastError = error;
|
|
2095
|
+
if (attempt >= policy.maxAttempts) break;
|
|
2096
|
+
await sleep4(computeDelay(policy, attempt));
|
|
1601
2097
|
}
|
|
1602
2098
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2099
|
+
try {
|
|
2100
|
+
await policy.poisonHandler?.(action, lastError);
|
|
2101
|
+
} catch {
|
|
2102
|
+
}
|
|
2103
|
+
return { ok: false, poisoned: true, error: lastError };
|
|
1606
2104
|
}
|
|
1607
|
-
function
|
|
1608
|
-
if (
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
2105
|
+
function getDefaultOnlineSource() {
|
|
2106
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
2107
|
+
return {
|
|
2108
|
+
getIsOnline: () => window.navigator.onLine,
|
|
2109
|
+
subscribe: (listener) => {
|
|
2110
|
+
const onlineHandler = () => listener(true);
|
|
2111
|
+
const offlineHandler = () => listener(false);
|
|
2112
|
+
window.addEventListener("online", onlineHandler);
|
|
2113
|
+
window.addEventListener("offline", offlineHandler);
|
|
2114
|
+
return () => {
|
|
2115
|
+
window.removeEventListener("online", onlineHandler);
|
|
2116
|
+
window.removeEventListener("offline", offlineHandler);
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
return {
|
|
2122
|
+
getIsOnline: () => true,
|
|
2123
|
+
subscribe: () => () => {
|
|
1613
2124
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
function collectGraphDevStats(entities, patches, entityStates, listsState) {
|
|
2128
|
+
const entityCounts = {};
|
|
2129
|
+
let totalEntities = 0;
|
|
2130
|
+
for (const type of Object.keys(entities)) {
|
|
2131
|
+
const bucket = entities[type];
|
|
2132
|
+
if (!bucket) continue;
|
|
2133
|
+
const n = Object.keys(bucket).length;
|
|
2134
|
+
if (n > 0) entityCounts[type] = n;
|
|
2135
|
+
totalEntities += n;
|
|
1617
2136
|
}
|
|
1618
|
-
|
|
2137
|
+
const listKeys = Object.keys(listsState);
|
|
2138
|
+
const listCount = listKeys.length;
|
|
2139
|
+
const patchedEntities = [];
|
|
2140
|
+
for (const type of Object.keys(patches)) {
|
|
2141
|
+
const bucket = patches[type];
|
|
2142
|
+
if (!bucket) continue;
|
|
2143
|
+
for (const id of Object.keys(bucket)) {
|
|
2144
|
+
const p = bucket[id];
|
|
2145
|
+
if (p && Object.keys(p).length > 0) patchedEntities.push({ type, id });
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const staleEntities = [];
|
|
2149
|
+
const fetchingEntities = [];
|
|
2150
|
+
for (const key of Object.keys(entityStates)) {
|
|
2151
|
+
const colon = key.indexOf(":");
|
|
2152
|
+
if (colon === -1) continue;
|
|
2153
|
+
const type = key.slice(0, colon);
|
|
2154
|
+
const id = key.slice(colon + 1);
|
|
2155
|
+
const es = entityStates[key];
|
|
2156
|
+
if (es.stale) staleEntities.push({ type, id });
|
|
2157
|
+
if (es.isFetching) fetchingEntities.push({ type, id });
|
|
2158
|
+
}
|
|
2159
|
+
const lists = listKeys.map((key) => ({
|
|
2160
|
+
key,
|
|
2161
|
+
idCount: listsState[key]?.ids.length ?? 0,
|
|
2162
|
+
isFetching: Boolean(listsState[key]?.isFetching || listsState[key]?.isFetchingMore),
|
|
2163
|
+
isStale: Boolean(listsState[key]?.stale)
|
|
2164
|
+
}));
|
|
2165
|
+
return {
|
|
2166
|
+
entityCounts,
|
|
2167
|
+
totalEntities,
|
|
2168
|
+
listCount,
|
|
2169
|
+
patchedEntities,
|
|
2170
|
+
staleEntities,
|
|
2171
|
+
fetchingEntities,
|
|
2172
|
+
lists
|
|
2173
|
+
};
|
|
1619
2174
|
}
|
|
1620
|
-
function
|
|
1621
|
-
return
|
|
2175
|
+
function subscriberCountServerSnapshot() {
|
|
2176
|
+
return 0;
|
|
1622
2177
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
2178
|
+
function useGraphDevTools() {
|
|
2179
|
+
const subscriberCount = useSyncExternalStore(
|
|
2180
|
+
subscribeSubscriberStats,
|
|
2181
|
+
getActiveSubscriberCount,
|
|
2182
|
+
subscriberCountServerSnapshot
|
|
2183
|
+
);
|
|
2184
|
+
const entities = useStore(useGraphStore, (state) => state.entities);
|
|
2185
|
+
const patches = useStore(useGraphStore, (state) => state.patches);
|
|
2186
|
+
const entityStates = useStore(useGraphStore, (state) => state.entityStates);
|
|
2187
|
+
const listsState = useStore(useGraphStore, (state) => state.lists);
|
|
2188
|
+
const graphPart = useMemo(
|
|
2189
|
+
() => collectGraphDevStats(entities, patches, entityStates, listsState),
|
|
2190
|
+
[entities, patches, entityStates, listsState]
|
|
2191
|
+
);
|
|
2192
|
+
return { ...graphPart, subscriberCount };
|
|
2193
|
+
}
|
|
2194
|
+
var listenersAttached = false;
|
|
2195
|
+
function ensureListeners() {
|
|
2196
|
+
if (!listenersAttached) {
|
|
2197
|
+
attachGlobalListeners();
|
|
2198
|
+
listenersAttached = true;
|
|
1634
2199
|
}
|
|
1635
|
-
if (view.sort) params["sort"] = view.sort.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
|
|
1636
|
-
if (view.search?.query) params["q"] = view.search.query;
|
|
1637
|
-
return params;
|
|
1638
2200
|
}
|
|
1639
|
-
function
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2201
|
+
function useEntity(opts) {
|
|
2202
|
+
const { type, id, staleTime = getEngineOptions().defaultStaleTime, enabled = true } = opts;
|
|
2203
|
+
ensureListeners();
|
|
2204
|
+
const fetchRef = useRef(opts.fetch);
|
|
2205
|
+
fetchRef.current = opts.fetch;
|
|
2206
|
+
const normalizeRef = useRef(opts.normalize);
|
|
2207
|
+
normalizeRef.current = opts.normalize;
|
|
2208
|
+
const dataSelector = useCallback((state) => {
|
|
2209
|
+
if (!id) return null;
|
|
2210
|
+
return state.readEntitySnapshot(type, id);
|
|
2211
|
+
}, [id, type]);
|
|
2212
|
+
const data = useStore(useGraphStore, useShallow(dataSelector));
|
|
2213
|
+
const entityState = useStore(useGraphStore, useCallback(
|
|
2214
|
+
(state) => state.entityStates[`${type}:${id}`] ?? EMPTY_ENTITY_STATE,
|
|
2215
|
+
[type, id]
|
|
2216
|
+
));
|
|
2217
|
+
const doFetch = useCallback(() => {
|
|
2218
|
+
if (!id || !enabled) return;
|
|
2219
|
+
fetchEntity({ type, id, fetch: fetchRef.current, normalize: normalizeRef.current }, getEngineOptions());
|
|
2220
|
+
}, [id, enabled, type]);
|
|
2221
|
+
useEffect(() => {
|
|
2222
|
+
if (!id || !enabled) return;
|
|
2223
|
+
const token = registerSubscriber(`${type}:${id}`);
|
|
2224
|
+
const state = useGraphStore.getState();
|
|
2225
|
+
const existingState = state.entityStates[`${type}:${id}`];
|
|
2226
|
+
const hasData = !!state.entities[type]?.[id];
|
|
2227
|
+
const isStale = !existingState?.lastFetched || existingState.stale || Date.now() - (existingState.lastFetched ?? 0) > staleTime;
|
|
2228
|
+
if (!hasData || isStale) doFetch();
|
|
2229
|
+
return () => unregisterSubscriber(`${type}:${id}`, token);
|
|
2230
|
+
}, [id, type, enabled, staleTime, doFetch]);
|
|
2231
|
+
useEffect(() => {
|
|
2232
|
+
if (entityState.stale && id && enabled && !entityState.isFetching) doFetch();
|
|
2233
|
+
}, [entityState.stale, id, enabled, entityState.isFetching, doFetch]);
|
|
2234
|
+
return { data, isLoading: !data && entityState.isFetching, isFetching: entityState.isFetching, error: entityState.error, isStale: entityState.stale, refetch: doFetch };
|
|
2235
|
+
}
|
|
2236
|
+
function useEntityList(opts) {
|
|
2237
|
+
console.warn(
|
|
2238
|
+
`[entity-management] useEntityList("${String(opts.type)}") is deprecated in 2.0.
|
|
2239
|
+
Register a transport at boot: registerEntityTransport("${String(opts.type)}", makeRestTransport(...))
|
|
2240
|
+
Then replace this call with: useEntities<T>("${String(opts.type)}")`
|
|
2241
|
+
);
|
|
2242
|
+
const { type, queryKey, staleTime = getEngineOptions().defaultStaleTime, enabled = true, mode = "replace" } = opts;
|
|
2243
|
+
ensureListeners();
|
|
2244
|
+
const key = useMemo(() => serializeKey(queryKey), [queryKey]);
|
|
2245
|
+
const fetchRef = useRef(opts.fetch);
|
|
2246
|
+
fetchRef.current = opts.fetch;
|
|
2247
|
+
const normalizeRef = useRef(opts.normalize);
|
|
2248
|
+
normalizeRef.current = opts.normalize;
|
|
2249
|
+
const listState = useStore(useGraphStore, useCallback((state) => state.lists[key] ?? EMPTY_LIST_STATE, [key]));
|
|
2250
|
+
const itemsSelector = useCallback((state) => {
|
|
2251
|
+
const ids = state.lists[key]?.ids ?? EMPTY_IDS;
|
|
2252
|
+
return ids.map((id) => state.readEntitySnapshot(type, id)).filter((x) => x !== null);
|
|
2253
|
+
}, [key, type]);
|
|
2254
|
+
const items = useStore(useGraphStore, useShallow(itemsSelector));
|
|
2255
|
+
const doFetch = useCallback((params = {}) => {
|
|
2256
|
+
if (!enabled) return;
|
|
2257
|
+
fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, params, getEngineOptions(), false);
|
|
2258
|
+
}, [enabled, type, queryKey, mode]);
|
|
2259
|
+
const fetchNextPage = useCallback(() => {
|
|
2260
|
+
if (!listState.hasNextPage || listState.isFetchingMore || !enabled) return;
|
|
2261
|
+
fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, { cursor: listState.nextCursor ?? void 0, page: (listState.currentPage ?? 0) + 1, pageSize: listState.pageSize ?? void 0 }, getEngineOptions(), true);
|
|
2262
|
+
}, [listState.hasNextPage, listState.isFetchingMore, listState.nextCursor, listState.currentPage, listState.pageSize, enabled, type, queryKey, mode]);
|
|
2263
|
+
useEffect(() => {
|
|
2264
|
+
if (!enabled) return;
|
|
2265
|
+
const state = useGraphStore.getState();
|
|
2266
|
+
const existing = state.lists[key];
|
|
2267
|
+
const isStale = !existing?.lastFetched || existing.stale || Date.now() - (existing.lastFetched ?? 0) > staleTime;
|
|
2268
|
+
if (!existing || isStale) doFetch({ page: 1, pageSize: listState.pageSize ?? void 0 });
|
|
2269
|
+
}, [key, enabled, staleTime, doFetch, listState.pageSize]);
|
|
2270
|
+
useEffect(() => {
|
|
2271
|
+
if (listState.stale && enabled && !listState.isFetching) doFetch();
|
|
2272
|
+
}, [listState.stale, enabled, listState.isFetching, doFetch]);
|
|
2273
|
+
return useMemo(
|
|
2274
|
+
() => ({
|
|
2275
|
+
items,
|
|
2276
|
+
ids: listState.ids,
|
|
2277
|
+
isLoading: listState.ids.length === 0 && listState.isFetching,
|
|
2278
|
+
isFetching: listState.isFetching,
|
|
2279
|
+
isFetchingMore: listState.isFetchingMore,
|
|
2280
|
+
error: listState.error,
|
|
2281
|
+
isError: listState.error !== null,
|
|
2282
|
+
hasNextPage: listState.hasNextPage,
|
|
2283
|
+
hasPrevPage: listState.hasPrevPage,
|
|
2284
|
+
total: listState.total,
|
|
2285
|
+
currentPage: listState.currentPage,
|
|
2286
|
+
fetchNextPage,
|
|
2287
|
+
refetch: doFetch
|
|
2288
|
+
}),
|
|
2289
|
+
[items, listState, fetchNextPage, doFetch]
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
function useEntityMutation(opts) {
|
|
2293
|
+
const [state, setState] = useState({ isPending: false, isSuccess: false, isError: false, error: null });
|
|
2294
|
+
const optsRef = useRef(opts);
|
|
2295
|
+
optsRef.current = opts;
|
|
2296
|
+
const mutate = useCallback(async (input) => {
|
|
2297
|
+
const { type, mutate: apiFn, normalize, optimistic, invalidateLists, invalidateEntities, onSuccess, onError } = optsRef.current;
|
|
2298
|
+
setState({ isPending: true, isSuccess: false, isError: false, error: null });
|
|
2299
|
+
let rollback = null;
|
|
2300
|
+
if (optimistic) {
|
|
2301
|
+
const opt = optimistic(input);
|
|
2302
|
+
if (opt) {
|
|
2303
|
+
const { id, patch } = opt;
|
|
2304
|
+
const store = useGraphStore.getState();
|
|
2305
|
+
const previous = { ...store.patches[type]?.[id] };
|
|
2306
|
+
const previousSync = store.syncMetadata[`${type}:${id}`];
|
|
2307
|
+
store.patchEntity(type, id, patch);
|
|
2308
|
+
store.setEntitySyncMetadata(type, id, { synced: false, origin: "optimistic", updatedAt: Date.now() });
|
|
2309
|
+
rollback = () => {
|
|
2310
|
+
const currentStore = useGraphStore.getState();
|
|
2311
|
+
if (Object.keys(previous).length > 0) currentStore.patchEntity(type, id, previous);
|
|
2312
|
+
else currentStore.clearPatch(type, id);
|
|
2313
|
+
if (previousSync) currentStore.setEntitySyncMetadata(type, id, previousSync);
|
|
2314
|
+
else currentStore.clearEntitySyncMetadata(type, id);
|
|
2315
|
+
};
|
|
1683
2316
|
}
|
|
1684
|
-
case "arrayContains":
|
|
1685
|
-
params.push(c.value);
|
|
1686
|
-
return `$${paramIdx++} = ANY(${col})`;
|
|
1687
|
-
default:
|
|
1688
|
-
return "TRUE";
|
|
1689
2317
|
}
|
|
2318
|
+
try {
|
|
2319
|
+
const result = await apiFn(input);
|
|
2320
|
+
if (normalize) {
|
|
2321
|
+
const { id, data } = normalize(result, input);
|
|
2322
|
+
const store = useGraphStore.getState();
|
|
2323
|
+
store.upsertEntity(type, id, data);
|
|
2324
|
+
store.setEntitySyncMetadata(type, id, { synced: true, origin: "server", updatedAt: Date.now() });
|
|
2325
|
+
if (optimistic) {
|
|
2326
|
+
const opt = optimistic(input);
|
|
2327
|
+
if (opt) store.clearPatch(type, opt.id);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (invalidateLists) for (const k of invalidateLists) useGraphStore.getState().invalidateLists(k);
|
|
2331
|
+
if (invalidateEntities) for (const { type: t, id } of invalidateEntities) useGraphStore.getState().invalidateEntity(t, id);
|
|
2332
|
+
setState({ isPending: false, isSuccess: true, isError: false, error: null });
|
|
2333
|
+
onSuccess?.(result, input);
|
|
2334
|
+
return result;
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2337
|
+
rollback?.();
|
|
2338
|
+
setState({ isPending: false, isSuccess: false, isError: true, error: error.message });
|
|
2339
|
+
onError?.(error, input);
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
}, []);
|
|
2343
|
+
const trigger = useCallback((input) => {
|
|
2344
|
+
void mutate(input);
|
|
2345
|
+
}, [mutate]);
|
|
2346
|
+
const reset = useCallback(() => setState({ isPending: false, isSuccess: false, isError: false, error: null }), []);
|
|
2347
|
+
return { mutate, trigger, reset, state };
|
|
2348
|
+
}
|
|
2349
|
+
function useEntityAugment(type, id) {
|
|
2350
|
+
const patch = useStore(useGraphStore, useCallback((state) => id ? state.patches[type]?.[id] ?? null : null, [type, id]));
|
|
2351
|
+
const augment = useCallback((fields) => {
|
|
2352
|
+
if (!id) return;
|
|
2353
|
+
useGraphStore.getState().patchEntity(type, id, fields);
|
|
2354
|
+
}, [type, id]);
|
|
2355
|
+
const unaugment = useCallback((keys) => {
|
|
2356
|
+
if (!id) return;
|
|
2357
|
+
useGraphStore.getState().unpatchEntity(type, id, keys);
|
|
2358
|
+
}, [type, id]);
|
|
2359
|
+
const clear = useCallback(() => {
|
|
2360
|
+
if (!id) return;
|
|
2361
|
+
useGraphStore.getState().clearPatch(type, id);
|
|
2362
|
+
}, [type, id]);
|
|
2363
|
+
return { patch, augment, unaugment, clear };
|
|
2364
|
+
}
|
|
2365
|
+
var suspenseEntityPromises = /* @__PURE__ */ new Map();
|
|
2366
|
+
var suspenseListPromises = /* @__PURE__ */ new Map();
|
|
2367
|
+
function getEntitySuspensePromise(type, id) {
|
|
2368
|
+
const key = `${type}:${id}`;
|
|
2369
|
+
const existing = suspenseEntityPromises.get(key);
|
|
2370
|
+
if (existing) return existing;
|
|
2371
|
+
let unsub = null;
|
|
2372
|
+
let settled = false;
|
|
2373
|
+
const promise = new Promise((resolve, reject) => {
|
|
2374
|
+
const settle = (fn) => {
|
|
2375
|
+
if (settled) return;
|
|
2376
|
+
settled = true;
|
|
2377
|
+
unsub?.();
|
|
2378
|
+
unsub = null;
|
|
2379
|
+
fn();
|
|
2380
|
+
};
|
|
2381
|
+
const inspect = (state) => {
|
|
2382
|
+
if (settled) return;
|
|
2383
|
+
const hasData = !!state.entities[type]?.[id];
|
|
2384
|
+
const es = state.entityStates[key];
|
|
2385
|
+
if (hasData) settle(() => resolve());
|
|
2386
|
+
else if (es != null && es.error != null && !es.isFetching) {
|
|
2387
|
+
const msg = es.error;
|
|
2388
|
+
settle(() => reject(new Error(msg)));
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
inspect(useGraphStore.getState());
|
|
2392
|
+
if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
|
|
2393
|
+
});
|
|
2394
|
+
const tracked = promise.finally(() => {
|
|
2395
|
+
suspenseEntityPromises.delete(key);
|
|
2396
|
+
});
|
|
2397
|
+
suspenseEntityPromises.set(key, tracked);
|
|
2398
|
+
return tracked;
|
|
2399
|
+
}
|
|
2400
|
+
function getListSuspensePromise(listKey) {
|
|
2401
|
+
const existing = suspenseListPromises.get(listKey);
|
|
2402
|
+
if (existing) return existing;
|
|
2403
|
+
let unsub = null;
|
|
2404
|
+
let settled = false;
|
|
2405
|
+
const promise = new Promise((resolve, reject) => {
|
|
2406
|
+
const settle = (fn) => {
|
|
2407
|
+
if (settled) return;
|
|
2408
|
+
settled = true;
|
|
2409
|
+
unsub?.();
|
|
2410
|
+
unsub = null;
|
|
2411
|
+
fn();
|
|
2412
|
+
};
|
|
2413
|
+
const inspect = (state) => {
|
|
2414
|
+
if (settled) return;
|
|
2415
|
+
const list = state.lists[listKey] ?? EMPTY_LIST_STATE;
|
|
2416
|
+
if (list.ids.length > 0) settle(() => resolve());
|
|
2417
|
+
else if (list.error != null && !list.isFetching) {
|
|
2418
|
+
const msg = list.error;
|
|
2419
|
+
settle(() => reject(new Error(msg)));
|
|
2420
|
+
} else if (list.ids.length === 0 && !list.isFetching && list.lastFetched != null) settle(() => resolve());
|
|
2421
|
+
};
|
|
2422
|
+
inspect(useGraphStore.getState());
|
|
2423
|
+
if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
|
|
2424
|
+
});
|
|
2425
|
+
const tracked = promise.finally(() => {
|
|
2426
|
+
suspenseListPromises.delete(listKey);
|
|
2427
|
+
});
|
|
2428
|
+
suspenseListPromises.set(listKey, tracked);
|
|
2429
|
+
return tracked;
|
|
2430
|
+
}
|
|
2431
|
+
function useSuspenseEntity(opts) {
|
|
2432
|
+
const result = useEntity(opts);
|
|
2433
|
+
const { type, id } = opts;
|
|
2434
|
+
if (result.isLoading) {
|
|
2435
|
+
if (!id) throw new Error("useSuspenseEntity requires a non-null entity id");
|
|
2436
|
+
throw getEntitySuspensePromise(type, id);
|
|
1690
2437
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
return parts.join(` ${g.logic.toUpperCase()} `);
|
|
1694
|
-
}
|
|
1695
|
-
let where = "TRUE";
|
|
1696
|
-
if (view.filter) {
|
|
1697
|
-
if (Array.isArray(view.filter)) where = view.filter.map(clauseToSQL).join(" AND ") || "TRUE";
|
|
1698
|
-
else where = groupToSQL(view.filter) || "TRUE";
|
|
1699
|
-
}
|
|
1700
|
-
if (view.search?.query) {
|
|
1701
|
-
params.push(`%${view.search.query}%`);
|
|
1702
|
-
where += ` AND (${view.search.fields.map((f) => `"${f}"`).join(" || ' ' || ")}) ILIKE $${paramIdx++}`;
|
|
2438
|
+
if (result.error != null && result.data == null) {
|
|
2439
|
+
throw new Error(result.error);
|
|
1703
2440
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
}
|
|
1707
|
-
function toGraphQLVariables(view) {
|
|
1708
|
-
const result = {};
|
|
1709
|
-
if (view.filter) {
|
|
1710
|
-
const clauses = flattenClauses(view.filter);
|
|
1711
|
-
const where = {};
|
|
1712
|
-
for (const c of clauses) {
|
|
1713
|
-
if (c.op === "custom") continue;
|
|
1714
|
-
where[c.field] = { [`_${c.op}`]: c.value };
|
|
1715
|
-
}
|
|
1716
|
-
if (Object.keys(where).length) result.where = where;
|
|
2441
|
+
if (result.data == null) {
|
|
2442
|
+
throw new Error(!id ? "useSuspenseEntity requires a non-null entity id" : "Entity not found");
|
|
1717
2443
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2444
|
+
return {
|
|
2445
|
+
data: result.data,
|
|
2446
|
+
isFetching: result.isFetching,
|
|
2447
|
+
isStale: result.isStale,
|
|
2448
|
+
refetch: result.refetch
|
|
2449
|
+
};
|
|
1721
2450
|
}
|
|
1722
|
-
function
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
2451
|
+
function useSuspenseEntityList(opts) {
|
|
2452
|
+
const key = useMemo(() => serializeKey(opts.queryKey), [opts.queryKey]);
|
|
2453
|
+
const result = useEntityList(opts);
|
|
2454
|
+
if (result.isLoading) throw getListSuspensePromise(key);
|
|
2455
|
+
if (result.error != null && result.items.length === 0) {
|
|
2456
|
+
throw new Error(result.error);
|
|
1726
2457
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
function hasCustomPredicates(filter) {
|
|
1730
|
-
return flattenClauses(filter).some((c) => c.op === "custom");
|
|
2458
|
+
const { isLoading: _isLoading, ...rest } = result;
|
|
2459
|
+
return rest;
|
|
1731
2460
|
}
|
|
1732
|
-
|
|
1733
|
-
// src/view/use-entity-view.ts
|
|
1734
|
-
var EMPTY_ENTITY_BUCKET = {};
|
|
2461
|
+
var EMPTY_ENTITY_BUCKET2 = {};
|
|
1735
2462
|
function useEntityView(opts) {
|
|
2463
|
+
console.warn(
|
|
2464
|
+
`[entity-management] useEntityView("${String(opts.type)}") is deprecated in 2.0.
|
|
2465
|
+
Register a transport at boot: registerEntityTransport("${String(opts.type)}", makeRestTransport(...))
|
|
2466
|
+
Then replace this call with: useEntityQuery<T>("${String(opts.type)}", { view })`
|
|
2467
|
+
);
|
|
1736
2468
|
const { type, baseQueryKey, mode: forcedMode, remoteFetch, remoteDebounce = 300, staleTime = getEngineOptions().defaultStaleTime, enabled = true, initialIds, initialTotal } = opts;
|
|
1737
2469
|
const optsRef = useRef(opts);
|
|
1738
2470
|
optsRef.current = opts;
|
|
@@ -1797,16 +2529,19 @@ function useEntityView(opts) {
|
|
|
1797
2529
|
setRemoteError(null);
|
|
1798
2530
|
const store = useGraphStore.getState();
|
|
1799
2531
|
store.setListFetching(rKey, true);
|
|
2532
|
+
const baseKeyStr = serializeKey(bqk);
|
|
1800
2533
|
try {
|
|
1801
2534
|
const response = await rf(params);
|
|
1802
2535
|
const normalized = norm ? response.items.map(norm) : response.items.map((item) => ({ id: String(item.id), data: item }));
|
|
1803
|
-
store.upsertEntities(type, normalized);
|
|
2536
|
+
store.upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
|
|
1804
2537
|
for (const { id } of normalized) store.setEntityFetched(type, id);
|
|
1805
2538
|
store.setListResult(rKey, normalized.map(({ id }) => id), { total: response.total ?? null, nextCursor: response.nextCursor ?? null, hasNextPage: response.hasNextPage ?? !!response.nextCursor });
|
|
2539
|
+
store.setListFetching(baseKeyStr, false);
|
|
1806
2540
|
} catch (err) {
|
|
1807
2541
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1808
2542
|
setRemoteError(msg);
|
|
1809
2543
|
store.setListError(rKey, msg);
|
|
2544
|
+
store.setListError(baseKeyStr, msg);
|
|
1810
2545
|
} finally {
|
|
1811
2546
|
setIsRemoteFetching(false);
|
|
1812
2547
|
}
|
|
@@ -1830,7 +2565,7 @@ function useEntityView(opts) {
|
|
|
1830
2565
|
if (!existing || isStale) fireRemoteFetch(liveViewRef.current);
|
|
1831
2566
|
}, [baseKey, enabled, staleTime, fireRemoteFetch]);
|
|
1832
2567
|
useEffect(() => {
|
|
1833
|
-
const unsub = useGraphStore.subscribe((state) => state.entities[type] ??
|
|
2568
|
+
const unsub = useGraphStore.subscribe((state) => state.entities[type] ?? EMPTY_ENTITY_BUCKET2, (newEntities, prevEntities) => {
|
|
1834
2569
|
const view = liveViewRef.current;
|
|
1835
2570
|
const store = useGraphStore.getState();
|
|
1836
2571
|
const list = store.lists[baseKey];
|
|
@@ -1862,15 +2597,17 @@ function useEntityView(opts) {
|
|
|
1862
2597
|
}, [completenessMode, isRemoteFetching, remoteListState?.nextCursor, fireRemoteFetch]);
|
|
1863
2598
|
const refetch = useCallback(() => fireRemoteFetch(liveViewRef.current), [fireRemoteFetch]);
|
|
1864
2599
|
const viewTotal = remoteListState?.total ?? (isComplete ? localViewIds.length : listState?.total ?? null);
|
|
2600
|
+
const error = remoteError ?? listState?.error ?? null;
|
|
1865
2601
|
return {
|
|
1866
2602
|
items,
|
|
1867
2603
|
viewIds: localViewIds,
|
|
1868
2604
|
viewTotal,
|
|
1869
|
-
isLoading: items.length === 0 && (listState?.isFetching ??
|
|
2605
|
+
isLoading: items.length === 0 && ((listState?.isFetching ?? false) || isRemoteFetching),
|
|
1870
2606
|
isFetching: (listState?.isFetching ?? false) || isRemoteFetching,
|
|
1871
2607
|
isRemoteFetching,
|
|
1872
2608
|
isShowingLocalPending: completenessMode === "hybrid" && isRemoteFetching && items.length > 0,
|
|
1873
|
-
error
|
|
2609
|
+
error,
|
|
2610
|
+
isError: error !== null,
|
|
1874
2611
|
hasNextPage: completenessMode === "local" ? false : remoteListState?.hasNextPage ?? listState?.hasNextPage ?? false,
|
|
1875
2612
|
fetchNextPage,
|
|
1876
2613
|
isLocallyComplete: isComplete,
|
|
@@ -2814,6 +3551,255 @@ function usePGliteQuery(opts) {
|
|
|
2814
3551
|
return { isLoading, error };
|
|
2815
3552
|
}
|
|
2816
3553
|
|
|
3554
|
+
// src/adapters/electricsql-tenant.ts
|
|
3555
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3556
|
+
function assertUuid(companyId) {
|
|
3557
|
+
if (typeof companyId !== "string" || !UUID_RE.test(companyId)) {
|
|
3558
|
+
throw new Error(
|
|
3559
|
+
`tenant-scoped adapter: companyId must be a UUID, received "${companyId}".`
|
|
3560
|
+
);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
function assertSafeColumn(column) {
|
|
3564
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
3565
|
+
throw new Error(`tenant-scoped adapter: unsafe tenantColumn "${column}".`);
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
function buildTenantWhere(tenantColumn, companyId, tableLabel) {
|
|
3569
|
+
if (typeof tenantColumn === "undefined") {
|
|
3570
|
+
throw new Error(
|
|
3571
|
+
`shape "${tableLabel}" lacks tenantColumn \u2014 tenant-scoped adapter refuses to attach unscoped shapes.`
|
|
3572
|
+
);
|
|
3573
|
+
}
|
|
3574
|
+
assertUuid(companyId);
|
|
3575
|
+
if (tenantColumn === null) {
|
|
3576
|
+
return `id = '${companyId}'`;
|
|
3577
|
+
}
|
|
3578
|
+
assertSafeColumn(tenantColumn);
|
|
3579
|
+
return `${tenantColumn} = '${companyId}'`;
|
|
3580
|
+
}
|
|
3581
|
+
function createTenantScopedElectricAdapter(opts) {
|
|
3582
|
+
const { pglite, tenantClaim, tables, onSynced } = opts;
|
|
3583
|
+
assertUuid(tenantClaim.companyId);
|
|
3584
|
+
const wired = tables.map((tc) => {
|
|
3585
|
+
const where = buildTenantWhere(tc.tenantColumn, tenantClaim.companyId, tc.table);
|
|
3586
|
+
const shapeStream = tc.shapeStreamFactory({ table: tc.table, where, tenantClaim });
|
|
3587
|
+
return {
|
|
3588
|
+
type: tc.type,
|
|
3589
|
+
table: tc.table,
|
|
3590
|
+
where,
|
|
3591
|
+
idColumn: tc.primaryKey?.[0] ?? "id",
|
|
3592
|
+
normalize: tc.normalize,
|
|
3593
|
+
shapeStream
|
|
3594
|
+
};
|
|
3595
|
+
});
|
|
3596
|
+
return createElectricAdapter({ pglite, tables: wired, onSynced });
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
// src/adapters/pglite-persistence.ts
|
|
3600
|
+
var DEFAULT_TABLE = "_graph_snapshot";
|
|
3601
|
+
async function createPGlitePersistenceAdapter(pglite, options = {}) {
|
|
3602
|
+
const tableName = options.tableName ?? DEFAULT_TABLE;
|
|
3603
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) {
|
|
3604
|
+
throw new Error(`createPGlitePersistenceAdapter: invalid tableName "${tableName}"`);
|
|
3605
|
+
}
|
|
3606
|
+
await pglite.exec(
|
|
3607
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
3608
|
+
key TEXT PRIMARY KEY,
|
|
3609
|
+
value TEXT NOT NULL,
|
|
3610
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
3611
|
+
)`
|
|
3612
|
+
);
|
|
3613
|
+
return {
|
|
3614
|
+
async get(key) {
|
|
3615
|
+
const result = await pglite.query(
|
|
3616
|
+
`SELECT value FROM ${tableName} WHERE key = $1`,
|
|
3617
|
+
[key]
|
|
3618
|
+
);
|
|
3619
|
+
const row = result.rows[0];
|
|
3620
|
+
return row?.value ?? null;
|
|
3621
|
+
},
|
|
3622
|
+
async set(key, value) {
|
|
3623
|
+
await pglite.query(
|
|
3624
|
+
`INSERT INTO ${tableName} (key, value, updated_at)
|
|
3625
|
+
VALUES ($1, $2, now())
|
|
3626
|
+
ON CONFLICT (key) DO UPDATE
|
|
3627
|
+
SET value = EXCLUDED.value,
|
|
3628
|
+
updated_at = now()`,
|
|
3629
|
+
[key, value]
|
|
3630
|
+
);
|
|
3631
|
+
},
|
|
3632
|
+
async remove(key) {
|
|
3633
|
+
await pglite.query(`DELETE FROM ${tableName} WHERE key = $1`, [key]);
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// src/schema-from-sql.ts
|
|
3639
|
+
function parseCreateTable(sql) {
|
|
3640
|
+
const headerMatch = sql.match(
|
|
3641
|
+
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["]?([A-Za-z_][A-Za-z0-9_]*)["]?\s*\(([\s\S]*)\)\s*;?\s*$/im
|
|
3642
|
+
);
|
|
3643
|
+
if (!headerMatch) {
|
|
3644
|
+
throw new Error("parseCreateTable: could not locate a CREATE TABLE block");
|
|
3645
|
+
}
|
|
3646
|
+
const tableName = headerMatch[1];
|
|
3647
|
+
const body = headerMatch[2];
|
|
3648
|
+
const columns = [];
|
|
3649
|
+
for (const rawLine of splitTopLevelCommas(body)) {
|
|
3650
|
+
const line = rawLine.trim();
|
|
3651
|
+
if (!line) continue;
|
|
3652
|
+
if (/^(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|LIKE|EXCLUDE)\b/i.test(line)) {
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
const colMatch = line.match(/^["]?([A-Za-z_][A-Za-z0-9_]*)["]?\s+([A-Za-z][A-Za-z0-9_]*(?:\s*\([^)]*\))?(?:\s*\[\s*\])?)([\s\S]*)$/);
|
|
3656
|
+
if (!colMatch) continue;
|
|
3657
|
+
const name = colMatch[1];
|
|
3658
|
+
const sqlType = colMatch[2].replace(/\s+/g, "").toUpperCase();
|
|
3659
|
+
const rest = colMatch[3].toUpperCase();
|
|
3660
|
+
const notNull = /\bNOT\s+NULL\b/.test(rest);
|
|
3661
|
+
const hasDefault = /\bDEFAULT\b/.test(rest);
|
|
3662
|
+
columns.push({ name, sqlType, notNull, hasDefault });
|
|
3663
|
+
}
|
|
3664
|
+
return { tableName, columns };
|
|
3665
|
+
}
|
|
3666
|
+
function splitTopLevelCommas(body) {
|
|
3667
|
+
const parts = [];
|
|
3668
|
+
let depth = 0;
|
|
3669
|
+
let current = "";
|
|
3670
|
+
for (const ch of body) {
|
|
3671
|
+
if (ch === "(") depth++;
|
|
3672
|
+
else if (ch === ")") depth--;
|
|
3673
|
+
if (ch === "," && depth === 0) {
|
|
3674
|
+
parts.push(current);
|
|
3675
|
+
current = "";
|
|
3676
|
+
} else {
|
|
3677
|
+
current += ch;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
if (current.trim()) parts.push(current);
|
|
3681
|
+
return parts;
|
|
3682
|
+
}
|
|
3683
|
+
function sqlTypeToJsonSchema(sqlType) {
|
|
3684
|
+
const t = sqlType.replace(/\s+/g, "").toUpperCase();
|
|
3685
|
+
if (/^TEXT\[\]$/.test(t)) {
|
|
3686
|
+
return { schema: { type: "array", items: { type: "string" } } };
|
|
3687
|
+
}
|
|
3688
|
+
if (t === "UUID" || t === "TEXT" || /^VARCHAR(\(.*\))?$/.test(t) || /^CHAR(\(.*\))?$/.test(t)) {
|
|
3689
|
+
return { schema: { type: "string" } };
|
|
3690
|
+
}
|
|
3691
|
+
if (t === "INTEGER" || t === "INT" || t === "BIGINT" || t === "SMALLINT" || t === "INT4" || t === "INT8" || t === "INT2") {
|
|
3692
|
+
return { schema: { type: "integer" } };
|
|
3693
|
+
}
|
|
3694
|
+
if (/^NUMERIC(\(.*\))?$/.test(t) || /^DECIMAL(\(.*\))?$/.test(t) || t === "REAL" || t === "DOUBLEPRECISION" || t === "FLOAT") {
|
|
3695
|
+
return { schema: { type: "number" } };
|
|
3696
|
+
}
|
|
3697
|
+
if (t === "BOOLEAN" || t === "BOOL") {
|
|
3698
|
+
return { schema: { type: "boolean" } };
|
|
3699
|
+
}
|
|
3700
|
+
if (t === "TIMESTAMPTZ" || t === "TIMESTAMP" || t === "TIMESTAMPWITHTIMEZONE" || t === "TIMESTAMPWITHOUTTIMEZONE") {
|
|
3701
|
+
return { schema: { type: "string", format: "date-time" } };
|
|
3702
|
+
}
|
|
3703
|
+
if (t === "DATE") {
|
|
3704
|
+
return { schema: { type: "string", format: "date" } };
|
|
3705
|
+
}
|
|
3706
|
+
if (t === "JSONB" || t === "JSON") {
|
|
3707
|
+
return { schema: { type: "object" } };
|
|
3708
|
+
}
|
|
3709
|
+
return {
|
|
3710
|
+
schema: { type: "string" },
|
|
3711
|
+
warning: `sql type "${sqlType}" not explicitly mapped; defaulting to string`
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
function deepMergeUnknown(base, override) {
|
|
3715
|
+
const out = { ...base };
|
|
3716
|
+
for (const [key, value] of Object.entries(override)) {
|
|
3717
|
+
const baseValue = base[key];
|
|
3718
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && baseValue !== null && typeof baseValue === "object" && !Array.isArray(baseValue)) {
|
|
3719
|
+
out[key] = deepMergeUnknown(
|
|
3720
|
+
baseValue,
|
|
3721
|
+
value
|
|
3722
|
+
);
|
|
3723
|
+
} else {
|
|
3724
|
+
out[key] = value;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
return out;
|
|
3728
|
+
}
|
|
3729
|
+
function mergeEntityConfig(generated, overrides) {
|
|
3730
|
+
if (!overrides) return generated;
|
|
3731
|
+
const merged = deepMergeUnknown(
|
|
3732
|
+
generated,
|
|
3733
|
+
overrides
|
|
3734
|
+
);
|
|
3735
|
+
const refined = merged;
|
|
3736
|
+
return {
|
|
3737
|
+
...refined,
|
|
3738
|
+
entityType: generated.entityType,
|
|
3739
|
+
schema: refined.schema ?? generated.schema
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
function registerEntityFromSql(opts) {
|
|
3743
|
+
const parsed = parseCreateTable(opts.createTableSql);
|
|
3744
|
+
const properties = {};
|
|
3745
|
+
const required = [];
|
|
3746
|
+
for (const col of parsed.columns) {
|
|
3747
|
+
const { schema, warning } = sqlTypeToJsonSchema(col.sqlType);
|
|
3748
|
+
const propSchema = { ...schema };
|
|
3749
|
+
if (warning) {
|
|
3750
|
+
propSchema["x-warning"] = warning;
|
|
3751
|
+
}
|
|
3752
|
+
properties[col.name] = propSchema;
|
|
3753
|
+
if (col.notNull && !col.hasDefault) {
|
|
3754
|
+
required.push(col.name);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
const generated = {
|
|
3758
|
+
entityType: opts.entityType,
|
|
3759
|
+
source: "runtime",
|
|
3760
|
+
schema: {
|
|
3761
|
+
type: "object",
|
|
3762
|
+
title: parsed.tableName,
|
|
3763
|
+
properties,
|
|
3764
|
+
required
|
|
3765
|
+
}
|
|
3766
|
+
};
|
|
3767
|
+
const config = mergeEntityConfig(generated, opts.overrides);
|
|
3768
|
+
registerEntityJsonSchema(config);
|
|
3769
|
+
return config;
|
|
3770
|
+
}
|
|
3771
|
+
function useEntityListAsTable(opts) {
|
|
3772
|
+
const queryKey = opts.queryKey ?? ["entity-list-as-table", opts.type];
|
|
3773
|
+
const list = useEntityList({
|
|
3774
|
+
...opts,
|
|
3775
|
+
queryKey
|
|
3776
|
+
});
|
|
3777
|
+
const lastDataRef = useRef(null);
|
|
3778
|
+
const data = useMemo(() => {
|
|
3779
|
+
const prev = lastDataRef.current;
|
|
3780
|
+
if (prev && prev.length === list.items.length) {
|
|
3781
|
+
let same = true;
|
|
3782
|
+
for (let i = 0; i < prev.length; i++) {
|
|
3783
|
+
if (prev[i] !== list.items[i]) {
|
|
3784
|
+
same = false;
|
|
3785
|
+
break;
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
if (same) return prev;
|
|
3789
|
+
}
|
|
3790
|
+
lastDataRef.current = list.items;
|
|
3791
|
+
return list.items;
|
|
3792
|
+
}, [list.items]);
|
|
3793
|
+
return {
|
|
3794
|
+
data,
|
|
3795
|
+
rowCount: list.total ?? list.ids.length,
|
|
3796
|
+
isLoading: list.isLoading,
|
|
3797
|
+
isFetching: list.isFetching,
|
|
3798
|
+
error: list.error,
|
|
3799
|
+
refetch: list.refetch
|
|
3800
|
+
};
|
|
3801
|
+
}
|
|
3802
|
+
|
|
2817
3803
|
// src/graphql/client.ts
|
|
2818
3804
|
async function executeGQL(cfg, document2, variables) {
|
|
2819
3805
|
const headers = { "Content-Type": "application/json", ...cfg.headers?.() ?? {} };
|
|
@@ -2854,6 +3840,7 @@ var GQLClient = class {
|
|
|
2854
3840
|
constructor(cfg) {
|
|
2855
3841
|
this.cfg = cfg;
|
|
2856
3842
|
}
|
|
3843
|
+
cfg;
|
|
2857
3844
|
async query(opts) {
|
|
2858
3845
|
const key = opts.cacheKey ?? `gql:${opts.document.slice(0, 60)}:${JSON.stringify(opts.variables ?? {})}`;
|
|
2859
3846
|
return dedupe(key, async () => {
|
|
@@ -6641,10 +7628,11 @@ function DefaultListItemContent({
|
|
|
6641
7628
|
descriptor
|
|
6642
7629
|
}) {
|
|
6643
7630
|
if (!descriptor) {
|
|
6644
|
-
const
|
|
7631
|
+
const rec = item;
|
|
7632
|
+
const keys = Object.keys(rec);
|
|
6645
7633
|
return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
6646
|
-
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: String(
|
|
6647
|
-
keys[1] && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: String(
|
|
7634
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: String(rec[keys[0]] ?? "") }),
|
|
7635
|
+
keys[1] && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: String(rec[keys[1]] ?? "") })
|
|
6648
7636
|
] });
|
|
6649
7637
|
}
|
|
6650
7638
|
const IconComponent = typeof descriptor.icon === "function" ? descriptor.icon : null;
|
|
@@ -8659,6 +9647,6 @@ function actionsColumn2() {
|
|
|
8659
9647
|
};
|
|
8660
9648
|
}
|
|
8661
9649
|
|
|
8662
|
-
export { ActionButtonRow, ActionDropdown, ColumnPresetDialog, DataTable, DataTableColumnHeader, DataTableFilter, DataTablePagination, DataTableToolbar, ElectricSQLAdapter as ElectricSQLPresetAdapter, EmptyState, EntityDetailSheet, EntityFormSheet, EntityListView, EntityTable, FilterPresetDialog, GQLClient, GalleryView, InlineCellEditor, InlineItemEditor, ListView, MarkdownFieldEditor, MarkdownFieldRenderer, MemoryAdapter, MultiSelectBar, PresetPicker, InlineCellEditor2 as PureInlineCellEditor, RealtimeManager, RestApiAdapter, SelectionContext, Sheet, SortHeader, SupabaseRealtimeAdapter as SupabasePresetAdapter, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, TableStorageProvider, ViewModeSwitcher, ZustandPersistAdapter, actionsColumn, applyView, booleanColumn, buildEntityFieldsFromSchema, cascadeInvalidation, checkCompleteness, compareEntities, configureEngine, createConvexAdapter, createElectricAdapter, createGQLClient, createGraphAction, createGraphEffect, createGraphQLSubscriptionAdapter, createGraphTool, createGraphTransaction, createPresetStore, createPrismaEntityConfig, createRow, createSchemaGraphTool, createSelectionStore, createSupabaseRealtimeAdapter, createWebSocketAdapter, dateColumn, dedupe, deleteAction, editAction, enumColumn, executeGQL, exportGraphSnapshot, exportGraphSnapshotWithSchemas, fetchEntity, fetchList, flattenClauses, getCoreRowModel2 as getCoreRowModel, getEntityJsonSchema, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginatedRowModel, getRealtimeManager, getSchema, getSelectedRowModel, getSortedRowModel2 as getSortedRowModel, hasCustomPredicates, hydrateGraphFromStorage, matchesFilter, matchesSearch, normalizeGQLResponse, numberColumn, persistGraphToStorage, prismaRelationsToSchema, actionsColumn2 as pureActionsColumn, booleanColumn2 as pureBooleanColumn, dateColumn2 as pureDateColumn, enumColumn2 as pureEnumColumn, numberColumn2 as pureNumberColumn, selectionColumn2 as pureSelectionColumn, textColumn2 as pureTextColumn, queryOnce, readRelations, registerEntityJsonSchema, registerRuntimeSchema, registerSchema, renderMarkdownToHtml, resetRealtimeManager, selectGraph, selectionColumn, serializeKey, startGarbageCollector, startLocalFirstGraph, stopGarbageCollector, textColumn, toGraphQLVariables, toPrismaInclude, toPrismaOrderBy, toPrismaWhere, toRestParams, toSQLClauses, useEntity, useEntityAugment, useEntityCRUD, useEntityList, useEntityMutation, useEntityView, useGQLEntity, useGQLList, useGQLMutation, useGQLSubscription, useGraphDevTools, useGraphStore, useGraphSyncStatus, useLocalFirst, usePGliteQuery, useSchemaEntityFields, useSelectionContext, useSelectionStore, useSuspenseEntity, useSuspenseEntityList, useTable, useTablePresets, useTableRealtimeMode, useTableStorageAdapter, viewAction };
|
|
9650
|
+
export { ActionButtonRow, ActionDropdown, ColumnPresetDialog, DataTable, DataTableColumnHeader, DataTableFilter, DataTablePagination, DataTableToolbar, ElectricSQLAdapter as ElectricSQLPresetAdapter, EmptyState, EntityDetailSheet, EntityFormSheet, EntityListView, EntityTable, FilterPresetDialog, GQLClient, GalleryView, InlineCellEditor, InlineItemEditor, ListView, MarkdownFieldEditor, MarkdownFieldRenderer, MemoryAdapter, MultiSelectBar, PresetPicker, InlineCellEditor2 as PureInlineCellEditor, RealtimeManager, RestApiAdapter, SelectionContext, Sheet, SortHeader, SupabaseRealtimeAdapter as SupabasePresetAdapter, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, TableStorageProvider, TerminalError, TransientError, ViewModeSwitcher, ZustandPersistAdapter, __resetEntityTransports, actionsColumn, applyView, booleanColumn, buildEntityFieldsFromSchema, buildTenantWhere, cascadeInvalidation, checkCompleteness, compareEntities, configureEngine, createConvexAdapter, createElectricAdapter, createGQLClient, createGraphAction, createGraphEffect, createGraphQLSubscriptionAdapter, createGraphTool, createGraphTransaction, createPGlitePersistenceAdapter, createPresetStore, createPrismaEntityConfig, createRow, createSchemaGraphTool, createSelectionStore, createSupabaseRealtimeAdapter, createTenantScopedElectricAdapter, createWebSocketAdapter, dateColumn, dedupe, deleteAction, editAction, enumColumn, executeGQL, exportGraphSnapshot, exportGraphSnapshotWithSchemas, fetchEntity, fetchList, flattenClauses, getCoreRowModel2 as getCoreRowModel, getEntityJsonSchema, getEntityTransport, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginatedRowModel, getRealtimeManager, getRegisteredEntityTypes, getSchema, getSelectedRowModel, getSortedRowModel2 as getSortedRowModel, hasCustomPredicates, hydrateGraphFromStorage, makeRestTransport, matchesFilter, matchesSearch, normalizeGQLResponse, numberColumn, parseCreateTable, persistGraphToStorage, prismaRelationsToSchema, actionsColumn2 as pureActionsColumn, booleanColumn2 as pureBooleanColumn, dateColumn2 as pureDateColumn, enumColumn2 as pureEnumColumn, numberColumn2 as pureNumberColumn, selectionColumn2 as pureSelectionColumn, textColumn2 as pureTextColumn, queryOnce, readRelations, registerEntityFromSql, registerEntityJsonSchema, registerEntityTransport, registerRuntimeSchema, registerSchema, renderMarkdownToHtml, replayActionWithRetry, resetRealtimeManager, selectGraph, selectionColumn, serializeKey, sqlTypeToJsonSchema, startGarbageCollector, startLocalFirstGraph, stopGarbageCollector, textColumn, toEntityError, toGraphQLVariables, toPrismaInclude, toPrismaOrderBy, toPrismaWhere, toRestParams, toSQLClauses, useEntities, useEntity, useEntityAugment, useEntityCRUD, useEntityList, useEntityListAsTable, useEntityMutation, useEntityQuery, useEntityView, useGQLEntity, useGQLList, useGQLMutation, useGQLSubscription, useGraphDevTools, useGraphStore, useGraphSyncStatus, useLocalFirst, usePGliteQuery, useSchemaEntityFields, useSelectionContext, useSelectionStore, useSuspenseEntity, useSuspenseEntityList, useTable, useTablePresets, useTableRealtimeMode, useTableStorageAdapter, viewAction };
|
|
8663
9651
|
//# sourceMappingURL=index.mjs.map
|
|
8664
9652
|
//# sourceMappingURL=index.mjs.map
|