@prometheus-ags/prometheus-entity-management 1.2.2 → 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 +2596 -1592
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2580 -1594
- 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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
768
|
+
useGraphStore.getState().setEntityError(type, id, error.message);
|
|
769
|
+
opts.onError?.(error);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
await dedupe(`${type}:${id}`, () => attempt(0));
|
|
622
773
|
}
|
|
623
|
-
function
|
|
624
|
-
const type =
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
+
}
|
|
634
819
|
};
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
if (type === "markdown") {
|
|
642
|
-
descriptor.render = createMarkdownDetailRenderer(field);
|
|
643
|
-
}
|
|
644
|
-
return descriptor;
|
|
645
|
-
}
|
|
646
|
-
function inferFieldType(schema) {
|
|
647
|
-
const forced = schema["x-field-type"];
|
|
648
|
-
if (forced === "markdown") return "markdown";
|
|
649
|
-
if (schema.format === "markdown") return "markdown";
|
|
650
|
-
if (schema.enum) return "enum";
|
|
651
|
-
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
652
|
-
switch (type) {
|
|
653
|
-
case "boolean":
|
|
654
|
-
return "boolean";
|
|
655
|
-
case "integer":
|
|
656
|
-
case "number":
|
|
657
|
-
return "number";
|
|
658
|
-
case "string":
|
|
659
|
-
if (schema.format === "email") return "email";
|
|
660
|
-
if (schema.format === "uri" || schema.format === "url") return "url";
|
|
661
|
-
if (schema.format === "date" || schema.format === "date-time") return "date";
|
|
662
|
-
return "text";
|
|
663
|
-
case "array":
|
|
664
|
-
case "object":
|
|
665
|
-
return "json";
|
|
666
|
-
default:
|
|
667
|
-
return "text";
|
|
820
|
+
if (engineOptions.revalidateOnFocus) {
|
|
821
|
+
window.addEventListener("visibilitychange", () => {
|
|
822
|
+
if (document.visibilityState === "visible") revalidateAll();
|
|
823
|
+
});
|
|
824
|
+
window.addEventListener("focus", revalidateAll);
|
|
668
825
|
}
|
|
826
|
+
if (engineOptions.revalidateOnReconnect) window.addEventListener("online", revalidateAll);
|
|
669
827
|
}
|
|
670
|
-
|
|
671
|
-
|
|
828
|
+
|
|
829
|
+
// src/hooks/use-entities.ts
|
|
830
|
+
function sleep2(ms) {
|
|
831
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
672
832
|
}
|
|
673
|
-
function
|
|
674
|
-
|
|
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
|
|
958
|
+
};
|
|
675
959
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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);
|
|
680
965
|
}
|
|
681
|
-
function
|
|
682
|
-
|
|
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));
|
|
683
970
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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;
|
|
1013
|
+
default:
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
693
1016
|
}
|
|
694
|
-
function
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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);
|
|
699
1023
|
});
|
|
700
1024
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
exportGraphSnapshotWithSchemas
|
|
708
|
-
});
|
|
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;
|
|
709
1031
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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;
|
|
1047
|
+
}
|
|
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;
|
|
726
1057
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return useGraphSyncStatusStore((state) => state.status);
|
|
1058
|
+
if (compareEntities(entity, me, sort) <= 0) hi = mid;
|
|
1059
|
+
else lo = mid + 1;
|
|
1060
|
+
}
|
|
1061
|
+
return lo;
|
|
732
1062
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
};
|
|
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);
|
|
753
1074
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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" };
|
|
1079
|
+
}
|
|
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;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/hooks/use-entity-query.ts
|
|
1091
|
+
function sleep3(ms) {
|
|
1092
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1093
|
+
}
|
|
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
|
-
|
|
1115
|
-
|
|
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];
|
|
1116
1670
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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);
|
|
1680
|
+
}
|
|
1681
|
+
return acc;
|
|
1127
1682
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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;
|
|
1704
|
+
}
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1719
|
+
function buildEntityFieldsFromSchema(opts) {
|
|
1720
|
+
return buildSchemaFields(opts.schema, opts.rootField ?? "", "");
|
|
1721
|
+
}
|
|
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
|
|
1160
1732
|
);
|
|
1161
|
-
return { ...graphPart, subscriberCount };
|
|
1162
1733
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (!listenersAttached) {
|
|
1166
|
-
attachGlobalListeners();
|
|
1167
|
-
listenersAttached = true;
|
|
1168
|
-
}
|
|
1734
|
+
function escapeHtml(input) {
|
|
1735
|
+
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1169
1736
|
}
|
|
1170
|
-
function
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
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 };
|
|
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("");
|
|
1204
1741
|
}
|
|
1205
|
-
function
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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 };
|
|
1742
|
+
function MarkdownFieldRenderer({ value, className }) {
|
|
1743
|
+
return /* @__PURE__ */ jsx(
|
|
1744
|
+
"div",
|
|
1745
|
+
{
|
|
1746
|
+
className,
|
|
1747
|
+
dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(value ?? "") }
|
|
1748
|
+
}
|
|
1749
|
+
);
|
|
1238
1750
|
}
|
|
1239
|
-
function
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
-
};
|
|
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"
|
|
1263
1764
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
+
] });
|
|
1768
|
+
}
|
|
1769
|
+
function createMarkdownDetailRenderer(field) {
|
|
1770
|
+
return (value, entity) => /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value: String(value ?? getValueAtPath(entity, field) ?? ""), className: "prose prose-sm max-w-none" });
|
|
1771
|
+
}
|
|
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);
|
|
1276
1785
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1786
|
+
return [schemaField(field, schemaPath, childSchema, schema.required?.includes(key) ?? false)];
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
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
|
+
}));
|
|
1808
|
+
}
|
|
1809
|
+
if (type === "markdown") {
|
|
1810
|
+
descriptor.render = createMarkdownDetailRenderer(field);
|
|
1811
|
+
}
|
|
1812
|
+
return descriptor;
|
|
1813
|
+
}
|
|
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";
|
|
1836
|
+
}
|
|
1295
1837
|
}
|
|
1296
|
-
function
|
|
1297
|
-
|
|
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 };
|
|
1838
|
+
function registryKey(entityType, field, schemaId) {
|
|
1839
|
+
return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
|
|
1311
1840
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
if (
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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);
|
|
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());
|
|
1843
|
+
}
|
|
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>`;
|
|
1848
|
+
}
|
|
1849
|
+
function renderInlineMarkdown(block) {
|
|
1850
|
+
return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
1851
|
+
}
|
|
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);
|
|
1861
|
+
}
|
|
1862
|
+
function createGraphTool(handler) {
|
|
1863
|
+
return (input) => handler(input, {
|
|
1864
|
+
store: useGraphStore.getState(),
|
|
1865
|
+
queryOnce,
|
|
1866
|
+
exportGraphSnapshot
|
|
1343
1867
|
});
|
|
1344
|
-
suspenseEntityPromises.set(key, tracked);
|
|
1345
|
-
return tracked;
|
|
1346
1868
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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));
|
|
1869
|
+
function createSchemaGraphTool(handler) {
|
|
1870
|
+
return (input) => handler(input, {
|
|
1871
|
+
store: useGraphStore.getState(),
|
|
1872
|
+
queryOnce,
|
|
1873
|
+
exportGraphSnapshot,
|
|
1874
|
+
getEntityJsonSchema,
|
|
1875
|
+
exportGraphSnapshotWithSchemas
|
|
1371
1876
|
});
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
|
1894
|
+
}
|
|
1895
|
+
}))
|
|
1896
|
+
}));
|
|
1897
|
+
var pendingActions = /* @__PURE__ */ new Map();
|
|
1898
|
+
function useGraphSyncStatus() {
|
|
1899
|
+
return useGraphSyncStatusStore((state) => state.status);
|
|
1900
|
+
}
|
|
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
|
|
1374
1914
|
});
|
|
1375
|
-
|
|
1376
|
-
|
|
1915
|
+
return {
|
|
1916
|
+
ok: true,
|
|
1917
|
+
key: opts.key,
|
|
1918
|
+
bytes: json.length,
|
|
1919
|
+
persistedAt
|
|
1920
|
+
};
|
|
1377
1921
|
}
|
|
1378
|
-
function
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
+
};
|
|
1387
1932
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
+
};
|
|
1390
1968
|
}
|
|
1969
|
+
}
|
|
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
|
+
})();
|
|
1391
2029
|
return {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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
|
+
}
|
|
1396
2046
|
};
|
|
1397
2047
|
}
|
|
1398
|
-
function
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// src/view/evaluator.ts
|
|
1410
|
-
function matchesFilter(entity, filter) {
|
|
1411
|
-
if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
|
|
1412
|
-
return matchesGroup(entity, filter);
|
|
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
|
+
};
|
|
1413
2057
|
}
|
|
1414
|
-
function
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
+
};
|
|
1418
2067
|
}
|
|
1419
|
-
function
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
case "
|
|
1426
|
-
return
|
|
1427
|
-
case "
|
|
1428
|
-
return
|
|
1429
|
-
case "
|
|
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;
|
|
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":
|
|
1461
2079
|
default:
|
|
1462
|
-
return
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
function matchesSearch(entity, query, fields) {
|
|
1466
|
-
if (!query.trim()) return true;
|
|
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
|
-
});
|
|
1472
|
-
}
|
|
1473
|
-
function compareEntities(a, b, sort) {
|
|
1474
|
-
for (const clause of sort) {
|
|
1475
|
-
const r = compareByClause(a, b, clause);
|
|
1476
|
-
if (r !== 0) return r;
|
|
2080
|
+
return base / 2 + Math.random() * (base / 2);
|
|
1477
2081
|
}
|
|
1478
|
-
return 0;
|
|
1479
2082
|
}
|
|
1480
|
-
function
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const bv = getNestedValue(b, field);
|
|
1484
|
-
const aNull = av == null;
|
|
1485
|
-
const bNull = bv == null;
|
|
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;
|
|
2083
|
+
function sleep4(ms) {
|
|
2084
|
+
if (ms <= 0) return Promise.resolve();
|
|
2085
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1495
2086
|
}
|
|
1496
|
-
function
|
|
1497
|
-
let
|
|
1498
|
-
let
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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));
|
|
1505
2097
|
}
|
|
1506
|
-
if (compareEntities(entity, me, sort) <= 0) hi = mid;
|
|
1507
|
-
else lo = mid + 1;
|
|
1508
|
-
}
|
|
1509
|
-
return lo;
|
|
1510
|
-
}
|
|
1511
|
-
function applyView(ids, getEntity, filter, sort, search) {
|
|
1512
|
-
let entries = [];
|
|
1513
|
-
for (const id of ids) {
|
|
1514
|
-
const entity = getEntity(id);
|
|
1515
|
-
if (!entity) continue;
|
|
1516
|
-
entries.push({ id, entity });
|
|
1517
|
-
}
|
|
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
|
-
}
|
|
1523
|
-
function checkCompleteness(loadedCount, total, hasNextPage) {
|
|
1524
|
-
if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
|
|
1525
|
-
if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
|
|
1526
|
-
return { isComplete: true, reason: "no-more-pages" };
|
|
1527
|
-
}
|
|
1528
|
-
function getNestedValue(obj, path) {
|
|
1529
|
-
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
1530
|
-
let current = obj;
|
|
1531
|
-
for (const part of parts) {
|
|
1532
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
1533
|
-
current = current[part];
|
|
1534
2098
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
// src/view/prisma-compile.ts
|
|
1539
|
-
function nestWhereField(parts, leaf) {
|
|
1540
|
-
if (parts.length === 0) return {};
|
|
1541
|
-
if (parts.length === 1) return { [parts[0]]: leaf };
|
|
1542
|
-
return { [parts[0]]: nestWhereField(parts.slice(1), leaf) };
|
|
1543
|
-
}
|
|
1544
|
-
function clauseToPrismaLeaf(c) {
|
|
1545
|
-
switch (c.op) {
|
|
1546
|
-
case "eq":
|
|
1547
|
-
return { equals: c.value };
|
|
1548
|
-
case "neq":
|
|
1549
|
-
return { not: c.value };
|
|
1550
|
-
case "gt":
|
|
1551
|
-
return { gt: c.value };
|
|
1552
|
-
case "gte":
|
|
1553
|
-
return { gte: c.value };
|
|
1554
|
-
case "lt":
|
|
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":
|
|
1574
|
-
default:
|
|
1575
|
-
return null;
|
|
2099
|
+
try {
|
|
2100
|
+
await policy.poisonHandler?.(action, lastError);
|
|
2101
|
+
} catch {
|
|
1576
2102
|
}
|
|
2103
|
+
return { ok: false, poisoned: true, error: lastError };
|
|
1577
2104
|
}
|
|
1578
|
-
function
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
+
};
|
|
1587
2120
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1592
|
-
function groupToPrismaWhere(g) {
|
|
1593
|
-
const parts = [];
|
|
1594
|
-
for (const item of g.clauses) {
|
|
1595
|
-
if ("logic" in item) {
|
|
1596
|
-
const nested = groupToPrismaWhere(item);
|
|
1597
|
-
if (Object.keys(nested).length > 0) parts.push(nested);
|
|
1598
|
-
} else {
|
|
1599
|
-
const entry = clauseToPrismaEntry(item);
|
|
1600
|
-
if (entry) parts.push(entry);
|
|
2121
|
+
return {
|
|
2122
|
+
getIsOnline: () => true,
|
|
2123
|
+
subscribe: () => () => {
|
|
1601
2124
|
}
|
|
1602
|
-
}
|
|
1603
|
-
if (parts.length === 0) return {};
|
|
1604
|
-
if (parts.length === 1) return parts[0];
|
|
1605
|
-
return g.logic === "or" ? { OR: parts } : { AND: parts };
|
|
2125
|
+
};
|
|
1606
2126
|
}
|
|
1607
|
-
function
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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;
|
|
2136
|
+
}
|
|
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 });
|
|
1613
2146
|
}
|
|
1614
|
-
if (parts.length === 0) return {};
|
|
1615
|
-
if (parts.length === 1) return parts[0];
|
|
1616
|
-
return { AND: parts };
|
|
1617
2147
|
}
|
|
1618
|
-
|
|
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;
|
|
@@ -1783,11 +2515,9 @@ function useEntityView(opts) {
|
|
|
1783
2515
|
);
|
|
1784
2516
|
})
|
|
1785
2517
|
);
|
|
1786
|
-
const items =
|
|
1787
|
-
useGraphStore,
|
|
1788
|
-
|
|
1789
|
-
(state) => localViewIds.map((id) => state.readEntitySnapshot(type, id)).filter((item) => item !== null)
|
|
1790
|
-
)
|
|
2518
|
+
const items = useMemo(
|
|
2519
|
+
() => localViewIds.map((id) => useGraphStore.getState().readEntitySnapshot(type, id)).filter((item) => item !== null),
|
|
2520
|
+
[localViewIds, type]
|
|
1791
2521
|
);
|
|
1792
2522
|
const fireRemoteFetch = useCallback(async (view, cursor) => {
|
|
1793
2523
|
const { remoteFetch: rf, normalize: norm, baseQueryKey: bqk } = optsRef.current;
|
|
@@ -1799,16 +2529,19 @@ function useEntityView(opts) {
|
|
|
1799
2529
|
setRemoteError(null);
|
|
1800
2530
|
const store = useGraphStore.getState();
|
|
1801
2531
|
store.setListFetching(rKey, true);
|
|
2532
|
+
const baseKeyStr = serializeKey(bqk);
|
|
1802
2533
|
try {
|
|
1803
2534
|
const response = await rf(params);
|
|
1804
2535
|
const normalized = norm ? response.items.map(norm) : response.items.map((item) => ({ id: String(item.id), data: item }));
|
|
1805
|
-
store.upsertEntities(type, normalized);
|
|
2536
|
+
store.upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
|
|
1806
2537
|
for (const { id } of normalized) store.setEntityFetched(type, id);
|
|
1807
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);
|
|
1808
2540
|
} catch (err) {
|
|
1809
2541
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1810
2542
|
setRemoteError(msg);
|
|
1811
2543
|
store.setListError(rKey, msg);
|
|
2544
|
+
store.setListError(baseKeyStr, msg);
|
|
1812
2545
|
} finally {
|
|
1813
2546
|
setIsRemoteFetching(false);
|
|
1814
2547
|
}
|
|
@@ -1832,7 +2565,7 @@ function useEntityView(opts) {
|
|
|
1832
2565
|
if (!existing || isStale) fireRemoteFetch(liveViewRef.current);
|
|
1833
2566
|
}, [baseKey, enabled, staleTime, fireRemoteFetch]);
|
|
1834
2567
|
useEffect(() => {
|
|
1835
|
-
const unsub = useGraphStore.subscribe((state) => state.entities[type] ??
|
|
2568
|
+
const unsub = useGraphStore.subscribe((state) => state.entities[type] ?? EMPTY_ENTITY_BUCKET2, (newEntities, prevEntities) => {
|
|
1836
2569
|
const view = liveViewRef.current;
|
|
1837
2570
|
const store = useGraphStore.getState();
|
|
1838
2571
|
const list = store.lists[baseKey];
|
|
@@ -1864,15 +2597,17 @@ function useEntityView(opts) {
|
|
|
1864
2597
|
}, [completenessMode, isRemoteFetching, remoteListState?.nextCursor, fireRemoteFetch]);
|
|
1865
2598
|
const refetch = useCallback(() => fireRemoteFetch(liveViewRef.current), [fireRemoteFetch]);
|
|
1866
2599
|
const viewTotal = remoteListState?.total ?? (isComplete ? localViewIds.length : listState?.total ?? null);
|
|
2600
|
+
const error = remoteError ?? listState?.error ?? null;
|
|
1867
2601
|
return {
|
|
1868
2602
|
items,
|
|
1869
2603
|
viewIds: localViewIds,
|
|
1870
2604
|
viewTotal,
|
|
1871
|
-
isLoading: items.length === 0 && (listState?.isFetching ??
|
|
2605
|
+
isLoading: items.length === 0 && ((listState?.isFetching ?? false) || isRemoteFetching),
|
|
1872
2606
|
isFetching: (listState?.isFetching ?? false) || isRemoteFetching,
|
|
1873
2607
|
isRemoteFetching,
|
|
1874
2608
|
isShowingLocalPending: completenessMode === "hybrid" && isRemoteFetching && items.length > 0,
|
|
1875
|
-
error
|
|
2609
|
+
error,
|
|
2610
|
+
isError: error !== null,
|
|
1876
2611
|
hasNextPage: completenessMode === "local" ? false : remoteListState?.hasNextPage ?? listState?.hasNextPage ?? false,
|
|
1877
2612
|
fetchNextPage,
|
|
1878
2613
|
isLocallyComplete: isComplete,
|
|
@@ -2816,6 +3551,255 @@ function usePGliteQuery(opts) {
|
|
|
2816
3551
|
return { isLoading, error };
|
|
2817
3552
|
}
|
|
2818
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
|
+
|
|
2819
3803
|
// src/graphql/client.ts
|
|
2820
3804
|
async function executeGQL(cfg, document2, variables) {
|
|
2821
3805
|
const headers = { "Content-Type": "application/json", ...cfg.headers?.() ?? {} };
|
|
@@ -2856,6 +3840,7 @@ var GQLClient = class {
|
|
|
2856
3840
|
constructor(cfg) {
|
|
2857
3841
|
this.cfg = cfg;
|
|
2858
3842
|
}
|
|
3843
|
+
cfg;
|
|
2859
3844
|
async query(opts) {
|
|
2860
3845
|
const key = opts.cacheKey ?? `gql:${opts.document.slice(0, 60)}:${JSON.stringify(opts.variables ?? {})}`;
|
|
2861
3846
|
return dedupe(key, async () => {
|
|
@@ -6643,10 +7628,11 @@ function DefaultListItemContent({
|
|
|
6643
7628
|
descriptor
|
|
6644
7629
|
}) {
|
|
6645
7630
|
if (!descriptor) {
|
|
6646
|
-
const
|
|
7631
|
+
const rec = item;
|
|
7632
|
+
const keys = Object.keys(rec);
|
|
6647
7633
|
return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
6648
|
-
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: String(
|
|
6649
|
-
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]] ?? "") })
|
|
6650
7636
|
] });
|
|
6651
7637
|
}
|
|
6652
7638
|
const IconComponent = typeof descriptor.icon === "function" ? descriptor.icon : null;
|
|
@@ -8661,6 +9647,6 @@ function actionsColumn2() {
|
|
|
8661
9647
|
};
|
|
8662
9648
|
}
|
|
8663
9649
|
|
|
8664
|
-
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 };
|
|
8665
9651
|
//# sourceMappingURL=index.mjs.map
|
|
8666
9652
|
//# sourceMappingURL=index.mjs.map
|