@actuate-media/cms-core 0.21.0 → 0.22.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 (105) hide show
  1. package/dist/__tests__/actions/document-crud.test.js +74 -2
  2. package/dist/__tests__/actions/document-crud.test.js.map +1 -1
  3. package/dist/__tests__/api/cache-headers.test.d.ts +2 -0
  4. package/dist/__tests__/api/cache-headers.test.d.ts.map +1 -0
  5. package/dist/__tests__/api/cache-headers.test.js +135 -0
  6. package/dist/__tests__/api/cache-headers.test.js.map +1 -0
  7. package/dist/__tests__/api/comments-routes.test.d.ts +2 -0
  8. package/dist/__tests__/api/comments-routes.test.d.ts.map +1 -0
  9. package/dist/__tests__/api/comments-routes.test.js +266 -0
  10. package/dist/__tests__/api/comments-routes.test.js.map +1 -0
  11. package/dist/__tests__/api/notifications-routes.test.d.ts +2 -0
  12. package/dist/__tests__/api/notifications-routes.test.d.ts.map +1 -0
  13. package/dist/__tests__/api/notifications-routes.test.js +225 -0
  14. package/dist/__tests__/api/notifications-routes.test.js.map +1 -0
  15. package/dist/__tests__/api/preview-and-scheduling.test.js +53 -0
  16. package/dist/__tests__/api/preview-and-scheduling.test.js.map +1 -1
  17. package/dist/__tests__/fields/relations.test.js +62 -8
  18. package/dist/__tests__/fields/relations.test.js.map +1 -1
  19. package/dist/__tests__/preview-event-bus.test.d.ts +2 -0
  20. package/dist/__tests__/preview-event-bus.test.d.ts.map +1 -0
  21. package/dist/__tests__/preview-event-bus.test.js +89 -0
  22. package/dist/__tests__/preview-event-bus.test.js.map +1 -0
  23. package/dist/__tests__/preview-sse-stream.test.d.ts +2 -0
  24. package/dist/__tests__/preview-sse-stream.test.d.ts.map +1 -0
  25. package/dist/__tests__/preview-sse-stream.test.js +117 -0
  26. package/dist/__tests__/preview-sse-stream.test.js.map +1 -0
  27. package/dist/__tests__/realtime/comments.test.d.ts +2 -0
  28. package/dist/__tests__/realtime/comments.test.d.ts.map +1 -0
  29. package/dist/__tests__/realtime/comments.test.js +508 -0
  30. package/dist/__tests__/realtime/comments.test.js.map +1 -0
  31. package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts +2 -0
  32. package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts.map +1 -0
  33. package/dist/__tests__/realtime/gateway-ws-integration.test.js +170 -0
  34. package/dist/__tests__/realtime/gateway-ws-integration.test.js.map +1 -0
  35. package/dist/__tests__/realtime/gateway.test.d.ts +2 -0
  36. package/dist/__tests__/realtime/gateway.test.d.ts.map +1 -0
  37. package/dist/__tests__/realtime/gateway.test.js +402 -0
  38. package/dist/__tests__/realtime/gateway.test.js.map +1 -0
  39. package/dist/__tests__/realtime/notifications.test.d.ts +2 -0
  40. package/dist/__tests__/realtime/notifications.test.d.ts.map +1 -0
  41. package/dist/__tests__/realtime/notifications.test.js +317 -0
  42. package/dist/__tests__/realtime/notifications.test.js.map +1 -0
  43. package/dist/__tests__/realtime/persistence.test.d.ts +2 -0
  44. package/dist/__tests__/realtime/persistence.test.d.ts.map +1 -0
  45. package/dist/__tests__/realtime/persistence.test.js +124 -0
  46. package/dist/__tests__/realtime/persistence.test.js.map +1 -0
  47. package/dist/__tests__/schema-preview-features.test.d.ts +2 -0
  48. package/dist/__tests__/schema-preview-features.test.d.ts.map +1 -0
  49. package/dist/__tests__/schema-preview-features.test.js +42 -0
  50. package/dist/__tests__/schema-preview-features.test.js.map +1 -0
  51. package/dist/actions.d.ts.map +1 -1
  52. package/dist/actions.js +71 -26
  53. package/dist/actions.js.map +1 -1
  54. package/dist/api/handlers.d.ts.map +1 -1
  55. package/dist/api/handlers.js +734 -188
  56. package/dist/api/handlers.js.map +1 -1
  57. package/dist/cache/__tests__/http.test.d.ts +2 -0
  58. package/dist/cache/__tests__/http.test.d.ts.map +1 -0
  59. package/dist/cache/__tests__/http.test.js +285 -0
  60. package/dist/cache/__tests__/http.test.js.map +1 -0
  61. package/dist/cache/http.d.ts +119 -0
  62. package/dist/cache/http.d.ts.map +1 -0
  63. package/dist/cache/http.js +188 -0
  64. package/dist/cache/http.js.map +1 -0
  65. package/dist/config/types.d.ts +92 -0
  66. package/dist/config/types.d.ts.map +1 -1
  67. package/dist/fields/relations.d.ts.map +1 -1
  68. package/dist/fields/relations.js +43 -11
  69. package/dist/fields/relations.js.map +1 -1
  70. package/dist/index.d.ts +5 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/page-builder/schema.d.ts +2 -2
  75. package/dist/preview/event-bus.d.ts +39 -0
  76. package/dist/preview/event-bus.d.ts.map +1 -0
  77. package/dist/preview/event-bus.js +73 -0
  78. package/dist/preview/event-bus.js.map +1 -0
  79. package/dist/preview/sse-stream.d.ts +50 -0
  80. package/dist/preview/sse-stream.d.ts.map +1 -0
  81. package/dist/preview/sse-stream.js +120 -0
  82. package/dist/preview/sse-stream.js.map +1 -0
  83. package/dist/realtime/comments.d.ts +217 -0
  84. package/dist/realtime/comments.d.ts.map +1 -0
  85. package/dist/realtime/comments.js +282 -0
  86. package/dist/realtime/comments.js.map +1 -0
  87. package/dist/realtime/gateway.d.ts +107 -0
  88. package/dist/realtime/gateway.d.ts.map +1 -0
  89. package/dist/realtime/gateway.js +215 -0
  90. package/dist/realtime/gateway.js.map +1 -0
  91. package/dist/realtime/index.d.ts +18 -0
  92. package/dist/realtime/index.d.ts.map +1 -0
  93. package/dist/realtime/index.js +18 -0
  94. package/dist/realtime/index.js.map +1 -0
  95. package/dist/realtime/notifications.d.ts +209 -0
  96. package/dist/realtime/notifications.d.ts.map +1 -0
  97. package/dist/realtime/notifications.js +209 -0
  98. package/dist/realtime/notifications.js.map +1 -0
  99. package/dist/realtime/persistence.d.ts +72 -0
  100. package/dist/realtime/persistence.d.ts.map +1 -0
  101. package/dist/realtime/persistence.js +53 -0
  102. package/dist/realtime/persistence.js.map +1 -0
  103. package/package.json +13 -4
  104. package/prisma/cms-schema.prisma +135 -4
  105. package/prisma/schema.prisma +17 -3
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Realtime gateway for `@actuate-media/realtime` rooms.
3
+ *
4
+ * The gateway is **transport-agnostic by design**: it does not depend on
5
+ * `ws`, `uWebSockets.js`, Cloudflare Workers, or any specific framework.
6
+ * Consumers create a `WebSocketLike` adapter around their server's socket
7
+ * primitive and pass it to `gateway.handleConnection(socket, req)`.
8
+ *
9
+ * Responsibilities:
10
+ * 1. Authenticate the connection (delegated to `options.authenticate`).
11
+ * 2. Look up (or lazily create) the `Room` for the document.
12
+ * 3. Pump bytes between the socket and the room.
13
+ * 4. Persist edits via the configured `DocumentPersistence`.
14
+ * 5. Reap idle rooms (no connections + no pending writes) to bound memory.
15
+ *
16
+ * The companion file `persistence.ts` provides
17
+ * `createPrismaDocumentPersistence(db)` which the gateway uses by default
18
+ * when callers don't supply their own.
19
+ */
20
+ import { type DocumentPersistence } from '@actuate-media/realtime';
21
+ /**
22
+ * Minimal contract the gateway needs from a real WebSocket implementation.
23
+ * Compatible with `ws.WebSocket`, browser-native `WebSocket`, the
24
+ * Cloudflare Workers `WebSocket` Hibernation API, and Bun's WebSocket.
25
+ *
26
+ * Adapter authors typically just wrap their library's socket in a thin
27
+ * shim that conforms to this shape.
28
+ */
29
+ export interface WebSocketLike {
30
+ /** Send a binary frame. */
31
+ send(data: Uint8Array): void;
32
+ /** Close the socket. Idempotent. */
33
+ close(code?: number, reason?: string): void;
34
+ /** Subscribe to inbound binary frames. */
35
+ on(event: 'message', handler: (data: Uint8Array) => void): void;
36
+ /** Subscribe to close events. */
37
+ on(event: 'close', handler: () => void): void;
38
+ /** Subscribe to error events. */
39
+ on(event: 'error', handler: (err: Error) => void): void;
40
+ }
41
+ /** Information about the inbound HTTP upgrade request. */
42
+ export interface UpgradeRequestInfo {
43
+ /** Path + query, e.g. `/api/cms/realtime/sync?documentId=doc_1`. */
44
+ url: string;
45
+ /** Lowercased header bag. Authentication usually reads `cookie` or `authorization`. */
46
+ headers: Readonly<Record<string, string | undefined>>;
47
+ }
48
+ /**
49
+ * Outcome of the authentication / routing decision. Return `null` to reject
50
+ * the connection (the gateway will close it with code 4401).
51
+ *
52
+ * `connectionId` should be globally unique within the gateway instance —
53
+ * usually a `crypto.randomUUID()` or `req.headers['sec-websocket-key']`.
54
+ */
55
+ export interface AuthenticatedConnection {
56
+ documentId: string;
57
+ connectionId: string;
58
+ /** Tag returned via `room.onUpdate` so applications can audit edits. */
59
+ userId?: string;
60
+ }
61
+ export interface AttachRealtimeGatewayOptions {
62
+ /**
63
+ * Validate the upgrade request and map it to a documentId. Return `null`
64
+ * to reject. Throws are treated as 401 and the connection is closed.
65
+ */
66
+ authenticate: (req: UpgradeRequestInfo) => Promise<AuthenticatedConnection | null>;
67
+ /** Storage adapter. Defaults to a noop in-memory persistence (rooms are not durable). */
68
+ persistence?: DocumentPersistence;
69
+ /**
70
+ * Reap a room after it has had zero connections for this many ms. Default 60000.
71
+ * Set to `Infinity` to disable reaping.
72
+ */
73
+ idleTimeoutMs?: number;
74
+ /** Debounce window for persistence writes (ms). Forwarded to `bindPersistence`. */
75
+ persistenceDebounceMs?: number;
76
+ /** Maximum time between writes (ms). Forwarded to `bindPersistence`. */
77
+ persistenceMaxIdleMs?: number;
78
+ /** Called whenever a room is created. Useful for metrics. */
79
+ onRoomOpen?: (documentId: string) => void;
80
+ /** Called whenever a room is destroyed. Useful for metrics. */
81
+ onRoomClose?: (documentId: string) => void;
82
+ /** Called when a persistence save fails. Defaults to console.error. */
83
+ onPersistenceError?: (documentId: string, err: unknown) => void;
84
+ }
85
+ export interface RealtimeGateway {
86
+ /** Number of currently active rooms. Useful for /health endpoints. */
87
+ readonly roomCount: number;
88
+ /** Number of currently connected sockets across all rooms. */
89
+ readonly connectionCount: number;
90
+ /**
91
+ * Bind an authenticated socket to its room. Call this from your HTTP
92
+ * upgrade handler after `wss.handleUpgrade(...)` has produced a socket.
93
+ */
94
+ handleConnection(socket: WebSocketLike, req: UpgradeRequestInfo): Promise<void>;
95
+ /**
96
+ * Force-close every connection, flush every room, and tear everything
97
+ * down. Call from your process's SIGTERM handler.
98
+ */
99
+ shutdown(): Promise<void>;
100
+ }
101
+ /**
102
+ * Create a realtime gateway. The returned object is intended to live for the
103
+ * lifetime of the server process — one per node. Calling
104
+ * `shutdown()` is mandatory for graceful exit (it flushes pending writes).
105
+ */
106
+ export declare function createRealtimeGateway(opts: AttachRealtimeGatewayOptions): RealtimeGateway;
107
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/realtime/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAIL,KAAK,mBAAmB,EAEzB,MAAM,yBAAyB,CAAA;AAEhC;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IAC5B,oCAAoC;IACpC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C,0CAA0C;IAC1C,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAA;IAC/D,iCAAiC;IACjC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC7C,iCAAiC;IACjC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAA;CACxD;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAA;IACX,uFAAuF;IACvF,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;CACtD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,YAAY,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAA;IAClF,yFAAyF;IACzF,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,wEAAwE;IACxE,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CAChE;AAED,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC;;;OAGG;IACH,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E;;;OAGG;IACH,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,eAAe,CAuLzF"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Realtime gateway for `@actuate-media/realtime` rooms.
3
+ *
4
+ * The gateway is **transport-agnostic by design**: it does not depend on
5
+ * `ws`, `uWebSockets.js`, Cloudflare Workers, or any specific framework.
6
+ * Consumers create a `WebSocketLike` adapter around their server's socket
7
+ * primitive and pass it to `gateway.handleConnection(socket, req)`.
8
+ *
9
+ * Responsibilities:
10
+ * 1. Authenticate the connection (delegated to `options.authenticate`).
11
+ * 2. Look up (or lazily create) the `Room` for the document.
12
+ * 3. Pump bytes between the socket and the room.
13
+ * 4. Persist edits via the configured `DocumentPersistence`.
14
+ * 5. Reap idle rooms (no connections + no pending writes) to bound memory.
15
+ *
16
+ * The companion file `persistence.ts` provides
17
+ * `createPrismaDocumentPersistence(db)` which the gateway uses by default
18
+ * when callers don't supply their own.
19
+ */
20
+ import { bindPersistence, createRoom, } from '@actuate-media/realtime';
21
+ /**
22
+ * Create a realtime gateway. The returned object is intended to live for the
23
+ * lifetime of the server process — one per node. Calling
24
+ * `shutdown()` is mandatory for graceful exit (it flushes pending writes).
25
+ */
26
+ export function createRealtimeGateway(opts) {
27
+ const persistence = opts.persistence ?? createNoopPersistence();
28
+ const idleTimeoutMs = opts.idleTimeoutMs ?? 60_000;
29
+ const onError = opts.onPersistenceError ??
30
+ ((documentId, err) => {
31
+ // eslint-disable-next-line no-console
32
+ console.error(`[cms-core realtime] save failed for ${documentId}:`, err);
33
+ });
34
+ const rooms = new Map();
35
+ let connectionCount = 0;
36
+ let shuttingDown = false;
37
+ async function getOrCreateRoom(documentId) {
38
+ const existing = rooms.get(documentId);
39
+ if (existing) {
40
+ if (existing.reapTimer) {
41
+ clearTimeout(existing.reapTimer);
42
+ existing.reapTimer = null;
43
+ }
44
+ return existing;
45
+ }
46
+ const initial = (await persistence.load(documentId)) ?? undefined;
47
+ const room = createRoom({ documentId, initialState: initial });
48
+ const binding = bindPersistence(room, persistence, {
49
+ debounceMs: opts.persistenceDebounceMs,
50
+ maxIdleMs: opts.persistenceMaxIdleMs,
51
+ onError: (err) => onError(documentId, err),
52
+ });
53
+ const entry = { room, binding, connections: 0, reapTimer: null };
54
+ rooms.set(documentId, entry);
55
+ opts.onRoomOpen?.(documentId);
56
+ return entry;
57
+ }
58
+ function scheduleReap(documentId) {
59
+ const entry = rooms.get(documentId);
60
+ if (!entry)
61
+ return;
62
+ if (entry.connections > 0)
63
+ return;
64
+ if (entry.reapTimer)
65
+ return;
66
+ if (!Number.isFinite(idleTimeoutMs))
67
+ return;
68
+ entry.reapTimer = setTimeout(() => {
69
+ void destroyRoom(documentId);
70
+ }, idleTimeoutMs);
71
+ }
72
+ async function destroyRoom(documentId) {
73
+ const entry = rooms.get(documentId);
74
+ if (!entry)
75
+ return;
76
+ rooms.delete(documentId);
77
+ if (entry.reapTimer) {
78
+ clearTimeout(entry.reapTimer);
79
+ entry.reapTimer = null;
80
+ }
81
+ try {
82
+ await entry.binding.flush();
83
+ }
84
+ catch (err) {
85
+ onError(documentId, err);
86
+ }
87
+ entry.binding.unbind();
88
+ entry.room.destroy();
89
+ opts.onRoomClose?.(documentId);
90
+ }
91
+ async function handleConnection(socket, req) {
92
+ if (shuttingDown) {
93
+ try {
94
+ socket.close(1001, 'server shutting down');
95
+ }
96
+ catch {
97
+ /* ignore */
98
+ }
99
+ return;
100
+ }
101
+ let auth;
102
+ try {
103
+ auth = await opts.authenticate(req);
104
+ }
105
+ catch {
106
+ auth = null;
107
+ }
108
+ if (!auth) {
109
+ try {
110
+ socket.close(4401, 'unauthorized');
111
+ }
112
+ catch {
113
+ /* ignore */
114
+ }
115
+ return;
116
+ }
117
+ const entry = await getOrCreateRoom(auth.documentId);
118
+ if (entry.reapTimer) {
119
+ clearTimeout(entry.reapTimer);
120
+ entry.reapTimer = null;
121
+ }
122
+ const conn = {
123
+ id: auth.connectionId,
124
+ send: (bytes) => {
125
+ try {
126
+ socket.send(bytes);
127
+ }
128
+ catch {
129
+ /* surfaced via close handler */
130
+ }
131
+ },
132
+ close: (reason) => {
133
+ try {
134
+ socket.close(1000, reason ?? 'closed');
135
+ }
136
+ catch {
137
+ /* ignore */
138
+ }
139
+ },
140
+ };
141
+ try {
142
+ entry.room.addConnection(conn);
143
+ }
144
+ catch {
145
+ // duplicate connection id — close and bail out.
146
+ try {
147
+ socket.close(4409, 'duplicate connection id');
148
+ }
149
+ catch {
150
+ /* ignore */
151
+ }
152
+ return;
153
+ }
154
+ entry.connections += 1;
155
+ connectionCount += 1;
156
+ socket.on('message', (data) => {
157
+ try {
158
+ entry.room.handleMessage(auth.connectionId, data);
159
+ }
160
+ catch {
161
+ // Malformed frame — close the offending peer; other connections continue.
162
+ try {
163
+ socket.close(1003, 'malformed frame');
164
+ }
165
+ catch {
166
+ /* ignore */
167
+ }
168
+ }
169
+ });
170
+ socket.on('error', () => {
171
+ try {
172
+ socket.close(1011, 'socket error');
173
+ }
174
+ catch {
175
+ /* ignore */
176
+ }
177
+ });
178
+ socket.on('close', () => {
179
+ entry.room.removeConnection(auth.connectionId);
180
+ entry.connections = Math.max(0, entry.connections - 1);
181
+ connectionCount = Math.max(0, connectionCount - 1);
182
+ if (entry.connections === 0) {
183
+ scheduleReap(auth.documentId);
184
+ }
185
+ });
186
+ }
187
+ async function shutdown() {
188
+ shuttingDown = true;
189
+ const docIds = Array.from(rooms.keys());
190
+ await Promise.all(docIds.map((id) => destroyRoom(id)));
191
+ connectionCount = 0;
192
+ }
193
+ return {
194
+ get roomCount() {
195
+ return rooms.size;
196
+ },
197
+ get connectionCount() {
198
+ return connectionCount;
199
+ },
200
+ handleConnection,
201
+ shutdown,
202
+ };
203
+ }
204
+ /** Persistence that swallows writes — used when consumers don't pass one. */
205
+ function createNoopPersistence() {
206
+ return {
207
+ async load() {
208
+ return null;
209
+ },
210
+ async save() {
211
+ /* discard */
212
+ },
213
+ };
214
+ }
215
+ //# sourceMappingURL=gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../src/realtime/gateway.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Realtime collaboration surface for `@actuate-media/cms-core`.
3
+ *
4
+ * - `createRealtimeGateway` — transport-agnostic gateway that owns rooms,
5
+ * debounces persistence, and reaps idle documents. Consumers wrap their
6
+ * server's socket primitive in a `WebSocketLike` adapter and feed it via
7
+ * `gateway.handleConnection(socket, req)`.
8
+ * - `createPrismaDocumentPersistence` — Prisma-backed implementation of
9
+ * `DocumentPersistence` against the `DocumentCRDT` table.
10
+ *
11
+ * See `packages/realtime/README.md` for the protocol details and
12
+ * `docs/roadmap-status.md` for the Phase 3 slice plan.
13
+ */
14
+ export { createRealtimeGateway, type AttachRealtimeGatewayOptions, type AuthenticatedConnection, type RealtimeGateway, type UpgradeRequestInfo, type WebSocketLike, } from './gateway.js';
15
+ export { createPrismaDocumentPersistence, type PrismaDocumentCRDTClient, type PrismaDocumentPersistenceOptions, } from './persistence.js';
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
+ 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
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Realtime collaboration surface for `@actuate-media/cms-core`.
3
+ *
4
+ * - `createRealtimeGateway` — transport-agnostic gateway that owns rooms,
5
+ * debounces persistence, and reaps idle documents. Consumers wrap their
6
+ * server's socket primitive in a `WebSocketLike` adapter and feed it via
7
+ * `gateway.handleConnection(socket, req)`.
8
+ * - `createPrismaDocumentPersistence` — Prisma-backed implementation of
9
+ * `DocumentPersistence` against the `DocumentCRDT` table.
10
+ *
11
+ * See `packages/realtime/README.md` for the protocol details and
12
+ * `docs/roadmap-status.md` for the Phase 3 slice plan.
13
+ */
14
+ export { createRealtimeGateway, } from './gateway.js';
15
+ export { createPrismaDocumentPersistence, } from './persistence.js';
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
+ 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
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Per-user notifications driven by collaboration events.
3
+ *
4
+ * Owns the pure notification logic: kind discriminator, payload validation,
5
+ * permission gates, and a storage-agnostic CRUD surface. The HTTP layer
6
+ * (in `api/handlers.ts`) is a thin wrapper that handles auth + JSON
7
+ * envelope; the comments hook layer (slice 6, `notification-triggers.ts`)
8
+ * calls these helpers when a comment lifecycle event fires.
9
+ *
10
+ * Storage shape — see `packages/cms-core/prisma/cms-schema.prisma`, model
11
+ * `DocumentNotification`. We require only a minimal Prisma surface so the
12
+ * helpers can be tested against an in-memory fake.
13
+ *
14
+ * ## Discriminated payloads
15
+ *
16
+ * The `payload` Json column varies by `kind`. The TypeScript types here
17
+ * are the source of truth for each variant; the service helpers narrow
18
+ * by `kind` so callers can't accidentally insert a malformed payload.
19
+ *
20
+ * ## Mentions
21
+ *
22
+ * Slice 6 ships a deliberately simple mention parser: `@<userId>` tokens
23
+ * in a comment body trigger a `comment_mention` notification. A richer
24
+ * `@username` parser that resolves against the user table is a Phase 4
25
+ * follow-up — the storage shape already supports it.
26
+ */
27
+ /** Hard cap on returned notifications per `listForUser` call. */
28
+ export declare const MAX_NOTIFICATION_LIST_PAGE_SIZE = 200;
29
+ /** Default page size when the caller does not request one. */
30
+ export declare const DEFAULT_NOTIFICATION_LIST_PAGE_SIZE = 50;
31
+ /**
32
+ * Notification kind discriminator. New variants extend the `Payload`
33
+ * union — never replace existing kinds, because old rows live forever
34
+ * in the table.
35
+ */
36
+ export type NotificationKind = 'comment_reply' | 'comment_resolved' | 'comment_mention';
37
+ export interface CommentReplyPayload {
38
+ /** User who replied. */
39
+ actorId: string | null;
40
+ actorName: string | null;
41
+ /** First 120 chars of the reply body, for the bell preview. */
42
+ excerpt: string;
43
+ }
44
+ export interface CommentResolvedPayload {
45
+ actorId: string | null;
46
+ actorName: string | null;
47
+ /** Excerpt of the resolved root comment, so the bell can show context. */
48
+ rootExcerpt: string;
49
+ }
50
+ export interface CommentMentionPayload {
51
+ actorId: string | null;
52
+ actorName: string | null;
53
+ /** Excerpt of the body that mentioned the recipient. */
54
+ excerpt: string;
55
+ }
56
+ export type NotificationPayload = ({
57
+ kind: 'comment_reply';
58
+ } & CommentReplyPayload) | ({
59
+ kind: 'comment_resolved';
60
+ } & CommentResolvedPayload) | ({
61
+ kind: 'comment_mention';
62
+ } & CommentMentionPayload);
63
+ /** Public REST shape. Mirrors the row plus the discriminated payload. */
64
+ export interface NotificationDTO {
65
+ id: string;
66
+ userId: string;
67
+ kind: NotificationKind;
68
+ documentId: string | null;
69
+ commentId: string | null;
70
+ payload: NotificationPayload;
71
+ readAt: string | null;
72
+ createdAt: string;
73
+ }
74
+ export interface CreateNotificationInput {
75
+ userId: string;
76
+ documentId?: string | null;
77
+ commentId?: string | null;
78
+ payload: NotificationPayload;
79
+ }
80
+ export interface ListNotificationsFilter {
81
+ userId: string;
82
+ /** Default: false. When true, returns read + unread notifications. */
83
+ includeRead?: boolean;
84
+ /** Default: DEFAULT_NOTIFICATION_LIST_PAGE_SIZE. Hard upper bound applied. */
85
+ pageSize?: number;
86
+ }
87
+ export type NotificationsError = {
88
+ code: 'validation';
89
+ message: string;
90
+ } | {
91
+ code: 'forbidden';
92
+ message: string;
93
+ } | {
94
+ code: 'not_found';
95
+ message: string;
96
+ };
97
+ export type NotificationsResult<T> = {
98
+ ok: true;
99
+ value: T;
100
+ } | {
101
+ ok: false;
102
+ error: NotificationsError;
103
+ };
104
+ /**
105
+ * Minimal subset of Prisma's notification delegate we depend on. Keeping
106
+ * this narrow lets the unit tests use a fake implementation that holds
107
+ * rows in an array — no schema generation required.
108
+ */
109
+ export interface DocumentNotificationRow {
110
+ id: string;
111
+ userId: string;
112
+ kind: string;
113
+ documentId: string | null;
114
+ commentId: string | null;
115
+ payload: unknown;
116
+ readAt: Date | null;
117
+ createdAt: Date;
118
+ }
119
+ export interface NotificationsDB {
120
+ documentNotification: {
121
+ create(args: {
122
+ data: Omit<DocumentNotificationRow, 'id' | 'createdAt' | 'readAt'> & {
123
+ readAt?: Date | null;
124
+ };
125
+ }): Promise<DocumentNotificationRow>;
126
+ findUnique(args: {
127
+ where: {
128
+ id: string;
129
+ };
130
+ }): Promise<DocumentNotificationRow | null>;
131
+ findMany(args: {
132
+ where: {
133
+ userId: string;
134
+ readAt?: null | {
135
+ not: null;
136
+ };
137
+ };
138
+ orderBy?: {
139
+ createdAt: 'desc' | 'asc';
140
+ };
141
+ take?: number;
142
+ }): Promise<DocumentNotificationRow[]>;
143
+ update(args: {
144
+ where: {
145
+ id: string;
146
+ };
147
+ data: {
148
+ readAt?: Date | null;
149
+ };
150
+ }): Promise<DocumentNotificationRow>;
151
+ updateMany(args: {
152
+ where: {
153
+ userId: string;
154
+ readAt?: null;
155
+ };
156
+ data: {
157
+ readAt: Date;
158
+ };
159
+ }): Promise<{
160
+ count: number;
161
+ }>;
162
+ count(args: {
163
+ where: {
164
+ userId: string;
165
+ readAt: null;
166
+ };
167
+ }): Promise<number>;
168
+ };
169
+ }
170
+ export declare function toNotificationDTO(row: DocumentNotificationRow): NotificationDTO;
171
+ export declare function isNotificationKind(value: unknown): value is NotificationKind;
172
+ /**
173
+ * Insert a new notification. The payload's `kind` field is the source
174
+ * of truth; `kind` on the DB row is derived from it so callers can't
175
+ * drift them apart.
176
+ */
177
+ export declare function createNotification(db: NotificationsDB, input: CreateNotificationInput): Promise<NotificationsResult<NotificationDTO>>;
178
+ /**
179
+ * List notifications for a user, newest first. By default returns only
180
+ * unread; pass `includeRead: true` for the full feed (the bell dropdown
181
+ * uses both depending on the tab the user is on).
182
+ */
183
+ export declare function listForUser(db: NotificationsDB, filter: ListNotificationsFilter): Promise<NotificationsResult<NotificationDTO[]>>;
184
+ /** Count unread notifications for the bell badge. */
185
+ export declare function unreadCount(db: NotificationsDB, userId: string): Promise<NotificationsResult<number>>;
186
+ /** Mark a single notification read (idempotent). */
187
+ export declare function markRead(db: NotificationsDB, id: string, actor: {
188
+ userId: string;
189
+ }): Promise<NotificationsResult<NotificationDTO>>;
190
+ /** Mark every unread notification for the user as read. */
191
+ export declare function markAllRead(db: NotificationsDB, userId: string): Promise<NotificationsResult<{
192
+ count: number;
193
+ }>>;
194
+ /**
195
+ * Cheap excerpt builder for notification payloads. Markdown is kept as-is;
196
+ * we just trim whitespace and clip to 120 chars so the bell can render
197
+ * a single-line preview.
198
+ */
199
+ export declare function excerptForNotification(body: string, max?: number): string;
200
+ /**
201
+ * Extract `@<userId>` mentions from a comment body. We intentionally
202
+ * match the *user id* form here (`@user_abc123`) — a slice-7 follow-up
203
+ * can wire up `@username` resolution against the User table.
204
+ *
205
+ * Returns a deduplicated array of mentioned user ids; the caller filters
206
+ * out the actor before issuing notifications.
207
+ */
208
+ export declare function extractMentionedUserIds(body: string): string[];
209
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../src/realtime/notifications.ts"],"names":[],"mappings":"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"}