@lsync/client 0.0.1 → 0.0.3
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/README.md +18 -0
- package/dist/index.d.mts +42 -52
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +846 -478
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -5
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { parseServerMessage, sendClientRpcRequest } from "@lsync/transport";
|
|
3
|
-
import { observable } from "@trpc/server/observable";
|
|
1
|
+
import { ProtocolError, parseServerMessage, sendClientMessage } from "@lsync/transport";
|
|
4
2
|
import { createCollection } from "@tanstack/db";
|
|
5
3
|
//#region src/batch.ts
|
|
6
4
|
function createBatch(collection, clientId, transaction) {
|
|
@@ -45,6 +43,13 @@ function toChangeMessage(update, existing) {
|
|
|
45
43
|
}
|
|
46
44
|
//#endregion
|
|
47
45
|
//#region src/client-rpc.ts
|
|
46
|
+
function rejectPending(pending, error) {
|
|
47
|
+
const requests = [...pending.values()];
|
|
48
|
+
pending.clear();
|
|
49
|
+
for (const request of requests) try {
|
|
50
|
+
request.reject(error);
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
48
53
|
function takePending(pending, id) {
|
|
49
54
|
if (id === null || id === void 0) return;
|
|
50
55
|
const key = String(id);
|
|
@@ -52,41 +57,6 @@ function takePending(pending, id) {
|
|
|
52
57
|
pending.delete(key);
|
|
53
58
|
return request;
|
|
54
59
|
}
|
|
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
60
|
function collectionScope$1(collection) {
|
|
91
61
|
return `/${collection.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).map(normalizeSegment$1).join("/")}/`;
|
|
92
62
|
}
|
|
@@ -108,7 +78,9 @@ var ClientSubscriptions = class {
|
|
|
108
78
|
if (!entry) {
|
|
109
79
|
entry = {
|
|
110
80
|
collection: key,
|
|
81
|
+
disconnectListeners: /* @__PURE__ */ new Set(),
|
|
111
82
|
listeners: /* @__PURE__ */ new Set(),
|
|
83
|
+
reconnectListeners: /* @__PURE__ */ new Set(),
|
|
112
84
|
ready: Promise.resolve()
|
|
113
85
|
};
|
|
114
86
|
this.subscriptions.set(key, entry);
|
|
@@ -117,9 +89,20 @@ var ClientSubscriptions = class {
|
|
|
117
89
|
if (shouldSubscribe) entry.ready = this.start(entry);
|
|
118
90
|
return {
|
|
119
91
|
ready: entry.ready,
|
|
92
|
+
onDisconnect: (next) => addLifecycleListener(entry.disconnectListeners, next),
|
|
93
|
+
onReconnect: (next) => addLifecycleListener(entry.reconnectListeners, next),
|
|
120
94
|
unsubscribe: () => this.unsubscribe(key, listener)
|
|
121
95
|
};
|
|
122
96
|
}
|
|
97
|
+
get hasSubscriptions() {
|
|
98
|
+
return this.subscriptions.size > 0;
|
|
99
|
+
}
|
|
100
|
+
disconnected() {
|
|
101
|
+
for (const entry of this.subscriptions.values()) for (const listener of entry.disconnectListeners) listener();
|
|
102
|
+
}
|
|
103
|
+
reconnected() {
|
|
104
|
+
for (const entry of this.subscriptions.values()) for (const listener of entry.reconnectListeners) listener();
|
|
105
|
+
}
|
|
123
106
|
async replay(ws) {
|
|
124
107
|
await Promise.all([...this.subscriptions.values()].map((entry) => this.subscribeOnSocket(ws, entry)));
|
|
125
108
|
}
|
|
@@ -165,164 +148,472 @@ var ClientSubscriptions = class {
|
|
|
165
148
|
if (entry.listeners.size > 0) return;
|
|
166
149
|
this.subscriptions.delete(key);
|
|
167
150
|
const socket = this.controls.currentSocket();
|
|
168
|
-
if (socket?.readyState === WebSocket.OPEN) this.controls.send(socket, "unsubscribe", entry.collection);
|
|
151
|
+
if (socket?.readyState === WebSocket.OPEN) this.controls.send(socket, "unsubscribe", entry.collection).catch(() => void 0);
|
|
169
152
|
}
|
|
170
153
|
};
|
|
154
|
+
function addLifecycleListener(listeners, listener) {
|
|
155
|
+
listeners.add(listener);
|
|
156
|
+
return () => listeners.delete(listener);
|
|
157
|
+
}
|
|
171
158
|
//#endregion
|
|
172
|
-
//#region src/client.ts
|
|
159
|
+
//#region src/client-reconnect.ts
|
|
160
|
+
var ConnectionClosedError = class extends Error {
|
|
161
|
+
constructor(message = "Sync WebSocket closed before the request completed") {
|
|
162
|
+
super(message);
|
|
163
|
+
this.name = "ConnectionClosedError";
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var RequestTimeoutError = class extends Error {
|
|
167
|
+
constructor() {
|
|
168
|
+
super("Sync RPC request timed out");
|
|
169
|
+
this.name = "RequestTimeoutError";
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
function reconnectPolicy(option) {
|
|
173
|
+
const input = typeof option === "object" ? option : {};
|
|
174
|
+
return {
|
|
175
|
+
enabled: option !== false,
|
|
176
|
+
initialDelayMs: input.initialDelayMs ?? 100,
|
|
177
|
+
maxDelayMs: input.maxDelayMs ?? 5e3,
|
|
178
|
+
multiplier: input.multiplier ?? 2,
|
|
179
|
+
maxAttempts: input.maxAttempts ?? 5
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function reconnectDelay(policy, attempt) {
|
|
183
|
+
return Math.min(policy.initialDelayMs * policy.multiplier ** attempt, policy.maxDelayMs);
|
|
184
|
+
}
|
|
185
|
+
function wait(delayMs) {
|
|
186
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
187
|
+
}
|
|
188
|
+
function retryableMutationError(error) {
|
|
189
|
+
if (error instanceof ConnectionClosedError || error instanceof RequestTimeoutError) return true;
|
|
190
|
+
if (typeof error !== "object" || error === null || !("cause" in error)) return false;
|
|
191
|
+
return retryableMutationError(error.cause);
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/client-shared.ts
|
|
173
195
|
const sharedClients = /* @__PURE__ */ new Map();
|
|
196
|
+
function acquireSharedClient(options) {
|
|
197
|
+
const key = sharedClientKey(options);
|
|
198
|
+
let entry = sharedClients.get(key);
|
|
199
|
+
if (!entry) {
|
|
200
|
+
entry = {
|
|
201
|
+
client: createClient(options),
|
|
202
|
+
refs: 0
|
|
203
|
+
};
|
|
204
|
+
sharedClients.set(key, entry);
|
|
205
|
+
}
|
|
206
|
+
entry.refs += 1;
|
|
207
|
+
let released = false;
|
|
208
|
+
const release = () => {
|
|
209
|
+
if (released) return;
|
|
210
|
+
released = true;
|
|
211
|
+
entry.refs -= 1;
|
|
212
|
+
if (entry.refs === 0 && sharedClients.get(key) === entry) {
|
|
213
|
+
sharedClients.delete(key);
|
|
214
|
+
entry.client.close();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
client: entry.client,
|
|
219
|
+
release,
|
|
220
|
+
[Symbol.dispose]: release
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function sharedClientKey(options) {
|
|
224
|
+
const url = new URL(options.url);
|
|
225
|
+
url.searchParams.delete("clientId");
|
|
226
|
+
const reconnect = typeof options.reconnect === "object" ? JSON.stringify(options.reconnect) : options.reconnect;
|
|
227
|
+
return [
|
|
228
|
+
url,
|
|
229
|
+
options.clientId,
|
|
230
|
+
reconnect,
|
|
231
|
+
options.connectionTimeoutMs,
|
|
232
|
+
options.requestTimeoutMs
|
|
233
|
+
].join("\0");
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/client.ts
|
|
174
237
|
function createClient(options) {
|
|
175
238
|
const clientId = options.clientId ?? crypto.randomUUID();
|
|
176
239
|
const pending = /* @__PURE__ */ new Map();
|
|
240
|
+
const reconnect = reconnectPolicy(options.reconnect);
|
|
241
|
+
const connectionTimeoutMs = options.connectionTimeoutMs ?? 1e4;
|
|
242
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
177
243
|
let nextId = 1;
|
|
178
244
|
let socket;
|
|
179
245
|
let connectPromise;
|
|
246
|
+
let reconnectTimer;
|
|
247
|
+
let closed = false;
|
|
248
|
+
let hasOpened = false;
|
|
180
249
|
let subscriptions;
|
|
181
250
|
const open = async () => {
|
|
251
|
+
if (closed) throw new ConnectionClosedError("Sync client is closed");
|
|
182
252
|
if (socket?.readyState === WebSocket.OPEN) return socket;
|
|
183
253
|
if (connectPromise) return connectPromise;
|
|
184
|
-
|
|
254
|
+
const connect = async () => {
|
|
255
|
+
const attempts = reconnect.enabled ? reconnect.maxAttempts : 1;
|
|
256
|
+
let lastError;
|
|
257
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) try {
|
|
258
|
+
return await connectOnce();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
lastError = error;
|
|
261
|
+
if (attempt + 1 < attempts) await wait(reconnectDelay(reconnect, attempt));
|
|
262
|
+
}
|
|
263
|
+
throw lastError;
|
|
264
|
+
};
|
|
265
|
+
const connectOnce = () => new Promise((resolve, reject) => {
|
|
185
266
|
const url = new URL(options.url);
|
|
186
267
|
url.searchParams.set("clientId", clientId);
|
|
187
268
|
const ws = new WebSocket(url);
|
|
188
269
|
ws.binaryType = "arraybuffer";
|
|
270
|
+
let opened = false;
|
|
271
|
+
const connectionTimer = setTimeout(() => {
|
|
272
|
+
reject(/* @__PURE__ */ new Error(`Sync WebSocket connection timed out after ${connectionTimeoutMs}ms`));
|
|
273
|
+
ws.close();
|
|
274
|
+
}, connectionTimeoutMs);
|
|
189
275
|
ws.addEventListener("open", () => {
|
|
276
|
+
opened = true;
|
|
277
|
+
clearTimeout(connectionTimer);
|
|
190
278
|
socket = ws;
|
|
191
279
|
subscriptions.replay(ws).then(() => {
|
|
192
|
-
|
|
280
|
+
if (hasOpened) subscriptions.reconnected();
|
|
281
|
+
hasOpened = true;
|
|
193
282
|
resolve(ws);
|
|
194
283
|
}).catch((error) => {
|
|
195
|
-
connectPromise = void 0;
|
|
196
284
|
reject(error);
|
|
285
|
+
ws.close();
|
|
197
286
|
});
|
|
198
287
|
});
|
|
199
288
|
ws.addEventListener("message", (event) => {
|
|
200
289
|
const message = parseServerMessage(event.data);
|
|
201
|
-
if ("
|
|
202
|
-
subscriptions.dispatch(message.
|
|
290
|
+
if (message.type === "updates") {
|
|
291
|
+
subscriptions.dispatch(message.payload);
|
|
203
292
|
return;
|
|
204
293
|
}
|
|
205
|
-
if ("error"
|
|
206
|
-
|
|
207
|
-
if (!request) return;
|
|
208
|
-
request.reject(new Error(message.error.message));
|
|
294
|
+
if (message.type === "error") {
|
|
295
|
+
takePending(pending, message.id)?.reject(new ProtocolError(message.error));
|
|
209
296
|
return;
|
|
210
297
|
}
|
|
211
|
-
|
|
212
|
-
if (!request) return;
|
|
213
|
-
request.resolve(message.result.data.json);
|
|
298
|
+
takePending(pending, message.id)?.resolve(message.payload);
|
|
214
299
|
});
|
|
215
300
|
ws.addEventListener("close", () => {
|
|
216
|
-
|
|
217
|
-
|
|
301
|
+
clearTimeout(connectionTimer);
|
|
302
|
+
if (socket === ws) {
|
|
303
|
+
socket = void 0;
|
|
304
|
+
rejectPending(pending, new ConnectionClosedError());
|
|
305
|
+
subscriptions.disconnected();
|
|
306
|
+
scheduleReconnect();
|
|
307
|
+
}
|
|
308
|
+
if (!opened) reject(/* @__PURE__ */ new Error("Unable to open sync WebSocket"));
|
|
218
309
|
});
|
|
219
310
|
ws.addEventListener("error", () => {
|
|
311
|
+
clearTimeout(connectionTimer);
|
|
220
312
|
reject(/* @__PURE__ */ new Error("Unable to open sync WebSocket"));
|
|
221
313
|
});
|
|
222
314
|
});
|
|
315
|
+
connectPromise = connect().finally(() => {
|
|
316
|
+
connectPromise = void 0;
|
|
317
|
+
});
|
|
223
318
|
return connectPromise;
|
|
224
319
|
};
|
|
320
|
+
function scheduleReconnect() {
|
|
321
|
+
if (closed || connectPromise || !reconnect.enabled || !subscriptions.hasSubscriptions || reconnectTimer) return;
|
|
322
|
+
reconnectTimer = setTimeout(() => {
|
|
323
|
+
reconnectTimer = void 0;
|
|
324
|
+
open().catch(() => scheduleReconnect());
|
|
325
|
+
}, reconnect.initialDelayMs);
|
|
326
|
+
}
|
|
225
327
|
const sendRequest = (ws, request) => {
|
|
226
328
|
return new Promise((resolve, reject) => {
|
|
227
329
|
const id = request.id;
|
|
228
|
-
|
|
229
|
-
reject(
|
|
230
|
-
|
|
231
|
-
}
|
|
330
|
+
const timeout = setTimeout(() => {
|
|
331
|
+
takePending(pending, id)?.reject(new RequestTimeoutError());
|
|
332
|
+
}, requestTimeoutMs);
|
|
232
333
|
pending.set(String(id), {
|
|
233
|
-
resolve: (value) =>
|
|
234
|
-
|
|
334
|
+
resolve: (value) => {
|
|
335
|
+
clearTimeout(timeout);
|
|
336
|
+
resolve(value);
|
|
337
|
+
},
|
|
338
|
+
reject: (error) => {
|
|
339
|
+
clearTimeout(timeout);
|
|
340
|
+
reject(error);
|
|
341
|
+
}
|
|
235
342
|
});
|
|
236
343
|
try {
|
|
237
|
-
|
|
344
|
+
sendClientMessage(ws, request);
|
|
238
345
|
} catch (error) {
|
|
239
|
-
pending
|
|
240
|
-
reject(error);
|
|
346
|
+
takePending(pending, id)?.reject(error instanceof Error ? error : new Error(String(error)));
|
|
241
347
|
}
|
|
242
348
|
});
|
|
243
349
|
};
|
|
244
|
-
const sendSubscriptionControl = async (ws, method, collection) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
});
|
|
251
|
-
};
|
|
350
|
+
const sendSubscriptionControl = async (ws, method, collection) => sendRequest(ws, {
|
|
351
|
+
version: 1,
|
|
352
|
+
id: String(nextId++),
|
|
353
|
+
type: method,
|
|
354
|
+
input: { collection }
|
|
355
|
+
});
|
|
252
356
|
subscriptions = new ClientSubscriptions({
|
|
253
357
|
currentSocket: () => socket,
|
|
254
358
|
open,
|
|
255
359
|
send: sendSubscriptionControl
|
|
256
360
|
});
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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] });
|
|
361
|
+
const sendPush = (batch) => open().then((ws) => sendRequest(ws, {
|
|
362
|
+
version: 1,
|
|
363
|
+
id: String(nextId++),
|
|
364
|
+
type: "push",
|
|
365
|
+
input: batch
|
|
366
|
+
}));
|
|
274
367
|
return {
|
|
275
368
|
clientId,
|
|
276
|
-
push: (batch) =>
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
369
|
+
push: async (batch) => {
|
|
370
|
+
const attempts = reconnect.enabled ? reconnect.maxAttempts : 1;
|
|
371
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
372
|
+
return await sendPush(batch);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (!retryableMutationError(error) || attempt + 1 >= attempts) throw error;
|
|
375
|
+
await wait(reconnectDelay(reconnect, attempt));
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
read: async (query) => {
|
|
379
|
+
const ws = await open();
|
|
380
|
+
return sendRequest(ws, {
|
|
381
|
+
version: 1,
|
|
382
|
+
id: String(nextId++),
|
|
383
|
+
type: "read",
|
|
384
|
+
input: query
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
changes: async (query) => {
|
|
388
|
+
const ws = await open();
|
|
389
|
+
return sendRequest(ws, {
|
|
390
|
+
version: 1,
|
|
391
|
+
id: String(nextId++),
|
|
392
|
+
type: "changes",
|
|
393
|
+
input: query
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
call: async (path, ...args) => {
|
|
280
397
|
const [input] = args;
|
|
281
398
|
const call = input === void 0 ? { path } : {
|
|
282
399
|
path,
|
|
283
400
|
input
|
|
284
401
|
};
|
|
285
|
-
|
|
402
|
+
const ws = await open();
|
|
403
|
+
return sendRequest(ws, {
|
|
404
|
+
version: 1,
|
|
405
|
+
id: String(nextId++),
|
|
406
|
+
type: "api",
|
|
407
|
+
input: call
|
|
408
|
+
});
|
|
286
409
|
},
|
|
287
410
|
subscribe: (collection, listener) => subscriptions.subscribe(collection, listener),
|
|
288
411
|
close() {
|
|
412
|
+
closed = true;
|
|
413
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
414
|
+
rejectPending(pending, new ConnectionClosedError("Sync client was closed"));
|
|
289
415
|
socket?.close();
|
|
290
416
|
socket = void 0;
|
|
291
417
|
}
|
|
292
418
|
};
|
|
293
419
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}),
|
|
303
|
-
refs: 0
|
|
304
|
-
};
|
|
305
|
-
sharedClients.set(key, entry);
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/subsets.ts
|
|
422
|
+
var SubsetTracker = class {
|
|
423
|
+
getKey;
|
|
424
|
+
subsets = /* @__PURE__ */ new Map();
|
|
425
|
+
keyRefs = /* @__PURE__ */ new Map();
|
|
426
|
+
constructor(getKey) {
|
|
427
|
+
this.getKey = getKey;
|
|
306
428
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
429
|
+
replace(subsetId, rows, filters, predicate) {
|
|
430
|
+
const previous = this.subsets.get(subsetId);
|
|
431
|
+
const refs = previous?.refs ?? 0;
|
|
432
|
+
if (previous) this.releaseKeys(previous.keys);
|
|
433
|
+
const keys = new Set(rows.map((row) => this.getKey(row)));
|
|
434
|
+
this.addKeys(keys);
|
|
435
|
+
this.subsets.set(subsetId, {
|
|
436
|
+
filters,
|
|
437
|
+
keys,
|
|
438
|
+
loaded: true,
|
|
439
|
+
...predicate ? { predicate } : {},
|
|
440
|
+
refs
|
|
441
|
+
});
|
|
442
|
+
const removed = [];
|
|
443
|
+
previous?.keys.forEach((key) => {
|
|
444
|
+
if (!keys.has(key) && !this.keyRefs.has(key)) removed.push(key);
|
|
445
|
+
});
|
|
446
|
+
return removed;
|
|
447
|
+
}
|
|
448
|
+
retain(subsetId) {
|
|
449
|
+
const subset = this.subsets.get(subsetId);
|
|
450
|
+
if (subset) {
|
|
451
|
+
subset.refs += 1;
|
|
452
|
+
return { loaded: subset.loaded };
|
|
453
|
+
}
|
|
454
|
+
this.subsets.set(subsetId, {
|
|
455
|
+
filters: [],
|
|
456
|
+
keys: /* @__PURE__ */ new Set(),
|
|
457
|
+
loaded: false,
|
|
458
|
+
refs: 1
|
|
459
|
+
});
|
|
460
|
+
return { loaded: false };
|
|
461
|
+
}
|
|
462
|
+
trackRow(row) {
|
|
463
|
+
const result = this.reconcileRow(row);
|
|
464
|
+
return result.before || result.after;
|
|
465
|
+
}
|
|
466
|
+
reconcileRow(row) {
|
|
467
|
+
const key = this.getKey(row);
|
|
468
|
+
let before = false;
|
|
469
|
+
this.subsets.forEach((subset) => {
|
|
470
|
+
const had = subset.keys.has(key);
|
|
471
|
+
before ||= had;
|
|
472
|
+
if (matchesSubset(row, subset)) {
|
|
473
|
+
if (!had) {
|
|
474
|
+
subset.keys.add(key);
|
|
475
|
+
this.keyRefs.set(key, (this.keyRefs.get(key) ?? 0) + 1);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
318
478
|
}
|
|
479
|
+
if (had) {
|
|
480
|
+
subset.keys.delete(key);
|
|
481
|
+
this.releaseKey(key);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
before,
|
|
486
|
+
after: this.keyRefs.has(key)
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
deleteKey(key) {
|
|
490
|
+
if (!this.keyRefs.has(key)) return false;
|
|
491
|
+
this.subsets.forEach((subset) => {
|
|
492
|
+
subset.keys.delete(key);
|
|
493
|
+
});
|
|
494
|
+
this.keyRefs.delete(key);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
release(subsetId, retain = false) {
|
|
498
|
+
const subset = this.subsets.get(subsetId);
|
|
499
|
+
if (!subset) return [];
|
|
500
|
+
subset.refs -= 1;
|
|
501
|
+
if (subset.refs > 0) return [];
|
|
502
|
+
subset.refs = 0;
|
|
503
|
+
if (retain) return [];
|
|
504
|
+
return this.expire(subsetId);
|
|
505
|
+
}
|
|
506
|
+
expire(subsetId) {
|
|
507
|
+
const subset = this.subsets.get(subsetId);
|
|
508
|
+
if (!subset || subset.refs > 0) return [];
|
|
509
|
+
this.subsets.delete(subsetId);
|
|
510
|
+
return this.releaseKeys(subset.keys);
|
|
511
|
+
}
|
|
512
|
+
clear() {
|
|
513
|
+
const keys = [...this.keyRefs.keys()];
|
|
514
|
+
this.subsets.clear();
|
|
515
|
+
this.keyRefs.clear();
|
|
516
|
+
return keys;
|
|
517
|
+
}
|
|
518
|
+
addKeys(keys) {
|
|
519
|
+
keys.forEach((key) => {
|
|
520
|
+
this.keyRefs.set(key, (this.keyRefs.get(key) ?? 0) + 1);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
releaseKeys(keys) {
|
|
524
|
+
const removed = [];
|
|
525
|
+
keys.forEach((key) => {
|
|
526
|
+
if (this.releaseKey(key)) removed.push(key);
|
|
527
|
+
});
|
|
528
|
+
return removed;
|
|
529
|
+
}
|
|
530
|
+
releaseKey(key) {
|
|
531
|
+
const refs = (this.keyRefs.get(key) ?? 0) - 1;
|
|
532
|
+
if (refs <= 0) {
|
|
533
|
+
this.keyRefs.delete(key);
|
|
534
|
+
return true;
|
|
319
535
|
}
|
|
320
|
-
|
|
536
|
+
this.keyRefs.set(key, refs);
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
function writeRows(collection, rows, getKey, write) {
|
|
541
|
+
for (const row of rows) {
|
|
542
|
+
const key = getKey(row);
|
|
543
|
+
write({
|
|
544
|
+
type: collection.has(key) ? "update" : "insert",
|
|
545
|
+
key,
|
|
546
|
+
value: row
|
|
547
|
+
});
|
|
548
|
+
}
|
|
321
549
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
550
|
+
function matchesFilters(row, filters) {
|
|
551
|
+
return filters.every((filter) => matchesFilter(row, filter));
|
|
552
|
+
}
|
|
553
|
+
function matchesSubset(row, subset) {
|
|
554
|
+
return matchesFilters(row, subset.filters) && matchesPredicate(row, subset.predicate);
|
|
555
|
+
}
|
|
556
|
+
function matchesPredicate(row, predicate) {
|
|
557
|
+
if (!predicate) return true;
|
|
558
|
+
if (predicate.type === "comparison") return matchesFilter(row, predicate);
|
|
559
|
+
if (predicate.type === "and") return predicate.predicates.every((child) => matchesPredicate(row, child));
|
|
560
|
+
return predicate.predicates.some((child) => matchesPredicate(row, child));
|
|
561
|
+
}
|
|
562
|
+
function matchesFilter(row, filter) {
|
|
563
|
+
const value = valueAtPath(row, filter.field);
|
|
564
|
+
switch (filter.op) {
|
|
565
|
+
case "eq": return value === filter.value;
|
|
566
|
+
case "ne": return value !== filter.value;
|
|
567
|
+
case "gt": return compareValues(value, filter.value, (left, right) => left > right);
|
|
568
|
+
case "gte": return compareValues(value, filter.value, (left, right) => left >= right);
|
|
569
|
+
case "lt": return compareValues(value, filter.value, (left, right) => left < right);
|
|
570
|
+
case "lte": return compareValues(value, filter.value, (left, right) => left <= right);
|
|
571
|
+
case "in": return Array.isArray(filter.value) && filter.value.includes(value);
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
function compareValues(left, right, compare) {
|
|
576
|
+
if (typeof left === "number" && typeof right === "number") return compare(left, right);
|
|
577
|
+
if (typeof left === "string" && typeof right === "string") return compare(left, right);
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
function valueAtPath(row, field) {
|
|
581
|
+
return field.split(".").reduce((value, segment) => {
|
|
582
|
+
if (typeof value !== "object" || value === null) return;
|
|
583
|
+
return value[segment];
|
|
584
|
+
}, row);
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/collection-broadcast.ts
|
|
588
|
+
function applyCollectionBroadcast(options) {
|
|
589
|
+
if ((options.broadcast.invalidations ?? []).length > 0 && options.syncMode === "on-demand") options.deleteKeys(options.subsets.clear());
|
|
590
|
+
const changes = options.broadcast.updates.flatMap((update) => {
|
|
591
|
+
if (collectionScope$1(update.collection) !== collectionScope$1(options.collectionName)) return [];
|
|
592
|
+
const ownUpdate = update.clientId === options.clientId;
|
|
593
|
+
if (options.ignoreOwnUpdates && ownUpdate) return [];
|
|
594
|
+
if (options.syncMode !== "on-demand") return [toChangeMessage(update, options.collection.has(update.key))];
|
|
595
|
+
const key = update.key;
|
|
596
|
+
const exists = options.collection.has(key);
|
|
597
|
+
if (update.type === "delete") {
|
|
598
|
+
options.subsets.deleteKey(key);
|
|
599
|
+
return exists ? [{
|
|
600
|
+
type: "delete",
|
|
601
|
+
key
|
|
602
|
+
}] : [];
|
|
603
|
+
}
|
|
604
|
+
const current = options.collection.get(key);
|
|
605
|
+
const row = update.type === "update" && current ? {
|
|
606
|
+
...current,
|
|
607
|
+
...update.value
|
|
608
|
+
} : update.value;
|
|
609
|
+
const tracked = options.subsets.reconcileRow(row);
|
|
610
|
+
return ownUpdate || exists || tracked.after ? [toChangeMessage(update, exists)] : [];
|
|
611
|
+
});
|
|
612
|
+
if (changes.length === 0) return [];
|
|
613
|
+
options.begin();
|
|
614
|
+
for (const change of changes) options.write(change);
|
|
615
|
+
options.commit();
|
|
616
|
+
return changes;
|
|
326
617
|
}
|
|
327
618
|
//#endregion
|
|
328
619
|
//#region src/read-query.ts
|
|
@@ -472,170 +763,292 @@ function isValExpression(value) {
|
|
|
472
763
|
return isExpression(value) && value.type === "val";
|
|
473
764
|
}
|
|
474
765
|
//#endregion
|
|
475
|
-
//#region src/
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
keys,
|
|
492
|
-
loaded: true,
|
|
493
|
-
...predicate ? { predicate } : {},
|
|
494
|
-
refs
|
|
766
|
+
//#region src/initial-sync.ts
|
|
767
|
+
const DEFAULT_MAX_SYNC_ROWS = 1e4;
|
|
768
|
+
const MAX_READ_ROWS = 1e3;
|
|
769
|
+
async function readInitialSyncRows(client, query, maxSyncRows) {
|
|
770
|
+
if (query.limit !== void 0 || query.cursor) return (await client.read(query)).rows;
|
|
771
|
+
const maximum = maxSyncRows ?? 1e4;
|
|
772
|
+
if (maximum !== false && (!Number.isInteger(maximum) || maximum <= 0)) throw new Error("maxSyncRows must be a positive integer or false");
|
|
773
|
+
const rows = [];
|
|
774
|
+
const baseOffset = query.offset ?? 0;
|
|
775
|
+
while (maximum === false || rows.length <= maximum) {
|
|
776
|
+
const remaining = maximum === false ? MAX_READ_ROWS : maximum + 1 - rows.length;
|
|
777
|
+
const limit = Math.min(MAX_READ_ROWS, remaining);
|
|
778
|
+
const page = await client.read({
|
|
779
|
+
...query,
|
|
780
|
+
limit,
|
|
781
|
+
offset: baseOffset + rows.length
|
|
495
782
|
});
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
783
|
+
rows.push(...page.rows);
|
|
784
|
+
if (page.rows.length < limit) return rows;
|
|
785
|
+
}
|
|
786
|
+
throw new Error(`Collection ${query.collection} exceeds the initial sync limit of ${maximum} rows; use syncMode: "on-demand" or maxSyncRows: false`);
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/sync-session.ts
|
|
790
|
+
var SyncSession = class {
|
|
791
|
+
options;
|
|
792
|
+
buffered = [];
|
|
793
|
+
cursor = 0;
|
|
794
|
+
disconnectCleanup;
|
|
795
|
+
reconnectCleanup;
|
|
796
|
+
queue = Promise.resolve();
|
|
797
|
+
subscription;
|
|
798
|
+
_state = "idle";
|
|
799
|
+
constructor(options) {
|
|
800
|
+
this.options = options;
|
|
801
|
+
}
|
|
802
|
+
get state() {
|
|
803
|
+
return this._state;
|
|
804
|
+
}
|
|
805
|
+
start() {
|
|
806
|
+
if (this._state !== "idle") return this.queue;
|
|
807
|
+
this._state = "subscribing";
|
|
808
|
+
this.subscription = this.options.client.subscribe(this.options.collection, (broadcast) => {
|
|
809
|
+
this.receive(broadcast);
|
|
499
810
|
});
|
|
500
|
-
|
|
811
|
+
this.disconnectCleanup = this.subscription.onDisconnect?.(() => this.disconnected());
|
|
812
|
+
this.reconnectCleanup = this.subscription.onReconnect?.(() => this.reconnected());
|
|
813
|
+
this.queue = this.initialize();
|
|
814
|
+
return this.queue;
|
|
501
815
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
816
|
+
stop() {
|
|
817
|
+
this._state = "stopped";
|
|
818
|
+
this.buffered = [];
|
|
819
|
+
this.disconnectCleanup?.();
|
|
820
|
+
this.reconnectCleanup?.();
|
|
821
|
+
this.subscription?.unsubscribe();
|
|
822
|
+
}
|
|
823
|
+
async initialize() {
|
|
824
|
+
if (this.options.hydrate) {
|
|
825
|
+
await this.options.hydrate();
|
|
826
|
+
if (this.isStopped()) return;
|
|
827
|
+
this.options.hydrated?.();
|
|
507
828
|
}
|
|
508
|
-
this.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
829
|
+
await this.subscription?.ready;
|
|
830
|
+
if (this.isStopped()) return;
|
|
831
|
+
const established = await this.options.client.changes({ collections: {} });
|
|
832
|
+
this._state = "loading";
|
|
833
|
+
await this.options.initialize();
|
|
834
|
+
this.cursor = established.watermark;
|
|
835
|
+
this.flush();
|
|
836
|
+
this.markSynchronized();
|
|
837
|
+
}
|
|
838
|
+
receive(broadcast) {
|
|
839
|
+
if (this._state !== "synchronized") {
|
|
840
|
+
this.buffered.push(broadcast);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
this.applyBroadcast(broadcast);
|
|
515
844
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
845
|
+
disconnected() {
|
|
846
|
+
if (this._state === "stopped") return;
|
|
847
|
+
this._state = "disconnected";
|
|
519
848
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
849
|
+
reconnected() {
|
|
850
|
+
if (this._state === "stopped") return;
|
|
851
|
+
this.queue = this.queue.catch(() => void 0).then(() => this.recover());
|
|
852
|
+
}
|
|
853
|
+
async recover() {
|
|
854
|
+
this._state = "catching-up";
|
|
855
|
+
while (!this.isStopped()) {
|
|
856
|
+
const result = await this.options.client.changes({
|
|
857
|
+
collections: { [this.options.collection]: this.cursor },
|
|
858
|
+
limit: 1e3
|
|
859
|
+
});
|
|
860
|
+
if (result.type === "resyncRequired") {
|
|
861
|
+
this._state = "resyncing";
|
|
862
|
+
await this.options.resync();
|
|
863
|
+
this.cursor = result.watermark;
|
|
864
|
+
break;
|
|
536
865
|
}
|
|
866
|
+
if (result.updates.length > 0) this.applyUpdates(result.updates, result.watermark);
|
|
867
|
+
const nextCursor = result.cursors?.[this.options.collection] ?? visibleCursor(result.updates);
|
|
868
|
+
if (result.hasMore && nextCursor <= this.cursor) throw new Error(`Sync history cursor did not advance for ${this.options.collection}`);
|
|
869
|
+
this.cursor = result.hasMore ? Math.max(this.cursor, nextCursor) : Math.max(this.cursor, result.watermark);
|
|
870
|
+
if (!result.hasMore) break;
|
|
871
|
+
}
|
|
872
|
+
if (this.isStopped()) return;
|
|
873
|
+
this.flush();
|
|
874
|
+
this.markSynchronized();
|
|
875
|
+
}
|
|
876
|
+
flush() {
|
|
877
|
+
const broadcasts = this.buffered.sort((left, right) => left.watermark - right.watermark);
|
|
878
|
+
this.buffered = [];
|
|
879
|
+
for (const broadcast of broadcasts) this.applyBroadcast(broadcast);
|
|
880
|
+
}
|
|
881
|
+
applyBroadcast(broadcast) {
|
|
882
|
+
if (broadcast.watermark <= this.cursor) return;
|
|
883
|
+
const updates = broadcast.updates.filter((update) => update.sequence > this.cursor);
|
|
884
|
+
this.options.apply({
|
|
885
|
+
...broadcast,
|
|
886
|
+
updates
|
|
537
887
|
});
|
|
538
|
-
|
|
539
|
-
before,
|
|
540
|
-
after: this.keyRefs.has(key)
|
|
541
|
-
};
|
|
888
|
+
this.cursor = Math.max(this.cursor, broadcast.watermark);
|
|
542
889
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
890
|
+
applyUpdates(updates, watermark) {
|
|
891
|
+
this.options.apply({
|
|
892
|
+
type: "updates",
|
|
893
|
+
shardId: "history",
|
|
894
|
+
updates,
|
|
895
|
+
watermark
|
|
547
896
|
});
|
|
548
|
-
this.keyRefs.delete(key);
|
|
549
|
-
return true;
|
|
550
897
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
subset.refs -= 1;
|
|
555
|
-
if (subset.refs > 0) return [];
|
|
556
|
-
subset.refs = 0;
|
|
557
|
-
if (retain) return [];
|
|
558
|
-
return this.expire(subsetId);
|
|
898
|
+
markSynchronized() {
|
|
899
|
+
this._state = "synchronized";
|
|
900
|
+
this.options.synchronized();
|
|
559
901
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (!subset || subset.refs > 0) return [];
|
|
563
|
-
this.subsets.delete(subsetId);
|
|
564
|
-
return this.releaseKeys(subset.keys);
|
|
902
|
+
isStopped() {
|
|
903
|
+
return this._state === "stopped";
|
|
565
904
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
905
|
+
};
|
|
906
|
+
function visibleCursor(updates) {
|
|
907
|
+
return updates.reduce((cursor, update) => Math.max(cursor, update.sequence), 0);
|
|
908
|
+
}
|
|
909
|
+
//#endregion
|
|
910
|
+
//#region src/offline-cache.ts
|
|
911
|
+
const databaseVersion = 1;
|
|
912
|
+
const rowStore = "collectionRows";
|
|
913
|
+
const collectionIndex = "byCollection";
|
|
914
|
+
function createOfflineCache(option, id, getKey) {
|
|
915
|
+
if (!option) return void 0;
|
|
916
|
+
if (!id) throw new Error("collectionOptions requires a stable id when offline is enabled");
|
|
917
|
+
if (!globalThis.indexedDB) throw new Error("IndexedDB is unavailable; offline collections must be created in a browser");
|
|
918
|
+
const options = option === true ? {} : option;
|
|
919
|
+
const collectionId = `${id}\0${options.cacheVersion ?? 1}`;
|
|
920
|
+
return new IndexedDBOfflineCache(globalThis.indexedDB, options.databaseName ?? "lsync", collectionId, getKey);
|
|
921
|
+
}
|
|
922
|
+
var IndexedDBOfflineCache = class {
|
|
923
|
+
collectionId;
|
|
924
|
+
getKey;
|
|
925
|
+
database;
|
|
926
|
+
queue = Promise.resolve();
|
|
927
|
+
constructor(factory, databaseName, collectionId, getKey) {
|
|
928
|
+
this.collectionId = collectionId;
|
|
929
|
+
this.getKey = getKey;
|
|
930
|
+
this.database = openDatabase(factory, databaseName);
|
|
571
931
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
932
|
+
async load() {
|
|
933
|
+
await this.queue;
|
|
934
|
+
return (await requestResult((await this.database).transaction(rowStore, "readonly").objectStore(rowStore).index(collectionIndex).getAll(this.collectionId))).map((record) => record.value);
|
|
935
|
+
}
|
|
936
|
+
put(rows) {
|
|
937
|
+
if (rows.length === 0) return this.queue;
|
|
938
|
+
return this.enqueue(async () => {
|
|
939
|
+
const transaction = (await this.database).transaction(rowStore, "readwrite");
|
|
940
|
+
const store = transaction.objectStore(rowStore);
|
|
941
|
+
for (const row of rows) store.put(this.record(row));
|
|
942
|
+
await transactionComplete(transaction);
|
|
575
943
|
});
|
|
576
944
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
945
|
+
delete(keys) {
|
|
946
|
+
if (keys.length === 0) return this.queue;
|
|
947
|
+
return this.enqueue(async () => {
|
|
948
|
+
const transaction = (await this.database).transaction(rowStore, "readwrite");
|
|
949
|
+
const store = transaction.objectStore(rowStore);
|
|
950
|
+
for (const key of keys) store.delete([this.collectionId, key]);
|
|
951
|
+
await transactionComplete(transaction);
|
|
581
952
|
});
|
|
582
|
-
return removed;
|
|
583
953
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
954
|
+
replace(rows) {
|
|
955
|
+
return this.enqueue(async () => {
|
|
956
|
+
const transaction = (await this.database).transaction(rowStore, "readwrite");
|
|
957
|
+
const store = transaction.objectStore(rowStore);
|
|
958
|
+
await deleteCollectionRows(store.index(collectionIndex), this.collectionId);
|
|
959
|
+
for (const row of rows) store.put(this.record(row));
|
|
960
|
+
await transactionComplete(transaction);
|
|
961
|
+
});
|
|
592
962
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
write({
|
|
598
|
-
type: collection.has(key) ? "update" : "insert",
|
|
599
|
-
key,
|
|
963
|
+
record(row) {
|
|
964
|
+
return {
|
|
965
|
+
collectionId: this.collectionId,
|
|
966
|
+
key: this.getKey(row),
|
|
600
967
|
value: row
|
|
601
|
-
}
|
|
968
|
+
};
|
|
602
969
|
}
|
|
970
|
+
enqueue(operation) {
|
|
971
|
+
const result = this.queue.then(operation);
|
|
972
|
+
this.queue = result.catch(() => void 0);
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
function openDatabase(factory, name) {
|
|
977
|
+
return new Promise((resolve, reject) => {
|
|
978
|
+
const request = factory.open(name, databaseVersion);
|
|
979
|
+
request.onupgradeneeded = () => {
|
|
980
|
+
request.result.createObjectStore(rowStore, { keyPath: ["collectionId", "key"] }).createIndex(collectionIndex, "collectionId");
|
|
981
|
+
};
|
|
982
|
+
request.onsuccess = () => resolve(request.result);
|
|
983
|
+
request.onerror = () => reject(request.error);
|
|
984
|
+
request.onblocked = () => reject(/* @__PURE__ */ new Error(`IndexedDB database ${name} is blocked`));
|
|
985
|
+
});
|
|
603
986
|
}
|
|
604
|
-
function
|
|
605
|
-
return
|
|
987
|
+
function deleteCollectionRows(index, id) {
|
|
988
|
+
return new Promise((resolve, reject) => {
|
|
989
|
+
const request = index.openKeyCursor(id);
|
|
990
|
+
request.onsuccess = () => {
|
|
991
|
+
const cursor = request.result;
|
|
992
|
+
if (!cursor) {
|
|
993
|
+
resolve();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
index.objectStore.delete(cursor.primaryKey);
|
|
997
|
+
cursor.continue();
|
|
998
|
+
};
|
|
999
|
+
request.onerror = () => reject(request.error);
|
|
1000
|
+
});
|
|
606
1001
|
}
|
|
607
|
-
function
|
|
608
|
-
return
|
|
1002
|
+
function requestResult(request) {
|
|
1003
|
+
return new Promise((resolve, reject) => {
|
|
1004
|
+
request.onsuccess = () => resolve(request.result);
|
|
1005
|
+
request.onerror = () => reject(request.error);
|
|
1006
|
+
});
|
|
609
1007
|
}
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1008
|
+
function transactionComplete(transaction) {
|
|
1009
|
+
return new Promise((resolve, reject) => {
|
|
1010
|
+
transaction.oncomplete = () => resolve();
|
|
1011
|
+
transaction.onerror = () => reject(transaction.error);
|
|
1012
|
+
transaction.onabort = () => reject(transaction.error);
|
|
1013
|
+
});
|
|
615
1014
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region src/offline-sync.ts
|
|
1017
|
+
function persistChanges(offline, changes, collection, getKey) {
|
|
1018
|
+
if (!offline) return;
|
|
1019
|
+
const rows = [];
|
|
1020
|
+
const deletedKeys = [];
|
|
1021
|
+
for (const change of changes) {
|
|
1022
|
+
if (change.type === "delete") {
|
|
1023
|
+
deletedKeys.push(change.key);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const row = collection.get(getKey(change.value));
|
|
1027
|
+
if (row) rows.push(row);
|
|
626
1028
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
1029
|
+
offline.put(rows);
|
|
1030
|
+
offline.delete(deletedKeys);
|
|
1031
|
+
}
|
|
1032
|
+
function persistTransaction(offline, transaction) {
|
|
1033
|
+
if (!offline) return;
|
|
1034
|
+
const rows = [];
|
|
1035
|
+
const deletedKeys = [];
|
|
1036
|
+
for (const mutation of transaction.mutations) if (mutation.type === "delete") deletedKeys.push(mutation.key);
|
|
1037
|
+
else rows.push(mutation.modified);
|
|
1038
|
+
offline.put(rows);
|
|
1039
|
+
offline.delete(deletedKeys);
|
|
633
1040
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/local-transaction.ts
|
|
1043
|
+
function trackLocalTransaction(subsets, syncMode, transaction) {
|
|
1044
|
+
if (syncMode !== "on-demand" || !subsets) return;
|
|
1045
|
+
for (const mutation of transaction.mutations) {
|
|
1046
|
+
if (mutation.type === "delete") {
|
|
1047
|
+
subsets.deleteKey(mutation.key);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
subsets.reconcileRow(mutation.modified);
|
|
1051
|
+
}
|
|
639
1052
|
}
|
|
640
1053
|
//#endregion
|
|
641
1054
|
//#region src/collection.ts
|
|
@@ -643,6 +1056,9 @@ function collectionOptions(options) {
|
|
|
643
1056
|
let lease;
|
|
644
1057
|
let client = options.client;
|
|
645
1058
|
let activeSubsets;
|
|
1059
|
+
if (options.offline && options.syncMode === "on-demand") throw new Error("IndexedDB offline caching currently requires eager sync mode");
|
|
1060
|
+
if (options.offline && options.read === false) throw new Error("IndexedDB offline caching requires an initial collection read");
|
|
1061
|
+
const offline = createOfflineCache(options.offline, options.id, options.getKey);
|
|
646
1062
|
if (!client) {
|
|
647
1063
|
if (!options.url) throw new Error("collectionOptions requires either client or url");
|
|
648
1064
|
lease = acquireSharedClient({
|
|
@@ -661,11 +1077,12 @@ function collectionOptions(options) {
|
|
|
661
1077
|
...options.defaultIndexType !== void 0 ? { defaultIndexType: options.defaultIndexType } : {},
|
|
662
1078
|
getKey: options.getKey,
|
|
663
1079
|
sync: {
|
|
664
|
-
sync: ({ begin, write, commit, markReady, collection }) => {
|
|
1080
|
+
sync: ({ begin, write, commit, markReady, truncate, collection }) => {
|
|
665
1081
|
const subsets = new SubsetTracker(options.getKey);
|
|
666
1082
|
activeSubsets = subsets;
|
|
667
1083
|
const subsetGcTime = options.gcTime ?? 3e5;
|
|
668
1084
|
const retentionTimers = /* @__PURE__ */ new Map();
|
|
1085
|
+
const subsetQueries = /* @__PURE__ */ new Map();
|
|
669
1086
|
const deleteKeys = (keys) => {
|
|
670
1087
|
if (keys.length === 0) return;
|
|
671
1088
|
begin();
|
|
@@ -674,6 +1091,7 @@ function collectionOptions(options) {
|
|
|
674
1091
|
key
|
|
675
1092
|
});
|
|
676
1093
|
commit();
|
|
1094
|
+
offline?.delete(keys);
|
|
677
1095
|
};
|
|
678
1096
|
const cancelRetention = (id) => {
|
|
679
1097
|
const timer = retentionTimers.get(id);
|
|
@@ -690,47 +1108,86 @@ function collectionOptions(options) {
|
|
|
690
1108
|
cancelRetention(id);
|
|
691
1109
|
retentionTimers.set(id, setTimeout(() => {
|
|
692
1110
|
retentionTimers.delete(id);
|
|
1111
|
+
subsetQueries.delete(id);
|
|
693
1112
|
deleteKeys(subsets.expire(id));
|
|
694
1113
|
}, subsetGcTime));
|
|
695
1114
|
};
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1115
|
+
const initialQuery = initialReadQuery(options.collection, options.read);
|
|
1116
|
+
const loadEager = async (replace = false) => {
|
|
1117
|
+
if (!initialQuery) return;
|
|
1118
|
+
const rows = await readInitialSyncRows(client, initialQuery, options.maxSyncRows);
|
|
1119
|
+
if (replace) truncate();
|
|
1120
|
+
if (rows.length > 0) {
|
|
1121
|
+
begin();
|
|
1122
|
+
writeRows(collection, rows, options.getKey, write);
|
|
1123
|
+
commit();
|
|
1124
|
+
}
|
|
1125
|
+
if (replace) await offline?.replace(rows);
|
|
1126
|
+
else await offline?.put(rows);
|
|
1127
|
+
};
|
|
1128
|
+
const reloadSubsets = async () => {
|
|
1129
|
+
for (const [id, query] of subsetQueries) {
|
|
1130
|
+
const result = await client.read(query);
|
|
1131
|
+
const removedKeys = subsets.replace(id, result.rows, query.filters ?? [], query.predicate);
|
|
1132
|
+
begin();
|
|
1133
|
+
writeRows(collection, result.rows, options.getKey, write);
|
|
1134
|
+
for (const key of removedKeys) write({
|
|
1135
|
+
type: "delete",
|
|
1136
|
+
key
|
|
1137
|
+
});
|
|
1138
|
+
commit();
|
|
1139
|
+
await offline?.put(result.rows);
|
|
1140
|
+
await offline?.delete(removedKeys);
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
let markedReady = false;
|
|
1144
|
+
const markCollectionReady = () => {
|
|
1145
|
+
if (!markedReady) markReady();
|
|
1146
|
+
markedReady = true;
|
|
1147
|
+
};
|
|
1148
|
+
const session = new SyncSession({
|
|
1149
|
+
client,
|
|
1150
|
+
collection: options.collection,
|
|
1151
|
+
initialize: () => options.syncMode === "on-demand" ? Promise.resolve() : loadEager(offline !== void 0),
|
|
1152
|
+
resync: () => options.syncMode === "on-demand" ? reloadSubsets() : loadEager(true),
|
|
1153
|
+
apply: (broadcast) => {
|
|
1154
|
+
const changes = applyCollectionBroadcast({
|
|
1155
|
+
broadcast,
|
|
1156
|
+
collectionName: options.collection,
|
|
1157
|
+
clientId: client.clientId,
|
|
1158
|
+
ignoreOwnUpdates: options.ignoreOwnUpdates ?? false,
|
|
1159
|
+
syncMode: options.syncMode === "on-demand" ? "on-demand" : "eager",
|
|
1160
|
+
collection,
|
|
1161
|
+
subsets,
|
|
1162
|
+
begin,
|
|
1163
|
+
write,
|
|
1164
|
+
commit,
|
|
1165
|
+
deleteKeys
|
|
1166
|
+
});
|
|
1167
|
+
persistChanges(offline, changes, collection, options.getKey);
|
|
1168
|
+
},
|
|
1169
|
+
...offline ? {
|
|
1170
|
+
hydrate: async () => {
|
|
1171
|
+
const rows = await offline.load();
|
|
1172
|
+
if (rows.length === 0) return;
|
|
1173
|
+
begin();
|
|
1174
|
+
writeRows(collection, rows, options.getKey, write);
|
|
1175
|
+
commit();
|
|
1176
|
+
},
|
|
1177
|
+
hydrated: markCollectionReady
|
|
1178
|
+
} : {},
|
|
1179
|
+
synchronized: markCollectionReady
|
|
725
1180
|
});
|
|
1181
|
+
session.start();
|
|
726
1182
|
const readSubset = (subsetOptions) => {
|
|
727
1183
|
const id = subsetId(subsetOptions);
|
|
728
1184
|
cancelRetention(id);
|
|
729
1185
|
if (subsets.retain(id).loaded) return true;
|
|
730
1186
|
return (async () => {
|
|
731
1187
|
const query = readQueryForSubset(options.collection, options.read === false ? void 0 : options.read, subsetOptions);
|
|
732
|
-
await
|
|
1188
|
+
await session.start();
|
|
733
1189
|
const result = await client.read(query);
|
|
1190
|
+
subsetQueries.set(id, query);
|
|
734
1191
|
const removedKeys = subsets.replace(id, result.rows, query.filters ?? [], query.predicate);
|
|
735
1192
|
begin();
|
|
736
1193
|
writeRows(collection, result.rows, options.getKey, write);
|
|
@@ -744,72 +1201,55 @@ function collectionOptions(options) {
|
|
|
744
1201
|
throw error;
|
|
745
1202
|
});
|
|
746
1203
|
};
|
|
747
|
-
if (options.syncMode === "on-demand") {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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();
|
|
1204
|
+
if (options.syncMode === "on-demand") return {
|
|
1205
|
+
loadSubset: readSubset,
|
|
1206
|
+
unloadSubset: (subsetOptions) => {
|
|
1207
|
+
const id = subsetId(subsetOptions);
|
|
1208
|
+
const removedKeys = subsets.release(id, subsetGcTime !== 0);
|
|
1209
|
+
deleteKeys(removedKeys);
|
|
1210
|
+
if (removedKeys.length === 0) scheduleRetention(id);
|
|
1211
|
+
else subsetQueries.delete(id);
|
|
1212
|
+
},
|
|
1213
|
+
cleanup: () => {
|
|
1214
|
+
retentionTimers.forEach((timer) => {
|
|
1215
|
+
clearTimeout(timer);
|
|
1216
|
+
});
|
|
1217
|
+
retentionTimers.clear();
|
|
1218
|
+
subsetQueries.clear();
|
|
1219
|
+
subsets.clear();
|
|
1220
|
+
if (activeSubsets === subsets) activeSubsets = void 0;
|
|
1221
|
+
session.stop();
|
|
1222
|
+
lease?.[Symbol.dispose]();
|
|
778
1223
|
}
|
|
779
|
-
|
|
780
|
-
});
|
|
1224
|
+
};
|
|
781
1225
|
return () => {
|
|
782
1226
|
if (activeSubsets === subsets) activeSubsets = void 0;
|
|
783
|
-
|
|
784
|
-
lease?.
|
|
1227
|
+
session.stop();
|
|
1228
|
+
lease?.[Symbol.dispose]();
|
|
785
1229
|
};
|
|
786
1230
|
},
|
|
787
1231
|
rowUpdateMode: "partial"
|
|
788
1232
|
},
|
|
789
1233
|
onInsert: async ({ transaction }) => {
|
|
790
1234
|
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
791
|
-
|
|
1235
|
+
const result = await client.push(createBatch(options.collection, client.clientId, transaction));
|
|
1236
|
+
persistTransaction(offline, transaction);
|
|
1237
|
+
return result;
|
|
792
1238
|
},
|
|
793
1239
|
onUpdate: async ({ transaction }) => {
|
|
794
1240
|
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
795
|
-
|
|
1241
|
+
const result = await client.push(createBatch(options.collection, client.clientId, transaction));
|
|
1242
|
+
persistTransaction(offline, transaction);
|
|
1243
|
+
return result;
|
|
796
1244
|
},
|
|
797
1245
|
onDelete: async ({ transaction }) => {
|
|
798
1246
|
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
799
|
-
|
|
1247
|
+
const result = await client.push(createBatch(options.collection, client.clientId, transaction));
|
|
1248
|
+
persistTransaction(offline, transaction);
|
|
1249
|
+
return result;
|
|
800
1250
|
}
|
|
801
1251
|
};
|
|
802
1252
|
}
|
|
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
1253
|
//#endregion
|
|
814
1254
|
//#region src/collection-path.ts
|
|
815
1255
|
function collectionScope(path, params) {
|
|
@@ -851,12 +1291,53 @@ function defaultCollectionApiPath(path, name) {
|
|
|
851
1291
|
const prefix = collectionPathSegments(path).filter((segment) => !isCollectionPathParam(segment)).join(".");
|
|
852
1292
|
return prefix ? `${prefix}.${name}` : name;
|
|
853
1293
|
}
|
|
1294
|
+
var CollectionCache = class extends Map {
|
|
1295
|
+
maxSize;
|
|
1296
|
+
constructor(maxSize) {
|
|
1297
|
+
super();
|
|
1298
|
+
this.maxSize = normalizeMaxSize(maxSize);
|
|
1299
|
+
}
|
|
1300
|
+
touch(key, entry) {
|
|
1301
|
+
this.delete(key);
|
|
1302
|
+
this.set(key, entry);
|
|
1303
|
+
}
|
|
1304
|
+
evictInactive(protectedKey) {
|
|
1305
|
+
while (this.size > this.maxSize) {
|
|
1306
|
+
const candidate = [...this.entries()].find(([key, entry]) => key !== protectedKey && entry.collection.subscriberCount === 0 && entry.collection.status !== "loading");
|
|
1307
|
+
if (!candidate) return;
|
|
1308
|
+
this.delete(candidate[0]);
|
|
1309
|
+
cleanupCollection(candidate[1].collection).catch(() => {});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async dispose() {
|
|
1313
|
+
const entries = [...this.values()];
|
|
1314
|
+
this.clear();
|
|
1315
|
+
await Promise.all(entries.map(({ collection }) => cleanupCollection(collection)));
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
async function cleanupCollection(collection) {
|
|
1319
|
+
if (collection.status === "loading") try {
|
|
1320
|
+
await collection.preload();
|
|
1321
|
+
} catch {}
|
|
1322
|
+
if (collection.status !== "cleaned-up") await collection.cleanup();
|
|
1323
|
+
}
|
|
1324
|
+
function normalizeMaxSize(maxSize) {
|
|
1325
|
+
if (maxSize === void 0) return 32;
|
|
1326
|
+
if (!Number.isInteger(maxSize) || maxSize < 1) throw new Error("maxCachedCollections must be a positive integer");
|
|
1327
|
+
return maxSize;
|
|
1328
|
+
}
|
|
1329
|
+
//#endregion
|
|
1330
|
+
//#region src/collection-type-config.ts
|
|
1331
|
+
function collectionConfig(options) {
|
|
1332
|
+
const { api, children, id, maxCachedCollections, name, parentParam, path, ...config } = options;
|
|
1333
|
+
return config;
|
|
1334
|
+
}
|
|
854
1335
|
//#endregion
|
|
855
1336
|
//#region src/collection-type.ts
|
|
856
1337
|
function createCollectionTypeFromOptions(options) {
|
|
857
1338
|
const connection = connectionFrom(options);
|
|
858
1339
|
if (!connection) throw new Error("CollectionTypes.builder requires either client or url");
|
|
859
|
-
return new CollectionTypeManager(options, connection, {},
|
|
1340
|
+
return new CollectionTypeManager(options, connection, {}, new CollectionCache(options.maxCachedCollections)).api();
|
|
860
1341
|
}
|
|
861
1342
|
var CollectionTypeManager = class CollectionTypeManager {
|
|
862
1343
|
options;
|
|
@@ -873,6 +1354,7 @@ var CollectionTypeManager = class CollectionTypeManager {
|
|
|
873
1354
|
return withChildren(withApiMethods({
|
|
874
1355
|
all: (params = {}) => this.all(params),
|
|
875
1356
|
delete: (...args) => this.all({}).delete(...args),
|
|
1357
|
+
dispose: () => this.dispose(),
|
|
876
1358
|
insert: (...args) => this.all({}).insert(...args),
|
|
877
1359
|
with: (params) => this.with(params),
|
|
878
1360
|
withId: (id, childParam) => this.withId(id, childParam),
|
|
@@ -890,6 +1372,8 @@ var CollectionTypeManager = class CollectionTypeManager {
|
|
|
890
1372
|
if (existing) {
|
|
891
1373
|
existing.usage.accessCount += 1;
|
|
892
1374
|
existing.usage.lastAccessedAt = Date.now();
|
|
1375
|
+
this.cache.touch(key, existing);
|
|
1376
|
+
this.cache.evictInactive(key);
|
|
893
1377
|
return existing.collection;
|
|
894
1378
|
}
|
|
895
1379
|
const id = collectionId(this.options.id, this.options.path, scope);
|
|
@@ -911,6 +1395,7 @@ var CollectionTypeManager = class CollectionTypeManager {
|
|
|
911
1395
|
collection,
|
|
912
1396
|
usage
|
|
913
1397
|
});
|
|
1398
|
+
this.cache.evictInactive(key);
|
|
914
1399
|
return collection;
|
|
915
1400
|
}
|
|
916
1401
|
with(params) {
|
|
@@ -930,6 +1415,9 @@ var CollectionTypeManager = class CollectionTypeManager {
|
|
|
930
1415
|
usage() {
|
|
931
1416
|
return [...this.cache.values()].map((entry) => ({ ...entry.usage }));
|
|
932
1417
|
}
|
|
1418
|
+
async dispose() {
|
|
1419
|
+
await this.cache.dispose();
|
|
1420
|
+
}
|
|
933
1421
|
apiMethods() {
|
|
934
1422
|
return Object.fromEntries(Object.entries(this.options.api ?? {}).map(([name, method]) => [name, (...args) => this.callApi(name, method, args[0])]));
|
|
935
1423
|
}
|
|
@@ -949,7 +1437,7 @@ var CollectionTypeManager = class CollectionTypeManager {
|
|
|
949
1437
|
try {
|
|
950
1438
|
return await client.call(path, input);
|
|
951
1439
|
} finally {
|
|
952
|
-
lease?.
|
|
1440
|
+
lease?.[Symbol.dispose]();
|
|
953
1441
|
}
|
|
954
1442
|
}
|
|
955
1443
|
childManagers(params) {
|
|
@@ -964,10 +1452,6 @@ var CollectionTypeManager = class CollectionTypeManager {
|
|
|
964
1452
|
} : this.params;
|
|
965
1453
|
}
|
|
966
1454
|
};
|
|
967
|
-
function collectionConfig(options) {
|
|
968
|
-
const { api, children, id, name, parentParam, path, ...config } = options;
|
|
969
|
-
return config;
|
|
970
|
-
}
|
|
971
1455
|
function withChildren(target, children) {
|
|
972
1456
|
return Object.assign(target, children);
|
|
973
1457
|
}
|
|
@@ -1038,6 +1522,24 @@ var ClientCollectionBuilder = class ClientCollectionBuilder {
|
|
|
1038
1522
|
syncMode
|
|
1039
1523
|
});
|
|
1040
1524
|
}
|
|
1525
|
+
maxSyncRows(maxSyncRows) {
|
|
1526
|
+
return new ClientCollectionBuilder({
|
|
1527
|
+
...this.override,
|
|
1528
|
+
maxSyncRows
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
offline(options = true) {
|
|
1532
|
+
return new ClientCollectionBuilder({
|
|
1533
|
+
...this.override,
|
|
1534
|
+
offline: options
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
maxCachedCollections(maxCachedCollections) {
|
|
1538
|
+
return new ClientCollectionBuilder({
|
|
1539
|
+
...this.override,
|
|
1540
|
+
maxCachedCollections
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1041
1543
|
index(autoIndex, defaultIndexType) {
|
|
1042
1544
|
return new ClientCollectionBuilder({
|
|
1043
1545
|
...this.override,
|
|
@@ -1067,140 +1569,6 @@ function collectionOptionsFromDefinition(definition, dotPath, overrides) {
|
|
|
1067
1569
|
};
|
|
1068
1570
|
}
|
|
1069
1571
|
//#endregion
|
|
1070
|
-
|
|
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 };
|
|
1572
|
+
export { DEFAULT_MAX_SYNC_ROWS, collectionOptions, collectionTypesFrom, createBatch, createClient, toChangeMessage };
|
|
1205
1573
|
|
|
1206
1574
|
//# sourceMappingURL=index.mjs.map
|