covara 0.6.0 → 0.7.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.
Files changed (141) hide show
  1. package/README.md +14 -9
  2. package/assets/logo.svg +19 -0
  3. package/dist/auth/adapter.d.ts +1 -0
  4. package/dist/auth/adapter.d.ts.map +1 -1
  5. package/dist/auth/adapter.js +37 -1
  6. package/dist/auth/adapter.js.map +1 -1
  7. package/dist/auth/routes.d.ts.map +1 -1
  8. package/dist/auth/routes.js +15 -2
  9. package/dist/auth/routes.js.map +1 -1
  10. package/dist/auth/types.d.ts +4 -0
  11. package/dist/auth/types.d.ts.map +1 -1
  12. package/dist/auth/types.js +10 -0
  13. package/dist/auth/types.js.map +1 -1
  14. package/dist/cli/templates/readme.js +2 -2
  15. package/dist/client/aggregate-subscription.d.ts +32 -0
  16. package/dist/client/aggregate-subscription.d.ts.map +1 -0
  17. package/dist/client/aggregate-subscription.js +131 -0
  18. package/dist/client/aggregate-subscription.js.map +1 -0
  19. package/dist/client/index.d.ts +2 -0
  20. package/dist/client/index.d.ts.map +1 -1
  21. package/dist/client/index.js +1 -0
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/client/live-store.d.ts.map +1 -1
  24. package/dist/client/live-store.js +18 -3
  25. package/dist/client/live-store.js.map +1 -1
  26. package/dist/client/react.d.ts +25 -1
  27. package/dist/client/react.d.ts.map +1 -1
  28. package/dist/client/react.js +53 -0
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/repository.d.ts +2 -0
  31. package/dist/client/repository.d.ts.map +1 -1
  32. package/dist/client/repository.js +9 -0
  33. package/dist/client/repository.js.map +1 -1
  34. package/dist/client/types.d.ts +17 -0
  35. package/dist/client/types.d.ts.map +1 -1
  36. package/dist/index.d.ts +4 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/middleware/securityHeaders.d.ts +1 -0
  41. package/dist/middleware/securityHeaders.d.ts.map +1 -1
  42. package/dist/middleware/securityHeaders.js +6 -3
  43. package/dist/middleware/securityHeaders.js.map +1 -1
  44. package/dist/resource/changelog.d.ts +3 -3
  45. package/dist/resource/changelog.d.ts.map +1 -1
  46. package/dist/resource/changelog.js +6 -3
  47. package/dist/resource/changelog.js.map +1 -1
  48. package/dist/resource/hook.d.ts.map +1 -1
  49. package/dist/resource/hook.js +141 -18
  50. package/dist/resource/hook.js.map +1 -1
  51. package/dist/resource/mutate.d.ts +3 -3
  52. package/dist/resource/mutate.d.ts.map +1 -1
  53. package/dist/resource/mutate.js +8 -8
  54. package/dist/resource/mutate.js.map +1 -1
  55. package/dist/resource/pagination.d.ts.map +1 -1
  56. package/dist/resource/pagination.js +32 -4
  57. package/dist/resource/pagination.js.map +1 -1
  58. package/dist/resource/query.d.ts +1 -0
  59. package/dist/resource/query.d.ts.map +1 -1
  60. package/dist/resource/query.js +12 -0
  61. package/dist/resource/query.js.map +1 -1
  62. package/dist/resource/subscription.d.ts +18 -1
  63. package/dist/resource/subscription.d.ts.map +1 -1
  64. package/dist/resource/subscription.js +310 -53
  65. package/dist/resource/subscription.js.map +1 -1
  66. package/dist/resource/track-mutations.d.ts.map +1 -1
  67. package/dist/resource/track-mutations.js +1 -1
  68. package/dist/resource/track-mutations.js.map +1 -1
  69. package/dist/resource/types.d.ts +2 -0
  70. package/dist/resource/types.d.ts.map +1 -1
  71. package/dist/server/app.d.ts.map +1 -1
  72. package/dist/server/app.js +28 -3
  73. package/dist/server/app.js.map +1 -1
  74. package/dist/ui/html/client/data-explorer-app.d.ts +2 -0
  75. package/dist/ui/html/client/data-explorer-app.d.ts.map +1 -0
  76. package/dist/ui/html/client/data-explorer-app.js +441 -0
  77. package/dist/ui/html/client/data-explorer-app.js.map +1 -0
  78. package/dist/ui/html/client/htmx-vendor.d.ts +3 -0
  79. package/dist/ui/html/client/htmx-vendor.d.ts.map +1 -0
  80. package/dist/ui/html/client/htmx-vendor.js +5 -0
  81. package/dist/ui/html/client/htmx-vendor.js.map +1 -0
  82. package/dist/ui/html/client/runtime.d.ts +2 -0
  83. package/dist/ui/html/client/runtime.d.ts.map +1 -0
  84. package/dist/ui/html/client/runtime.js +198 -0
  85. package/dist/ui/html/client/runtime.js.map +1 -0
  86. package/dist/ui/html/components/index.d.ts +5 -1
  87. package/dist/ui/html/components/index.d.ts.map +1 -1
  88. package/dist/ui/html/components/index.js +10 -5
  89. package/dist/ui/html/components/index.js.map +1 -1
  90. package/dist/ui/html/icons.d.ts +3 -0
  91. package/dist/ui/html/icons.d.ts.map +1 -0
  92. package/dist/ui/html/icons.js +23 -0
  93. package/dist/ui/html/icons.js.map +1 -0
  94. package/dist/ui/html/layout.d.ts.map +1 -1
  95. package/dist/ui/html/layout.js +38 -18
  96. package/dist/ui/html/layout.js.map +1 -1
  97. package/dist/ui/html/logo.d.ts +2 -0
  98. package/dist/ui/html/logo.d.ts.map +1 -0
  99. package/dist/ui/html/logo.js +23 -0
  100. package/dist/ui/html/logo.js.map +1 -0
  101. package/dist/ui/html/pages/dashboard.d.ts.map +1 -1
  102. package/dist/ui/html/pages/dashboard.js +6 -5
  103. package/dist/ui/html/pages/dashboard.js.map +1 -1
  104. package/dist/ui/html/pages/data-explorer.d.ts.map +1 -1
  105. package/dist/ui/html/pages/data-explorer.js +5 -94
  106. package/dist/ui/html/pages/data-explorer.js.map +1 -1
  107. package/dist/ui/html/pages/kv-inspector.d.ts.map +1 -1
  108. package/dist/ui/html/pages/kv-inspector.js +0 -1
  109. package/dist/ui/html/pages/kv-inspector.js.map +1 -1
  110. package/dist/ui/html/pages/sessions.d.ts.map +1 -1
  111. package/dist/ui/html/pages/sessions.js +4 -3
  112. package/dist/ui/html/pages/sessions.js.map +1 -1
  113. package/dist/ui/html/pages/subscriptions.d.ts.map +1 -1
  114. package/dist/ui/html/pages/subscriptions.js +2 -3
  115. package/dist/ui/html/pages/subscriptions.js.map +1 -1
  116. package/dist/ui/html/pages/tasks.d.ts.map +1 -1
  117. package/dist/ui/html/pages/tasks.js +0 -1
  118. package/dist/ui/html/pages/tasks.js.map +1 -1
  119. package/dist/ui/html/pages/users.js +1 -1
  120. package/dist/ui/html/pages/users.js.map +1 -1
  121. package/dist/ui/html/styles.d.ts +1 -1
  122. package/dist/ui/html/styles.d.ts.map +1 -1
  123. package/dist/ui/html/styles.js +367 -40
  124. package/dist/ui/html/styles.js.map +1 -1
  125. package/dist/ui/html/utils.d.ts +1 -1
  126. package/dist/ui/html/utils.d.ts.map +1 -1
  127. package/dist/ui/html/utils.js +4 -0
  128. package/dist/ui/html/utils.js.map +1 -1
  129. package/dist/ui/index.d.ts +2 -2
  130. package/dist/ui/index.d.ts.map +1 -1
  131. package/dist/ui/index.js +1 -1
  132. package/dist/ui/index.js.map +1 -1
  133. package/dist/ui/middleware.d.ts +7 -2
  134. package/dist/ui/middleware.d.ts.map +1 -1
  135. package/dist/ui/middleware.js +362 -117
  136. package/dist/ui/middleware.js.map +1 -1
  137. package/dist/ui/schema-registry.d.ts +1 -0
  138. package/dist/ui/schema-registry.d.ts.map +1 -1
  139. package/dist/ui/schema-registry.js +1 -0
  140. package/dist/ui/schema-registry.js.map +1 -1
  141. package/package.json +6 -4
@@ -3,6 +3,10 @@ import { Filter } from "./filter.js";
3
3
  import { Subscription, ChangelogEntry } from "./types.js";
4
4
  export type BackpressurePolicy = "invalidate" | "disconnect" | "drop";
5
5
  export declare const registerResourceMask: (resource: string, mask: (item: Record<string, unknown>) => Record<string, unknown>) => void;
6
+ type AggregateWatcher = (changed?: Record<string, unknown>[]) => void;
7
+ export declare const registerAggregateWatcher: (resource: string, watcher: AggregateWatcher) => (() => void);
8
+ export declare const notifyAggregateWatchers: (resource: string, changed?: Record<string, unknown>[]) => Promise<void>;
9
+ export declare const getAggregateWatcherCount: (resource?: string) => number;
6
10
  export declare const addRelevantObject: (subscriptionId: string, objectId: string) => Promise<void>;
7
11
  export declare const registerKnownIds: (subscriptionId: string, ids: string[]) => Promise<void>;
8
12
  export interface CreateSubscriptionOptions {
@@ -24,7 +28,7 @@ export declare const sendExistingItems: <T extends Record<string, unknown>>(subs
24
28
  export type RelationLoader<T> = (items: T[], include: string) => Promise<T[]>;
25
29
  export declare const pushInsertsToSubscriptions: <T extends Record<string, unknown>>(resource: string, filterFactory: Filter, items: T[], idColumn: string, optimisticIds?: Map<string, string>, relationLoader?: RelationLoader<T>) => Promise<void>;
26
30
  export declare const pushUpdatesToSubscriptions: <T extends Record<string, unknown>>(resource: string, filterFactory: Filter, items: T[], idColumn: string, previousItems?: Map<string, T>, relationLoader?: RelationLoader<T>) => Promise<void>;
27
- export declare const pushDeletesToSubscriptions: (resource: string, deletedIds: string[]) => Promise<void>;
31
+ export declare const pushDeletesToSubscriptions: (resource: string, deletedIds: string[], deletedObjects?: Record<string, unknown>[]) => Promise<void>;
28
32
  export declare const sendInvalidateEvent: (subscriptionId: string, reason?: string) => Promise<void>;
29
33
  export declare const invalidateResourceSubscriptions: (resource: string, reason?: string) => Promise<void>;
30
34
  export declare const processChangelogEntries: (entries: ChangelogEntry[], filterFactory: Filter, idColumn: string) => Promise<void>;
@@ -42,5 +46,18 @@ export declare const getSubscriptionStats: () => Promise<{
42
46
  }>;
43
47
  export declare const closeAllHandlers: () => number;
44
48
  export declare const getActiveHandlerCount: () => number;
49
+ export interface ActiveSubscriptionInfo {
50
+ id: string;
51
+ resource: string;
52
+ filter?: string;
53
+ userId?: string;
54
+ connectedAt: string;
55
+ eventCount: number;
56
+ lastEventAt?: string;
57
+ connected: boolean;
58
+ }
59
+ export declare const listActiveSubscriptions: () => Promise<ActiveSubscriptionInfo[]>;
60
+ export declare const disconnectSubscription: (subscriptionId: string) => Promise<boolean>;
45
61
  export declare const clearAllSubscriptions: () => Promise<void>;
62
+ export {};
46
63
  //# sourceMappingURL=subscription.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../src/resource/subscription.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../src/resource/subscription.ts"],"names":[],"mappings":"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"}
@@ -21,15 +21,59 @@ const maskForResource = (resource, item) => {
21
21
  const compiledFiltersCache = new Map();
22
22
  // Track which handler IDs are local to this process
23
23
  const localHandlerIds = new Set();
24
+ const aggregateWatchers = new Map();
24
25
  // In-memory fallback storage (used when KV is not configured)
25
26
  const localSubscriptions = new Map();
26
27
  const localRelevantObjects = new Map();
27
28
  const localSeqCounters = new Map();
29
+ const localSubsByResource = new Map();
30
+ // Last event delivery time per subscription (process-local, best effort) for
31
+ // the admin UI's "Last Event" column.
32
+ const localEventTimestamps = new Map();
33
+ // SSE handlers are strictly process-local, so every subscription created for a
34
+ // handler is known to this process. Tracking handler → subscription IDs here
35
+ // lets disconnect cleanup run in O(own subscriptions) without scanning KV.
36
+ const localHandlerSubs = new Map();
37
+ const localSubHandlers = new Map();
38
+ const trackHandlerSubscription = (handlerId, subscriptionId) => {
39
+ let subs = localHandlerSubs.get(handlerId);
40
+ if (!subs) {
41
+ subs = new Set();
42
+ localHandlerSubs.set(handlerId, subs);
43
+ }
44
+ subs.add(subscriptionId);
45
+ localSubHandlers.set(subscriptionId, handlerId);
46
+ };
47
+ const untrackSubscription = (subscriptionId) => {
48
+ const handlerId = localSubHandlers.get(subscriptionId);
49
+ if (handlerId === undefined)
50
+ return;
51
+ localSubHandlers.delete(subscriptionId);
52
+ const subs = localHandlerSubs.get(handlerId);
53
+ if (subs) {
54
+ subs.delete(subscriptionId);
55
+ if (subs.size === 0)
56
+ localHandlerSubs.delete(handlerId);
57
+ }
58
+ };
28
59
  // KV keys
29
- const SUBSCRIPTIONS_HASH = "covara:subscriptions";
60
+ // Subscriptions are sharded into one hash per resource so a mutation only ever
61
+ // loads the subscriptions of the resource it touched — never the whole fleet.
62
+ // A small set of resource names (the index) lets cold paths (stats, admin
63
+ // listing, clearAll) enumerate the shards.
64
+ const SUBSCRIPTIONS_BY_RESOURCE_PREFIX = "covara:subs:byres:";
65
+ const SUBSCRIPTIONS_RESOURCE_INDEX = "covara:subs:resources";
66
+ const subscriptionHashKey = (resource) => `${SUBSCRIPTIONS_BY_RESOURCE_PREFIX}${resource}`;
67
+ // The resource is embedded in the subscription ID (`<uuid>:<resource>`) so
68
+ // ID-only operations (get/remove/updateSeq) can address the right shard
69
+ // without a secondary lookup. UUIDs are exactly 36 chars and contain no colon.
70
+ const resourceFromSubscriptionId = (subscriptionId) => subscriptionId.length > 37 && subscriptionId[36] === ":"
71
+ ? subscriptionId.slice(37)
72
+ : null;
30
73
  const SUBSCRIPTION_OBJECTS_PREFIX = "covara:sub:objects:";
31
74
  const SUBSCRIPTION_SEQ_PREFIX = "covara:sub:seq:";
32
75
  const EVENTS_CHANNEL = "covara:events";
76
+ const AGGREGATE_CHANNEL = "covara:aggregate";
33
77
  const serializeSubscription = (sub) => {
34
78
  const serialized = {
35
79
  id: sub.id,
@@ -64,6 +108,61 @@ const deserializeSubscription = (data) => {
64
108
  const getKV = () => {
65
109
  return hasGlobalKV() ? getGlobalKV() : null;
66
110
  };
111
+ // Register a callback invoked whenever the given resource is mutated, for
112
+ // aggregate subscriptions to recompute. Returns an unsubscribe function.
113
+ export const registerAggregateWatcher = (resource, watcher) => {
114
+ let set = aggregateWatchers.get(resource);
115
+ if (!set) {
116
+ set = new Set();
117
+ aggregateWatchers.set(resource, set);
118
+ }
119
+ set.add(watcher);
120
+ return () => {
121
+ const current = aggregateWatchers.get(resource);
122
+ if (!current)
123
+ return;
124
+ current.delete(watcher);
125
+ if (current.size === 0)
126
+ aggregateWatchers.delete(resource);
127
+ };
128
+ };
129
+ const notifyLocalAggregateWatchers = (resource, changed) => {
130
+ const set = aggregateWatchers.get(resource);
131
+ if (!set)
132
+ return;
133
+ for (const watcher of set) {
134
+ try {
135
+ watcher(changed);
136
+ }
137
+ catch {
138
+ // a failing watcher must not break mutation processing
139
+ }
140
+ }
141
+ };
142
+ // Signal that a resource was mutated so aggregate subscriptions recompute.
143
+ // Notifies local watchers immediately (with the changed rows, when available,
144
+ // so watchers can skip recompute for out-of-scope mutations) and (when KV is
145
+ // configured) publishes to other processes. The cross-process notification
146
+ // carries no row data — remote watchers recompute conservatively — to avoid
147
+ // shipping raw rows over pub/sub. Double-delivery to the publisher is harmless;
148
+ // watchers debounce before recomputing.
149
+ export const notifyAggregateWatchers = async (resource, changed) => {
150
+ if (aggregateWatchers.size > 0) {
151
+ notifyLocalAggregateWatchers(resource, changed);
152
+ }
153
+ const kv = getKV();
154
+ if (kv) {
155
+ await kv.publish(AGGREGATE_CHANNEL, JSON.stringify({ resource }));
156
+ }
157
+ };
158
+ export const getAggregateWatcherCount = (resource) => {
159
+ if (resource)
160
+ return aggregateWatchers.get(resource)?.size ?? 0;
161
+ let total = 0;
162
+ for (const set of aggregateWatchers.values())
163
+ total += set.size;
164
+ return total;
165
+ };
67
166
  // Load relevant object IDs from KV or local storage
68
167
  const loadRelevantObjects = async (subscriptionId) => {
69
168
  const kv = getKV();
@@ -126,7 +225,7 @@ const isObjectRelevant = async (subscriptionId, objectId) => {
126
225
  return objects?.has(objectId) ?? false;
127
226
  };
128
227
  export const createSubscription = async (options) => {
129
- const subscriptionId = uuidv4();
228
+ const subscriptionId = `${uuidv4()}:${options.resource}`;
130
229
  const subscription = {
131
230
  id: subscriptionId,
132
231
  createdAt: new Date(),
@@ -142,24 +241,53 @@ export const createSubscription = async (options) => {
142
241
  };
143
242
  const kv = getKV();
144
243
  if (kv) {
145
- await kv.hset(SUBSCRIPTIONS_HASH, subscriptionId, serializeSubscription(subscription));
244
+ await kv.hset(subscriptionHashKey(options.resource), subscriptionId, serializeSubscription(subscription));
245
+ await kv.sadd(SUBSCRIPTIONS_RESOURCE_INDEX, options.resource);
146
246
  await kv.set(`${SUBSCRIPTION_SEQ_PREFIX}${subscriptionId}`, "0");
147
247
  }
148
248
  else {
149
249
  // Store in local memory
150
250
  localSubscriptions.set(subscriptionId, subscription);
151
251
  localSeqCounters.set(subscriptionId, 0);
252
+ let byResource = localSubsByResource.get(options.resource);
253
+ if (!byResource) {
254
+ byResource = new Set();
255
+ localSubsByResource.set(options.resource, byResource);
256
+ }
257
+ byResource.add(subscriptionId);
152
258
  }
259
+ trackHandlerSubscription(options.handlerId, subscriptionId);
153
260
  return subscriptionId;
154
261
  };
155
262
  export const removeSubscription = async (subscriptionId) => {
156
263
  compiledFiltersCache.delete(subscriptionId);
264
+ untrackSubscription(subscriptionId);
265
+ localEventTimestamps.delete(subscriptionId);
157
266
  const kv = getKV();
158
267
  if (kv) {
159
- await kv.hdel(SUBSCRIPTIONS_HASH, subscriptionId);
268
+ const resource = resourceFromSubscriptionId(subscriptionId);
269
+ if (resource) {
270
+ await kv.hdel(subscriptionHashKey(resource), subscriptionId);
271
+ }
272
+ else {
273
+ // Unknown ID format: clear it from every shard (cold fallback).
274
+ const resources = await kv.smembers(SUBSCRIPTIONS_RESOURCE_INDEX);
275
+ for (const res of resources) {
276
+ await kv.hdel(subscriptionHashKey(res), subscriptionId);
277
+ }
278
+ }
160
279
  await kv.del(`${SUBSCRIPTION_OBJECTS_PREFIX}${subscriptionId}`, `${SUBSCRIPTION_SEQ_PREFIX}${subscriptionId}`);
161
280
  }
162
281
  else {
282
+ const sub = localSubscriptions.get(subscriptionId);
283
+ if (sub) {
284
+ const byResource = localSubsByResource.get(sub.resource);
285
+ if (byResource) {
286
+ byResource.delete(subscriptionId);
287
+ if (byResource.size === 0)
288
+ localSubsByResource.delete(sub.resource);
289
+ }
290
+ }
163
291
  localSubscriptions.delete(subscriptionId);
164
292
  localRelevantObjects.delete(subscriptionId);
165
293
  localSeqCounters.delete(subscriptionId);
@@ -168,7 +296,20 @@ export const removeSubscription = async (subscriptionId) => {
168
296
  export const getSubscription = async (subscriptionId) => {
169
297
  const kv = getKV();
170
298
  if (kv) {
171
- const data = await kv.hget(SUBSCRIPTIONS_HASH, subscriptionId);
299
+ const resource = resourceFromSubscriptionId(subscriptionId);
300
+ let data = null;
301
+ if (resource) {
302
+ data = await kv.hget(subscriptionHashKey(resource), subscriptionId);
303
+ }
304
+ else {
305
+ // Unknown ID format: check every shard (cold fallback).
306
+ const resources = await kv.smembers(SUBSCRIPTIONS_RESOURCE_INDEX);
307
+ for (const res of resources) {
308
+ data = await kv.hget(subscriptionHashKey(res), subscriptionId);
309
+ if (data)
310
+ break;
311
+ }
312
+ }
172
313
  if (!data)
173
314
  return undefined;
174
315
  const subscription = deserializeSubscription(data);
@@ -190,27 +331,26 @@ export const unregisterHandler = async (handlerId) => {
190
331
  localHandlers.delete(handlerId);
191
332
  localHandlerIds.delete(handlerId);
192
333
  localHandlerPolicies.delete(handlerId);
193
- const kv = getKV();
194
- if (kv) {
195
- // Get all subscriptions and remove those belonging to this handler
196
- const allSubs = await kv.hgetall(SUBSCRIPTIONS_HASH);
197
- for (const [subId, data] of Object.entries(allSubs)) {
198
- const sub = deserializeSubscription(data);
199
- if (sub.handlerId === handlerId) {
200
- await removeSubscription(subId);
201
- }
334
+ // Handlers are process-local, so their subscriptions were created here and
335
+ // are tracked locally — disconnect cleanup is O(own subscriptions), no scan.
336
+ const tracked = localHandlerSubs.get(handlerId);
337
+ if (tracked) {
338
+ for (const subId of Array.from(tracked)) {
339
+ await removeSubscription(subId);
202
340
  }
341
+ return;
203
342
  }
204
- else {
205
- // Clean up local subscriptions for this handler
206
- for (const [subId, sub] of localSubscriptions.entries()) {
207
- if (sub.handlerId === handlerId) {
208
- await removeSubscription(subId);
209
- }
343
+ // Untracked handler (e.g. cleanup of subscriptions created by a previous
344
+ // process incarnation): fall back to scanning every shard.
345
+ const allSubs = await getAllSubscriptions();
346
+ for (const [subId, sub] of allSubs) {
347
+ if (sub.handlerId === handlerId) {
348
+ await removeSubscription(subId);
210
349
  }
211
350
  }
212
351
  };
213
352
  const getNextSeq = async (subscriptionId) => {
353
+ localEventTimestamps.set(subscriptionId, Date.now());
214
354
  const kv = getKV();
215
355
  if (kv) {
216
356
  return kv.incr(`${SUBSCRIPTION_SEQ_PREFIX}${subscriptionId}`);
@@ -220,17 +360,45 @@ const getNextSeq = async (subscriptionId) => {
220
360
  localSeqCounters.set(subscriptionId, next);
221
361
  return next;
222
362
  };
223
- // Helper to get all subscriptions from KV or local storage
224
- const getAllSubscriptions = async () => {
363
+ // Load one resource's subscriptions. This is the hot-path accessor used by the
364
+ // mutation push functions cost scales with that resource's subscriber count,
365
+ // not the total subscription count.
366
+ const getSubscriptionsForResourceMap = async (resource) => {
225
367
  const kv = getKV();
226
368
  if (kv) {
227
369
  const result = new Map();
228
- const allSubs = await kv.hgetall(SUBSCRIPTIONS_HASH);
229
- for (const [subId, data] of Object.entries(allSubs)) {
370
+ const subs = await kv.hgetall(subscriptionHashKey(resource));
371
+ for (const [subId, data] of Object.entries(subs)) {
230
372
  result.set(subId, deserializeSubscription(data));
231
373
  }
232
374
  return result;
233
375
  }
376
+ const result = new Map();
377
+ const ids = localSubsByResource.get(resource);
378
+ if (!ids)
379
+ return result;
380
+ for (const id of ids) {
381
+ const sub = localSubscriptions.get(id);
382
+ if (sub)
383
+ result.set(id, sub);
384
+ }
385
+ return result;
386
+ };
387
+ // Enumerate every shard. Cold paths only (stats, admin listing, fallback
388
+ // cleanup) — never called per mutation.
389
+ const getAllSubscriptions = async () => {
390
+ const kv = getKV();
391
+ if (kv) {
392
+ const result = new Map();
393
+ const resources = await kv.smembers(SUBSCRIPTIONS_RESOURCE_INDEX);
394
+ for (const resource of resources) {
395
+ const subs = await kv.hgetall(subscriptionHashKey(resource));
396
+ for (const [subId, data] of Object.entries(subs)) {
397
+ result.set(subId, deserializeSubscription(data));
398
+ }
399
+ }
400
+ return result;
401
+ }
234
402
  return new Map(localSubscriptions);
235
403
  };
236
404
  const getCompiledFilter = (subscription, filterFactory) => {
@@ -320,6 +488,18 @@ export const initializeEventSubscription = async () => {
320
488
  // Ignore malformed messages
321
489
  }
322
490
  });
491
+ // Fan out cross-process mutation signals to local aggregate watchers.
492
+ await kv.subscribe(AGGREGATE_CHANNEL, (message) => {
493
+ try {
494
+ const { resource } = JSON.parse(message);
495
+ if (typeof resource === "string") {
496
+ notifyLocalAggregateWatchers(resource);
497
+ }
498
+ }
499
+ catch {
500
+ // Ignore malformed messages
501
+ }
502
+ });
323
503
  };
324
504
  export const sendExistingItems = async (subscriptionId, items, idColumn) => {
325
505
  const subscription = await getSubscription(subscriptionId);
@@ -346,7 +526,7 @@ export const sendExistingItems = async (subscriptionId, items, idColumn) => {
346
526
  }
347
527
  };
348
528
  export const pushInsertsToSubscriptions = async (resource, filterFactory, items, idColumn, optimisticIds, relationLoader) => {
349
- const allSubs = await getAllSubscriptions();
529
+ const allSubs = await getSubscriptionsForResourceMap(resource);
350
530
  // Cache for items with relations loaded (keyed by include string)
351
531
  const itemsWithRelationsCache = new Map();
352
532
  const getItemWithRelations = async (item, include) => {
@@ -398,9 +578,10 @@ export const pushInsertsToSubscriptions = async (resource, filterFactory, items,
398
578
  }
399
579
  }
400
580
  }
581
+ await notifyAggregateWatchers(resource, items);
401
582
  };
402
583
  export const pushUpdatesToSubscriptions = async (resource, filterFactory, items, idColumn, previousItems, relationLoader) => {
403
- const allSubs = await getAllSubscriptions();
584
+ const allSubs = await getSubscriptionsForResourceMap(resource);
404
585
  // Cache for items with relations loaded (keyed by include string)
405
586
  const itemsWithRelationsCache = new Map();
406
587
  const getItemWithRelations = async (item, include) => {
@@ -482,9 +663,17 @@ export const pushUpdatesToSubscriptions = async (resource, filterFactory, items,
482
663
  }
483
664
  }
484
665
  }
666
+ // Pass both new and previous state: an update can move a row into or out of a
667
+ // filtered aggregate's scope, so a watcher must recompute if either matches.
668
+ const changed = [...items];
669
+ if (previousItems) {
670
+ for (const prev of previousItems.values())
671
+ changed.push(prev);
672
+ }
673
+ await notifyAggregateWatchers(resource, changed);
485
674
  };
486
- export const pushDeletesToSubscriptions = async (resource, deletedIds) => {
487
- const allSubs = await getAllSubscriptions();
675
+ export const pushDeletesToSubscriptions = async (resource, deletedIds, deletedObjects) => {
676
+ const allSubs = await getSubscriptionsForResourceMap(resource);
488
677
  for (const [subId, subscription] of allSubs) {
489
678
  if (subscription.resource !== resource)
490
679
  continue;
@@ -506,6 +695,10 @@ export const pushDeletesToSubscriptions = async (resource, deletedIds) => {
506
695
  }
507
696
  }
508
697
  }
698
+ // When the deleted rows' prior content is available, watchers can scope-skip:
699
+ // a row that wasn't in a subscription's scope can't change its aggregate.
700
+ // Without it (IDs only) every watcher must recompute conservatively.
701
+ await notifyAggregateWatchers(resource, deletedObjects);
509
702
  };
510
703
  export const sendInvalidateEvent = async (subscriptionId, reason) => {
511
704
  const subscription = await getSubscription(subscriptionId);
@@ -527,12 +720,11 @@ export const sendInvalidateEvent = async (subscriptionId, reason) => {
527
720
  // refetch. Used for mutations the framework can't observe row-by-row: raw SQL
528
721
  // and writes from external processes (via recordExternalMutation).
529
722
  export const invalidateResourceSubscriptions = async (resource, reason) => {
530
- const allSubs = await getAllSubscriptions();
531
- for (const [subId, subscription] of allSubs) {
532
- if (subscription.resource !== resource)
533
- continue;
723
+ const allSubs = await getSubscriptionsForResourceMap(resource);
724
+ for (const subId of allSubs.keys()) {
534
725
  await sendInvalidateEvent(subId, reason);
535
726
  }
727
+ await notifyAggregateWatchers(resource);
536
728
  };
537
729
  export const processChangelogEntries = async (entries, filterFactory, idColumn) => {
538
730
  for (const entry of entries) {
@@ -562,13 +754,10 @@ export const getSubscriptionsForResource = async (resource) => {
562
754
  if (!kv)
563
755
  return [];
564
756
  const result = [];
565
- const allSubs = await kv.hgetall(SUBSCRIPTIONS_HASH);
566
- for (const data of Object.values(allSubs)) {
567
- const subscription = deserializeSubscription(data);
568
- if (subscription.resource === resource) {
569
- subscription.relevantObjectIds = await loadRelevantObjects(subscription.id);
570
- result.push(subscription);
571
- }
757
+ const subs = await getSubscriptionsForResourceMap(resource);
758
+ for (const subscription of subs.values()) {
759
+ subscription.relevantObjectIds = await loadRelevantObjects(subscription.id);
760
+ result.push(subscription);
572
761
  }
573
762
  return result;
574
763
  };
@@ -576,12 +765,16 @@ export const updateSubscriptionSeq = async (subscriptionId, seq) => {
576
765
  const kv = getKV();
577
766
  if (!kv)
578
767
  return;
579
- const data = await kv.hget(SUBSCRIPTIONS_HASH, subscriptionId);
768
+ const resource = resourceFromSubscriptionId(subscriptionId);
769
+ if (!resource)
770
+ return;
771
+ const hashKey = subscriptionHashKey(resource);
772
+ const data = await kv.hget(hashKey, subscriptionId);
580
773
  if (!data)
581
774
  return;
582
775
  const subscription = deserializeSubscription(data);
583
776
  subscription.lastSeq = seq;
584
- await kv.hset(SUBSCRIPTIONS_HASH, subscriptionId, serializeSubscription(subscription));
777
+ await kv.hset(hashKey, subscriptionId, serializeSubscription(subscription));
585
778
  };
586
779
  export const getCatchupEvents = async (subscriptionId, sinceSeq) => {
587
780
  const subscription = await getSubscription(subscriptionId);
@@ -604,10 +797,13 @@ export const getHandlerSubscriptions = async (handlerId) => {
604
797
  const kv = getKV();
605
798
  if (!kv)
606
799
  return [];
800
+ // Fast path: handlers are process-local, so their subscriptions are tracked.
801
+ const tracked = localHandlerSubs.get(handlerId);
802
+ if (tracked)
803
+ return Array.from(tracked);
607
804
  const result = [];
608
- const allSubs = await kv.hgetall(SUBSCRIPTIONS_HASH);
609
- for (const [id, data] of Object.entries(allSubs)) {
610
- const sub = deserializeSubscription(data);
805
+ const allSubs = await getAllSubscriptions();
806
+ for (const [id, sub] of allSubs) {
611
807
  if (sub.handlerId === handlerId) {
612
808
  result.push(id);
613
809
  }
@@ -633,14 +829,13 @@ export const getSubscriptionStats = async () => {
633
829
  };
634
830
  }
635
831
  const subscriptionsByResource = {};
636
- const allSubs = await kv.hgetall(SUBSCRIPTIONS_HASH);
637
- for (const data of Object.values(allSubs)) {
638
- const sub = deserializeSubscription(data);
832
+ const allSubs = await getAllSubscriptions();
833
+ for (const sub of allSubs.values()) {
639
834
  subscriptionsByResource[sub.resource] =
640
835
  (subscriptionsByResource[sub.resource] ?? 0) + 1;
641
836
  }
642
837
  return {
643
- totalSubscriptions: Object.keys(allSubs).length,
838
+ totalSubscriptions: allSubs.size,
644
839
  totalHandlers: localHandlers.size,
645
840
  subscriptionsByResource,
646
841
  };
@@ -662,20 +857,82 @@ export const closeAllHandlers = () => {
662
857
  return closed;
663
858
  };
664
859
  export const getActiveHandlerCount = () => localHandlers.size;
860
+ // Snapshot of registered subscriptions (KV-backed or local), flagged by whether
861
+ // their SSE handler is connected to this process. Shaped for the admin UI's
862
+ // subscriptions view.
863
+ export const listActiveSubscriptions = async () => {
864
+ const all = await getAllSubscriptions();
865
+ const kv = getKV();
866
+ const result = [];
867
+ for (const s of all.values()) {
868
+ // The per-subscription seq counter increments once per delivered event, so
869
+ // it doubles as the event count.
870
+ let eventCount = 0;
871
+ if (kv) {
872
+ const raw = await kv.get(`${SUBSCRIPTION_SEQ_PREFIX}${s.id}`);
873
+ eventCount = raw ? parseInt(raw, 10) || 0 : 0;
874
+ }
875
+ else {
876
+ eventCount = localSeqCounters.get(s.id) ?? 0;
877
+ }
878
+ const lastEventAt = localEventTimestamps.get(s.id);
879
+ result.push({
880
+ id: s.id,
881
+ resource: s.resource,
882
+ filter: s.filter || undefined,
883
+ userId: s.authId ?? undefined,
884
+ connectedAt: s.createdAt instanceof Date ? s.createdAt.toISOString() : String(s.createdAt ?? ""),
885
+ eventCount,
886
+ lastEventAt: lastEventAt ? new Date(lastEventAt).toISOString() : undefined,
887
+ connected: localHandlerIds.has(s.handlerId),
888
+ });
889
+ }
890
+ return result;
891
+ };
892
+ // Force-close a subscription from the admin UI. Closing the local SSE writer
893
+ // triggers the route's cleanup (handler unregister + record removal). When the
894
+ // connection lives on another process, invalidate it (so the client refetches)
895
+ // and drop the record so delivery stops.
896
+ export const disconnectSubscription = async (subscriptionId) => {
897
+ const subscription = await getSubscription(subscriptionId);
898
+ if (!subscription)
899
+ return false;
900
+ const handler = localHandlers.get(subscription.handlerId);
901
+ if (handler) {
902
+ handler.close();
903
+ return true;
904
+ }
905
+ await sendInvalidateEvent(subscriptionId, "Disconnected by admin");
906
+ await removeSubscription(subscriptionId);
907
+ return true;
908
+ };
665
909
  export const clearAllSubscriptions = async () => {
666
910
  compiledFiltersCache.clear();
667
911
  localHandlers.clear();
668
912
  localHandlerIds.clear();
669
913
  localHandlerPolicies.clear();
914
+ localHandlerSubs.clear();
915
+ localSubHandlers.clear();
916
+ localSubscriptions.clear();
917
+ localRelevantObjects.clear();
918
+ localSeqCounters.clear();
919
+ localSubsByResource.clear();
920
+ localEventTimestamps.clear();
670
921
  eventSubscriptionInitialized = false;
671
922
  const kv = getKV();
672
923
  if (!kv)
673
924
  return;
674
- // Get all subscription IDs to clear their related keys
675
- const allSubs = await kv.hgetall(SUBSCRIPTIONS_HASH);
676
- const keysToDelete = [SUBSCRIPTIONS_HASH];
677
- for (const subId of Object.keys(allSubs)) {
678
- keysToDelete.push(`${SUBSCRIPTION_OBJECTS_PREFIX}${subId}`, `${SUBSCRIPTION_SEQ_PREFIX}${subId}`);
925
+ // Enumerate every resource shard and clear it plus each subscription's
926
+ // related keys, then drop the resource index itself.
927
+ const resources = await kv.smembers(SUBSCRIPTIONS_RESOURCE_INDEX);
928
+ const keysToDelete = [SUBSCRIPTIONS_RESOURCE_INDEX];
929
+ for (const resource of resources) {
930
+ const hashKey = subscriptionHashKey(resource);
931
+ keysToDelete.push(hashKey);
932
+ const subs = await kv.hgetall(hashKey);
933
+ for (const subId of Object.keys(subs)) {
934
+ keysToDelete.push(`${SUBSCRIPTION_OBJECTS_PREFIX}${subId}`, `${SUBSCRIPTION_SEQ_PREFIX}${subId}`);
935
+ }
679
936
  }
680
937
  if (keysToDelete.length > 0) {
681
938
  await kv.del(...keysToDelete);