@lsync/client 0.0.2 → 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 +50 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +890 -388
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -5
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
+
import { ProtocolError, parseServerMessage, sendClientMessage } from "@lsync/transport";
|
|
1
2
|
import { createCollection } from "@tanstack/db";
|
|
2
|
-
import { createTRPCProxyClient } from "@trpc/client";
|
|
3
|
-
import { parseServerMessage, sendClientRpcRequest } from "@lsync/transport";
|
|
4
|
-
import { observable } from "@trpc/server/observable";
|
|
5
3
|
//#region src/batch.ts
|
|
6
4
|
function createBatch(collection, clientId, transaction) {
|
|
7
5
|
return { updates: transaction.mutations.map((mutation) => toUpdate(collection, clientId, mutation)) };
|
|
@@ -44,42 +42,14 @@ function toChangeMessage(update, existing) {
|
|
|
44
42
|
};
|
|
45
43
|
}
|
|
46
44
|
//#endregion
|
|
47
|
-
//#region src/collection-path.ts
|
|
48
|
-
function collectionScope$1(path, params) {
|
|
49
|
-
return `/${collectionPathSegments(path).map((segment) => segmentValue(segment, params)).map(normalizeSegment$1).join("/")}/`;
|
|
50
|
-
}
|
|
51
|
-
function firstNewPathParam(childPath, parentPath) {
|
|
52
|
-
if (!childPath) return void 0;
|
|
53
|
-
const parentParams = new Set(pathParams(parentPath));
|
|
54
|
-
return pathParams(childPath).find((param) => !parentParams.has(param));
|
|
55
|
-
}
|
|
56
|
-
function collectionPathSegments(path) {
|
|
57
|
-
return path.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
58
|
-
}
|
|
59
|
-
function collectionPathParamName(segment) {
|
|
60
|
-
return segment.match(/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/)?.[1] ?? segment.match(/^:([A-Za-z_][A-Za-z0-9_]*)$/)?.[1];
|
|
61
|
-
}
|
|
62
|
-
function isCollectionPathParam(segment) {
|
|
63
|
-
return collectionPathParamName(segment) !== void 0;
|
|
64
|
-
}
|
|
65
|
-
function segmentValue(segment, params) {
|
|
66
|
-
const param = collectionPathParamName(segment);
|
|
67
|
-
if (!param) return segment;
|
|
68
|
-
const value = params[param];
|
|
69
|
-
if (value === void 0) throw new Error(`Missing collection path parameter: ${param}`);
|
|
70
|
-
return String(value);
|
|
71
|
-
}
|
|
72
|
-
function pathParams(path) {
|
|
73
|
-
return collectionPathSegments(path).flatMap((segment) => {
|
|
74
|
-
const param = collectionPathParamName(segment);
|
|
75
|
-
return param ? [param] : [];
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
function normalizeSegment$1(segment) {
|
|
79
|
-
return encodeURIComponent(decodeURIComponent(segment));
|
|
80
|
-
}
|
|
81
|
-
//#endregion
|
|
82
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
|
+
}
|
|
83
53
|
function takePending(pending, id) {
|
|
84
54
|
if (id === null || id === void 0) return;
|
|
85
55
|
const key = String(id);
|
|
@@ -87,45 +57,10 @@ function takePending(pending, id) {
|
|
|
87
57
|
pending.delete(key);
|
|
88
58
|
return request;
|
|
89
59
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
id,
|
|
93
|
-
method: "mutation",
|
|
94
|
-
params: {
|
|
95
|
-
path: "push",
|
|
96
|
-
input: { json: op.input }
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
if (op.type === "query" && op.path === "read") return {
|
|
100
|
-
id,
|
|
101
|
-
method: "query",
|
|
102
|
-
params: {
|
|
103
|
-
path: "read",
|
|
104
|
-
input: { json: op.input }
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
if (op.type === "query" && op.path === "changes") return {
|
|
108
|
-
id,
|
|
109
|
-
method: "query",
|
|
110
|
-
params: {
|
|
111
|
-
path: "changes",
|
|
112
|
-
input: { json: op.input }
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
if (op.type === "mutation" && op.path === "api") return {
|
|
116
|
-
id,
|
|
117
|
-
method: "mutation",
|
|
118
|
-
params: {
|
|
119
|
-
path: "api",
|
|
120
|
-
input: { json: op.input }
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
throw new Error(`Unsupported operation: ${op.type}.${op.path}`);
|
|
124
|
-
}
|
|
125
|
-
function collectionScope(collection) {
|
|
126
|
-
return `/${collection.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).map(normalizeSegment).join("/")}/`;
|
|
60
|
+
function collectionScope$1(collection) {
|
|
61
|
+
return `/${collection.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).map(normalizeSegment$1).join("/")}/`;
|
|
127
62
|
}
|
|
128
|
-
function normalizeSegment(segment) {
|
|
63
|
+
function normalizeSegment$1(segment) {
|
|
129
64
|
return encodeURIComponent(decodeURIComponent(segment));
|
|
130
65
|
}
|
|
131
66
|
//#endregion
|
|
@@ -137,13 +72,15 @@ var ClientSubscriptions = class {
|
|
|
137
72
|
this.controls = controls;
|
|
138
73
|
}
|
|
139
74
|
subscribe(collection, listener) {
|
|
140
|
-
const key = collectionScope(collection);
|
|
75
|
+
const key = collectionScope$1(collection);
|
|
141
76
|
let entry = this.subscriptions.get(key);
|
|
142
77
|
const shouldSubscribe = !entry;
|
|
143
78
|
if (!entry) {
|
|
144
79
|
entry = {
|
|
145
80
|
collection: key,
|
|
81
|
+
disconnectListeners: /* @__PURE__ */ new Set(),
|
|
146
82
|
listeners: /* @__PURE__ */ new Set(),
|
|
83
|
+
reconnectListeners: /* @__PURE__ */ new Set(),
|
|
147
84
|
ready: Promise.resolve()
|
|
148
85
|
};
|
|
149
86
|
this.subscriptions.set(key, entry);
|
|
@@ -152,9 +89,20 @@ var ClientSubscriptions = class {
|
|
|
152
89
|
if (shouldSubscribe) entry.ready = this.start(entry);
|
|
153
90
|
return {
|
|
154
91
|
ready: entry.ready,
|
|
92
|
+
onDisconnect: (next) => addLifecycleListener(entry.disconnectListeners, next),
|
|
93
|
+
onReconnect: (next) => addLifecycleListener(entry.reconnectListeners, next),
|
|
155
94
|
unsubscribe: () => this.unsubscribe(key, listener)
|
|
156
95
|
};
|
|
157
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
|
+
}
|
|
158
106
|
async replay(ws) {
|
|
159
107
|
await Promise.all([...this.subscriptions.values()].map((entry) => this.subscribeOnSocket(ws, entry)));
|
|
160
108
|
}
|
|
@@ -162,20 +110,20 @@ var ClientSubscriptions = class {
|
|
|
162
110
|
const updatesByCollection = /* @__PURE__ */ new Map();
|
|
163
111
|
const invalidationsByCollection = /* @__PURE__ */ new Map();
|
|
164
112
|
for (const update of broadcast.updates) {
|
|
165
|
-
const collection = collectionScope(update.collection);
|
|
113
|
+
const collection = collectionScope$1(update.collection);
|
|
166
114
|
const updates = updatesByCollection.get(collection) ?? [];
|
|
167
115
|
updates.push(update);
|
|
168
116
|
updatesByCollection.set(collection, updates);
|
|
169
117
|
}
|
|
170
118
|
for (const invalidation of broadcast.invalidations ?? []) {
|
|
171
|
-
const collection = collectionScope(invalidation.collection);
|
|
119
|
+
const collection = collectionScope$1(invalidation.collection);
|
|
172
120
|
const invalidations = invalidationsByCollection.get(collection) ?? [];
|
|
173
121
|
invalidations.push(invalidation);
|
|
174
122
|
invalidationsByCollection.set(collection, invalidations);
|
|
175
123
|
}
|
|
176
124
|
for (const entry of this.subscriptions.values()) {
|
|
177
|
-
const updates = updatesByCollection.get(collectionScope(entry.collection));
|
|
178
|
-
const invalidations = invalidationsByCollection.get(collectionScope(entry.collection));
|
|
125
|
+
const updates = updatesByCollection.get(collectionScope$1(entry.collection));
|
|
126
|
+
const invalidations = invalidationsByCollection.get(collectionScope$1(entry.collection));
|
|
179
127
|
if ((!updates || updates.length === 0) && (!invalidations || invalidations.length === 0)) continue;
|
|
180
128
|
const scopedBroadcast = {
|
|
181
129
|
...broadcast,
|
|
@@ -190,7 +138,7 @@ var ClientSubscriptions = class {
|
|
|
190
138
|
return socket?.readyState === WebSocket.OPEN ? this.subscribeOnSocket(socket, entry) : this.controls.open().then(() => void 0);
|
|
191
139
|
}
|
|
192
140
|
async subscribeOnSocket(ws, entry) {
|
|
193
|
-
if (this.subscriptions.get(collectionScope(entry.collection)) !== entry) return;
|
|
141
|
+
if (this.subscriptions.get(collectionScope$1(entry.collection)) !== entry) return;
|
|
194
142
|
entry.collection = (await this.controls.send(ws, "subscribe", entry.collection)).collection;
|
|
195
143
|
}
|
|
196
144
|
unsubscribe(key, listener) {
|
|
@@ -200,164 +148,472 @@ var ClientSubscriptions = class {
|
|
|
200
148
|
if (entry.listeners.size > 0) return;
|
|
201
149
|
this.subscriptions.delete(key);
|
|
202
150
|
const socket = this.controls.currentSocket();
|
|
203
|
-
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);
|
|
204
152
|
}
|
|
205
153
|
};
|
|
154
|
+
function addLifecycleListener(listeners, listener) {
|
|
155
|
+
listeners.add(listener);
|
|
156
|
+
return () => listeners.delete(listener);
|
|
157
|
+
}
|
|
206
158
|
//#endregion
|
|
207
|
-
//#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
|
|
208
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
|
|
209
237
|
function createClient(options) {
|
|
210
238
|
const clientId = options.clientId ?? crypto.randomUUID();
|
|
211
239
|
const pending = /* @__PURE__ */ new Map();
|
|
240
|
+
const reconnect = reconnectPolicy(options.reconnect);
|
|
241
|
+
const connectionTimeoutMs = options.connectionTimeoutMs ?? 1e4;
|
|
242
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
212
243
|
let nextId = 1;
|
|
213
244
|
let socket;
|
|
214
245
|
let connectPromise;
|
|
246
|
+
let reconnectTimer;
|
|
247
|
+
let closed = false;
|
|
248
|
+
let hasOpened = false;
|
|
215
249
|
let subscriptions;
|
|
216
250
|
const open = async () => {
|
|
251
|
+
if (closed) throw new ConnectionClosedError("Sync client is closed");
|
|
217
252
|
if (socket?.readyState === WebSocket.OPEN) return socket;
|
|
218
253
|
if (connectPromise) return connectPromise;
|
|
219
|
-
|
|
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) => {
|
|
220
266
|
const url = new URL(options.url);
|
|
221
267
|
url.searchParams.set("clientId", clientId);
|
|
222
268
|
const ws = new WebSocket(url);
|
|
223
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);
|
|
224
275
|
ws.addEventListener("open", () => {
|
|
276
|
+
opened = true;
|
|
277
|
+
clearTimeout(connectionTimer);
|
|
225
278
|
socket = ws;
|
|
226
279
|
subscriptions.replay(ws).then(() => {
|
|
227
|
-
|
|
280
|
+
if (hasOpened) subscriptions.reconnected();
|
|
281
|
+
hasOpened = true;
|
|
228
282
|
resolve(ws);
|
|
229
283
|
}).catch((error) => {
|
|
230
|
-
connectPromise = void 0;
|
|
231
284
|
reject(error);
|
|
285
|
+
ws.close();
|
|
232
286
|
});
|
|
233
287
|
});
|
|
234
288
|
ws.addEventListener("message", (event) => {
|
|
235
289
|
const message = parseServerMessage(event.data);
|
|
236
|
-
if ("
|
|
237
|
-
subscriptions.dispatch(message.
|
|
290
|
+
if (message.type === "updates") {
|
|
291
|
+
subscriptions.dispatch(message.payload);
|
|
238
292
|
return;
|
|
239
293
|
}
|
|
240
|
-
if ("error"
|
|
241
|
-
|
|
242
|
-
if (!request) return;
|
|
243
|
-
request.reject(new Error(message.error.message));
|
|
294
|
+
if (message.type === "error") {
|
|
295
|
+
takePending(pending, message.id)?.reject(new ProtocolError(message.error));
|
|
244
296
|
return;
|
|
245
297
|
}
|
|
246
|
-
|
|
247
|
-
if (!request) return;
|
|
248
|
-
request.resolve(message.result.data.json);
|
|
298
|
+
takePending(pending, message.id)?.resolve(message.payload);
|
|
249
299
|
});
|
|
250
300
|
ws.addEventListener("close", () => {
|
|
251
|
-
|
|
252
|
-
|
|
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"));
|
|
253
309
|
});
|
|
254
310
|
ws.addEventListener("error", () => {
|
|
311
|
+
clearTimeout(connectionTimer);
|
|
255
312
|
reject(/* @__PURE__ */ new Error("Unable to open sync WebSocket"));
|
|
256
313
|
});
|
|
257
314
|
});
|
|
315
|
+
connectPromise = connect().finally(() => {
|
|
316
|
+
connectPromise = void 0;
|
|
317
|
+
});
|
|
258
318
|
return connectPromise;
|
|
259
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
|
+
}
|
|
260
327
|
const sendRequest = (ws, request) => {
|
|
261
328
|
return new Promise((resolve, reject) => {
|
|
262
329
|
const id = request.id;
|
|
263
|
-
|
|
264
|
-
reject(
|
|
265
|
-
|
|
266
|
-
}
|
|
330
|
+
const timeout = setTimeout(() => {
|
|
331
|
+
takePending(pending, id)?.reject(new RequestTimeoutError());
|
|
332
|
+
}, requestTimeoutMs);
|
|
267
333
|
pending.set(String(id), {
|
|
268
|
-
resolve: (value) =>
|
|
269
|
-
|
|
334
|
+
resolve: (value) => {
|
|
335
|
+
clearTimeout(timeout);
|
|
336
|
+
resolve(value);
|
|
337
|
+
},
|
|
338
|
+
reject: (error) => {
|
|
339
|
+
clearTimeout(timeout);
|
|
340
|
+
reject(error);
|
|
341
|
+
}
|
|
270
342
|
});
|
|
271
343
|
try {
|
|
272
|
-
|
|
344
|
+
sendClientMessage(ws, request);
|
|
273
345
|
} catch (error) {
|
|
274
|
-
pending
|
|
275
|
-
reject(error);
|
|
346
|
+
takePending(pending, id)?.reject(error instanceof Error ? error : new Error(String(error)));
|
|
276
347
|
}
|
|
277
348
|
});
|
|
278
349
|
};
|
|
279
|
-
const sendSubscriptionControl = async (ws, method, collection) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
286
|
-
};
|
|
350
|
+
const sendSubscriptionControl = async (ws, method, collection) => sendRequest(ws, {
|
|
351
|
+
version: 1,
|
|
352
|
+
id: String(nextId++),
|
|
353
|
+
type: method,
|
|
354
|
+
input: { collection }
|
|
355
|
+
});
|
|
287
356
|
subscriptions = new ClientSubscriptions({
|
|
288
357
|
currentSocket: () => socket,
|
|
289
358
|
open,
|
|
290
359
|
send: sendSubscriptionControl
|
|
291
360
|
});
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
sendRequest(ws, requestForOperation(id, op)).then((value) => {
|
|
299
|
-
observer.next({ result: { data: value } });
|
|
300
|
-
observer.complete();
|
|
301
|
-
}).catch((error) => observer.error(error));
|
|
302
|
-
}).catch((error) => observer.error(error));
|
|
303
|
-
return () => {
|
|
304
|
-
cancelled = true;
|
|
305
|
-
};
|
|
306
|
-
});
|
|
307
|
-
};
|
|
308
|
-
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
|
+
}));
|
|
309
367
|
return {
|
|
310
368
|
clientId,
|
|
311
|
-
push: (batch) =>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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) => {
|
|
315
397
|
const [input] = args;
|
|
316
398
|
const call = input === void 0 ? { path } : {
|
|
317
399
|
path,
|
|
318
400
|
input
|
|
319
401
|
};
|
|
320
|
-
|
|
402
|
+
const ws = await open();
|
|
403
|
+
return sendRequest(ws, {
|
|
404
|
+
version: 1,
|
|
405
|
+
id: String(nextId++),
|
|
406
|
+
type: "api",
|
|
407
|
+
input: call
|
|
408
|
+
});
|
|
321
409
|
},
|
|
322
410
|
subscribe: (collection, listener) => subscriptions.subscribe(collection, listener),
|
|
323
411
|
close() {
|
|
412
|
+
closed = true;
|
|
413
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
414
|
+
rejectPending(pending, new ConnectionClosedError("Sync client was closed"));
|
|
324
415
|
socket?.close();
|
|
325
416
|
socket = void 0;
|
|
326
417
|
}
|
|
327
418
|
};
|
|
328
419
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}),
|
|
338
|
-
refs: 0
|
|
339
|
-
};
|
|
340
|
-
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;
|
|
341
428
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
478
|
+
}
|
|
479
|
+
if (had) {
|
|
480
|
+
subset.keys.delete(key);
|
|
481
|
+
this.releaseKey(key);
|
|
353
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;
|
|
354
535
|
}
|
|
355
|
-
|
|
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
|
+
}
|
|
356
549
|
}
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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;
|
|
361
617
|
}
|
|
362
618
|
//#endregion
|
|
363
619
|
//#region src/read-query.ts
|
|
@@ -507,170 +763,292 @@ function isValExpression(value) {
|
|
|
507
763
|
return isExpression(value) && value.type === "val";
|
|
508
764
|
}
|
|
509
765
|
//#endregion
|
|
510
|
-
//#region src/
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
keys,
|
|
527
|
-
loaded: true,
|
|
528
|
-
...predicate ? { predicate } : {},
|
|
529
|
-
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
|
|
530
782
|
});
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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);
|
|
534
810
|
});
|
|
535
|
-
|
|
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;
|
|
536
815
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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?.();
|
|
542
828
|
}
|
|
543
|
-
this.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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();
|
|
550
837
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
838
|
+
receive(broadcast) {
|
|
839
|
+
if (this._state !== "synchronized") {
|
|
840
|
+
this.buffered.push(broadcast);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
this.applyBroadcast(broadcast);
|
|
554
844
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
845
|
+
disconnected() {
|
|
846
|
+
if (this._state === "stopped") return;
|
|
847
|
+
this._state = "disconnected";
|
|
848
|
+
}
|
|
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;
|
|
571
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
|
|
572
887
|
});
|
|
573
|
-
|
|
574
|
-
before,
|
|
575
|
-
after: this.keyRefs.has(key)
|
|
576
|
-
};
|
|
888
|
+
this.cursor = Math.max(this.cursor, broadcast.watermark);
|
|
577
889
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
890
|
+
applyUpdates(updates, watermark) {
|
|
891
|
+
this.options.apply({
|
|
892
|
+
type: "updates",
|
|
893
|
+
shardId: "history",
|
|
894
|
+
updates,
|
|
895
|
+
watermark
|
|
582
896
|
});
|
|
583
|
-
this.keyRefs.delete(key);
|
|
584
|
-
return true;
|
|
585
897
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
subset.refs -= 1;
|
|
590
|
-
if (subset.refs > 0) return [];
|
|
591
|
-
subset.refs = 0;
|
|
592
|
-
if (retain) return [];
|
|
593
|
-
return this.expire(subsetId);
|
|
898
|
+
markSynchronized() {
|
|
899
|
+
this._state = "synchronized";
|
|
900
|
+
this.options.synchronized();
|
|
594
901
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (!subset || subset.refs > 0) return [];
|
|
598
|
-
this.subsets.delete(subsetId);
|
|
599
|
-
return this.releaseKeys(subset.keys);
|
|
902
|
+
isStopped() {
|
|
903
|
+
return this._state === "stopped";
|
|
600
904
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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);
|
|
606
931
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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);
|
|
610
943
|
});
|
|
611
944
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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);
|
|
616
952
|
});
|
|
617
|
-
return removed;
|
|
618
953
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
+
});
|
|
627
962
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
write({
|
|
633
|
-
type: collection.has(key) ? "update" : "insert",
|
|
634
|
-
key,
|
|
963
|
+
record(row) {
|
|
964
|
+
return {
|
|
965
|
+
collectionId: this.collectionId,
|
|
966
|
+
key: this.getKey(row),
|
|
635
967
|
value: row
|
|
636
|
-
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
enqueue(operation) {
|
|
971
|
+
const result = this.queue.then(operation);
|
|
972
|
+
this.queue = result.catch(() => void 0);
|
|
973
|
+
return result;
|
|
637
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
|
+
});
|
|
638
986
|
}
|
|
639
|
-
function
|
|
640
|
-
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
|
+
});
|
|
641
1001
|
}
|
|
642
|
-
function
|
|
643
|
-
return
|
|
1002
|
+
function requestResult(request) {
|
|
1003
|
+
return new Promise((resolve, reject) => {
|
|
1004
|
+
request.onsuccess = () => resolve(request.result);
|
|
1005
|
+
request.onerror = () => reject(request.error);
|
|
1006
|
+
});
|
|
644
1007
|
}
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
});
|
|
650
1014
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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);
|
|
661
1028
|
}
|
|
662
|
-
|
|
1029
|
+
offline.put(rows);
|
|
1030
|
+
offline.delete(deletedKeys);
|
|
663
1031
|
}
|
|
664
|
-
function
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
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);
|
|
668
1040
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
}
|
|
674
1052
|
}
|
|
675
1053
|
//#endregion
|
|
676
1054
|
//#region src/collection.ts
|
|
@@ -678,6 +1056,9 @@ function collectionOptions(options) {
|
|
|
678
1056
|
let lease;
|
|
679
1057
|
let client = options.client;
|
|
680
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);
|
|
681
1062
|
if (!client) {
|
|
682
1063
|
if (!options.url) throw new Error("collectionOptions requires either client or url");
|
|
683
1064
|
lease = acquireSharedClient({
|
|
@@ -696,11 +1077,12 @@ function collectionOptions(options) {
|
|
|
696
1077
|
...options.defaultIndexType !== void 0 ? { defaultIndexType: options.defaultIndexType } : {},
|
|
697
1078
|
getKey: options.getKey,
|
|
698
1079
|
sync: {
|
|
699
|
-
sync: ({ begin, write, commit, markReady, collection }) => {
|
|
1080
|
+
sync: ({ begin, write, commit, markReady, truncate, collection }) => {
|
|
700
1081
|
const subsets = new SubsetTracker(options.getKey);
|
|
701
1082
|
activeSubsets = subsets;
|
|
702
1083
|
const subsetGcTime = options.gcTime ?? 3e5;
|
|
703
1084
|
const retentionTimers = /* @__PURE__ */ new Map();
|
|
1085
|
+
const subsetQueries = /* @__PURE__ */ new Map();
|
|
704
1086
|
const deleteKeys = (keys) => {
|
|
705
1087
|
if (keys.length === 0) return;
|
|
706
1088
|
begin();
|
|
@@ -709,6 +1091,7 @@ function collectionOptions(options) {
|
|
|
709
1091
|
key
|
|
710
1092
|
});
|
|
711
1093
|
commit();
|
|
1094
|
+
offline?.delete(keys);
|
|
712
1095
|
};
|
|
713
1096
|
const cancelRetention = (id) => {
|
|
714
1097
|
const timer = retentionTimers.get(id);
|
|
@@ -725,47 +1108,86 @@ function collectionOptions(options) {
|
|
|
725
1108
|
cancelRetention(id);
|
|
726
1109
|
retentionTimers.set(id, setTimeout(() => {
|
|
727
1110
|
retentionTimers.delete(id);
|
|
1111
|
+
subsetQueries.delete(id);
|
|
728
1112
|
deleteKeys(subsets.expire(id));
|
|
729
1113
|
}, subsetGcTime));
|
|
730
1114
|
};
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
760
1180
|
});
|
|
1181
|
+
session.start();
|
|
761
1182
|
const readSubset = (subsetOptions) => {
|
|
762
1183
|
const id = subsetId(subsetOptions);
|
|
763
1184
|
cancelRetention(id);
|
|
764
1185
|
if (subsets.retain(id).loaded) return true;
|
|
765
1186
|
return (async () => {
|
|
766
1187
|
const query = readQueryForSubset(options.collection, options.read === false ? void 0 : options.read, subsetOptions);
|
|
767
|
-
await
|
|
1188
|
+
await session.start();
|
|
768
1189
|
const result = await client.read(query);
|
|
1190
|
+
subsetQueries.set(id, query);
|
|
769
1191
|
const removedKeys = subsets.replace(id, result.rows, query.filters ?? [], query.predicate);
|
|
770
1192
|
begin();
|
|
771
1193
|
writeRows(collection, result.rows, options.getKey, write);
|
|
@@ -779,71 +1201,89 @@ function collectionOptions(options) {
|
|
|
779
1201
|
throw error;
|
|
780
1202
|
});
|
|
781
1203
|
};
|
|
782
|
-
if (options.syncMode === "on-demand") {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
const initialQuery = initialReadQuery(options.collection, options.read);
|
|
805
|
-
if (!initialQuery) subscription.ready.then(() => {
|
|
806
|
-
markReady();
|
|
807
|
-
});
|
|
808
|
-
else subscription.ready.then(() => client.read(initialQuery)).then((result) => {
|
|
809
|
-
if (result.rows.length > 0) {
|
|
810
|
-
begin();
|
|
811
|
-
writeRows(collection, result.rows, options.getKey, write);
|
|
812
|
-
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]();
|
|
813
1223
|
}
|
|
814
|
-
|
|
815
|
-
});
|
|
1224
|
+
};
|
|
816
1225
|
return () => {
|
|
817
1226
|
if (activeSubsets === subsets) activeSubsets = void 0;
|
|
818
|
-
|
|
819
|
-
lease?.
|
|
1227
|
+
session.stop();
|
|
1228
|
+
lease?.[Symbol.dispose]();
|
|
820
1229
|
};
|
|
821
1230
|
},
|
|
822
1231
|
rowUpdateMode: "partial"
|
|
823
1232
|
},
|
|
824
1233
|
onInsert: async ({ transaction }) => {
|
|
825
1234
|
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
826
|
-
|
|
1235
|
+
const result = await client.push(createBatch(options.collection, client.clientId, transaction));
|
|
1236
|
+
persistTransaction(offline, transaction);
|
|
1237
|
+
return result;
|
|
827
1238
|
},
|
|
828
1239
|
onUpdate: async ({ transaction }) => {
|
|
829
1240
|
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
830
|
-
|
|
1241
|
+
const result = await client.push(createBatch(options.collection, client.clientId, transaction));
|
|
1242
|
+
persistTransaction(offline, transaction);
|
|
1243
|
+
return result;
|
|
831
1244
|
},
|
|
832
1245
|
onDelete: async ({ transaction }) => {
|
|
833
1246
|
trackLocalTransaction(activeSubsets, options.syncMode, transaction);
|
|
834
|
-
|
|
1247
|
+
const result = await client.push(createBatch(options.collection, client.clientId, transaction));
|
|
1248
|
+
persistTransaction(offline, transaction);
|
|
1249
|
+
return result;
|
|
835
1250
|
}
|
|
836
1251
|
};
|
|
837
1252
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/collection-path.ts
|
|
1255
|
+
function collectionScope(path, params) {
|
|
1256
|
+
return `/${collectionPathSegments(path).map((segment) => segmentValue(segment, params)).map(normalizeSegment).join("/")}/`;
|
|
1257
|
+
}
|
|
1258
|
+
function firstNewPathParam(childPath, parentPath) {
|
|
1259
|
+
if (!childPath) return void 0;
|
|
1260
|
+
const parentParams = new Set(pathParams(parentPath));
|
|
1261
|
+
return pathParams(childPath).find((param) => !parentParams.has(param));
|
|
1262
|
+
}
|
|
1263
|
+
function collectionPathSegments(path) {
|
|
1264
|
+
return path.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
1265
|
+
}
|
|
1266
|
+
function collectionPathParamName(segment) {
|
|
1267
|
+
return segment.match(/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/)?.[1] ?? segment.match(/^:([A-Za-z_][A-Za-z0-9_]*)$/)?.[1];
|
|
1268
|
+
}
|
|
1269
|
+
function isCollectionPathParam(segment) {
|
|
1270
|
+
return collectionPathParamName(segment) !== void 0;
|
|
1271
|
+
}
|
|
1272
|
+
function segmentValue(segment, params) {
|
|
1273
|
+
const param = collectionPathParamName(segment);
|
|
1274
|
+
if (!param) return segment;
|
|
1275
|
+
const value = params[param];
|
|
1276
|
+
if (value === void 0) throw new Error(`Missing collection path parameter: ${param}`);
|
|
1277
|
+
return String(value);
|
|
1278
|
+
}
|
|
1279
|
+
function pathParams(path) {
|
|
1280
|
+
return collectionPathSegments(path).flatMap((segment) => {
|
|
1281
|
+
const param = collectionPathParamName(segment);
|
|
1282
|
+
return param ? [param] : [];
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
function normalizeSegment(segment) {
|
|
1286
|
+
return encodeURIComponent(decodeURIComponent(segment));
|
|
847
1287
|
}
|
|
848
1288
|
//#endregion
|
|
849
1289
|
//#region src/collection-api.ts
|
|
@@ -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),
|
|
@@ -881,7 +1363,7 @@ var CollectionTypeManager = class CollectionTypeManager {
|
|
|
881
1363
|
}, this.apiMethods()), this.childManagers(this.params));
|
|
882
1364
|
}
|
|
883
1365
|
all(params) {
|
|
884
|
-
const scope = collectionScope
|
|
1366
|
+
const scope = collectionScope(this.options.path, {
|
|
885
1367
|
...this.params,
|
|
886
1368
|
...params
|
|
887
1369
|
});
|
|
@@ -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,12 +1415,15 @@ 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
|
}
|
|
936
1424
|
async callApi(name, method, input) {
|
|
937
1425
|
if (method.handler) {
|
|
938
|
-
const scope = collectionScope
|
|
1426
|
+
const scope = collectionScope(this.options.path, this.params);
|
|
939
1427
|
return method.handler({
|
|
940
1428
|
input,
|
|
941
1429
|
collection: this.all({}),
|
|
@@ -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,6 +1569,6 @@ function collectionOptionsFromDefinition(definition, dotPath, overrides) {
|
|
|
1067
1569
|
};
|
|
1068
1570
|
}
|
|
1069
1571
|
//#endregion
|
|
1070
|
-
export { collectionTypesFrom, createBatch, toChangeMessage };
|
|
1572
|
+
export { DEFAULT_MAX_SYNC_ROWS, collectionOptions, collectionTypesFrom, createBatch, createClient, toChangeMessage };
|
|
1071
1573
|
|
|
1072
1574
|
//# sourceMappingURL=index.mjs.map
|