@absolutejs/sync 1.12.2 → 1.13.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.
@@ -7,7 +7,7 @@ import type { PermissionsDefinition, TablePermissions } from './permissions';
7
7
  import type { SyncPack } from './pack';
8
8
  import type { SearchCollectionDefinition } from './search';
9
9
  import type { ScheduleDefinition } from './schedule';
10
- import type { EngineActivity, EngineInspection } from './devtools';
10
+ import type { EngineActivity, EngineInspection, EngineMetrics } from './devtools';
11
11
  import type { SchemaDefinition, TableSchema } from './schema';
12
12
  import type { CrdtMergeable } from '../crdt';
13
13
  import type { ClusterBus } from './cluster';
@@ -206,6 +206,16 @@ export type SyncEngine = {
206
206
  * writers, the change-feed version, and recent changes. See `syncDevtools`.
207
207
  */
208
208
  inspect: () => EngineInspection;
209
+ /**
210
+ * Operator-shaped engine metrics — counters + memory estimates + throughput
211
+ * totals since engine start. Distinct from {@link SyncEngine.inspect}: this
212
+ * is what a PaaS host scrapes on an interval to answer "is this engine
213
+ * healthy" and "what's its resource footprint." Feed it to
214
+ * `@absolutejs/metering` for per-engine cost attribution.
215
+ *
216
+ * Added in 1.13.0.
217
+ */
218
+ metrics: () => EngineMetrics;
209
219
  /**
210
220
  * Subscribe to the live engine activity stream (changes, mutation outcomes,
211
221
  * subscribe/unsubscribe). Returns an unsubscribe. Powers the devtools feed.
@@ -259,6 +269,13 @@ export type LoggedChange = {
259
269
  version: number;
260
270
  table: string;
261
271
  change: RowChange<unknown>;
272
+ /**
273
+ * Wall-clock when this change was logged (Date.now()). Used by the
274
+ * engine's time-based retention sweep (`changeLogRetainMs`) and
275
+ * surfaced as the change-log age in {@link SyncEngine.metrics}.
276
+ * Added in 1.13.0; pre-1.13.0 consumers of `LoggedChange` ignore it.
277
+ */
278
+ at: number;
262
279
  };
263
280
  /** Thrown by {@link SyncEngine.streamChanges} when `since` is older than the
264
281
  * oldest entry retained in the bounded change log (i.e. the consumer was
@@ -305,6 +322,16 @@ export type SyncEngineOptions = {
305
322
  * snapshot. Defaults to 1024.
306
323
  */
307
324
  changeLogSize?: number;
325
+ /**
326
+ * Time-based change-log retention: drop entries older than this many ms,
327
+ * in addition to the count cap above. Lets a high-throughput engine keep
328
+ * a SHORT log (e.g. "60s of changes") regardless of count, which both
329
+ * bounds memory and bounds the catch-up work on reconnect. Defaults to
330
+ * `null` — only the count cap (`changeLogSize`) applies.
331
+ *
332
+ * Added in 1.13.0.
333
+ */
334
+ changeLogRetainMs?: number | null;
308
335
  /**
309
336
  * Run every mutation inside your database's transaction (see
310
337
  * {@link TransactionRunner}): the handler's writes commit all-or-nothing, and
package/dist/index.js CHANGED
@@ -1130,8 +1130,14 @@ var createSyncEngine = (options = {}) => {
1130
1130
  const active = new Map;
1131
1131
  const tableIndex = new Map;
1132
1132
  const changeLogSize = options.changeLogSize ?? 1024;
1133
+ const changeLogRetainMs = options.changeLogRetainMs ?? null;
1133
1134
  const changeLog = [];
1134
1135
  let version = 0;
1136
+ const engineStartedAt = Date.now();
1137
+ let mutationsCompleted = 0;
1138
+ let mutationsFailed = 0;
1139
+ let mutationsRetried = 0;
1140
+ let mutationsInFlight = 0;
1135
1141
  const reactiveCacheMax = options.reactiveCache?.max ?? 256;
1136
1142
  const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
1137
1143
  const cachedReruns = new Map;
@@ -1542,6 +1548,12 @@ var createSyncEngine = (options = {}) => {
1542
1548
  if (changeLog.length > changeLogSize) {
1543
1549
  changeLog.shift();
1544
1550
  }
1551
+ if (changeLogRetainMs !== null && changeLogRetainMs > 0) {
1552
+ const cutoff = entry.at - changeLogRetainMs;
1553
+ while (changeLog.length > 0 && changeLog[0].at < cutoff) {
1554
+ changeLog.shift();
1555
+ }
1556
+ }
1545
1557
  for (const subscriber of streamSubscribers) {
1546
1558
  subscriber(entry);
1547
1559
  }
@@ -1549,10 +1561,11 @@ var createSyncEngine = (options = {}) => {
1549
1561
  const applyChange = async (table, change, shouldBroadcast = true) => {
1550
1562
  version += 1;
1551
1563
  const changeVersion = version;
1552
- logChange(changeVersion, { version: changeVersion, table, change });
1564
+ const at = Date.now();
1565
+ logChange(changeVersion, { version: changeVersion, table, change, at });
1553
1566
  emitActivity({
1554
1567
  type: "change",
1555
- at: Date.now(),
1568
+ at,
1556
1569
  table,
1557
1570
  op: change.op,
1558
1571
  version: changeVersion
@@ -1583,11 +1596,12 @@ var createSyncEngine = (options = {}) => {
1583
1596
  const batchVersion = version;
1584
1597
  const perSubscription = new Map;
1585
1598
  const reactiveChanges = [];
1599
+ const batchAt = Date.now();
1586
1600
  for (const { table, change } of changes) {
1587
- logChange(batchVersion, { version: batchVersion, table, change });
1601
+ logChange(batchVersion, { version: batchVersion, table, change, at: batchAt });
1588
1602
  emitActivity({
1589
1603
  type: "change",
1590
- at: Date.now(),
1604
+ at: batchAt,
1591
1605
  table,
1592
1606
  op: change.op,
1593
1607
  version: batchVersion
@@ -2038,53 +2052,61 @@ var createSyncEngine = (options = {}) => {
2038
2052
  const startedAt = Date.now();
2039
2053
  let lastError;
2040
2054
  let attemptsMade = 0;
2041
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2042
- attemptsMade = attempt;
2043
- try {
2044
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2045
- await applyChangeBatch(buffered);
2046
- emitActivity({
2047
- type: "mutation",
2048
- at: Date.now(),
2049
- name,
2050
- status: "ok"
2051
- });
2052
- return result;
2053
- } catch (error) {
2054
- lastError = error;
2055
- const elapsedMs = Date.now() - startedAt;
2056
- const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2057
- if (!canRetry)
2058
- break;
2059
- const rawDelay = computeDelay(attempt);
2060
- const remaining = maxElapsedMs - elapsedMs;
2061
- if (remaining <= 0)
2062
- break;
2063
- const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2064
- emitActivity({
2065
- type: "mutationRetry",
2066
- at: Date.now(),
2067
- name,
2068
- attempt,
2069
- delayMs,
2070
- errorName: error instanceof Error ? error.name : "Error",
2071
- errorMessage: error instanceof Error ? error.message : String(error)
2072
- });
2073
- if (delayMs > 0) {
2074
- await new Promise((resolve) => setTimeout(resolve, delayMs));
2055
+ mutationsInFlight += 1;
2056
+ try {
2057
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2058
+ attemptsMade = attempt;
2059
+ try {
2060
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2061
+ await applyChangeBatch(buffered);
2062
+ mutationsCompleted += 1;
2063
+ emitActivity({
2064
+ type: "mutation",
2065
+ at: Date.now(),
2066
+ name,
2067
+ status: "ok"
2068
+ });
2069
+ return result;
2070
+ } catch (error) {
2071
+ lastError = error;
2072
+ const elapsedMs = Date.now() - startedAt;
2073
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2074
+ if (!canRetry)
2075
+ break;
2076
+ mutationsRetried += 1;
2077
+ const rawDelay = computeDelay(attempt);
2078
+ const remaining = maxElapsedMs - elapsedMs;
2079
+ if (remaining <= 0)
2080
+ break;
2081
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2082
+ emitActivity({
2083
+ type: "mutationRetry",
2084
+ at: Date.now(),
2085
+ name,
2086
+ attempt,
2087
+ delayMs,
2088
+ errorName: error instanceof Error ? error.name : "Error",
2089
+ errorMessage: error instanceof Error ? error.message : String(error)
2090
+ });
2091
+ if (delayMs > 0) {
2092
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2093
+ }
2075
2094
  }
2076
2095
  }
2096
+ mutationsFailed += 1;
2097
+ emitActivity({
2098
+ type: "mutation",
2099
+ at: Date.now(),
2100
+ name,
2101
+ status: "error"
2102
+ });
2103
+ if (attemptsMade > 1) {
2104
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2105
+ }
2106
+ throw lastError;
2107
+ } finally {
2108
+ mutationsInFlight -= 1;
2077
2109
  }
2078
- emitActivity({
2079
- type: "mutation",
2080
- at: Date.now(),
2081
- name,
2082
- status: "error"
2083
- });
2084
- if (attemptsMade > 1) {
2085
- throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2086
- }
2087
- throw lastError;
2088
2110
  },
2089
2111
  runMutations: async (specs, ctx) => {
2090
2112
  if (specs.length === 0)
@@ -2323,6 +2345,45 @@ var createSyncEngine = (options = {}) => {
2323
2345
  }))
2324
2346
  };
2325
2347
  },
2348
+ metrics: () => {
2349
+ const now = Date.now();
2350
+ const byCollection = {};
2351
+ let totalSubscriptions = 0;
2352
+ for (const [name, subs] of active) {
2353
+ byCollection[name] = subs.size;
2354
+ totalSubscriptions += subs.size;
2355
+ }
2356
+ const oldest = changeLog[0];
2357
+ return {
2358
+ at: now,
2359
+ changeLog: {
2360
+ capacity: changeLogSize,
2361
+ entries: changeLog.length,
2362
+ oldestAgeMs: oldest ? now - oldest.at : null,
2363
+ oldestVersion: oldest ? oldest.version : null,
2364
+ retainMs: changeLogRetainMs
2365
+ },
2366
+ mutations: {
2367
+ completed: mutationsCompleted,
2368
+ failed: mutationsFailed,
2369
+ inFlight: mutationsInFlight,
2370
+ retried: mutationsRetried
2371
+ },
2372
+ reactiveCache: {
2373
+ capacity: reactiveCacheMax,
2374
+ entries: cachedReruns.size
2375
+ },
2376
+ schedules: {
2377
+ registered: schedules.size
2378
+ },
2379
+ subscriptions: {
2380
+ byCollection,
2381
+ total: totalSubscriptions
2382
+ },
2383
+ uptimeMs: now - engineStartedAt,
2384
+ version
2385
+ };
2386
+ },
2326
2387
  onActivity: (listener) => {
2327
2388
  activityListeners.add(listener);
2328
2389
  return () => {
@@ -2698,5 +2759,5 @@ export {
2698
2759
  createPresenceHub
2699
2760
  };
2700
2761
 
2701
- //# debugId=D6679B754081BCC764756E2164756E21
2762
+ //# debugId=202C826A4AA9A02264756E2164756E21
2702
2763
  //# sourceMappingURL=index.js.map