@actuate-media/cms-core 0.23.0 → 0.24.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 (34) hide show
  1. package/dist/__tests__/api/notifications-routes.test.js +45 -0
  2. package/dist/__tests__/api/notifications-routes.test.js.map +1 -1
  3. package/dist/__tests__/realtime/notification-bus.test.d.ts +2 -0
  4. package/dist/__tests__/realtime/notification-bus.test.d.ts.map +1 -0
  5. package/dist/__tests__/realtime/notification-bus.test.js +79 -0
  6. package/dist/__tests__/realtime/notification-bus.test.js.map +1 -0
  7. package/dist/__tests__/realtime/notification-stream.test.d.ts +2 -0
  8. package/dist/__tests__/realtime/notification-stream.test.d.ts.map +1 -0
  9. package/dist/__tests__/realtime/notification-stream.test.js +157 -0
  10. package/dist/__tests__/realtime/notification-stream.test.js.map +1 -0
  11. package/dist/api/handlers.d.ts.map +1 -1
  12. package/dist/api/handlers.js +41 -3
  13. package/dist/api/handlers.js.map +1 -1
  14. package/dist/preview/sse-stream.d.ts +2 -5
  15. package/dist/preview/sse-stream.d.ts.map +1 -1
  16. package/dist/preview/sse-stream.js +7 -16
  17. package/dist/preview/sse-stream.js.map +1 -1
  18. package/dist/realtime/index.d.ts +3 -0
  19. package/dist/realtime/index.d.ts.map +1 -1
  20. package/dist/realtime/index.js +9 -0
  21. package/dist/realtime/index.js.map +1 -1
  22. package/dist/realtime/notification-bus.d.ts +60 -0
  23. package/dist/realtime/notification-bus.d.ts.map +1 -0
  24. package/dist/realtime/notification-bus.js +77 -0
  25. package/dist/realtime/notification-bus.js.map +1 -0
  26. package/dist/realtime/notification-stream.d.ts +50 -0
  27. package/dist/realtime/notification-stream.d.ts.map +1 -0
  28. package/dist/realtime/notification-stream.js +148 -0
  29. package/dist/realtime/notification-stream.js.map +1 -0
  30. package/dist/realtime/sse.d.ts +20 -0
  31. package/dist/realtime/sse.d.ts.map +1 -0
  32. package/dist/realtime/sse.js +31 -0
  33. package/dist/realtime/sse.js.map +1 -0
  34. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import type { PreviewSession } from './index.js';
2
+ import { encodeSseEvent } from '../realtime/sse.js';
3
+ export { encodeSseEvent };
2
4
  /**
3
5
  * Default keepalive interval. 25 s sits safely under the most aggressive
4
6
  * proxy/idle timeouts we've encountered in production (Cloudflare 30 s,
@@ -18,11 +20,6 @@ export interface PreviewStreamOptions {
18
20
  /** Override for tests so we don't tie up the suite for 4 minutes. */
19
21
  maxLifetimeMs?: number;
20
22
  }
21
- /**
22
- * Encode a single SSE message. `event` is optional; when omitted the
23
- * default `message` event is used.
24
- */
25
- export declare function encodeSseEvent(data: unknown, event?: string): string;
26
23
  /**
27
24
  * Build a `ReadableStream` of Server-Sent Events for the given preview
28
25
  * session. The stream:
@@ -1 +1 @@
1
- {"version":3,"file":"sse-stream.d.ts","sourceRoot":"","sources":["../../src/preview/sse-stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAUhD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAAS,CAAA;AAE9C;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QAAgB,CAAA;AAExD,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAUpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,GAAG,YAAY,CAAC,EAC1D,IAAI,GAAE,oBAAyB,GAC9B,cAAc,CAAC,UAAU,CAAC,CAwD5B;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOtD,CAAA;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,GAAG,YAAY,CAAC,EAC1D,IAAI,CAAC,EAAE,oBAAoB,GAC1B,QAAQ,CAKV"}
1
+ {"version":3,"file":"sse-stream.d.ts","sourceRoot":"","sources":["../../src/preview/sse-stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEnD,OAAO,EAAE,cAAc,EAAE,CAAA;AAezB;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAAS,CAAA;AAE9C;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QAAgB,CAAA;AAExD,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,GAAG,YAAY,CAAC,EAC1D,IAAI,GAAE,oBAAyB,GAC9B,cAAc,CAAC,UAAU,CAAC,CAwD5B;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOtD,CAAA;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,GAAG,YAAY,CAAC,EAC1D,IAAI,CAAC,EAAE,oBAAoB,GAC1B,QAAQ,CAKV"}
@@ -1,8 +1,15 @@
1
1
  import { subscribePreviewUpdates } from './event-bus.js';
2
+ import { encodeSseEvent } from '../realtime/sse.js';
3
+ export { encodeSseEvent };
2
4
  /**
3
5
  * SSE wire format helpers. We keep them small and pure so the route handler
4
6
  * can build a `ReadableStream<Uint8Array>` without pulling in any extra
5
7
  * dependencies (this also keeps the file edge-compatible).
8
+ *
9
+ * The encoder itself lives in `../realtime/sse.ts` — a single source of
10
+ * truth across every SSE channel in cms-core (preview + notifications,
11
+ * today). Re-exported here so existing callers (and tests) that import
12
+ * it from this module keep working.
6
13
  */
7
14
  const encoder = new TextEncoder();
8
15
  /**
@@ -18,22 +25,6 @@ export const PREVIEW_SSE_HEARTBEAT_MS = 25_000;
18
25
  * few minutes regardless of activity.
19
26
  */
20
27
  export const PREVIEW_SSE_MAX_LIFETIME_MS = 4 * 60 * 1000;
21
- /**
22
- * Encode a single SSE message. `event` is optional; when omitted the
23
- * default `message` event is used.
24
- */
25
- export function encodeSseEvent(data, event) {
26
- const lines = [];
27
- if (event)
28
- lines.push(`event: ${event}`);
29
- const serialized = typeof data === 'string' ? data : JSON.stringify(data);
30
- for (const line of serialized.split('\n')) {
31
- lines.push(`data: ${line}`);
32
- }
33
- lines.push('');
34
- lines.push('');
35
- return lines.join('\n');
36
- }
37
28
  /**
38
29
  * Build a `ReadableStream` of Server-Sent Events for the given preview
39
30
  * session. The stream:
@@ -1 +1 @@
1
- {"version":3,"file":"sse-stream.js","sourceRoot":"","sources":["../../src/preview/sse-stream.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sse-stream.js","sourceRoot":"","sources":["../../src/preview/sse-stream.ts"],"names":[],"mappings":"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"}
@@ -15,4 +15,7 @@ export { createRealtimeGateway, type AttachRealtimeGatewayOptions, type Authenti
15
15
  export { createPrismaDocumentPersistence, type PrismaDocumentCRDTClient, type PrismaDocumentPersistenceOptions, } from './persistence.js';
16
16
  export { createComment, deleteComment, listComments, normaliseBody, parseAnchor, reopenComment, resolveComment, toCommentDTO, updateComment, DEFAULT_COMMENT_LIST_PAGE_SIZE, MAX_COMMENT_BODY_LENGTH, MAX_COMMENT_LIST_PAGE_SIZE, type CommentAnchor, type CommentDTO, type CommentsDB, type CommentsError, type CommentsResult, type CreateCommentInput, type DocumentCommentRow, type ListCommentsFilter, type UpdateCommentInput, } from './comments.js';
17
17
  export { createNotification, excerptForNotification, extractMentionedUserIds, isNotificationKind, listForUser as listNotificationsForUser, markAllRead as markAllNotificationsRead, markRead as markNotificationRead, toNotificationDTO, unreadCount as notificationsUnreadCount, DEFAULT_NOTIFICATION_LIST_PAGE_SIZE, MAX_NOTIFICATION_LIST_PAGE_SIZE, type CreateNotificationInput, type DocumentNotificationRow, type ListNotificationsFilter, type NotificationDTO, type NotificationKind, type NotificationPayload, type NotificationsDB, type NotificationsError, type NotificationsResult, } from './notifications.js';
18
+ export { publishNotification, subscribeUserNotifications, notificationSubscriberCount, type NotificationListener, } from './notification-bus.js';
19
+ export { createNotificationStream, createNotificationStreamResponse, NOTIFICATION_SSE_HEADERS, NOTIFICATION_SSE_HEARTBEAT_MS, NOTIFICATION_SSE_MAX_LIFETIME_MS, type NotificationStreamOptions, } from './notification-stream.js';
20
+ export { encodeSseEvent } from './sse.js';
18
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,+BAA+B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,gCAAgC,GACtC,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,8BAA8B,EAC9B,uBAAuB,EACvB,0BAA0B,EAC1B,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,IAAI,wBAAwB,EACvC,WAAW,IAAI,wBAAwB,EACvC,QAAQ,IAAI,oBAAoB,EAChC,iBAAiB,EACjB,WAAW,IAAI,wBAAwB,EACvC,mCAAmC,EACnC,+BAA+B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,qBAAqB,EACrB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,+BAA+B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,gCAAgC,GACtC,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,8BAA8B,EAC9B,uBAAuB,EACvB,0BAA0B,EAC1B,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,IAAI,wBAAwB,EACvC,WAAW,IAAI,wBAAwB,EACvC,QAAQ,IAAI,oBAAoB,EAChC,iBAAiB,EACjB,WAAW,IAAI,wBAAwB,EACvC,mCAAmC,EACnC,+BAA+B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,uBAAuB,CAAA;AAM9B,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,wBAAwB,EACxB,6BAA6B,EAC7B,gCAAgC,EAChC,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAA;AAIjC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA"}
@@ -15,4 +15,13 @@ export { createRealtimeGateway, } from './gateway.js';
15
15
  export { createPrismaDocumentPersistence, } from './persistence.js';
16
16
  export { createComment, deleteComment, listComments, normaliseBody, parseAnchor, reopenComment, resolveComment, toCommentDTO, updateComment, DEFAULT_COMMENT_LIST_PAGE_SIZE, MAX_COMMENT_BODY_LENGTH, MAX_COMMENT_LIST_PAGE_SIZE, } from './comments.js';
17
17
  export { createNotification, excerptForNotification, extractMentionedUserIds, isNotificationKind, listForUser as listNotificationsForUser, markAllRead as markAllNotificationsRead, markRead as markNotificationRead, toNotificationDTO, unreadCount as notificationsUnreadCount, DEFAULT_NOTIFICATION_LIST_PAGE_SIZE, MAX_NOTIFICATION_LIST_PAGE_SIZE, } from './notifications.js';
18
+ export { publishNotification, subscribeUserNotifications, notificationSubscriberCount, } from './notification-bus.js';
19
+ // `__resetNotificationBus` is intentionally NOT re-exported here. It
20
+ // is a test helper — tests import it directly from
21
+ // `./notification-bus.js` so production consumers can't reach it via
22
+ // the package's public realtime surface.
23
+ export { createNotificationStream, createNotificationStreamResponse, NOTIFICATION_SSE_HEADERS, NOTIFICATION_SSE_HEARTBEAT_MS, NOTIFICATION_SSE_MAX_LIFETIME_MS, } from './notification-stream.js';
24
+ // Shared SSE encoder. Single source of truth across every Server-Sent
25
+ // Events stream in cms-core (preview + notifications).
26
+ export { encodeSseEvent } from './sse.js';
18
27
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,qBAAqB,GAMtB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,+BAA+B,GAGhC,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,8BAA8B,EAC9B,uBAAuB,EACvB,0BAA0B,GAU3B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,IAAI,wBAAwB,EACvC,WAAW,IAAI,wBAAwB,EACvC,QAAQ,IAAI,oBAAoB,EAChC,iBAAiB,EACjB,WAAW,IAAI,wBAAwB,EACvC,mCAAmC,EACnC,+BAA+B,GAUhC,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,qBAAqB,GAMtB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,+BAA+B,GAGhC,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,8BAA8B,EAC9B,uBAAuB,EACvB,0BAA0B,GAU3B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,IAAI,wBAAwB,EACvC,WAAW,IAAI,wBAAwB,EACvC,QAAQ,IAAI,oBAAoB,EAChC,iBAAiB,EACjB,WAAW,IAAI,wBAAwB,EACvC,mCAAmC,EACnC,+BAA+B,GAUhC,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,GAE5B,MAAM,uBAAuB,CAAA;AAC9B,qEAAqE;AACrE,mDAAmD;AACnD,qEAAqE;AACrE,yCAAyC;AAEzC,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,wBAAwB,EACxB,6BAA6B,EAC7B,gCAAgC,GAEjC,MAAM,0BAA0B,CAAA;AAEjC,sEAAsE;AACtE,uDAAuD;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * In-process pub/sub for per-user notification events.
3
+ *
4
+ * Mirrors the design of `preview/event-bus.ts` but keys subscriptions
5
+ * by `userId` instead of `(collection, documentId)`. When the comments
6
+ * hook layer persists a new `DocumentNotification`, it also calls
7
+ * `publishNotification(userId, dto)` so any active SSE stream for that
8
+ * user receives the row in real time. The HTTP layer still works for
9
+ * clients that don't subscribe — the bus is strictly additive.
10
+ *
11
+ * Why in-process:
12
+ *
13
+ * - Single-node deployments get push-to-bell delivery "for free" with
14
+ * zero infrastructure dependency. This is the dominant deployment
15
+ * shape for self-hosted Actuate today.
16
+ * - Multi-node deployments lose cross-node delivery — node A only
17
+ * pushes to its own subscribers. The poll fallback in the admin UI
18
+ * keeps the bell correct on those nodes; SSE is treated as a
19
+ * latency-win optimisation, not a correctness primitive. A future
20
+ * slice can swap this for a Redis/Postgres-listen-backed bus
21
+ * without changing the public contract (`publishNotification` /
22
+ * `subscribeUserNotifications`).
23
+ *
24
+ * Memory safety: the per-user `Set<Listener>` is dropped from the map
25
+ * when the last subscriber unsubscribes, so an idle process holding
26
+ * thousands of users does not pay any storage cost.
27
+ */
28
+ import type { NotificationDTO } from './notifications.js';
29
+ export type NotificationListener = (notification: NotificationDTO) => void;
30
+ /**
31
+ * Register a listener for `userId`'s notifications. Returns an
32
+ * unsubscribe function — callers MUST invoke it on disconnect, or the
33
+ * bus will leak listener references indefinitely (each listener pins
34
+ * its enclosing closure).
35
+ */
36
+ export declare function subscribeUserNotifications(userId: string, listener: NotificationListener): () => void;
37
+ /**
38
+ * Publish `notification` to every listener registered for its
39
+ * `userId`. Listener exceptions are isolated — one buggy stream must
40
+ * not take down every other subscriber for the same user (and never
41
+ * the calling write path).
42
+ *
43
+ * Returns `true` when at least one listener was reachable, `false`
44
+ * otherwise (useful in tests to assert SSE wiring; production code can
45
+ * ignore the return value).
46
+ */
47
+ export declare function publishNotification(notification: NotificationDTO): boolean;
48
+ /**
49
+ * How many active subscribers does this user have? Useful for tests
50
+ * and operator dashboards ("is anyone listening?"). Not part of the
51
+ * write-path contract.
52
+ */
53
+ export declare function notificationSubscriberCount(userId: string): number;
54
+ /**
55
+ * Test helper. Drops every subscription. Production code never calls
56
+ * this — listener leaks should be visible in the test suite, not
57
+ * papered over in production.
58
+ */
59
+ export declare function __resetNotificationBus(): void;
60
+ //# sourceMappingURL=notification-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-bus.d.ts","sourceRoot":"","sources":["../../src/realtime/notification-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,MAAM,MAAM,oBAAoB,GAAG,CAAC,YAAY,EAAE,eAAe,KAAK,IAAI,CAAA;AAI1E;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,oBAAoB,GAC7B,MAAM,IAAI,CAmBZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,eAAe,GAAG,OAAO,CAkB1E;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
@@ -0,0 +1,77 @@
1
+ const listenersByUser = new Map();
2
+ /**
3
+ * Register a listener for `userId`'s notifications. Returns an
4
+ * unsubscribe function — callers MUST invoke it on disconnect, or the
5
+ * bus will leak listener references indefinitely (each listener pins
6
+ * its enclosing closure).
7
+ */
8
+ export function subscribeUserNotifications(userId, listener) {
9
+ if (!userId) {
10
+ // Defensive: an empty userId would silently bucket every "unknown"
11
+ // subscriber into the same set, leaking notifications across
12
+ // sessions. Reject at the entry point.
13
+ throw new Error('subscribeUserNotifications requires a non-empty userId');
14
+ }
15
+ let set = listenersByUser.get(userId);
16
+ if (!set) {
17
+ set = new Set();
18
+ listenersByUser.set(userId, set);
19
+ }
20
+ set.add(listener);
21
+ return () => {
22
+ const current = listenersByUser.get(userId);
23
+ if (!current)
24
+ return;
25
+ current.delete(listener);
26
+ if (current.size === 0)
27
+ listenersByUser.delete(userId);
28
+ };
29
+ }
30
+ /**
31
+ * Publish `notification` to every listener registered for its
32
+ * `userId`. Listener exceptions are isolated — one buggy stream must
33
+ * not take down every other subscriber for the same user (and never
34
+ * the calling write path).
35
+ *
36
+ * Returns `true` when at least one listener was reachable, `false`
37
+ * otherwise (useful in tests to assert SSE wiring; production code can
38
+ * ignore the return value).
39
+ */
40
+ export function publishNotification(notification) {
41
+ // The DTO's `userId` is the source of truth; we deliberately don't
42
+ // accept a separate userId arg so callers can't accidentally publish
43
+ // to the wrong channel (e.g. publishing reply notifications to the
44
+ // actor instead of the recipient).
45
+ const set = listenersByUser.get(notification.userId);
46
+ if (!set || set.size === 0)
47
+ return false;
48
+ // Snapshot the listener set before iteration — a listener may
49
+ // synchronously unsubscribe (e.g. close its stream on the first
50
+ // event), and mutating the Set during forEach would skip neighbours.
51
+ for (const listener of [...set]) {
52
+ try {
53
+ listener(notification);
54
+ }
55
+ catch {
56
+ /* isolated; see contract above */
57
+ }
58
+ }
59
+ return true;
60
+ }
61
+ /**
62
+ * How many active subscribers does this user have? Useful for tests
63
+ * and operator dashboards ("is anyone listening?"). Not part of the
64
+ * write-path contract.
65
+ */
66
+ export function notificationSubscriberCount(userId) {
67
+ return listenersByUser.get(userId)?.size ?? 0;
68
+ }
69
+ /**
70
+ * Test helper. Drops every subscription. Production code never calls
71
+ * this — listener leaks should be visible in the test suite, not
72
+ * papered over in production.
73
+ */
74
+ export function __resetNotificationBus() {
75
+ listenersByUser.clear();
76
+ }
77
+ //# sourceMappingURL=notification-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-bus.js","sourceRoot":"","sources":["../../src/realtime/notification-bus.ts"],"names":[],"mappings":"AA+BA,MAAM,eAAe,GAAG,IAAI,GAAG,EAAqC,CAAA;AAEpE;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CACxC,MAAc,EACd,QAA8B;IAE9B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,mEAAmE;QACnE,6DAA6D;QAC7D,uCAAuC;QACvC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,IAAI,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACrC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;QACf,eAAe,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAClC,CAAC;IACD,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACjB,OAAO,GAAG,EAAE;QACV,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAC3C,IAAI,CAAC,OAAO;YAAE,OAAM;QACpB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QACxB,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACxD,CAAC,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,YAA6B;IAC/D,mEAAmE;IACnE,qEAAqE;IACrE,mEAAmE;IACnE,mCAAmC;IACnC,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;IACpD,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACxC,8DAA8D;IAC9D,gEAAgE;IAChE,qEAAqE;IACrE,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,QAAQ,CAAC,YAAY,CAAC,CAAA;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,kCAAkC;QACpC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CAAC,MAAc;IACxD,OAAO,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,CAAA;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB;IACpC,eAAe,CAAC,KAAK,EAAE,CAAA;AACzB,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { encodeSseEvent } from './sse.js';
2
+ export { encodeSseEvent };
3
+ /**
4
+ * Heartbeat interval. 25 s sits safely under the most aggressive
5
+ * proxy/idle timeouts we've encountered in production (Cloudflare 30 s,
6
+ * Vercel Serverless 30 s, most reverse-proxies 60 s). Identical to the
7
+ * preview stream's value so both channels behave the same way under
8
+ * the same proxy fleet.
9
+ */
10
+ export declare const NOTIFICATION_SSE_HEARTBEAT_MS = 25000;
11
+ /**
12
+ * Maximum stream lifetime in ms. After this, the server closes the
13
+ * stream cleanly so `EventSource` reconnects — important because some
14
+ * platforms (Vercel Fluid Compute, Cloudflare Workers) cap any single
15
+ * response at a few minutes regardless of activity.
16
+ */
17
+ export declare const NOTIFICATION_SSE_MAX_LIFETIME_MS: number;
18
+ export interface NotificationStreamOptions {
19
+ /** Override for tests so we don't have to wait 25 seconds. */
20
+ heartbeatMs?: number;
21
+ /** Override for tests so we don't tie up the suite for 4 minutes. */
22
+ maxLifetimeMs?: number;
23
+ }
24
+ /**
25
+ * Build a `ReadableStream<Uint8Array>` of notification SSE events for
26
+ * the given userId. See the file header for the wire format.
27
+ *
28
+ * The stream:
29
+ *
30
+ * 1. Emits the `ready` frame.
31
+ * 2. Subscribes to the notification bus for `userId`.
32
+ * 3. Forwards every `publishNotification(...)` to the client.
33
+ * 4. Sends heartbeats every `heartbeatMs`.
34
+ * 5. Closes cleanly after `maxLifetimeMs`, letting `EventSource`
35
+ * auto-reconnect (the browser does this for us at the HTTP/2
36
+ * stream layer).
37
+ *
38
+ * On `cancel()` (client disconnect or `controller.close()`), every
39
+ * timer is cleared and the bus subscription is dropped — no leaked
40
+ * listener after a hang-up.
41
+ */
42
+ export declare function createNotificationStream(userId: string, opts?: NotificationStreamOptions): ReadableStream<Uint8Array>;
43
+ /**
44
+ * Standard SSE response headers. Identical to the preview stream's
45
+ * header set — same proxy fleet, same X-Accel-Buffering disable, same
46
+ * cache-control directives.
47
+ */
48
+ export declare const NOTIFICATION_SSE_HEADERS: Record<string, string>;
49
+ export declare function createNotificationStreamResponse(userId: string, opts?: NotificationStreamOptions): Response;
50
+ //# sourceMappingURL=notification-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-stream.d.ts","sourceRoot":"","sources":["../../src/realtime/notification-stream.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAEzC,OAAO,EAAE,cAAc,EAAE,CAAA;AAIzB;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,QAAS,CAAA;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,QAAgB,CAAA;AAE7D,MAAM,WAAW,yBAAyB;IACxC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,yBAA8B,GACnC,cAAc,CAAC,UAAU,CAAC,CAmE5B;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAO3D,CAAA;AAED,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,yBAAyB,GAC/B,QAAQ,CAKV"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Per-user notification SSE stream.
3
+ *
4
+ * Mirrors `preview/sse-stream.ts` for the notification bus: subscribe
5
+ * to the user's channel, forward every `NotificationDTO` to the
6
+ * connected `EventSource` client, emit periodic heartbeats so proxies
7
+ * don't kill the idle connection, and close cleanly after a bounded
8
+ * lifetime so the client reconnects before platform max-duration caps
9
+ * fire.
10
+ *
11
+ * Wire format:
12
+ *
13
+ * event: ready (sent once on connect)
14
+ * data: {"userId":"<uid>"}
15
+ *
16
+ * event: notification (sent for each published DTO)
17
+ * data: <NotificationDTO JSON>
18
+ *
19
+ * : heartbeat\n\n (every 25 s, no `event:` line)
20
+ *
21
+ * The `ready` frame is the client's signal that the server-side
22
+ * subscription is wired up. Until it arrives, the client must NOT
23
+ * stop polling — there's still a window where the open() handshake
24
+ * has fired but the bus subscription hasn't been attached yet.
25
+ */
26
+ import { subscribeUserNotifications } from './notification-bus.js';
27
+ import { encodeSseEvent } from './sse.js';
28
+ export { encodeSseEvent };
29
+ const encoder = new TextEncoder();
30
+ /**
31
+ * Heartbeat interval. 25 s sits safely under the most aggressive
32
+ * proxy/idle timeouts we've encountered in production (Cloudflare 30 s,
33
+ * Vercel Serverless 30 s, most reverse-proxies 60 s). Identical to the
34
+ * preview stream's value so both channels behave the same way under
35
+ * the same proxy fleet.
36
+ */
37
+ export const NOTIFICATION_SSE_HEARTBEAT_MS = 25_000;
38
+ /**
39
+ * Maximum stream lifetime in ms. After this, the server closes the
40
+ * stream cleanly so `EventSource` reconnects — important because some
41
+ * platforms (Vercel Fluid Compute, Cloudflare Workers) cap any single
42
+ * response at a few minutes regardless of activity.
43
+ */
44
+ export const NOTIFICATION_SSE_MAX_LIFETIME_MS = 4 * 60 * 1000;
45
+ /**
46
+ * Build a `ReadableStream<Uint8Array>` of notification SSE events for
47
+ * the given userId. See the file header for the wire format.
48
+ *
49
+ * The stream:
50
+ *
51
+ * 1. Emits the `ready` frame.
52
+ * 2. Subscribes to the notification bus for `userId`.
53
+ * 3. Forwards every `publishNotification(...)` to the client.
54
+ * 4. Sends heartbeats every `heartbeatMs`.
55
+ * 5. Closes cleanly after `maxLifetimeMs`, letting `EventSource`
56
+ * auto-reconnect (the browser does this for us at the HTTP/2
57
+ * stream layer).
58
+ *
59
+ * On `cancel()` (client disconnect or `controller.close()`), every
60
+ * timer is cleared and the bus subscription is dropped — no leaked
61
+ * listener after a hang-up.
62
+ */
63
+ export function createNotificationStream(userId, opts = {}) {
64
+ if (!userId) {
65
+ throw new Error('createNotificationStream requires a non-empty userId');
66
+ }
67
+ const heartbeatMs = opts.heartbeatMs ?? NOTIFICATION_SSE_HEARTBEAT_MS;
68
+ const maxLifetimeMs = opts.maxLifetimeMs ?? NOTIFICATION_SSE_MAX_LIFETIME_MS;
69
+ let heartbeat;
70
+ let lifetime;
71
+ let unsubscribe;
72
+ let torn = false;
73
+ // Single teardown path shared by:
74
+ // - the lifetime timer (server-initiated close before the
75
+ // platform's max-duration cap fires), and
76
+ // - the consumer's reader.cancel() (client disconnect).
77
+ // `cancel()` is NOT invoked when the controller closes itself —
78
+ // only on consumer-initiated cancellation. Without this shared
79
+ // helper we'd leak the bus subscription whenever the lifetime
80
+ // timer fires.
81
+ const teardown = () => {
82
+ if (torn)
83
+ return;
84
+ torn = true;
85
+ if (heartbeat)
86
+ clearInterval(heartbeat);
87
+ if (lifetime)
88
+ clearTimeout(lifetime);
89
+ if (unsubscribe)
90
+ unsubscribe();
91
+ };
92
+ return new ReadableStream({
93
+ start(controller) {
94
+ const enqueue = (chunk) => {
95
+ try {
96
+ controller.enqueue(encoder.encode(chunk));
97
+ }
98
+ catch {
99
+ // Controller can be closed mid-emit if the client disconnected
100
+ // between the publishNotification call and the enqueue.
101
+ // Swallow — teardown() takes care of the rest.
102
+ }
103
+ };
104
+ enqueue(encodeSseEvent({ userId }, 'ready'));
105
+ unsubscribe = subscribeUserNotifications(userId, (notification) => {
106
+ enqueue(encodeSseEvent(notification, 'notification'));
107
+ });
108
+ heartbeat = setInterval(() => {
109
+ enqueue(`: heartbeat\n\n`);
110
+ }, heartbeatMs);
111
+ lifetime = setTimeout(() => {
112
+ // Drop the bus subscription BEFORE closing the controller —
113
+ // otherwise the listener would still be attached for the
114
+ // microtask between close() and the next event loop tick.
115
+ teardown();
116
+ try {
117
+ controller.close();
118
+ }
119
+ catch {
120
+ /* already closed */
121
+ }
122
+ }, maxLifetimeMs);
123
+ },
124
+ cancel() {
125
+ teardown();
126
+ },
127
+ });
128
+ }
129
+ /**
130
+ * Standard SSE response headers. Identical to the preview stream's
131
+ * header set — same proxy fleet, same X-Accel-Buffering disable, same
132
+ * cache-control directives.
133
+ */
134
+ export const NOTIFICATION_SSE_HEADERS = {
135
+ 'Content-Type': 'text/event-stream; charset=utf-8',
136
+ 'Cache-Control': 'no-cache, no-transform',
137
+ Connection: 'keep-alive',
138
+ // Discourage proxies/CDNs from buffering — without this nginx will
139
+ // hold events back until 4 KB of payload accumulates.
140
+ 'X-Accel-Buffering': 'no',
141
+ };
142
+ export function createNotificationStreamResponse(userId, opts) {
143
+ return new Response(createNotificationStream(userId, opts), {
144
+ status: 200,
145
+ headers: NOTIFICATION_SSE_HEADERS,
146
+ });
147
+ }
148
+ //# sourceMappingURL=notification-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-stream.js","sourceRoot":"","sources":["../../src/realtime/notification-stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Minimal SSE wire-format helpers shared by every Server-Sent Events
3
+ * stream in `cms-core` (preview updates and per-user notifications,
4
+ * today). Kept dependency-free so any future stream can `import` it
5
+ * without dragging in module state from sibling channels.
6
+ *
7
+ * A single source of truth here means a bug in the encoder is fixed
8
+ * once, not in N parallel copies.
9
+ */
10
+ /**
11
+ * Encode a single SSE message. `event` is optional; when omitted the
12
+ * default `message` event name is used (per the SSE spec). Multi-line
13
+ * payloads are split across multiple `data:` lines so the receiving
14
+ * `EventSource` reassembles them.
15
+ *
16
+ * The trailing blank line is the SSE event delimiter — DO NOT drop
17
+ * it. Browsers will hold the event in their buffer until they see it.
18
+ */
19
+ export declare function encodeSseEvent(data: unknown, event?: string): string;
20
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/realtime/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAUpE"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Minimal SSE wire-format helpers shared by every Server-Sent Events
3
+ * stream in `cms-core` (preview updates and per-user notifications,
4
+ * today). Kept dependency-free so any future stream can `import` it
5
+ * without dragging in module state from sibling channels.
6
+ *
7
+ * A single source of truth here means a bug in the encoder is fixed
8
+ * once, not in N parallel copies.
9
+ */
10
+ /**
11
+ * Encode a single SSE message. `event` is optional; when omitted the
12
+ * default `message` event name is used (per the SSE spec). Multi-line
13
+ * payloads are split across multiple `data:` lines so the receiving
14
+ * `EventSource` reassembles them.
15
+ *
16
+ * The trailing blank line is the SSE event delimiter — DO NOT drop
17
+ * it. Browsers will hold the event in their buffer until they see it.
18
+ */
19
+ export function encodeSseEvent(data, event) {
20
+ const lines = [];
21
+ if (event)
22
+ lines.push(`event: ${event}`);
23
+ const serialized = typeof data === 'string' ? data : JSON.stringify(data);
24
+ for (const line of serialized.split('\n')) {
25
+ lines.push(`data: ${line}`);
26
+ }
27
+ lines.push('');
28
+ lines.push('');
29
+ return lines.join('\n');
30
+ }
31
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/realtime/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,KAAc;IAC1D,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,EAAE,CAAC,CAAA;IACxC,MAAM,UAAU,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IACzE,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAA;IAC7B,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACd,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACd,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-core",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",