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