@absolutejs/sync 1.20.0 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/devtools.d.ts +8 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +131 -72
- package/dist/engine/index.js.map +3 -3
- package/dist/engine/syncEngine.d.ts +36 -0
- package/dist/index.js +130 -72
- package/dist/index.js.map +3 -3
- package/dist/testing.js +130 -72
- package/dist/testing.js.map +3 -3
- package/package.json +1 -1
|
@@ -425,6 +425,18 @@ export declare class MutationQueueOverflowError extends Error {
|
|
|
425
425
|
readonly queueLimit: number;
|
|
426
426
|
constructor(queueLimit: number);
|
|
427
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Thrown by `engine.subscribe` when the calling tenant's active-subscription
|
|
430
|
+
* count is already at the configured `subscriptionLimit.max`. The caller sees
|
|
431
|
+
* this immediately — BEFORE authorize, hydrate, or any subscription state
|
|
432
|
+
* allocation — so a rejected call leaks nothing. Added in 1.20.1.
|
|
433
|
+
*/
|
|
434
|
+
export declare class SubscriptionLimitError extends Error {
|
|
435
|
+
readonly tenantKey: string;
|
|
436
|
+
readonly limit: number;
|
|
437
|
+
readonly active: number;
|
|
438
|
+
constructor(tenantKey: string, limit: number, active: number);
|
|
439
|
+
}
|
|
428
440
|
/**
|
|
429
441
|
* Serializable snapshot of an engine's change log + monotonic version, returned
|
|
430
442
|
* by {@link SyncEngine.exportChangeLog} and consumed by
|
|
@@ -597,6 +609,30 @@ export type SyncEngineOptions = {
|
|
|
597
609
|
* Added in 1.20.0.
|
|
598
610
|
*/
|
|
599
611
|
mutationQueueLimit?: number;
|
|
612
|
+
/**
|
|
613
|
+
* Per-tenant active-subscription cap. Symmetric to
|
|
614
|
+
* {@link SyncEngineOptions.mutationConcurrency} on the read side: a
|
|
615
|
+
* single tenant opening thousands of subscriptions would otherwise
|
|
616
|
+
* exhaust the engine's per-subscription bookkeeping
|
|
617
|
+
* (`active`/`tableIndex` Maps, the reactive cache, per-row diff
|
|
618
|
+
* computation cost).
|
|
619
|
+
*
|
|
620
|
+
* `key` derives a tenant identifier from `(ctx, args)`; returning
|
|
621
|
+
* `undefined` skips the cap for that call (e.g. internal/system
|
|
622
|
+
* subscriptions). When the active count for a key reaches `max`, the
|
|
623
|
+
* next `subscribe` throws {@link SubscriptionLimitError} BEFORE any
|
|
624
|
+
* authorize, hydrate, or state allocation — so a denied call leaks
|
|
625
|
+
* nothing.
|
|
626
|
+
*
|
|
627
|
+
* Active counts are surfaced through `engine.metrics().subscriptions.byTenant`
|
|
628
|
+
* for tier monitoring. Added in 1.20.1.
|
|
629
|
+
*/
|
|
630
|
+
subscriptionLimit?: {
|
|
631
|
+
max: number;
|
|
632
|
+
key: (ctx: unknown, args: {
|
|
633
|
+
collection: string;
|
|
634
|
+
}) => string | undefined;
|
|
635
|
+
};
|
|
600
636
|
};
|
|
601
637
|
/**
|
|
602
638
|
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
package/dist/index.js
CHANGED
|
@@ -1161,6 +1161,19 @@ class MutationQueueOverflowError extends Error {
|
|
|
1161
1161
|
this.queueLimit = queueLimit;
|
|
1162
1162
|
}
|
|
1163
1163
|
}
|
|
1164
|
+
|
|
1165
|
+
class SubscriptionLimitError extends Error {
|
|
1166
|
+
tenantKey;
|
|
1167
|
+
limit;
|
|
1168
|
+
active;
|
|
1169
|
+
constructor(tenantKey, limit, active) {
|
|
1170
|
+
super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
|
|
1171
|
+
this.name = "SubscriptionLimitError";
|
|
1172
|
+
this.tenantKey = tenantKey;
|
|
1173
|
+
this.limit = limit;
|
|
1174
|
+
this.active = active;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1164
1177
|
var defaultKey = (row) => row.id;
|
|
1165
1178
|
var shallowEqual3 = (a, b) => {
|
|
1166
1179
|
if (a === b) {
|
|
@@ -1311,6 +1324,31 @@ var createSyncEngine = (options = {}) => {
|
|
|
1311
1324
|
if (next !== undefined)
|
|
1312
1325
|
next();
|
|
1313
1326
|
};
|
|
1327
|
+
const subscriptionsByTenant = new Map;
|
|
1328
|
+
const acquireSubscriptionSlot = (ctx, args) => {
|
|
1329
|
+
const cap = options.subscriptionLimit;
|
|
1330
|
+
if (cap === undefined)
|
|
1331
|
+
return;
|
|
1332
|
+
const tenantKey = cap.key(ctx, args);
|
|
1333
|
+
if (tenantKey === undefined)
|
|
1334
|
+
return;
|
|
1335
|
+
const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
|
|
1336
|
+
if (active2 >= cap.max) {
|
|
1337
|
+
throw new SubscriptionLimitError(tenantKey, cap.max, active2);
|
|
1338
|
+
}
|
|
1339
|
+
subscriptionsByTenant.set(tenantKey, active2 + 1);
|
|
1340
|
+
return tenantKey;
|
|
1341
|
+
};
|
|
1342
|
+
const releaseSubscriptionSlot = (tenantKey) => {
|
|
1343
|
+
if (tenantKey === undefined)
|
|
1344
|
+
return;
|
|
1345
|
+
const active2 = subscriptionsByTenant.get(tenantKey);
|
|
1346
|
+
if (active2 === undefined || active2 <= 1) {
|
|
1347
|
+
subscriptionsByTenant.delete(tenantKey);
|
|
1348
|
+
} else {
|
|
1349
|
+
subscriptionsByTenant.set(tenantKey, active2 - 1);
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1314
1352
|
const reactiveCacheMax = options.reactiveCache?.max ?? 256;
|
|
1315
1353
|
const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
|
|
1316
1354
|
const cachedReruns = new Map;
|
|
@@ -2148,84 +2186,103 @@ var createSyncEngine = (options = {}) => {
|
|
|
2148
2186
|
if (registered === undefined) {
|
|
2149
2187
|
throw new Error(`Unknown collection "${collection}"`);
|
|
2150
2188
|
}
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2189
|
+
const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
|
|
2190
|
+
let slotHandedOff = false;
|
|
2191
|
+
try {
|
|
2192
|
+
const typedOnDiff = onDiff;
|
|
2193
|
+
const subscribeSet = subsFor(collection);
|
|
2194
|
+
const wrapReturn = (sub) => {
|
|
2195
|
+
checkAborted(signal);
|
|
2196
|
+
const innerUnsubscribe = sub.unsubscribe;
|
|
2197
|
+
let released = false;
|
|
2198
|
+
const wrappedUnsubscribe = () => {
|
|
2199
|
+
if (released)
|
|
2200
|
+
return;
|
|
2201
|
+
released = true;
|
|
2202
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2203
|
+
innerUnsubscribe();
|
|
2204
|
+
};
|
|
2205
|
+
const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
|
|
2206
|
+
linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
|
|
2207
|
+
slotHandedOff = true;
|
|
2208
|
+
return wrapped;
|
|
2209
|
+
};
|
|
2210
|
+
const registeredKind = registered.kind;
|
|
2211
|
+
if (registeredKind === "join") {
|
|
2212
|
+
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2213
|
+
return wrapReturn(joined);
|
|
2214
|
+
}
|
|
2215
|
+
if (registeredKind === "graph") {
|
|
2216
|
+
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2217
|
+
return wrapReturn(graphed);
|
|
2218
|
+
}
|
|
2219
|
+
if (registeredKind === "reactive") {
|
|
2220
|
+
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2221
|
+
return wrapReturn(reactived);
|
|
2222
|
+
}
|
|
2223
|
+
if (registeredKind === "search") {
|
|
2224
|
+
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2225
|
+
return wrapReturn(searched);
|
|
2226
|
+
}
|
|
2227
|
+
const definition = registered;
|
|
2228
|
+
if (definition.authorize !== undefined) {
|
|
2229
|
+
const allowed = await definition.authorize(params, ctx);
|
|
2230
|
+
if (!allowed) {
|
|
2231
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const key = definition.key ?? defaultKey;
|
|
2235
|
+
const match = definition.match;
|
|
2236
|
+
const tables = definition.tables ?? [collection];
|
|
2237
|
+
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2238
|
+
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2239
|
+
const rehydrate = async () => {
|
|
2240
|
+
const raw = [...await definition.hydrate(params, ctx)];
|
|
2241
|
+
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2242
|
+
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2243
|
+
};
|
|
2244
|
+
const incremental = match !== undefined && tables.length === 1;
|
|
2245
|
+
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2246
|
+
const view = createMaterializedView({
|
|
2247
|
+
key,
|
|
2248
|
+
match: boundMatch
|
|
2249
|
+
});
|
|
2250
|
+
const resuming = since !== undefined && canResume(since, incremental);
|
|
2251
|
+
view.hydrate([...await rehydrate()]);
|
|
2252
|
+
const atVersion = version;
|
|
2253
|
+
const subscription = {
|
|
2254
|
+
kind: "view",
|
|
2255
|
+
collection,
|
|
2256
|
+
view,
|
|
2257
|
+
incremental,
|
|
2258
|
+
rehydrate,
|
|
2259
|
+
key,
|
|
2260
|
+
onDiff: typedOnDiff
|
|
2261
|
+
};
|
|
2262
|
+
subscribeSet.add(subscription);
|
|
2263
|
+
const unsubscribe = () => {
|
|
2264
|
+
subscribeSet.delete(subscription);
|
|
2265
|
+
};
|
|
2266
|
+
if (resuming) {
|
|
2267
|
+
return wrapReturn({
|
|
2268
|
+
initial: [],
|
|
2269
|
+
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2270
|
+
cursor: currentCursor(),
|
|
2271
|
+
version: atVersion,
|
|
2272
|
+
unsubscribe
|
|
2273
|
+
});
|
|
2180
2274
|
}
|
|
2181
|
-
}
|
|
2182
|
-
const key = definition.key ?? defaultKey;
|
|
2183
|
-
const match = definition.match;
|
|
2184
|
-
const tables = definition.tables ?? [collection];
|
|
2185
|
-
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2186
|
-
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2187
|
-
const rehydrate = async () => {
|
|
2188
|
-
const raw = [...await definition.hydrate(params, ctx)];
|
|
2189
|
-
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2190
|
-
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2191
|
-
};
|
|
2192
|
-
const incremental = match !== undefined && tables.length === 1;
|
|
2193
|
-
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2194
|
-
const view = createMaterializedView({
|
|
2195
|
-
key,
|
|
2196
|
-
match: boundMatch
|
|
2197
|
-
});
|
|
2198
|
-
const resuming = since !== undefined && canResume(since, incremental);
|
|
2199
|
-
view.hydrate([...await rehydrate()]);
|
|
2200
|
-
const atVersion = version;
|
|
2201
|
-
const subscription = {
|
|
2202
|
-
kind: "view",
|
|
2203
|
-
collection,
|
|
2204
|
-
view,
|
|
2205
|
-
incremental,
|
|
2206
|
-
rehydrate,
|
|
2207
|
-
key,
|
|
2208
|
-
onDiff: typedOnDiff
|
|
2209
|
-
};
|
|
2210
|
-
subscribeSet.add(subscription);
|
|
2211
|
-
const unsubscribe = () => {
|
|
2212
|
-
subscribeSet.delete(subscription);
|
|
2213
|
-
};
|
|
2214
|
-
if (resuming) {
|
|
2215
2275
|
return wrapReturn({
|
|
2216
|
-
initial:
|
|
2217
|
-
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2276
|
+
initial: view.rows(),
|
|
2218
2277
|
cursor: currentCursor(),
|
|
2219
2278
|
version: atVersion,
|
|
2220
2279
|
unsubscribe
|
|
2221
2280
|
});
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
if (!slotHandedOff)
|
|
2283
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2284
|
+
throw error;
|
|
2222
2285
|
}
|
|
2223
|
-
return wrapReturn({
|
|
2224
|
-
initial: view.rows(),
|
|
2225
|
-
cursor: currentCursor(),
|
|
2226
|
-
version: atVersion,
|
|
2227
|
-
unsubscribe
|
|
2228
|
-
});
|
|
2229
2286
|
},
|
|
2230
2287
|
hydrate: async (collection, params, ctx, options2) => {
|
|
2231
2288
|
const signal = options2?.signal;
|
|
@@ -2690,6 +2747,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
2690
2747
|
},
|
|
2691
2748
|
subscriptions: {
|
|
2692
2749
|
byCollection,
|
|
2750
|
+
byTenant: Object.fromEntries(subscriptionsByTenant),
|
|
2693
2751
|
total: totalSubscriptions
|
|
2694
2752
|
},
|
|
2695
2753
|
uptimeMs: now - engineStartedAt,
|
|
@@ -3072,5 +3130,5 @@ export {
|
|
|
3072
3130
|
createPresenceHub
|
|
3073
3131
|
};
|
|
3074
3132
|
|
|
3075
|
-
//# debugId=
|
|
3133
|
+
//# debugId=27A5DE0DF43D569D64756E2164756E21
|
|
3076
3134
|
//# sourceMappingURL=index.js.map
|