@prometheus-ags/prometheus-entity-management 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +263 -0
- package/README.md +85 -6
- package/dist/index.d.mts +1004 -163
- package/dist/index.d.ts +1004 -163
- package/dist/index.js +2589 -1583
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2573 -1585
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.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
|
-
|
|
774
|
+
useGraphStore.getState().setEntityError(type, id, error.message);
|
|
775
|
+
opts.onError?.(error);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
await dedupe(`${type}:${id}`, () => attempt(0));
|
|
779
|
+
}
|
|
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
|
+
}
|
|
825
|
+
};
|
|
826
|
+
if (engineOptions.revalidateOnFocus) {
|
|
827
|
+
window.addEventListener("visibilitychange", () => {
|
|
828
|
+
if (document.visibilityState === "visible") revalidateAll();
|
|
625
829
|
});
|
|
830
|
+
window.addEventListener("focus", revalidateAll);
|
|
626
831
|
}
|
|
627
|
-
|
|
832
|
+
if (engineOptions.revalidateOnReconnect) window.addEventListener("online", revalidateAll);
|
|
628
833
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
834
|
+
|
|
835
|
+
// src/hooks/use-entities.ts
|
|
836
|
+
function sleep2(ms) {
|
|
837
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
838
|
+
}
|
|
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
|
|
640
964
|
};
|
|
641
|
-
if (schema.enum) {
|
|
642
|
-
descriptor.options = schema.enum.map((value) => ({
|
|
643
|
-
value: String(value),
|
|
644
|
-
label: String(value)
|
|
645
|
-
}));
|
|
646
|
-
}
|
|
647
|
-
if (type === "markdown") {
|
|
648
|
-
descriptor.render = createMarkdownDetailRenderer(field);
|
|
649
|
-
}
|
|
650
|
-
return descriptor;
|
|
651
965
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
return
|
|
669
|
-
case "
|
|
670
|
-
|
|
671
|
-
|
|
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);
|
|
971
|
+
}
|
|
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));
|
|
976
|
+
}
|
|
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;
|
|
672
1019
|
default:
|
|
673
|
-
return
|
|
1020
|
+
return true;
|
|
674
1021
|
}
|
|
675
1022
|
}
|
|
676
|
-
function
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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);
|
|
1029
|
+
});
|
|
681
1030
|
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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;
|
|
686
1037
|
}
|
|
687
|
-
function
|
|
688
|
-
|
|
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;
|
|
689
1053
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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;
|
|
1063
|
+
}
|
|
1064
|
+
if (compareEntities(entity, me, sort) <= 0) hi = mid;
|
|
1065
|
+
else lo = mid + 1;
|
|
1066
|
+
}
|
|
1067
|
+
return lo;
|
|
699
1068
|
}
|
|
700
|
-
function
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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);
|
|
706
1080
|
}
|
|
707
|
-
function
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
exportGraphSnapshot,
|
|
712
|
-
getEntityJsonSchema,
|
|
713
|
-
exportGraphSnapshotWithSchemas
|
|
714
|
-
});
|
|
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" };
|
|
715
1085
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
lastPersistedAt: null,
|
|
725
|
-
storageKey: null,
|
|
726
|
-
error: null
|
|
727
|
-
},
|
|
728
|
-
setStatus: (status) => set((state) => ({
|
|
729
|
-
status: {
|
|
730
|
-
...state.status,
|
|
731
|
-
...status
|
|
732
|
-
}
|
|
733
|
-
}))
|
|
734
|
-
}));
|
|
735
|
-
var pendingActions = /* @__PURE__ */ new Map();
|
|
736
|
-
function useGraphSyncStatus() {
|
|
737
|
-
return useGraphSyncStatusStore((state) => state.status);
|
|
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;
|
|
738
1094
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
pendingActions: opts.pendingActions ?? Array.from(pendingActions.values())
|
|
744
|
-
};
|
|
745
|
-
const json = JSON.stringify(payload);
|
|
746
|
-
await opts.storage.set(opts.key, json);
|
|
747
|
-
const persistedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
748
|
-
useGraphSyncStatusStore.getState().setStatus({
|
|
749
|
-
lastPersistedAt: persistedAt,
|
|
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
|
-
};
|
|
1095
|
+
|
|
1096
|
+
// src/hooks/use-entity-query.ts
|
|
1097
|
+
function sleep3(ms) {
|
|
1098
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
759
1099
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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];
|
|
1676
|
+
}
|
|
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);
|
|
1121
1686
|
}
|
|
1687
|
+
return acc;
|
|
1122
1688
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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;
|
|
1133
1710
|
}
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
return {
|
|
1141
|
-
entityCounts,
|
|
1142
|
-
totalEntities,
|
|
1143
|
-
listCount,
|
|
1144
|
-
patchedEntities,
|
|
1145
|
-
staleEntities,
|
|
1146
|
-
fetchingEntities,
|
|
1147
|
-
lists
|
|
1148
|
-
};
|
|
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
|
-
subscribeSubscriberStats,
|
|
1156
|
-
getActiveSubscriberCount,
|
|
1157
|
-
subscriberCountServerSnapshot
|
|
1158
|
-
);
|
|
1159
|
-
const entities = zustand.useStore(useGraphStore, (state) => state.entities);
|
|
1160
|
-
const patches = zustand.useStore(useGraphStore, (state) => state.patches);
|
|
1161
|
-
const entityStates = zustand.useStore(useGraphStore, (state) => state.entityStates);
|
|
1162
|
-
const listsState = zustand.useStore(useGraphStore, (state) => state.lists);
|
|
1163
|
-
const graphPart = React6.useMemo(
|
|
1164
|
-
() => collectGraphDevStats(entities, patches, entityStates, listsState),
|
|
1165
|
-
[entities, patches, entityStates, listsState]
|
|
1166
|
-
);
|
|
1167
|
-
return { ...graphPart, subscriberCount };
|
|
1725
|
+
function buildEntityFieldsFromSchema(opts) {
|
|
1726
|
+
return buildSchemaFields(opts.schema, opts.rootField ?? "", "");
|
|
1168
1727
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
|
1738
|
+
);
|
|
1175
1739
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
ensureListeners();
|
|
1179
|
-
const fetchRef = React6.useRef(opts.fetch);
|
|
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 };
|
|
1740
|
+
function escapeHtml(input) {
|
|
1741
|
+
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1210
1742
|
}
|
|
1211
|
-
function
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
const fetchRef = React6.useRef(opts.fetch);
|
|
1216
|
-
fetchRef.current = opts.fetch;
|
|
1217
|
-
const normalizeRef = React6.useRef(opts.normalize);
|
|
1218
|
-
normalizeRef.current = opts.normalize;
|
|
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 };
|
|
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("");
|
|
1244
1747
|
}
|
|
1245
|
-
function
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
setState({ isPending: true, isSuccess: false, isError: false, error: null });
|
|
1252
|
-
let rollback = null;
|
|
1253
|
-
if (optimistic) {
|
|
1254
|
-
const opt = optimistic(input);
|
|
1255
|
-
if (opt) {
|
|
1256
|
-
const { id, patch } = opt;
|
|
1257
|
-
const store = useGraphStore.getState();
|
|
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
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
try {
|
|
1272
|
-
const result = await apiFn(input);
|
|
1273
|
-
if (normalize) {
|
|
1274
|
-
const { id, data } = normalize(result, input);
|
|
1275
|
-
const store = useGraphStore.getState();
|
|
1276
|
-
store.upsertEntity(type, id, data);
|
|
1277
|
-
store.setEntitySyncMetadata(type, id, { synced: true, origin: "server", updatedAt: Date.now() });
|
|
1278
|
-
if (optimistic) {
|
|
1279
|
-
const opt = optimistic(input);
|
|
1280
|
-
if (opt) store.clearPatch(type, opt.id);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
if (invalidateLists) for (const k of invalidateLists) useGraphStore.getState().invalidateLists(k);
|
|
1284
|
-
if (invalidateEntities) for (const { type: t, id } of invalidateEntities) useGraphStore.getState().invalidateEntity(t, id);
|
|
1285
|
-
setState({ isPending: false, isSuccess: true, isError: false, error: null });
|
|
1286
|
-
onSuccess?.(result, input);
|
|
1287
|
-
return result;
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1290
|
-
rollback?.();
|
|
1291
|
-
setState({ isPending: false, isSuccess: false, isError: true, error: error.message });
|
|
1292
|
-
onError?.(error, input);
|
|
1293
|
-
return null;
|
|
1748
|
+
function MarkdownFieldRenderer({ value, className }) {
|
|
1749
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1750
|
+
"div",
|
|
1751
|
+
{
|
|
1752
|
+
className,
|
|
1753
|
+
dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(value ?? "") }
|
|
1294
1754
|
}
|
|
1295
|
-
|
|
1296
|
-
const trigger = React6.useCallback((input) => {
|
|
1297
|
-
void mutate(input);
|
|
1298
|
-
}, [mutate]);
|
|
1299
|
-
const reset = React6.useCallback(() => setState({ isPending: false, isSuccess: false, isError: false, error: null }), []);
|
|
1300
|
-
return { mutate, trigger, reset, state };
|
|
1301
|
-
}
|
|
1302
|
-
function useEntityAugment(type, id) {
|
|
1303
|
-
const patch = zustand.useStore(useGraphStore, React6.useCallback((state) => id ? state.patches[type]?.[id] ?? null : null, [type, id]));
|
|
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 };
|
|
1755
|
+
);
|
|
1317
1756
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
if (settled) return;
|
|
1336
|
-
const hasData = !!state.entities[type]?.[id];
|
|
1337
|
-
const es = state.entityStates[key];
|
|
1338
|
-
if (hasData) settle(() => resolve());
|
|
1339
|
-
else if (es != null && es.error != null && !es.isFetching) {
|
|
1340
|
-
const msg = es.error;
|
|
1341
|
-
settle(() => reject(new Error(msg)));
|
|
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);
|
|
1349
|
-
});
|
|
1350
|
-
suspenseEntityPromises.set(key, tracked);
|
|
1351
|
-
return tracked;
|
|
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"
|
|
1770
|
+
}
|
|
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
|
+
] });
|
|
1352
1774
|
}
|
|
1353
|
-
function
|
|
1354
|
-
|
|
1355
|
-
if (existing) return existing;
|
|
1356
|
-
let unsub = null;
|
|
1357
|
-
let settled = false;
|
|
1358
|
-
const promise = new Promise((resolve, reject) => {
|
|
1359
|
-
const settle = (fn) => {
|
|
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));
|
|
1377
|
-
});
|
|
1378
|
-
const tracked = promise.finally(() => {
|
|
1379
|
-
suspenseListPromises.delete(listKey);
|
|
1380
|
-
});
|
|
1381
|
-
suspenseListPromises.set(listKey, tracked);
|
|
1382
|
-
return tracked;
|
|
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" });
|
|
1383
1777
|
}
|
|
1384
|
-
function
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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);
|
|
1791
|
+
}
|
|
1792
|
+
return [schemaField(field, schemaPath, childSchema, schema.required?.includes(key) ?? false)];
|
|
1793
|
+
});
|
|
1390
1794
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
+
}));
|
|
1393
1814
|
}
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1815
|
+
if (type === "markdown") {
|
|
1816
|
+
descriptor.render = createMarkdownDetailRenderer(field);
|
|
1396
1817
|
}
|
|
1397
|
-
return
|
|
1398
|
-
data: result.data,
|
|
1399
|
-
isFetching: result.isFetching,
|
|
1400
|
-
isStale: result.isStale,
|
|
1401
|
-
refetch: result.refetch
|
|
1402
|
-
};
|
|
1818
|
+
return descriptor;
|
|
1403
1819
|
}
|
|
1404
|
-
function
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
if (
|
|
1408
|
-
if (
|
|
1409
|
-
|
|
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";
|
|
1410
1842
|
}
|
|
1411
|
-
const { isLoading: _isLoading, ...rest } = result;
|
|
1412
|
-
return rest;
|
|
1413
1843
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
function matchesFilter(entity, filter) {
|
|
1417
|
-
if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
|
|
1418
|
-
return matchesGroup(entity, filter);
|
|
1844
|
+
function registryKey(entityType, field, schemaId) {
|
|
1845
|
+
return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
|
|
1419
1846
|
}
|
|
1420
|
-
function
|
|
1421
|
-
|
|
1422
|
-
if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
1423
|
-
return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
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());
|
|
1424
1849
|
}
|
|
1425
|
-
function
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
case "eq":
|
|
1430
|
-
return fv === value;
|
|
1431
|
-
case "neq":
|
|
1432
|
-
return fv !== value;
|
|
1433
|
-
case "gt":
|
|
1434
|
-
return fv > value;
|
|
1435
|
-
case "gte":
|
|
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;
|
|
1467
|
-
default:
|
|
1468
|
-
return true;
|
|
1469
|
-
}
|
|
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>`;
|
|
1470
1854
|
}
|
|
1471
|
-
function
|
|
1472
|
-
|
|
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
|
-
});
|
|
1855
|
+
function renderInlineMarkdown(block) {
|
|
1856
|
+
return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
1478
1857
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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);
|
|
1485
1867
|
}
|
|
1486
|
-
function
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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;
|
|
1868
|
+
function createGraphTool(handler) {
|
|
1869
|
+
return (input) => handler(input, {
|
|
1870
|
+
store: useGraphStore.getState(),
|
|
1871
|
+
queryOnce,
|
|
1872
|
+
exportGraphSnapshot
|
|
1873
|
+
});
|
|
1501
1874
|
}
|
|
1502
|
-
function
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1875
|
+
function createSchemaGraphTool(handler) {
|
|
1876
|
+
return (input) => handler(input, {
|
|
1877
|
+
store: useGraphStore.getState(),
|
|
1878
|
+
queryOnce,
|
|
1879
|
+
exportGraphSnapshot,
|
|
1880
|
+
getEntityJsonSchema,
|
|
1881
|
+
exportGraphSnapshotWithSchemas
|
|
1882
|
+
});
|
|
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
|
|
1511
1900
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1901
|
+
}))
|
|
1902
|
+
}));
|
|
1903
|
+
var pendingActions = /* @__PURE__ */ new Map();
|
|
1904
|
+
function useGraphSyncStatus() {
|
|
1905
|
+
return useGraphSyncStatusStore((state) => state.status);
|
|
1516
1906
|
}
|
|
1517
|
-
function
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
|
1920
|
+
});
|
|
1921
|
+
return {
|
|
1922
|
+
ok: true,
|
|
1923
|
+
key: opts.key,
|
|
1924
|
+
bytes: json.length,
|
|
1925
|
+
persistedAt
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
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
|
+
};
|
|
1938
|
+
}
|
|
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
|
+
};
|
|
1523
1974
|
}
|
|
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
1975
|
}
|
|
1529
|
-
function
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
+
})();
|
|
2035
|
+
return {
|
|
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
|
+
}
|
|
2052
|
+
};
|
|
1533
2053
|
}
|
|
1534
|
-
function
|
|
1535
|
-
const
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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
|
+
};
|
|
1542
2063
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
+
};
|
|
1549
2073
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
case "
|
|
1557
|
-
return
|
|
1558
|
-
case "
|
|
1559
|
-
return
|
|
1560
|
-
case "
|
|
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":
|
|
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":
|
|
1580
2085
|
default:
|
|
1581
|
-
return
|
|
2086
|
+
return base / 2 + Math.random() * (base / 2);
|
|
1582
2087
|
}
|
|
1583
2088
|
}
|
|
1584
|
-
function
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (c.op === "isNull") {
|
|
1588
|
-
const equalsNull = c.value === void 0 || c.value === true;
|
|
1589
|
-
return nestWhereField(parts, equalsNull ? null : { not: null });
|
|
1590
|
-
}
|
|
1591
|
-
if (c.op === "isNotNull") {
|
|
1592
|
-
return nestWhereField(parts, { not: null });
|
|
1593
|
-
}
|
|
1594
|
-
const leaf = clauseToPrismaLeaf(c);
|
|
1595
|
-
if (leaf === null) return null;
|
|
1596
|
-
return nestWhereField(parts, leaf);
|
|
2089
|
+
function sleep4(ms) {
|
|
2090
|
+
if (ms <= 0) return Promise.resolve();
|
|
2091
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1597
2092
|
}
|
|
1598
|
-
function
|
|
1599
|
-
|
|
1600
|
-
for (
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (
|
|
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));
|
|
1607
2103
|
}
|
|
1608
2104
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2105
|
+
try {
|
|
2106
|
+
await policy.poisonHandler?.(action, lastError);
|
|
2107
|
+
} catch {
|
|
2108
|
+
}
|
|
2109
|
+
return { ok: false, poisoned: true, error: lastError };
|
|
1612
2110
|
}
|
|
1613
|
-
function
|
|
1614
|
-
if (
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
return {
|
|
2128
|
+
getIsOnline: () => true,
|
|
2129
|
+
subscribe: () => () => {
|
|
1619
2130
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
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;
|
|
1623
2142
|
}
|
|
1624
|
-
|
|
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 });
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
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;
|
|
@@ -1803,16 +2535,19 @@ function useEntityView(opts) {
|
|
|
1803
2535
|
setRemoteError(null);
|
|
1804
2536
|
const store = useGraphStore.getState();
|
|
1805
2537
|
store.setListFetching(rKey, true);
|
|
2538
|
+
const baseKeyStr = serializeKey(bqk);
|
|
1806
2539
|
try {
|
|
1807
2540
|
const response = await rf(params);
|
|
1808
2541
|
const normalized = norm ? response.items.map(norm) : response.items.map((item) => ({ id: String(item.id), data: item }));
|
|
1809
|
-
store.upsertEntities(type, normalized);
|
|
2542
|
+
store.upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
|
|
1810
2543
|
for (const { id } of normalized) store.setEntityFetched(type, id);
|
|
1811
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);
|
|
1812
2546
|
} catch (err) {
|
|
1813
2547
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1814
2548
|
setRemoteError(msg);
|
|
1815
2549
|
store.setListError(rKey, msg);
|
|
2550
|
+
store.setListError(baseKeyStr, msg);
|
|
1816
2551
|
} finally {
|
|
1817
2552
|
setIsRemoteFetching(false);
|
|
1818
2553
|
}
|
|
@@ -1836,7 +2571,7 @@ function useEntityView(opts) {
|
|
|
1836
2571
|
if (!existing || isStale) fireRemoteFetch(liveViewRef.current);
|
|
1837
2572
|
}, [baseKey, enabled, staleTime, fireRemoteFetch]);
|
|
1838
2573
|
React6.useEffect(() => {
|
|
1839
|
-
const unsub = useGraphStore.subscribe((state) => state.entities[type] ??
|
|
2574
|
+
const unsub = useGraphStore.subscribe((state) => state.entities[type] ?? EMPTY_ENTITY_BUCKET2, (newEntities, prevEntities) => {
|
|
1840
2575
|
const view = liveViewRef.current;
|
|
1841
2576
|
const store = useGraphStore.getState();
|
|
1842
2577
|
const list = store.lists[baseKey];
|
|
@@ -1868,15 +2603,17 @@ function useEntityView(opts) {
|
|
|
1868
2603
|
}, [completenessMode, isRemoteFetching, remoteListState?.nextCursor, fireRemoteFetch]);
|
|
1869
2604
|
const refetch = React6.useCallback(() => fireRemoteFetch(liveViewRef.current), [fireRemoteFetch]);
|
|
1870
2605
|
const viewTotal = remoteListState?.total ?? (isComplete ? localViewIds.length : listState?.total ?? null);
|
|
2606
|
+
const error = remoteError ?? listState?.error ?? null;
|
|
1871
2607
|
return {
|
|
1872
2608
|
items,
|
|
1873
2609
|
viewIds: localViewIds,
|
|
1874
2610
|
viewTotal,
|
|
1875
|
-
isLoading: items.length === 0 && (listState?.isFetching ??
|
|
2611
|
+
isLoading: items.length === 0 && ((listState?.isFetching ?? false) || isRemoteFetching),
|
|
1876
2612
|
isFetching: (listState?.isFetching ?? false) || isRemoteFetching,
|
|
1877
2613
|
isRemoteFetching,
|
|
1878
2614
|
isShowingLocalPending: completenessMode === "hybrid" && isRemoteFetching && items.length > 0,
|
|
1879
|
-
error
|
|
2615
|
+
error,
|
|
2616
|
+
isError: error !== null,
|
|
1880
2617
|
hasNextPage: completenessMode === "local" ? false : remoteListState?.hasNextPage ?? listState?.hasNextPage ?? false,
|
|
1881
2618
|
fetchNextPage,
|
|
1882
2619
|
isLocallyComplete: isComplete,
|
|
@@ -2820,6 +3557,255 @@ function usePGliteQuery(opts) {
|
|
|
2820
3557
|
return { isLoading, error };
|
|
2821
3558
|
}
|
|
2822
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
|
+
|
|
2823
3809
|
// src/graphql/client.ts
|
|
2824
3810
|
async function executeGQL(cfg, document2, variables) {
|
|
2825
3811
|
const headers = { "Content-Type": "application/json", ...cfg.headers?.() ?? {} };
|
|
@@ -2860,6 +3846,7 @@ var GQLClient = class {
|
|
|
2860
3846
|
constructor(cfg) {
|
|
2861
3847
|
this.cfg = cfg;
|
|
2862
3848
|
}
|
|
3849
|
+
cfg;
|
|
2863
3850
|
async query(opts) {
|
|
2864
3851
|
const key = opts.cacheKey ?? `gql:${opts.document.slice(0, 60)}:${JSON.stringify(opts.variables ?? {})}`;
|
|
2865
3852
|
return dedupe(key, async () => {
|
|
@@ -6647,10 +7634,11 @@ function DefaultListItemContent({
|
|
|
6647
7634
|
descriptor
|
|
6648
7635
|
}) {
|
|
6649
7636
|
if (!descriptor) {
|
|
6650
|
-
const
|
|
7637
|
+
const rec = item;
|
|
7638
|
+
const keys = Object.keys(rec);
|
|
6651
7639
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
6652
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-medium", children: String(
|
|
6653
|
-
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]] ?? "") })
|
|
6654
7642
|
] });
|
|
6655
7643
|
}
|
|
6656
7644
|
const IconComponent = typeof descriptor.icon === "function" ? descriptor.icon : null;
|
|
@@ -8706,12 +9694,16 @@ exports.TableHead = TableHead;
|
|
|
8706
9694
|
exports.TableHeader = TableHeader;
|
|
8707
9695
|
exports.TableRow = TableRow;
|
|
8708
9696
|
exports.TableStorageProvider = TableStorageProvider;
|
|
9697
|
+
exports.TerminalError = TerminalError;
|
|
9698
|
+
exports.TransientError = TransientError;
|
|
8709
9699
|
exports.ViewModeSwitcher = ViewModeSwitcher;
|
|
8710
9700
|
exports.ZustandPersistAdapter = ZustandPersistAdapter;
|
|
9701
|
+
exports.__resetEntityTransports = __resetEntityTransports;
|
|
8711
9702
|
exports.actionsColumn = actionsColumn;
|
|
8712
9703
|
exports.applyView = applyView;
|
|
8713
9704
|
exports.booleanColumn = booleanColumn;
|
|
8714
9705
|
exports.buildEntityFieldsFromSchema = buildEntityFieldsFromSchema;
|
|
9706
|
+
exports.buildTenantWhere = buildTenantWhere;
|
|
8715
9707
|
exports.cascadeInvalidation = cascadeInvalidation;
|
|
8716
9708
|
exports.checkCompleteness = checkCompleteness;
|
|
8717
9709
|
exports.compareEntities = compareEntities;
|
|
@@ -8724,12 +9716,14 @@ exports.createGraphEffect = createGraphEffect;
|
|
|
8724
9716
|
exports.createGraphQLSubscriptionAdapter = createGraphQLSubscriptionAdapter;
|
|
8725
9717
|
exports.createGraphTool = createGraphTool;
|
|
8726
9718
|
exports.createGraphTransaction = createGraphTransaction;
|
|
9719
|
+
exports.createPGlitePersistenceAdapter = createPGlitePersistenceAdapter;
|
|
8727
9720
|
exports.createPresetStore = createPresetStore;
|
|
8728
9721
|
exports.createPrismaEntityConfig = createPrismaEntityConfig;
|
|
8729
9722
|
exports.createRow = createRow;
|
|
8730
9723
|
exports.createSchemaGraphTool = createSchemaGraphTool;
|
|
8731
9724
|
exports.createSelectionStore = createSelectionStore;
|
|
8732
9725
|
exports.createSupabaseRealtimeAdapter = createSupabaseRealtimeAdapter;
|
|
9726
|
+
exports.createTenantScopedElectricAdapter = createTenantScopedElectricAdapter;
|
|
8733
9727
|
exports.createWebSocketAdapter = createWebSocketAdapter;
|
|
8734
9728
|
exports.dateColumn = dateColumn;
|
|
8735
9729
|
exports.dedupe = dedupe;
|
|
@@ -8744,6 +9738,7 @@ exports.fetchList = fetchList;
|
|
|
8744
9738
|
exports.flattenClauses = flattenClauses;
|
|
8745
9739
|
exports.getCoreRowModel = getCoreRowModel2;
|
|
8746
9740
|
exports.getEntityJsonSchema = getEntityJsonSchema;
|
|
9741
|
+
exports.getEntityTransport = getEntityTransport;
|
|
8747
9742
|
exports.getExpandedRowModel = getExpandedRowModel;
|
|
8748
9743
|
exports.getFacetedMinMaxValues = getFacetedMinMaxValues;
|
|
8749
9744
|
exports.getFacetedRowModel = getFacetedRowModel;
|
|
@@ -8752,15 +9747,18 @@ exports.getFilteredRowModel = getFilteredRowModel;
|
|
|
8752
9747
|
exports.getGroupedRowModel = getGroupedRowModel;
|
|
8753
9748
|
exports.getPaginatedRowModel = getPaginatedRowModel;
|
|
8754
9749
|
exports.getRealtimeManager = getRealtimeManager;
|
|
9750
|
+
exports.getRegisteredEntityTypes = getRegisteredEntityTypes;
|
|
8755
9751
|
exports.getSchema = getSchema;
|
|
8756
9752
|
exports.getSelectedRowModel = getSelectedRowModel;
|
|
8757
9753
|
exports.getSortedRowModel = getSortedRowModel2;
|
|
8758
9754
|
exports.hasCustomPredicates = hasCustomPredicates;
|
|
8759
9755
|
exports.hydrateGraphFromStorage = hydrateGraphFromStorage;
|
|
9756
|
+
exports.makeRestTransport = makeRestTransport;
|
|
8760
9757
|
exports.matchesFilter = matchesFilter;
|
|
8761
9758
|
exports.matchesSearch = matchesSearch;
|
|
8762
9759
|
exports.normalizeGQLResponse = normalizeGQLResponse;
|
|
8763
9760
|
exports.numberColumn = numberColumn;
|
|
9761
|
+
exports.parseCreateTable = parseCreateTable;
|
|
8764
9762
|
exports.persistGraphToStorage = persistGraphToStorage;
|
|
8765
9763
|
exports.prismaRelationsToSchema = prismaRelationsToSchema;
|
|
8766
9764
|
exports.pureActionsColumn = actionsColumn2;
|
|
@@ -8772,29 +9770,37 @@ exports.pureSelectionColumn = selectionColumn2;
|
|
|
8772
9770
|
exports.pureTextColumn = textColumn2;
|
|
8773
9771
|
exports.queryOnce = queryOnce;
|
|
8774
9772
|
exports.readRelations = readRelations;
|
|
9773
|
+
exports.registerEntityFromSql = registerEntityFromSql;
|
|
8775
9774
|
exports.registerEntityJsonSchema = registerEntityJsonSchema;
|
|
9775
|
+
exports.registerEntityTransport = registerEntityTransport;
|
|
8776
9776
|
exports.registerRuntimeSchema = registerRuntimeSchema;
|
|
8777
9777
|
exports.registerSchema = registerSchema;
|
|
8778
9778
|
exports.renderMarkdownToHtml = renderMarkdownToHtml;
|
|
9779
|
+
exports.replayActionWithRetry = replayActionWithRetry;
|
|
8779
9780
|
exports.resetRealtimeManager = resetRealtimeManager;
|
|
8780
9781
|
exports.selectGraph = selectGraph;
|
|
8781
9782
|
exports.selectionColumn = selectionColumn;
|
|
8782
9783
|
exports.serializeKey = serializeKey;
|
|
9784
|
+
exports.sqlTypeToJsonSchema = sqlTypeToJsonSchema;
|
|
8783
9785
|
exports.startGarbageCollector = startGarbageCollector;
|
|
8784
9786
|
exports.startLocalFirstGraph = startLocalFirstGraph;
|
|
8785
9787
|
exports.stopGarbageCollector = stopGarbageCollector;
|
|
8786
9788
|
exports.textColumn = textColumn;
|
|
9789
|
+
exports.toEntityError = toEntityError;
|
|
8787
9790
|
exports.toGraphQLVariables = toGraphQLVariables;
|
|
8788
9791
|
exports.toPrismaInclude = toPrismaInclude;
|
|
8789
9792
|
exports.toPrismaOrderBy = toPrismaOrderBy;
|
|
8790
9793
|
exports.toPrismaWhere = toPrismaWhere;
|
|
8791
9794
|
exports.toRestParams = toRestParams;
|
|
8792
9795
|
exports.toSQLClauses = toSQLClauses;
|
|
9796
|
+
exports.useEntities = useEntities;
|
|
8793
9797
|
exports.useEntity = useEntity;
|
|
8794
9798
|
exports.useEntityAugment = useEntityAugment;
|
|
8795
9799
|
exports.useEntityCRUD = useEntityCRUD;
|
|
8796
9800
|
exports.useEntityList = useEntityList;
|
|
9801
|
+
exports.useEntityListAsTable = useEntityListAsTable;
|
|
8797
9802
|
exports.useEntityMutation = useEntityMutation;
|
|
9803
|
+
exports.useEntityQuery = useEntityQuery;
|
|
8798
9804
|
exports.useEntityView = useEntityView;
|
|
8799
9805
|
exports.useGQLEntity = useGQLEntity;
|
|
8800
9806
|
exports.useGQLList = useGQLList;
|