@absolutejs/sync 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -414,6 +414,17 @@ export declare class CdcConsumerSlowError extends Error {
414
414
  readonly lastDeliveredVersion: number;
415
415
  constructor(maxBuffer: number, lastDeliveredVersion: number);
416
416
  }
417
+ /**
418
+ * Thrown by `runMutation` / `runMutations` when `mutationConcurrency` is
419
+ * saturated AND the waiting queue is already at `mutationQueueLimit`. The
420
+ * caller sees this immediately (no queue time) so the host can shed load
421
+ * with a clean 429 instead of letting the queue grow unboundedly. Added
422
+ * in 1.20.0.
423
+ */
424
+ export declare class MutationQueueOverflowError extends Error {
425
+ readonly queueLimit: number;
426
+ constructor(queueLimit: number);
427
+ }
417
428
  /**
418
429
  * Serializable snapshot of an engine's change log + monotonic version, returned
419
430
  * by {@link SyncEngine.exportChangeLog} and consumed by
@@ -557,6 +568,35 @@ export type SyncEngineOptions = {
557
568
  * `createSyncEngine` returns. Added in 1.19.0.
558
569
  */
559
570
  initialChangeLog?: ChangeLogSnapshot;
571
+ /**
572
+ * Maximum concurrent in-flight mutations (`runMutation` + `runMutations`).
573
+ * Calls beyond the limit wait in a FIFO queue and run as slots free up;
574
+ * `engine.metrics().mutations.queued` surfaces the queue depth.
575
+ *
576
+ * A single tenant flooding `runMutation` can otherwise drive unbounded
577
+ * memory growth (per-mutation `actions` buffers, retry timers, sandbox
578
+ * invocations queued against the isolate pool). Set this to a value
579
+ * appropriate for the host's tenant tier — e.g. `32` for a free tier,
580
+ * `256` for paid. Without this option the engine is unbounded
581
+ * (matching pre-1.20 behavior).
582
+ *
583
+ * Sandboxed mutations are gated by the same semaphore. If you need
584
+ * finer-grained control (sandbox-only throttling), see
585
+ * `@absolutejs/isolated-jsc`'s pool size — that's the lower layer.
586
+ *
587
+ * Added in 1.20.0.
588
+ */
589
+ mutationConcurrency?: number;
590
+ /**
591
+ * Cap on the queue of waiting mutations once `mutationConcurrency` is
592
+ * saturated. Calls beyond this cap throw {@link MutationQueueOverflowError}
593
+ * immediately instead of queueing — the host can surface a clean 429 or
594
+ * apply a tenant-specific shed policy. Defaults to unbounded (queue
595
+ * never rejects). Only meaningful when `mutationConcurrency` is set.
596
+ *
597
+ * Added in 1.20.0.
598
+ */
599
+ mutationQueueLimit?: number;
560
600
  };
561
601
  /**
562
602
  * The Tier 3 sync engine: a registry of collections plus the view syncer. It is
package/dist/index.js CHANGED
@@ -1152,6 +1152,15 @@ class CdcConsumerSlowError extends Error {
1152
1152
  this.lastDeliveredVersion = lastDeliveredVersion;
1153
1153
  }
1154
1154
  }
1155
+
1156
+ class MutationQueueOverflowError extends Error {
1157
+ queueLimit;
1158
+ constructor(queueLimit) {
1159
+ super(`Mutation queue overflowed (limit ${queueLimit}); the engine is at ` + `its mutationConcurrency cap and the waiting queue is full. ` + `Retry later or shed load at the gateway.`);
1160
+ this.name = "MutationQueueOverflowError";
1161
+ this.queueLimit = queueLimit;
1162
+ }
1163
+ }
1155
1164
  var defaultKey = (row) => row.id;
1156
1165
  var shallowEqual3 = (a, b) => {
1157
1166
  if (a === b) {
@@ -1268,6 +1277,40 @@ var createSyncEngine = (options = {}) => {
1268
1277
  let mutationsFailed = 0;
1269
1278
  let mutationsRetried = 0;
1270
1279
  let mutationsInFlight = 0;
1280
+ const mutationWaiters = [];
1281
+ let mutationsQueued = 0;
1282
+ const acquireMutationSlot = async () => {
1283
+ const limit = options.mutationConcurrency;
1284
+ if (limit === undefined) {
1285
+ mutationsInFlight += 1;
1286
+ return;
1287
+ }
1288
+ if (mutationsInFlight < limit && mutationWaiters.length === 0) {
1289
+ mutationsInFlight += 1;
1290
+ return;
1291
+ }
1292
+ const queueLimit = options.mutationQueueLimit;
1293
+ if (queueLimit !== undefined && mutationsQueued >= queueLimit) {
1294
+ throw new MutationQueueOverflowError(queueLimit);
1295
+ }
1296
+ mutationsQueued += 1;
1297
+ try {
1298
+ await new Promise((resolve) => {
1299
+ mutationWaiters.push(resolve);
1300
+ });
1301
+ } finally {
1302
+ mutationsQueued -= 1;
1303
+ }
1304
+ mutationsInFlight += 1;
1305
+ };
1306
+ const releaseMutationSlot = () => {
1307
+ mutationsInFlight -= 1;
1308
+ if (options.mutationConcurrency === undefined)
1309
+ return;
1310
+ const next = mutationWaiters.shift();
1311
+ if (next !== undefined)
1312
+ next();
1313
+ };
1271
1314
  const reactiveCacheMax = options.reactiveCache?.max ?? 256;
1272
1315
  const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
1273
1316
  const cachedReruns = new Map;
@@ -2295,6 +2338,7 @@ var createSyncEngine = (options = {}) => {
2295
2338
  throw new UnauthorizedError(`run mutation "${name}"`);
2296
2339
  }
2297
2340
  }
2341
+ await acquireMutationSlot();
2298
2342
  const sandboxRunner = sandboxRunners.get(name);
2299
2343
  const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
2300
2344
  const runHandler = async (tx) => {
@@ -2310,7 +2354,6 @@ var createSyncEngine = (options = {}) => {
2310
2354
  const startedAt = Date.now();
2311
2355
  let lastError;
2312
2356
  let attemptsMade = 0;
2313
- mutationsInFlight += 1;
2314
2357
  try {
2315
2358
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2316
2359
  attemptsMade = attempt;
@@ -2363,7 +2406,7 @@ var createSyncEngine = (options = {}) => {
2363
2406
  }
2364
2407
  throw lastError;
2365
2408
  } finally {
2366
- mutationsInFlight -= 1;
2409
+ releaseMutationSlot();
2367
2410
  }
2368
2411
  },
2369
2412
  runMutations: async (specs, ctx) => {
@@ -2376,6 +2419,7 @@ var createSyncEngine = (options = {}) => {
2376
2419
  }
2377
2420
  return { args: spec.args, mutation, name: spec.name };
2378
2421
  });
2422
+ await acquireMutationSlot();
2379
2423
  const runBatch = async (tx) => {
2380
2424
  const results = [];
2381
2425
  const accumulated = [];
@@ -2413,6 +2457,8 @@ var createSyncEngine = (options = {}) => {
2413
2457
  status: "error"
2414
2458
  });
2415
2459
  throw error;
2460
+ } finally {
2461
+ releaseMutationSlot();
2416
2462
  }
2417
2463
  },
2418
2464
  registerSchedule: (schedule) => {
@@ -2632,6 +2678,7 @@ var createSyncEngine = (options = {}) => {
2632
2678
  completed: mutationsCompleted,
2633
2679
  failed: mutationsFailed,
2634
2680
  inFlight: mutationsInFlight,
2681
+ queued: mutationsQueued,
2635
2682
  retried: mutationsRetried
2636
2683
  },
2637
2684
  reactiveCache: {
@@ -3025,5 +3072,5 @@ export {
3025
3072
  createPresenceHub
3026
3073
  };
3027
3074
 
3028
- //# debugId=EB8C381967D0311A64756E2164756E21
3075
+ //# debugId=9DD57DF563EB9CB564756E2164756E21
3029
3076
  //# sourceMappingURL=index.js.map