@absolutejs/sync 2.0.1 → 2.2.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.
@@ -1,4 +1,4 @@
1
- import type { RowKey } from './types';
1
+ import type { RowChange, RowKey } from './types';
2
2
  /**
3
3
  * App-provided context for a subscription — typically the authenticated session
4
4
  * (user id, roles). Passed to `authorize`, `hydrate`, and `match` so a
@@ -32,6 +32,19 @@ export type CollectionDefinition<T, P = void, Ctx = CollectionContext> = {
32
32
  * two in lockstep — the planned adapter convenience.)
33
33
  */
34
34
  match?: (row: T, params: P, ctx: Ctx) => boolean;
35
+ /**
36
+ * Refetch-fallback gate (multi-table / shape-mismatched collections that can't
37
+ * use `match`). Without it, ANY change to a read table re-hydrates EVERY
38
+ * subscription — even ones the change can't touch. Given the RAW change (the
39
+ * source row, which may differ from `T` and be partial), return `false` ONLY
40
+ * when the change provably can't affect this subscription's result, to skip
41
+ * its re-hydrate; the fan-out drops from O(all subscribers) to O(affected).
42
+ *
43
+ * Conservative: a `false` that should have been `true` DROPS an update, so
44
+ * default to `true` when unsure (e.g. a delete whose row lacks your scope
45
+ * field). Ignored when `match` is set (incremental routing is already exact).
46
+ */
47
+ affects?: (change: RowChange<unknown>, params: P, ctx: Ctx) => boolean;
35
48
  /**
36
49
  * Access control: return `false` (or throw) to deny the subscription. Runs
37
50
  * before `hydrate`. Without it a collection is world-readable, so treat it as
@@ -113,6 +113,21 @@ export type EngineMetrics = {
113
113
  schedules: {
114
114
  registered: number;
115
115
  };
116
+ /**
117
+ * Change-source liveness, aggregated across every `connectSource` (e.g. a
118
+ * CDC adapter). Lets a health check distinguish a healthy-but-quiet feed
119
+ * from a wired-but-silent or disconnected one. Added in 2.1.0.
120
+ */
121
+ source: {
122
+ /** Currently-connected change sources (0 once all have disconnected). */
123
+ connected: number;
124
+ /** Total changes delivered by connected sources since engine start. */
125
+ changesReceived: number;
126
+ /** `Date.now()` of the most recent source change, or `null` if none yet. */
127
+ lastChangeAt: number | null;
128
+ /** Wall-clock age of the most recent source change in ms, or `null`. */
129
+ lastChangeAgeMs: number | null;
130
+ };
116
131
  };
117
132
  /**
118
133
  * A live engine event (see {@link SyncEngine.onActivity}): a committed change or
@@ -1611,6 +1611,9 @@ var createSyncEngine = (options = {}) => {
1611
1611
  let mutationsFailed = 0;
1612
1612
  let mutationsRetried = 0;
1613
1613
  let mutationsInFlight = 0;
1614
+ let connectedSources = 0;
1615
+ let sourceChangesReceived = 0;
1616
+ let sourceLastChangeAt = null;
1614
1617
  const mutationWaiters = [];
1615
1618
  let mutationsQueued = 0;
1616
1619
  const activeFences = new Set;
@@ -1796,6 +1799,9 @@ var createSyncEngine = (options = {}) => {
1796
1799
  return subscription.view.reset(await subscription.rehydrate());
1797
1800
  }
1798
1801
  }
1802
+ if (subscription.affects && !subscription.affects(change)) {
1803
+ return EMPTY_DIFF;
1804
+ }
1799
1805
  return subscription.view.reset(await subscription.rehydrate());
1800
1806
  };
1801
1807
  const subscriptionsForTable = function* (table) {
@@ -2588,6 +2594,7 @@ var createSyncEngine = (options = {}) => {
2588
2594
  return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
2589
2595
  };
2590
2596
  const incremental = match !== undefined && tables.length === 1;
2597
+ const boundAffects = !incremental && definition.affects ? (change) => definition.affects(change, params, ctx) : undefined;
2591
2598
  const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
2592
2599
  const view = createMaterializedView({
2593
2600
  key,
@@ -2602,6 +2609,7 @@ var createSyncEngine = (options = {}) => {
2602
2609
  view,
2603
2610
  incremental,
2604
2611
  rehydrate,
2612
+ ...boundAffects ? { affects: boundAffects } : {},
2605
2613
  key,
2606
2614
  onDiff: typedOnDiff
2607
2615
  };
@@ -2664,8 +2672,18 @@ var createSyncEngine = (options = {}) => {
2664
2672
  },
2665
2673
  applyChange: (table, change) => applyChange(table, change),
2666
2674
  connectSource: async (source) => {
2667
- await source.start((table, change) => applyChange(table, change));
2675
+ await source.start((table, change) => {
2676
+ sourceChangesReceived += 1;
2677
+ sourceLastChangeAt = Date.now();
2678
+ return applyChange(table, change);
2679
+ });
2680
+ connectedSources += 1;
2681
+ let stopped = false;
2668
2682
  return async () => {
2683
+ if (!stopped) {
2684
+ stopped = true;
2685
+ connectedSources -= 1;
2686
+ }
2669
2687
  await source.stop();
2670
2688
  };
2671
2689
  },
@@ -3226,6 +3244,12 @@ var createSyncEngine = (options = {}) => {
3226
3244
  schedules: {
3227
3245
  registered: schedules.size
3228
3246
  },
3247
+ source: {
3248
+ changesReceived: sourceChangesReceived,
3249
+ connected: connectedSources,
3250
+ lastChangeAgeMs: sourceLastChangeAt === null ? null : now - sourceLastChangeAt,
3251
+ lastChangeAt: sourceLastChangeAt
3252
+ },
3229
3253
  subscriptions: {
3230
3254
  byCollection,
3231
3255
  byTenant: Object.fromEntries(subscriptionsByTenant),
@@ -3769,5 +3793,5 @@ export {
3769
3793
  CdcConsumerSlowError
3770
3794
  };
3771
3795
 
3772
- //# debugId=6D23BAC246E5201264756E2164756E21
3796
+ //# debugId=40999B3553DAC88C64756E2164756E21
3773
3797
  //# sourceMappingURL=index.js.map