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.
- package/README.md +14 -9
- package/assets/logo.svg +19 -0
- package/dist/auth/adapter.d.ts +1 -0
- package/dist/auth/adapter.d.ts.map +1 -1
- package/dist/auth/adapter.js +37 -1
- package/dist/auth/adapter.js.map +1 -1
- package/dist/auth/routes.d.ts.map +1 -1
- package/dist/auth/routes.js +15 -2
- package/dist/auth/routes.js.map +1 -1
- package/dist/auth/types.d.ts +4 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/types.js +10 -0
- package/dist/auth/types.js.map +1 -1
- package/dist/cli/templates/readme.js +2 -2
- package/dist/client/aggregate-subscription.d.ts +32 -0
- package/dist/client/aggregate-subscription.d.ts.map +1 -0
- package/dist/client/aggregate-subscription.js +131 -0
- package/dist/client/aggregate-subscription.js.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/live-store.d.ts.map +1 -1
- package/dist/client/live-store.js +18 -3
- package/dist/client/live-store.js.map +1 -1
- package/dist/client/react.d.ts +25 -1
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +53 -0
- package/dist/client/react.js.map +1 -1
- package/dist/client/repository.d.ts +2 -0
- package/dist/client/repository.d.ts.map +1 -1
- package/dist/client/repository.js +9 -0
- package/dist/client/repository.js.map +1 -1
- package/dist/client/types.d.ts +17 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/securityHeaders.d.ts +1 -0
- package/dist/middleware/securityHeaders.d.ts.map +1 -1
- package/dist/middleware/securityHeaders.js +6 -3
- package/dist/middleware/securityHeaders.js.map +1 -1
- package/dist/resource/changelog.d.ts +3 -3
- package/dist/resource/changelog.d.ts.map +1 -1
- package/dist/resource/changelog.js +6 -3
- package/dist/resource/changelog.js.map +1 -1
- package/dist/resource/hook.d.ts.map +1 -1
- package/dist/resource/hook.js +141 -18
- package/dist/resource/hook.js.map +1 -1
- package/dist/resource/mutate.d.ts +3 -3
- package/dist/resource/mutate.d.ts.map +1 -1
- package/dist/resource/mutate.js +8 -8
- package/dist/resource/mutate.js.map +1 -1
- package/dist/resource/pagination.d.ts.map +1 -1
- package/dist/resource/pagination.js +32 -4
- package/dist/resource/pagination.js.map +1 -1
- package/dist/resource/query.d.ts +1 -0
- package/dist/resource/query.d.ts.map +1 -1
- package/dist/resource/query.js +12 -0
- package/dist/resource/query.js.map +1 -1
- package/dist/resource/subscription.d.ts +18 -1
- package/dist/resource/subscription.d.ts.map +1 -1
- package/dist/resource/subscription.js +310 -53
- package/dist/resource/subscription.js.map +1 -1
- package/dist/resource/track-mutations.d.ts.map +1 -1
- package/dist/resource/track-mutations.js +1 -1
- package/dist/resource/track-mutations.js.map +1 -1
- package/dist/resource/types.d.ts +2 -0
- package/dist/resource/types.d.ts.map +1 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +28 -3
- package/dist/server/app.js.map +1 -1
- package/dist/ui/html/client/data-explorer-app.d.ts +2 -0
- package/dist/ui/html/client/data-explorer-app.d.ts.map +1 -0
- package/dist/ui/html/client/data-explorer-app.js +441 -0
- package/dist/ui/html/client/data-explorer-app.js.map +1 -0
- package/dist/ui/html/client/htmx-vendor.d.ts +3 -0
- package/dist/ui/html/client/htmx-vendor.d.ts.map +1 -0
- package/dist/ui/html/client/htmx-vendor.js +5 -0
- package/dist/ui/html/client/htmx-vendor.js.map +1 -0
- package/dist/ui/html/client/runtime.d.ts +2 -0
- package/dist/ui/html/client/runtime.d.ts.map +1 -0
- package/dist/ui/html/client/runtime.js +198 -0
- package/dist/ui/html/client/runtime.js.map +1 -0
- package/dist/ui/html/components/index.d.ts +5 -1
- package/dist/ui/html/components/index.d.ts.map +1 -1
- package/dist/ui/html/components/index.js +10 -5
- package/dist/ui/html/components/index.js.map +1 -1
- package/dist/ui/html/icons.d.ts +3 -0
- package/dist/ui/html/icons.d.ts.map +1 -0
- package/dist/ui/html/icons.js +23 -0
- package/dist/ui/html/icons.js.map +1 -0
- package/dist/ui/html/layout.d.ts.map +1 -1
- package/dist/ui/html/layout.js +38 -18
- package/dist/ui/html/layout.js.map +1 -1
- package/dist/ui/html/logo.d.ts +2 -0
- package/dist/ui/html/logo.d.ts.map +1 -0
- package/dist/ui/html/logo.js +23 -0
- package/dist/ui/html/logo.js.map +1 -0
- package/dist/ui/html/pages/dashboard.d.ts.map +1 -1
- package/dist/ui/html/pages/dashboard.js +6 -5
- package/dist/ui/html/pages/dashboard.js.map +1 -1
- package/dist/ui/html/pages/data-explorer.d.ts.map +1 -1
- package/dist/ui/html/pages/data-explorer.js +5 -94
- package/dist/ui/html/pages/data-explorer.js.map +1 -1
- package/dist/ui/html/pages/kv-inspector.d.ts.map +1 -1
- package/dist/ui/html/pages/kv-inspector.js +0 -1
- package/dist/ui/html/pages/kv-inspector.js.map +1 -1
- package/dist/ui/html/pages/sessions.d.ts.map +1 -1
- package/dist/ui/html/pages/sessions.js +4 -3
- package/dist/ui/html/pages/sessions.js.map +1 -1
- package/dist/ui/html/pages/subscriptions.d.ts.map +1 -1
- package/dist/ui/html/pages/subscriptions.js +2 -3
- package/dist/ui/html/pages/subscriptions.js.map +1 -1
- package/dist/ui/html/pages/tasks.d.ts.map +1 -1
- package/dist/ui/html/pages/tasks.js +0 -1
- package/dist/ui/html/pages/tasks.js.map +1 -1
- package/dist/ui/html/pages/users.js +1 -1
- package/dist/ui/html/pages/users.js.map +1 -1
- package/dist/ui/html/styles.d.ts +1 -1
- package/dist/ui/html/styles.d.ts.map +1 -1
- package/dist/ui/html/styles.js +367 -40
- package/dist/ui/html/styles.js.map +1 -1
- package/dist/ui/html/utils.d.ts +1 -1
- package/dist/ui/html/utils.d.ts.map +1 -1
- package/dist/ui/html/utils.js +4 -0
- package/dist/ui/html/utils.js.map +1 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/middleware.d.ts +7 -2
- package/dist/ui/middleware.d.ts.map +1 -1
- package/dist/ui/middleware.js +362 -117
- package/dist/ui/middleware.js.map +1 -1
- package/dist/ui/schema-registry.d.ts +1 -0
- package/dist/ui/schema-registry.d.ts.map +1 -1
- package/dist/ui/schema-registry.js +1 -0
- package/dist/ui/schema-registry.js.map +1 -1
- 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":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,MAAM,EAA4B,MAAM,UAAU,CAAC;AAE5D,OAAO,EACL,YAAY,EAOZ,cAAc,EACf,MAAM,SAAS,CAAC;AAUjB,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,YAAY,GAAG,MAAM,CAAC;AAStE,eAAO,MAAM,oBAAoB,GAC/B,UAAU,MAAM,EAChB,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC/D,IAEF,CAAC;
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
for (const
|
|
198
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
//
|
|
224
|
-
|
|
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
|
|
229
|
-
for (const [subId, data] of Object.entries(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
531
|
-
for (const
|
|
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
|
|
566
|
-
for (const
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
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(
|
|
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
|
|
609
|
-
for (const [id,
|
|
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
|
|
637
|
-
for (const
|
|
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:
|
|
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
|
-
//
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
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);
|