@lsync/client 0.0.1
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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/index.d.mts +233 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1206 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
import { createTRPCProxyClient } from "@trpc/client";
|
|
2
|
+
import { parseServerMessage, sendClientRpcRequest } from "@lsync/transport";
|
|
3
|
+
import { observable } from "@trpc/server/observable";
|
|
4
|
+
import { createCollection } from "@tanstack/db";
|
|
5
|
+
//#region src/batch.ts
|
|
6
|
+
function createBatch(collection, clientId, transaction) {
|
|
7
|
+
return { updates: transaction.mutations.map((mutation) => toUpdate(collection, clientId, mutation)) };
|
|
8
|
+
}
|
|
9
|
+
function toUpdate(collection, clientId, mutation) {
|
|
10
|
+
const base = {
|
|
11
|
+
id: mutation.mutationId,
|
|
12
|
+
collection,
|
|
13
|
+
key: mutation.key,
|
|
14
|
+
type: mutation.type,
|
|
15
|
+
clientId,
|
|
16
|
+
createdAt: mutation.createdAt.getTime()
|
|
17
|
+
};
|
|
18
|
+
if (mutation.type === "delete") return {
|
|
19
|
+
...base,
|
|
20
|
+
type: "delete",
|
|
21
|
+
previousValue: mutation.original
|
|
22
|
+
};
|
|
23
|
+
if (mutation.type === "update") return {
|
|
24
|
+
...base,
|
|
25
|
+
type: "update",
|
|
26
|
+
value: mutation.modified,
|
|
27
|
+
previousValue: mutation.original
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
...base,
|
|
31
|
+
type: "insert",
|
|
32
|
+
value: mutation.modified
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function toChangeMessage(update, existing) {
|
|
36
|
+
if (update.type === "delete") return {
|
|
37
|
+
type: "delete",
|
|
38
|
+
key: update.key
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
type: update.type === "update" && existing === false ? "insert" : update.type === "insert" && existing === true ? "update" : update.type,
|
|
42
|
+
key: update.key,
|
|
43
|
+
value: update.value
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/client-rpc.ts
|
|
48
|
+
function takePending(pending, id) {
|
|
49
|
+
if (id === null || id === void 0) return;
|
|
50
|
+
const key = String(id);
|
|
51
|
+
const request = pending.get(key);
|
|
52
|
+
pending.delete(key);
|
|
53
|
+
return request;
|
|
54
|
+
}
|
|
55
|
+
function requestForOperation(id, op) {
|
|
56
|
+
if (op.type === "mutation" && op.path === "push") return {
|
|
57
|
+
id,
|
|
58
|
+
method: "mutation",
|
|
59
|
+
params: {
|
|
60
|
+
path: "push",
|
|
61
|
+
input: { json: op.input }
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
if (op.type === "query" && op.path === "read") return {
|
|
65
|
+
id,
|
|
66
|
+
method: "query",
|
|
67
|
+
params: {
|
|
68
|
+
path: "read",
|
|
69
|
+
input: { json: op.input }
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
if (op.type === "query" && op.path === "changes") return {
|
|
73
|
+
id,
|
|
74
|
+
method: "query",
|
|
75
|
+
params: {
|
|
76
|
+
path: "changes",
|
|
77
|
+
input: { json: op.input }
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
if (op.type === "mutation" && op.path === "api") return {
|
|
81
|
+
id,
|
|
82
|
+
method: "mutation",
|
|
83
|
+
params: {
|
|
84
|
+
path: "api",
|
|
85
|
+
input: { json: op.input }
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
throw new Error(`Unsupported operation: ${op.type}.${op.path}`);
|
|
89
|
+
}
|
|
90
|
+
function collectionScope$1(collection) {
|
|
91
|
+
return `/${collection.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).map(normalizeSegment$1).join("/")}/`;
|
|
92
|
+
}
|
|
93
|
+
function normalizeSegment$1(segment) {
|
|
94
|
+
return encodeURIComponent(decodeURIComponent(segment));
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/client-subscriptions.ts
|
|
98
|
+
var ClientSubscriptions = class {
|
|
99
|
+
controls;
|
|
100
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
101
|
+
constructor(controls) {
|
|
102
|
+
this.controls = controls;
|
|
103
|
+
}
|
|
104
|
+
subscribe(collection, listener) {
|
|
105
|
+
const key = collectionScope$1(collection);
|
|
106
|
+
let entry = this.subscriptions.get(key);
|
|
107
|
+
const shouldSubscribe = !entry;
|
|
108
|
+
if (!entry) {
|
|
109
|
+
entry = {
|
|
110
|
+
collection: key,
|
|
111
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
112
|
+
ready: Promise.resolve()
|
|
113
|
+
};
|
|
114
|
+
this.subscriptions.set(key, entry);
|
|
115
|
+
}
|
|
116
|
+
entry.listeners.add(listener);
|
|
117
|
+
if (shouldSubscribe) entry.ready = this.start(entry);
|
|
118
|
+
return {
|
|
119
|
+
ready: entry.ready,
|
|
120
|
+
unsubscribe: () => this.unsubscribe(key, listener)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async replay(ws) {
|
|
124
|
+
await Promise.all([...this.subscriptions.values()].map((entry) => this.subscribeOnSocket(ws, entry)));
|
|
125
|
+
}
|
|
126
|
+
dispatch(broadcast) {
|
|
127
|
+
const updatesByCollection = /* @__PURE__ */ new Map();
|
|
128
|
+
const invalidationsByCollection = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const update of broadcast.updates) {
|
|
130
|
+
const collection = collectionScope$1(update.collection);
|
|
131
|
+
const updates = updatesByCollection.get(collection) ?? [];
|
|
132
|
+
updates.push(update);
|
|
133
|
+
updatesByCollection.set(collection, updates);
|
|
134
|
+
}
|
|
135
|
+
for (const invalidation of broadcast.invalidations ?? []) {
|
|
136
|
+
const collection = collectionScope$1(invalidation.collection);
|
|
137
|
+
const invalidations = invalidationsByCollection.get(collection) ?? [];
|
|
138
|
+
invalidations.push(invalidation);
|
|
139
|
+
invalidationsByCollection.set(collection, invalidations);
|
|
140
|
+
}
|
|
141
|
+
for (const entry of this.subscriptions.values()) {
|
|
142
|
+
const updates = updatesByCollection.get(collectionScope$1(entry.collection));
|
|
143
|
+
const invalidations = invalidationsByCollection.get(collectionScope$1(entry.collection));
|
|
144
|
+
if ((!updates || updates.length === 0) && (!invalidations || invalidations.length === 0)) continue;
|
|
145
|
+
const scopedBroadcast = {
|
|
146
|
+
...broadcast,
|
|
147
|
+
...invalidations ? { invalidations } : {},
|
|
148
|
+
updates: updates ?? []
|
|
149
|
+
};
|
|
150
|
+
for (const listener of entry.listeners) listener(scopedBroadcast);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
start(entry) {
|
|
154
|
+
const socket = this.controls.currentSocket();
|
|
155
|
+
return socket?.readyState === WebSocket.OPEN ? this.subscribeOnSocket(socket, entry) : this.controls.open().then(() => void 0);
|
|
156
|
+
}
|
|
157
|
+
async subscribeOnSocket(ws, entry) {
|
|
158
|
+
if (this.subscriptions.get(collectionScope$1(entry.collection)) !== entry) return;
|
|
159
|
+
entry.collection = (await this.controls.send(ws, "subscribe", entry.collection)).collection;
|
|
160
|
+
}
|
|
161
|
+
unsubscribe(key, listener) {
|
|
162
|
+
const entry = this.subscriptions.get(key);
|
|
163
|
+
if (!entry) return;
|
|
164
|
+
entry.listeners.delete(listener);
|
|
165
|
+
if (entry.listeners.size > 0) return;
|
|
166
|
+
this.subscriptions.delete(key);
|
|
167
|
+
const socket = this.controls.currentSocket();
|
|
168
|
+
if (socket?.readyState === WebSocket.OPEN) this.controls.send(socket, "unsubscribe", entry.collection);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/client.ts
|
|
173
|
+
const sharedClients = /* @__PURE__ */ new Map();
|
|
174
|
+
function createClient(options) {
|
|
175
|
+
const clientId = options.clientId ?? crypto.randomUUID();
|
|
176
|
+
const pending = /* @__PURE__ */ new Map();
|
|
177
|
+
let nextId = 1;
|
|
178
|
+
let socket;
|
|
179
|
+
let connectPromise;
|
|
180
|
+
let subscriptions;
|
|
181
|
+
const open = async () => {
|
|
182
|
+
if (socket?.readyState === WebSocket.OPEN) return socket;
|
|
183
|
+
if (connectPromise) return connectPromise;
|
|
184
|
+
connectPromise = new Promise((resolve, reject) => {
|
|
185
|
+
const url = new URL(options.url);
|
|
186
|
+
url.searchParams.set("clientId", clientId);
|
|
187
|
+
const ws = new WebSocket(url);
|
|
188
|
+
ws.binaryType = "arraybuffer";
|
|
189
|
+
ws.addEventListener("open", () => {
|
|
190
|
+
socket = ws;
|
|
191
|
+
subscriptions.replay(ws).then(() => {
|
|
192
|
+
connectPromise = void 0;
|
|
193
|
+
resolve(ws);
|
|
194
|
+
}).catch((error) => {
|
|
195
|
+
connectPromise = void 0;
|
|
196
|
+
reject(error);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
ws.addEventListener("message", (event) => {
|
|
200
|
+
const message = parseServerMessage(event.data);
|
|
201
|
+
if ("method" in message) {
|
|
202
|
+
subscriptions.dispatch(message.params.input.json);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if ("error" in message) {
|
|
206
|
+
const request = takePending(pending, message.id);
|
|
207
|
+
if (!request) return;
|
|
208
|
+
request.reject(new Error(message.error.message));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const request = takePending(pending, message.id);
|
|
212
|
+
if (!request) return;
|
|
213
|
+
request.resolve(message.result.data.json);
|
|
214
|
+
});
|
|
215
|
+
ws.addEventListener("close", () => {
|
|
216
|
+
socket = void 0;
|
|
217
|
+
connectPromise = void 0;
|
|
218
|
+
});
|
|
219
|
+
ws.addEventListener("error", () => {
|
|
220
|
+
reject(/* @__PURE__ */ new Error("Unable to open sync WebSocket"));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
return connectPromise;
|
|
224
|
+
};
|
|
225
|
+
const sendRequest = (ws, request) => {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const id = request.id;
|
|
228
|
+
if (id === null || id === void 0) {
|
|
229
|
+
reject(/* @__PURE__ */ new Error("RPC request requires an id"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
pending.set(String(id), {
|
|
233
|
+
resolve: (value) => resolve(value),
|
|
234
|
+
reject
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
sendClientRpcRequest(ws, request);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
pending.delete(String(id));
|
|
240
|
+
reject(error);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
const sendSubscriptionControl = async (ws, method, collection) => {
|
|
245
|
+
const id = String(nextId++);
|
|
246
|
+
return sendRequest(ws, {
|
|
247
|
+
id,
|
|
248
|
+
method,
|
|
249
|
+
params: { input: { json: { collection } } }
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
subscriptions = new ClientSubscriptions({
|
|
253
|
+
currentSocket: () => socket,
|
|
254
|
+
open,
|
|
255
|
+
send: sendSubscriptionControl
|
|
256
|
+
});
|
|
257
|
+
const link = () => {
|
|
258
|
+
return ({ op }) => observable((observer) => {
|
|
259
|
+
let cancelled = false;
|
|
260
|
+
open().then((ws) => {
|
|
261
|
+
if (cancelled) return;
|
|
262
|
+
const id = String(nextId++);
|
|
263
|
+
sendRequest(ws, requestForOperation(id, op)).then((value) => {
|
|
264
|
+
observer.next({ result: { data: value } });
|
|
265
|
+
observer.complete();
|
|
266
|
+
}).catch((error) => observer.error(error));
|
|
267
|
+
}).catch((error) => observer.error(error));
|
|
268
|
+
return () => {
|
|
269
|
+
cancelled = true;
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
const trpc = createTRPCProxyClient({ links: [link] });
|
|
274
|
+
return {
|
|
275
|
+
clientId,
|
|
276
|
+
push: (batch) => trpc.push.mutate(batch),
|
|
277
|
+
read: (query) => trpc.read.query(query),
|
|
278
|
+
changes: (query) => trpc.changes.query(query),
|
|
279
|
+
call: (path, ...args) => {
|
|
280
|
+
const [input] = args;
|
|
281
|
+
const call = input === void 0 ? { path } : {
|
|
282
|
+
path,
|
|
283
|
+
input
|
|
284
|
+
};
|
|
285
|
+
return trpc.api.mutate(call);
|
|
286
|
+
},
|
|
287
|
+
subscribe: (collection, listener) => subscriptions.subscribe(collection, listener),
|
|
288
|
+
close() {
|
|
289
|
+
socket?.close();
|
|
290
|
+
socket = void 0;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function acquireSharedClient(options) {
|
|
295
|
+
const key = sharedClientKey(options);
|
|
296
|
+
let entry = sharedClients.get(key);
|
|
297
|
+
if (!entry) {
|
|
298
|
+
entry = {
|
|
299
|
+
client: createClient({
|
|
300
|
+
url: options.url,
|
|
301
|
+
...options.clientId ? { clientId: options.clientId } : {}
|
|
302
|
+
}),
|
|
303
|
+
refs: 0
|
|
304
|
+
};
|
|
305
|
+
sharedClients.set(key, entry);
|
|
306
|
+
}
|
|
307
|
+
entry.refs += 1;
|
|
308
|
+
let released = false;
|
|
309
|
+
return {
|
|
310
|
+
client: entry.client,
|
|
311
|
+
release() {
|
|
312
|
+
if (released) return;
|
|
313
|
+
released = true;
|
|
314
|
+
entry.refs -= 1;
|
|
315
|
+
if (entry.refs === 0 && sharedClients.get(key) === entry) {
|
|
316
|
+
sharedClients.delete(key);
|
|
317
|
+
entry.client.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function sharedClientKey(options) {
|
|
323
|
+
const url = new URL(options.url);
|
|
324
|
+
url.searchParams.delete("clientId");
|
|
325
|
+
return `${url.toString()}\0${options.clientId ?? ""}`;
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/read-query.ts
|
|
329
|
+
function initialReadQuery(collection, read) {
|
|
330
|
+
if (read === false) return;
|
|
331
|
+
return {
|
|
332
|
+
collection,
|
|
333
|
+
...read
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function subsetId(options) {
|
|
337
|
+
return JSON.stringify({
|
|
338
|
+
cursor: options.cursor ? {
|
|
339
|
+
whereCurrent: toReadPredicate(options.cursor.whereCurrent),
|
|
340
|
+
whereFrom: toReadPredicate(options.cursor.whereFrom)
|
|
341
|
+
} : void 0,
|
|
342
|
+
predicate: options.where ? toReadPredicate(options.where) : void 0,
|
|
343
|
+
limit: options.limit,
|
|
344
|
+
offset: options.offset,
|
|
345
|
+
orderBy: toReadOrderByList(options.orderBy)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function readQueryForSubset(collection, baseRead, options) {
|
|
349
|
+
const query = {
|
|
350
|
+
collection,
|
|
351
|
+
...baseRead,
|
|
352
|
+
filters: [...baseRead?.filters ?? []]
|
|
353
|
+
};
|
|
354
|
+
const predicate = combinePredicates(baseRead?.predicate, options.where ? toReadPredicate(options.where) : void 0);
|
|
355
|
+
if (predicate) query.predicate = predicate;
|
|
356
|
+
const limit = options.limit ?? baseRead?.limit;
|
|
357
|
+
if (limit !== void 0) query.limit = limit;
|
|
358
|
+
const offset = options.cursor ? void 0 : options.offset ?? baseRead?.offset;
|
|
359
|
+
if (offset !== void 0) query.offset = offset;
|
|
360
|
+
const subsetOrderBy = toReadOrderByList(options.orderBy);
|
|
361
|
+
const orderBy = subsetOrderBy.length > 0 ? subsetOrderBy : baseRead?.orderBy;
|
|
362
|
+
if (orderBy) query.orderBy = orderBy;
|
|
363
|
+
if (options.cursor) query.cursor = {
|
|
364
|
+
whereCurrent: toReadPredicate(options.cursor.whereCurrent),
|
|
365
|
+
whereFrom: toReadPredicate(options.cursor.whereFrom),
|
|
366
|
+
...options.cursor.lastKey !== void 0 ? { lastKey: options.cursor.lastKey } : {}
|
|
367
|
+
};
|
|
368
|
+
return query;
|
|
369
|
+
}
|
|
370
|
+
function combinePredicates(left, right) {
|
|
371
|
+
if (!left) return right;
|
|
372
|
+
if (!right) return left;
|
|
373
|
+
return {
|
|
374
|
+
type: "and",
|
|
375
|
+
predicates: [left, right]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function toReadPredicate(expression) {
|
|
379
|
+
const unwrapped = unwrapExpression(expression);
|
|
380
|
+
if (!isFuncExpression(unwrapped)) throw new Error("On-demand cursor predicates must be function expressions");
|
|
381
|
+
if (unwrapped.name === "and" || unwrapped.name === "or") return {
|
|
382
|
+
type: unwrapped.name,
|
|
383
|
+
predicates: unwrapped.args.map(toReadPredicate)
|
|
384
|
+
};
|
|
385
|
+
if (unwrapped.name === "not") return negatePredicate(toReadPredicate(unwrapped.args[0]));
|
|
386
|
+
const comparison = comparisonFromExpression(unwrapped);
|
|
387
|
+
if (!comparison) throw new Error(`Unsupported on-demand cursor operator: ${unwrapped.name}`);
|
|
388
|
+
return {
|
|
389
|
+
type: "comparison",
|
|
390
|
+
...comparison
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function negatePredicate(predicate) {
|
|
394
|
+
if (predicate.type === "and" || predicate.type === "or") return {
|
|
395
|
+
type: predicate.type === "and" ? "or" : "and",
|
|
396
|
+
predicates: predicate.predicates.map(negatePredicate)
|
|
397
|
+
};
|
|
398
|
+
const comparison = predicate;
|
|
399
|
+
return {
|
|
400
|
+
...comparison,
|
|
401
|
+
op: negateOperator(comparison.op)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function negateOperator(operator) {
|
|
405
|
+
switch (operator) {
|
|
406
|
+
case "eq": return "ne";
|
|
407
|
+
case "ne": return "eq";
|
|
408
|
+
case "gt": return "lte";
|
|
409
|
+
case "gte": return "lt";
|
|
410
|
+
case "lt": return "gte";
|
|
411
|
+
case "lte": return "gt";
|
|
412
|
+
case "in": throw new Error("Unsupported on-demand cursor operator: not(in)");
|
|
413
|
+
default: throw new Error("Unsupported on-demand cursor operator");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function comparisonFromExpression(expression) {
|
|
417
|
+
const op = toReadFilterOperator(expression.name);
|
|
418
|
+
const [left, right] = expression.args;
|
|
419
|
+
if (!op || !isRefExpression(left) || !isValExpression(right)) return;
|
|
420
|
+
return {
|
|
421
|
+
field: left.path.join("."),
|
|
422
|
+
op,
|
|
423
|
+
value: right.value
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function toReadOrderByList(orderBy) {
|
|
427
|
+
return (orderBy ?? []).map((sort) => {
|
|
428
|
+
const expression = unwrapExpression(sort.expression);
|
|
429
|
+
if (!isRefExpression(expression)) throw new Error(`On-demand orderBy must be a field reference`);
|
|
430
|
+
return toReadOrderBy({
|
|
431
|
+
field: expression.path,
|
|
432
|
+
direction: sort.compareOptions.direction
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function toReadOrderBy(sort) {
|
|
437
|
+
return {
|
|
438
|
+
field: sort.field.join("."),
|
|
439
|
+
direction: sort.direction
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function toReadFilterOperator(operator) {
|
|
443
|
+
switch (operator) {
|
|
444
|
+
case "eq":
|
|
445
|
+
case "gt":
|
|
446
|
+
case "gte":
|
|
447
|
+
case "lt":
|
|
448
|
+
case "lte":
|
|
449
|
+
case "in": return operator;
|
|
450
|
+
case "not_eq": return "ne";
|
|
451
|
+
case "not_gt": return "lte";
|
|
452
|
+
case "not_gte": return "lt";
|
|
453
|
+
case "not_lt": return "gte";
|
|
454
|
+
case "not_lte": return "gt";
|
|
455
|
+
default: return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function unwrapExpression(value) {
|
|
459
|
+
if (typeof value === "object" && value !== null && "expression" in value && isExpression(value.expression)) return value.expression;
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
function isExpression(value) {
|
|
463
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
464
|
+
}
|
|
465
|
+
function isFuncExpression(value) {
|
|
466
|
+
return isExpression(value) && value.type === "func" && typeof value.name === "string" && Array.isArray(value.args);
|
|
467
|
+
}
|
|
468
|
+
function isRefExpression(value) {
|
|
469
|
+
return isExpression(value) && value.type === "ref" && Array.isArray(value.path);
|
|
470
|
+
}
|
|
471
|
+
function isValExpression(value) {
|
|
472
|
+
return isExpression(value) && value.type === "val";
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/subsets.ts
|
|
476
|
+
var SubsetTracker = class {
|
|
477
|
+
getKey;
|
|
478
|
+
subsets = /* @__PURE__ */ new Map();
|
|
479
|
+
keyRefs = /* @__PURE__ */ new Map();
|
|
480
|
+
constructor(getKey) {
|
|
481
|
+
this.getKey = getKey;
|
|
482
|
+
}
|
|
483
|
+
replace(subsetId, rows, filters, predicate) {
|
|
484
|
+
const previous = this.subsets.get(subsetId);
|
|
485
|
+
const refs = previous?.refs ?? 0;
|
|
486
|
+
if (previous) this.releaseKeys(previous.keys);
|
|
487
|
+
const keys = new Set(rows.map((row) => this.getKey(row)));
|
|
488
|
+
this.addKeys(keys);
|
|
489
|
+
this.subsets.set(subsetId, {
|
|
490
|
+
filters,
|
|
491
|
+
keys,
|
|
492
|
+
loaded: true,
|
|
493
|
+
...predicate ? { predicate } : {},
|
|
494
|
+
refs
|
|
495
|
+
});
|
|
496
|
+
const removed = [];
|
|
497
|
+
previous?.keys.forEach((key) => {
|
|
498
|
+
if (!keys.has(key) && !this.keyRefs.has(key)) removed.push(key);
|
|
499
|
+
});
|
|
500
|
+
return removed;
|
|
501
|
+
}
|
|
502
|
+
retain(subsetId) {
|
|
503
|
+
const subset = this.subsets.get(subsetId);
|
|
504
|
+
if (subset) {
|
|
505
|
+
subset.refs += 1;
|
|
506
|
+
return { loaded: subset.loaded };
|
|
507
|
+
}
|
|
508
|
+
this.subsets.set(subsetId, {
|
|
509
|
+
filters: [],
|
|
510
|
+
keys: /* @__PURE__ */ new Set(),
|
|
511
|
+
loaded: false,
|
|
512
|
+
refs: 1
|
|
513
|
+
});
|
|
514
|
+
return { loaded: false };
|
|
515
|
+
}
|
|
516
|
+
trackRow(row) {
|
|
517
|
+
const result = this.reconcileRow(row);
|
|
518
|
+
return result.before || result.after;
|
|
519
|
+
}
|
|
520
|
+
reconcileRow(row) {
|
|
521
|
+
const key = this.getKey(row);
|
|
522
|
+
let before = false;
|
|
523
|
+
this.subsets.forEach((subset) => {
|
|
524
|
+
const had = subset.keys.has(key);
|
|
525
|
+
before ||= had;
|
|
526
|
+
if (matchesSubset(row, subset)) {
|
|
527
|
+
if (!had) {
|
|
528
|
+
subset.keys.add(key);
|
|
529
|
+
this.keyRefs.set(key, (this.keyRefs.get(key) ?? 0) + 1);
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (had) {
|
|
534
|
+
subset.keys.delete(key);
|
|
535
|
+
this.releaseKey(key);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
return {
|
|
539
|
+
before,
|
|
540
|
+
after: this.keyRefs.has(key)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
deleteKey(key) {
|
|
544
|
+
if (!this.keyRefs.has(key)) return false;
|
|
545
|
+
this.subsets.forEach((subset) => {
|
|
546
|
+
subset.keys.delete(key);
|
|
547
|
+
});
|
|
548
|
+
this.keyRefs.delete(key);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
release(subsetId, retain = false) {
|
|
552
|
+
const subset = this.subsets.get(subsetId);
|
|
553
|
+
if (!subset) return [];
|
|
554
|
+
subset.refs -= 1;
|
|
555
|
+
if (subset.refs > 0) return [];
|
|
556
|
+
subset.refs = 0;
|
|
557
|
+
if (retain) return [];
|
|
558
|
+
return this.expire(subsetId);
|
|
559
|
+
}
|
|
560
|
+
expire(subsetId) {
|
|
561
|
+
const subset = this.subsets.get(subsetId);
|
|
562
|
+
if (!subset || subset.refs > 0) return [];
|
|
563
|
+
this.subsets.delete(subsetId);
|
|
564
|
+
return this.releaseKeys(subset.keys);
|
|
565
|
+
}
|
|
566
|
+
clear() {
|
|
567
|
+
const keys = [...this.keyRefs.keys()];
|
|
568
|
+
this.subsets.clear();
|
|
569
|
+
this.keyRefs.clear();
|
|
570
|
+
return keys;
|
|
571
|
+
}
|
|
572
|
+
addKeys(keys) {
|
|
573
|
+
keys.forEach((key) => {
|
|
574
|
+
this.keyRefs.set(key, (this.keyRefs.get(key) ?? 0) + 1);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
releaseKeys(keys) {
|
|
578
|
+
const removed = [];
|
|
579
|
+
keys.forEach((key) => {
|
|
580
|
+
if (this.releaseKey(key)) removed.push(key);
|
|
581
|
+
});
|
|
582
|
+
return removed;
|
|
583
|
+
}
|
|
584
|
+
releaseKey(key) {
|
|
585
|
+
const refs = (this.keyRefs.get(key) ?? 0) - 1;
|
|
586
|
+
if (refs <= 0) {
|
|
587
|
+
this.keyRefs.delete(key);
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
this.keyRefs.set(key, refs);
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
function writeRows(collection, rows, getKey, write) {
|
|
595
|
+
for (const row of rows) {
|
|
596
|
+
const key = getKey(row);
|
|
597
|
+
write({
|
|
598
|
+
type: collection.has(key) ? "update" : "insert",
|
|
599
|
+
key,
|
|
600
|
+
value: row
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function matchesFilters(row, filters) {
|
|
605
|
+
return filters.every((filter) => matchesFilter(row, filter));
|
|
606
|
+
}
|
|
607
|
+
function matchesSubset(row, subset) {
|
|
608
|
+
return matchesFilters(row, subset.filters) && matchesPredicate(row, subset.predicate);
|
|
609
|
+
}
|
|
610
|
+
function matchesPredicate(row, predicate) {
|
|
611
|
+
if (!predicate) return true;
|
|
612
|
+
if (predicate.type === "comparison") return matchesFilter(row, predicate);
|
|
613
|
+
if (predicate.type === "and") return predicate.predicates.every((child) => matchesPredicate(row, child));
|
|
614
|
+
return predicate.predicates.some((child) => matchesPredicate(row, child));
|
|
615
|
+
}
|
|
616
|
+
function matchesFilter(row, filter) {
|
|
617
|
+
const value = valueAtPath(row, filter.field);
|
|
618
|
+
switch (filter.op) {
|
|
619
|
+
case "eq": return value === filter.value;
|
|
620
|
+
case "ne": return value !== filter.value;
|
|
621
|
+
case "gt": return compareValues(value, filter.value, (left, right) => left > right);
|
|
622
|
+
case "gte": return compareValues(value, filter.value, (left, right) => left >= right);
|
|
623
|
+
case "lt": return compareValues(value, filter.value, (left, right) => left < right);
|
|
624
|
+
case "lte": return compareValues(value, filter.value, (left, right) => left <= right);
|
|
625
|
+
case "in": return Array.isArray(filter.value) && filter.value.includes(value);
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
function compareValues(left, right, compare) {
|
|
630
|
+
if (typeof left === "number" && typeof right === "number") return compare(left, right);
|
|
631
|
+
if (typeof left === "string" && typeof right === "string") return compare(left, right);
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
function valueAtPath(row, field) {
|
|
635
|
+
return field.split(".").reduce((value, segment) => {
|
|
636
|
+
if (typeof value !== "object" || value === null) return;
|
|
637
|
+
return value[segment];
|
|
638
|
+
}, row);
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/collection.ts
|
|
642
|
+
function collectionOptions(options) {
|
|
643
|
+
let lease;
|
|
644
|
+
let client = options.client;
|
|
645
|
+
let activeSubsets;
|
|
646
|
+
if (!client) {
|
|
647
|
+
if (!options.url) throw new Error("collectionOptions requires either client or url");
|
|
648
|
+
lease = acquireSharedClient({
|
|
649
|
+
url: options.url,
|
|
650
|
+
...options.clientId ? { clientId: options.clientId } : {}
|
|
651
|
+
});
|
|
652
|
+
client = lease.client;
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
...options.id ? { id: options.id } : {},
|
|
656
|
+
...options.gcTime !== void 0 ? { gcTime: options.gcTime } : {},
|
|
657
|
+
...options.schema ? { schema: options.schema } : {},
|
|
658
|
+
...options.startSync !== void 0 ? { startSync: options.startSync } : {},
|
|
659
|
+
...options.syncMode ? { syncMode: options.syncMode } : {},
|
|
660
|
+
...options.autoIndex !== void 0 ? { autoIndex: options.autoIndex } : {},
|
|
661
|
+
...options.defaultIndexType !== void 0 ? { defaultIndexType: options.defaultIndexType } : {},
|
|
662
|
+
getKey: options.getKey,
|
|
663
|
+
sync: {
|
|
664
|
+
sync: ({ begin, write, commit, markReady, collection }) => {
|
|
665
|
+
const subsets = new SubsetTracker(options.getKey);
|
|
666
|
+
activeSubsets = subsets;
|
|
667
|
+
const subsetGcTime = options.gcTime ?? 3e5;
|
|
668
|
+
const retentionTimers = /* @__PURE__ */ new Map();
|
|
669
|
+
const deleteKeys = (keys) => {
|
|
670
|
+
if (keys.length === 0) return;
|
|
671
|
+
begin();
|
|
672
|
+
for (const key of keys) write({
|
|
673
|
+
type: "delete",
|
|
674
|
+
key
|
|
675
|
+
});
|
|
676
|
+
commit();
|
|
677
|
+
};
|
|
678
|
+
const cancelRetention = (id) => {
|
|
679
|
+
const timer = retentionTimers.get(id);
|
|
680
|
+
if (!timer) return;
|
|
681
|
+
clearTimeout(timer);
|
|
682
|
+
retentionTimers.delete(id);
|
|
683
|
+
};
|
|
684
|
+
const scheduleRetention = (id) => {
|
|
685
|
+
if (!Number.isFinite(subsetGcTime)) return;
|
|
686
|
+
if (subsetGcTime <= 0) {
|
|
687
|
+
deleteKeys(subsets.expire(id));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
cancelRetention(id);
|
|
691
|
+
retentionTimers.set(id, setTimeout(() => {
|
|
692
|
+
retentionTimers.delete(id);
|
|
693
|
+
deleteKeys(subsets.expire(id));
|
|
694
|
+
}, subsetGcTime));
|
|
695
|
+
};
|
|
696
|
+
const subscription = client.subscribe(options.collection, (broadcast) => {
|
|
697
|
+
if ((broadcast.invalidations ?? []).length > 0 && options.syncMode === "on-demand") deleteKeys(subsets.clear());
|
|
698
|
+
const changes = broadcast.updates.flatMap((update) => {
|
|
699
|
+
if (update.collection !== options.collection) return [];
|
|
700
|
+
const ownUpdate = update.clientId === client.clientId;
|
|
701
|
+
if ((options.ignoreOwnUpdates ?? false) && ownUpdate) return [];
|
|
702
|
+
if (options.syncMode !== "on-demand") return [toChangeMessage(update, collection.has(update.key))];
|
|
703
|
+
const key = update.key;
|
|
704
|
+
const exists = collection.has(key);
|
|
705
|
+
if (update.type === "delete") {
|
|
706
|
+
subsets.deleteKey(key);
|
|
707
|
+
return exists ? [{
|
|
708
|
+
type: "delete",
|
|
709
|
+
key
|
|
710
|
+
}] : [];
|
|
711
|
+
}
|
|
712
|
+
const current = collection.get(key);
|
|
713
|
+
const row = update.type === "update" && current ? {
|
|
714
|
+
...current,
|
|
715
|
+
...update.value
|
|
716
|
+
} : update.value;
|
|
717
|
+
const tracked = subsets.reconcileRow(row);
|
|
718
|
+
if (ownUpdate || exists || tracked.after) return [toChangeMessage(update, exists)];
|
|
719
|
+
return [];
|
|
720
|
+
});
|
|
721
|
+
if (changes.length === 0) return;
|
|
722
|
+
begin();
|
|
723
|
+
for (const change of changes) write(change);
|
|
724
|
+
commit();
|
|
725
|
+
});
|
|
726
|
+
const readSubset = (subsetOptions) => {
|
|
727
|
+
const id = subsetId(subsetOptions);
|
|
728
|
+
cancelRetention(id);
|
|
729
|
+
if (subsets.retain(id).loaded) return true;
|
|
730
|
+
return (async () => {
|
|
731
|
+
const query = readQueryForSubset(options.collection, options.read === false ? void 0 : options.read, subsetOptions);
|
|
732
|
+
await subscription.ready;
|
|
733
|
+
const result = await client.read(query);
|
|
734
|
+
const removedKeys = subsets.replace(id, result.rows, query.filters ?? [], query.predicate);
|
|
735
|
+
begin();
|
|
736
|
+
writeRows(collection, result.rows, options.getKey, write);
|
|
737
|
+
for (const key of removedKeys) write({
|
|
738
|
+
type: "delete",
|
|
739
|
+
key
|
|
740
|
+
});
|
|
741
|
+
commit();
|
|
742
|
+
})().catch((error) => {
|
|
743
|
+
deleteKeys(subsets.release(id));
|
|
744
|
+
throw error;
|
|
745
|
+
});
|
|
746
|
+
};
|
|
747
|
+
if (options.syncMode === "on-demand") {
|
|
748
|
+
markReady();
|
|
749
|
+
return {
|
|
750
|
+
loadSubset: readSubset,
|
|
751
|
+
unloadSubset: (subsetOptions) => {
|
|
752
|
+
const id = subsetId(subsetOptions);
|
|
753
|
+
const removedKeys = subsets.release(id, subsetGcTime !== 0);
|
|
754
|
+
deleteKeys(removedKeys);
|
|
755
|
+
if (removedKeys.length === 0) scheduleRetention(id);
|
|
756
|
+
},
|
|
757
|
+
cleanup: () => {
|
|
758
|
+
retentionTimers.forEach((timer) => {
|
|
759
|
+
clearTimeout(timer);
|
|
760
|
+
});
|
|
761
|
+
retentionTimers.clear();
|
|
762
|
+
subsets.clear();
|
|
763
|
+
if (activeSubsets === subsets) activeSubsets = void 0;
|
|
764
|
+
subscription.unsubscribe();
|
|
765
|
+
lease?.release();
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const initialQuery = initialReadQuery(options.collection, options.read);
|
|
770
|
+
if (!initialQuery) subscription.ready.then(() => {
|
|
771
|
+
markReady();
|
|
772
|
+
});
|
|
773
|
+
else subscription.ready.then(() => client.read(initialQuery)).then((result) => {
|
|
774
|
+
if (result.rows.length > 0) {
|
|
775
|
+
begin();
|
|
776
|
+
writeRows(collection, result.rows, options.getKey, write);
|
|
777
|
+
commit();
|
|
778
|
+
}
|
|
779
|
+
markReady();
|
|
780
|
+
});
|
|
781
|
+
return () => {
|
|
782
|
+
if (activeSubsets === subsets) activeSubsets = void 0;
|
|
783
|
+
subscription.unsubscribe();
|
|
784
|
+
lease?.release();
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
rowUpdateMode: "partial"
|
|
788
|
+
},
|
|
789
|
+
onInsert: async ({ transaction }) => {
|
|
790
|
+
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
791
|
+
return client.push(createBatch(options.collection, client.clientId, transaction));
|
|
792
|
+
},
|
|
793
|
+
onUpdate: async ({ transaction }) => {
|
|
794
|
+
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
795
|
+
return client.push(createBatch(options.collection, client.clientId, transaction));
|
|
796
|
+
},
|
|
797
|
+
onDelete: async ({ transaction }) => {
|
|
798
|
+
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
799
|
+
return client.push(createBatch(options.collection, client.clientId, transaction));
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function trackLocalTransaction(subsets, syncMode, transaction) {
|
|
804
|
+
if (syncMode !== "on-demand" || !subsets) return;
|
|
805
|
+
for (const mutation of transaction.mutations) {
|
|
806
|
+
if (mutation.type === "delete") {
|
|
807
|
+
subsets.deleteKey(mutation.key);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
subsets.reconcileRow(mutation.modified);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/collection-path.ts
|
|
815
|
+
function collectionScope(path, params) {
|
|
816
|
+
return `/${collectionPathSegments(path).map((segment) => segmentValue(segment, params)).map(normalizeSegment).join("/")}/`;
|
|
817
|
+
}
|
|
818
|
+
function firstNewPathParam(childPath, parentPath) {
|
|
819
|
+
if (!childPath) return void 0;
|
|
820
|
+
const parentParams = new Set(pathParams(parentPath));
|
|
821
|
+
return pathParams(childPath).find((param) => !parentParams.has(param));
|
|
822
|
+
}
|
|
823
|
+
function collectionPathSegments(path) {
|
|
824
|
+
return path.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
825
|
+
}
|
|
826
|
+
function collectionPathParamName(segment) {
|
|
827
|
+
return segment.match(/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/)?.[1] ?? segment.match(/^:([A-Za-z_][A-Za-z0-9_]*)$/)?.[1];
|
|
828
|
+
}
|
|
829
|
+
function isCollectionPathParam(segment) {
|
|
830
|
+
return collectionPathParamName(segment) !== void 0;
|
|
831
|
+
}
|
|
832
|
+
function segmentValue(segment, params) {
|
|
833
|
+
const param = collectionPathParamName(segment);
|
|
834
|
+
if (!param) return segment;
|
|
835
|
+
const value = params[param];
|
|
836
|
+
if (value === void 0) throw new Error(`Missing collection path parameter: ${param}`);
|
|
837
|
+
return String(value);
|
|
838
|
+
}
|
|
839
|
+
function pathParams(path) {
|
|
840
|
+
return collectionPathSegments(path).flatMap((segment) => {
|
|
841
|
+
const param = collectionPathParamName(segment);
|
|
842
|
+
return param ? [param] : [];
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function normalizeSegment(segment) {
|
|
846
|
+
return encodeURIComponent(decodeURIComponent(segment));
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/collection-api.ts
|
|
850
|
+
function defaultCollectionApiPath(path, name) {
|
|
851
|
+
const prefix = collectionPathSegments(path).filter((segment) => !isCollectionPathParam(segment)).join(".");
|
|
852
|
+
return prefix ? `${prefix}.${name}` : name;
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/collection-type.ts
|
|
856
|
+
function createCollectionTypeFromOptions(options) {
|
|
857
|
+
const connection = connectionFrom(options);
|
|
858
|
+
if (!connection) throw new Error("CollectionTypes.builder requires either client or url");
|
|
859
|
+
return new CollectionTypeManager(options, connection, {}, /* @__PURE__ */ new Map()).api();
|
|
860
|
+
}
|
|
861
|
+
var CollectionTypeManager = class CollectionTypeManager {
|
|
862
|
+
options;
|
|
863
|
+
connection;
|
|
864
|
+
params;
|
|
865
|
+
cache;
|
|
866
|
+
constructor(options, connection, params, cache) {
|
|
867
|
+
this.options = options;
|
|
868
|
+
this.connection = connection;
|
|
869
|
+
this.params = params;
|
|
870
|
+
this.cache = cache;
|
|
871
|
+
}
|
|
872
|
+
api() {
|
|
873
|
+
return withChildren(withApiMethods({
|
|
874
|
+
all: (params = {}) => this.all(params),
|
|
875
|
+
delete: (...args) => this.all({}).delete(...args),
|
|
876
|
+
insert: (...args) => this.all({}).insert(...args),
|
|
877
|
+
with: (params) => this.with(params),
|
|
878
|
+
withId: (id, childParam) => this.withId(id, childParam),
|
|
879
|
+
update: (...args) => this.all({}).update(...args),
|
|
880
|
+
usage: () => this.usage()
|
|
881
|
+
}, this.apiMethods()), this.childManagers(this.params));
|
|
882
|
+
}
|
|
883
|
+
all(params) {
|
|
884
|
+
const scope = collectionScope(this.options.path, {
|
|
885
|
+
...this.params,
|
|
886
|
+
...params
|
|
887
|
+
});
|
|
888
|
+
const key = `${this.options.path}\n${scope}`;
|
|
889
|
+
const existing = this.cache.get(key);
|
|
890
|
+
if (existing) {
|
|
891
|
+
existing.usage.accessCount += 1;
|
|
892
|
+
existing.usage.lastAccessedAt = Date.now();
|
|
893
|
+
return existing.collection;
|
|
894
|
+
}
|
|
895
|
+
const id = collectionId(this.options.id, this.options.path, scope);
|
|
896
|
+
const collection = createCollection(collectionOptions({
|
|
897
|
+
...collectionConfig(this.options),
|
|
898
|
+
...this.connection,
|
|
899
|
+
id,
|
|
900
|
+
collection: scope
|
|
901
|
+
}));
|
|
902
|
+
const usage = {
|
|
903
|
+
id,
|
|
904
|
+
path: this.options.path,
|
|
905
|
+
scope,
|
|
906
|
+
accessCount: 1,
|
|
907
|
+
createdAt: Date.now(),
|
|
908
|
+
lastAccessedAt: Date.now()
|
|
909
|
+
};
|
|
910
|
+
this.cache.set(key, {
|
|
911
|
+
collection,
|
|
912
|
+
usage
|
|
913
|
+
});
|
|
914
|
+
return collection;
|
|
915
|
+
}
|
|
916
|
+
with(params) {
|
|
917
|
+
return new CollectionTypeManager(this.options, this.connection, {
|
|
918
|
+
...this.params,
|
|
919
|
+
...params
|
|
920
|
+
}, this.cache).api();
|
|
921
|
+
}
|
|
922
|
+
withId(id, childParam) {
|
|
923
|
+
return withChildren({
|
|
924
|
+
all: () => this.all({}),
|
|
925
|
+
delete: (config) => this.all({}).delete(id, config),
|
|
926
|
+
get: () => this.all({}).get(id),
|
|
927
|
+
update: (...args) => this.all({}).update(id, ...args)
|
|
928
|
+
}, this.childManagers(this.childParams(id, childParam)));
|
|
929
|
+
}
|
|
930
|
+
usage() {
|
|
931
|
+
return [...this.cache.values()].map((entry) => ({ ...entry.usage }));
|
|
932
|
+
}
|
|
933
|
+
apiMethods() {
|
|
934
|
+
return Object.fromEntries(Object.entries(this.options.api ?? {}).map(([name, method]) => [name, (...args) => this.callApi(name, method, args[0])]));
|
|
935
|
+
}
|
|
936
|
+
async callApi(name, method, input) {
|
|
937
|
+
if (method.handler) {
|
|
938
|
+
const scope = collectionScope(this.options.path, this.params);
|
|
939
|
+
return method.handler({
|
|
940
|
+
input,
|
|
941
|
+
collection: this.all({}),
|
|
942
|
+
params: this.params,
|
|
943
|
+
scope
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const path = method.path ?? defaultCollectionApiPath(this.options.path, name);
|
|
947
|
+
const lease = this.connection.client ? void 0 : acquireSharedClient(this.connection);
|
|
948
|
+
const client = this.connection.client ?? lease?.client;
|
|
949
|
+
try {
|
|
950
|
+
return await client.call(path, input);
|
|
951
|
+
} finally {
|
|
952
|
+
lease?.release();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
childManagers(params) {
|
|
956
|
+
const children = this.options.children ?? {};
|
|
957
|
+
return Object.fromEntries(Object.entries(children).map(([name, child]) => [name, new CollectionTypeManager(child, connectionFrom(child) ?? this.connection, params, this.cache).api()]));
|
|
958
|
+
}
|
|
959
|
+
childParams(id, childParam) {
|
|
960
|
+
const param = childParam ?? firstChildParam(this.options);
|
|
961
|
+
return param ? {
|
|
962
|
+
...this.params,
|
|
963
|
+
[param]: id
|
|
964
|
+
} : this.params;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
function collectionConfig(options) {
|
|
968
|
+
const { api, children, id, name, parentParam, path, ...config } = options;
|
|
969
|
+
return config;
|
|
970
|
+
}
|
|
971
|
+
function withChildren(target, children) {
|
|
972
|
+
return Object.assign(target, children);
|
|
973
|
+
}
|
|
974
|
+
function withApiMethods(target, api) {
|
|
975
|
+
for (const name of Object.keys(api)) if (name in target) throw new Error(`Collection API method conflicts with existing property: ${name}`);
|
|
976
|
+
return Object.assign(target, api);
|
|
977
|
+
}
|
|
978
|
+
function connectionFrom(options) {
|
|
979
|
+
if (options.client) return { client: options.client };
|
|
980
|
+
if (options.url) return {
|
|
981
|
+
url: options.url,
|
|
982
|
+
...options.clientId ? { clientId: options.clientId } : {}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function collectionId(id, path, scope) {
|
|
986
|
+
return typeof id === "function" ? id({
|
|
987
|
+
path,
|
|
988
|
+
scope
|
|
989
|
+
}) : id ?? scope;
|
|
990
|
+
}
|
|
991
|
+
function firstChildParam(options) {
|
|
992
|
+
const child = Object.values(options.children ?? {})[0];
|
|
993
|
+
return child?.parentParam ?? firstNewPathParam(child?.path, options.path);
|
|
994
|
+
}
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/definition-builder.ts
|
|
997
|
+
function collectionTypesFrom(definitions) {
|
|
998
|
+
return new DefinitionCollectionTypesBuilder(definitions.collections, {}, void 0);
|
|
999
|
+
}
|
|
1000
|
+
var DefinitionCollectionTypesBuilder = class DefinitionCollectionTypesBuilder {
|
|
1001
|
+
definitions;
|
|
1002
|
+
overrides;
|
|
1003
|
+
connection;
|
|
1004
|
+
constructor(definitions, overrides, connection) {
|
|
1005
|
+
this.definitions = definitions;
|
|
1006
|
+
this.overrides = overrides;
|
|
1007
|
+
this.connection = connection;
|
|
1008
|
+
}
|
|
1009
|
+
client(client) {
|
|
1010
|
+
return new DefinitionCollectionTypesBuilder(this.definitions, this.overrides, { client });
|
|
1011
|
+
}
|
|
1012
|
+
url(url, options = {}) {
|
|
1013
|
+
return new DefinitionCollectionTypesBuilder(this.definitions, this.overrides, {
|
|
1014
|
+
url,
|
|
1015
|
+
...options.clientId ? { clientId: options.clientId } : {}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
collection(path, configure) {
|
|
1019
|
+
const override = configure(new ClientCollectionBuilder({})).toOverride();
|
|
1020
|
+
return new DefinitionCollectionTypesBuilder(this.definitions, {
|
|
1021
|
+
...this.overrides,
|
|
1022
|
+
[path]: override
|
|
1023
|
+
}, this.connection);
|
|
1024
|
+
}
|
|
1025
|
+
build() {
|
|
1026
|
+
if (!this.connection) throw new Error("CollectionTypes.from requires either client or url");
|
|
1027
|
+
return Object.fromEntries(Object.entries(this.definitions).map(([name, definition]) => [name, collectionTypeFromDefinition(definition, name, this.overrides, this.connection)]));
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
var ClientCollectionBuilder = class ClientCollectionBuilder {
|
|
1031
|
+
override;
|
|
1032
|
+
constructor(override) {
|
|
1033
|
+
this.override = override;
|
|
1034
|
+
}
|
|
1035
|
+
sync(syncMode) {
|
|
1036
|
+
return new ClientCollectionBuilder({
|
|
1037
|
+
...this.override,
|
|
1038
|
+
syncMode
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
index(autoIndex, defaultIndexType) {
|
|
1042
|
+
return new ClientCollectionBuilder({
|
|
1043
|
+
...this.override,
|
|
1044
|
+
autoIndex,
|
|
1045
|
+
...defaultIndexType ? { defaultIndexType } : {}
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
toOverride() {
|
|
1049
|
+
return this.override;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
function collectionTypeFromDefinition(definition, dotPath, overrides, connection) {
|
|
1053
|
+
return createCollectionTypeFromOptions({
|
|
1054
|
+
...collectionOptionsFromDefinition(definition, dotPath, overrides),
|
|
1055
|
+
...connection
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function collectionOptionsFromDefinition(definition, dotPath, overrides) {
|
|
1059
|
+
return {
|
|
1060
|
+
...overrides[dotPath],
|
|
1061
|
+
name: definition.name,
|
|
1062
|
+
path: definition.path,
|
|
1063
|
+
schema: definition.schema,
|
|
1064
|
+
getKey: (item) => item[definition.key],
|
|
1065
|
+
children: Object.fromEntries(Object.entries(definition.children).map(([name, child]) => [name, collectionOptionsFromDefinition(child, `${dotPath}.${name}`, overrides)])),
|
|
1066
|
+
api: Object.fromEntries(Object.entries(definition.api).map(([name, api]) => [name, api.path ? { path: api.path } : {}]))
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/collection-name.ts
|
|
1071
|
+
function rootPathForName(name) {
|
|
1072
|
+
return `/${encodeURIComponent(name)}/`;
|
|
1073
|
+
}
|
|
1074
|
+
function childPathForName(parentPath, parentParam, name) {
|
|
1075
|
+
return `${parentPath.replace(/\/+$/g, "")}/{${parentParam}}/${encodeURIComponent(name)}/`;
|
|
1076
|
+
}
|
|
1077
|
+
function parentIdParamForName(name) {
|
|
1078
|
+
return `${name}Id`;
|
|
1079
|
+
}
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region src/immutable.ts
|
|
1082
|
+
function cloneImmutable(value) {
|
|
1083
|
+
if (Array.isArray(value)) return Object.freeze(value.map(cloneImmutable));
|
|
1084
|
+
if (!isPlainObject(value)) return value;
|
|
1085
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([nestedKey, nestedValue]) => [nestedKey, cloneImmutable(nestedValue)])));
|
|
1086
|
+
}
|
|
1087
|
+
function isPlainObject(value) {
|
|
1088
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1089
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1090
|
+
return prototype === Object.prototype || prototype === null;
|
|
1091
|
+
}
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/collection-type-builder-state.ts
|
|
1094
|
+
function freezeBuilderState(state) {
|
|
1095
|
+
return Object.freeze(Object.fromEntries(Object.entries(state).filter(([, value]) => value !== void 0).map(([key, value]) => [key, copyBuilderStateValue(key, value)])));
|
|
1096
|
+
}
|
|
1097
|
+
function copyBuilderStateValue(key, value) {
|
|
1098
|
+
if (key === "api" && isApiMap(value)) return freezeApiMap(value);
|
|
1099
|
+
if (key === "url" && value instanceof URL) return value.toString();
|
|
1100
|
+
return cloneImmutable(value);
|
|
1101
|
+
}
|
|
1102
|
+
function freezeApiMap(api) {
|
|
1103
|
+
return Object.freeze(Object.fromEntries(Object.entries(api).map(([name, method]) => [name, freezeApiMethod(method)])));
|
|
1104
|
+
}
|
|
1105
|
+
function freezeApiMethod(method) {
|
|
1106
|
+
return Object.freeze({ ...method });
|
|
1107
|
+
}
|
|
1108
|
+
function isApiMap(value) {
|
|
1109
|
+
return typeof value === "object" && value !== null;
|
|
1110
|
+
}
|
|
1111
|
+
//#endregion
|
|
1112
|
+
//#region src/collection-type-builder.ts
|
|
1113
|
+
function collectionTypeBuilder() {
|
|
1114
|
+
return new CollectionTypeBuilder({});
|
|
1115
|
+
}
|
|
1116
|
+
const CollectionTypes = {
|
|
1117
|
+
builder: collectionTypeBuilder,
|
|
1118
|
+
from: collectionTypesFrom
|
|
1119
|
+
};
|
|
1120
|
+
var CollectionTypeBuilder = class CollectionTypeBuilder {
|
|
1121
|
+
state;
|
|
1122
|
+
constructor(state) {
|
|
1123
|
+
this.state = freezeBuilderState(state);
|
|
1124
|
+
}
|
|
1125
|
+
client(client) {
|
|
1126
|
+
return this.next({
|
|
1127
|
+
client,
|
|
1128
|
+
url: void 0,
|
|
1129
|
+
clientId: void 0
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
url(url, options = {}) {
|
|
1133
|
+
return this.next({
|
|
1134
|
+
client: void 0,
|
|
1135
|
+
url,
|
|
1136
|
+
...options.clientId ? { clientId: options.clientId } : {}
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
name(name) {
|
|
1140
|
+
return this.next({
|
|
1141
|
+
name,
|
|
1142
|
+
path: rootPathForName(name)
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
schema(schema) {
|
|
1146
|
+
return this.next({ schema });
|
|
1147
|
+
}
|
|
1148
|
+
sync(syncMode) {
|
|
1149
|
+
return this.next({ syncMode });
|
|
1150
|
+
}
|
|
1151
|
+
index(autoIndex, defaultIndexType) {
|
|
1152
|
+
return this.next({
|
|
1153
|
+
autoIndex,
|
|
1154
|
+
...defaultIndexType ? { defaultIndexType } : {}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
child(name, configure) {
|
|
1158
|
+
const child = configure(this.childBuilder(name));
|
|
1159
|
+
const children = typeof this.state.children === "object" && this.state.children !== null ? this.state.children : {};
|
|
1160
|
+
return this.next({ children: {
|
|
1161
|
+
...children,
|
|
1162
|
+
[name]: child.state
|
|
1163
|
+
} });
|
|
1164
|
+
}
|
|
1165
|
+
key(getKey) {
|
|
1166
|
+
return this.next({ getKey });
|
|
1167
|
+
}
|
|
1168
|
+
api(name, method) {
|
|
1169
|
+
const entry = typeof method === "function" ? localApiMethod(method) : method ?? {};
|
|
1170
|
+
return this.next({ api: {
|
|
1171
|
+
...this.state.api,
|
|
1172
|
+
[name]: entry
|
|
1173
|
+
} });
|
|
1174
|
+
}
|
|
1175
|
+
build() {
|
|
1176
|
+
if (!this.state.name || !this.state.path) throw new Error("CollectionTypes.builder requires a name");
|
|
1177
|
+
if (!this.state.getKey) throw new Error("CollectionTypes.builder requires a key function");
|
|
1178
|
+
return createCollectionTypeFromOptions(this.state);
|
|
1179
|
+
}
|
|
1180
|
+
next(updates) {
|
|
1181
|
+
return new CollectionTypeBuilder({
|
|
1182
|
+
...this.state,
|
|
1183
|
+
...updates
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
childBuilder(name) {
|
|
1187
|
+
if (!this.state.name || !this.state.path) throw new Error("CollectionTypes.builder.child requires a parent name");
|
|
1188
|
+
const parentParam = parentIdParamForName(this.state.name);
|
|
1189
|
+
return new CollectionTypeBuilder({
|
|
1190
|
+
name,
|
|
1191
|
+
parentParam,
|
|
1192
|
+
path: childPathForName(this.state.path, parentParam, name)
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
function localApiMethod(handler) {
|
|
1197
|
+
return Object.freeze({ handler: ({ input, collection, params, scope }) => handler(input, {
|
|
1198
|
+
collection,
|
|
1199
|
+
params,
|
|
1200
|
+
scope
|
|
1201
|
+
}) });
|
|
1202
|
+
}
|
|
1203
|
+
//#endregion
|
|
1204
|
+
export { CollectionTypeBuilder, CollectionTypes, DefinitionCollectionTypesBuilder, collectionOptions, createBatch, createClient, toChangeMessage };
|
|
1205
|
+
|
|
1206
|
+
//# sourceMappingURL=index.mjs.map
|