@pageloop/client 0.5.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 (70) hide show
  1. package/LICENSE +94 -0
  2. package/README.md +97 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/.tsbuildinfo.preact +1 -0
  5. package/dist/.tsbuildinfo.react +1 -0
  6. package/dist/.tsbuildinfo.solid +1 -0
  7. package/dist/ApiClient.d.ts +121 -0
  8. package/dist/ApiClient.d.ts.map +1 -0
  9. package/dist/ApiClient.js +512 -0
  10. package/dist/ApiClient.js.map +1 -0
  11. package/dist/CommentEngine.d.ts +111 -0
  12. package/dist/CommentEngine.d.ts.map +1 -0
  13. package/dist/CommentEngine.js +277 -0
  14. package/dist/CommentEngine.js.map +1 -0
  15. package/dist/EventBus.d.ts +122 -0
  16. package/dist/EventBus.d.ts.map +1 -0
  17. package/dist/EventBus.js +34 -0
  18. package/dist/EventBus.js.map +1 -0
  19. package/dist/IdbCache.d.ts +22 -0
  20. package/dist/IdbCache.d.ts.map +1 -0
  21. package/dist/IdbCache.js +79 -0
  22. package/dist/IdbCache.js.map +1 -0
  23. package/dist/PageLoop.d.ts +424 -0
  24. package/dist/PageLoop.d.ts.map +1 -0
  25. package/dist/PageLoop.js +1092 -0
  26. package/dist/PageLoop.js.map +1 -0
  27. package/dist/PageTracker.d.ts +32 -0
  28. package/dist/PageTracker.d.ts.map +1 -0
  29. package/dist/PageTracker.js +105 -0
  30. package/dist/PageTracker.js.map +1 -0
  31. package/dist/UIRenderer.d.ts +218 -0
  32. package/dist/UIRenderer.d.ts.map +1 -0
  33. package/dist/UIRenderer.js +2 -0
  34. package/dist/UIRenderer.js.map +1 -0
  35. package/dist/auth/resolveInitialToken.d.ts +49 -0
  36. package/dist/auth/resolveInitialToken.d.ts.map +1 -0
  37. package/dist/auth/resolveInitialToken.js +97 -0
  38. package/dist/auth/resolveInitialToken.js.map +1 -0
  39. package/dist/errorCode.d.ts +12 -0
  40. package/dist/errorCode.d.ts.map +1 -0
  41. package/dist/errorCode.js +21 -0
  42. package/dist/errorCode.js.map +1 -0
  43. package/dist/index.d.ts +20 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +18 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/notifications/BrowserNotifications.d.ts +68 -0
  48. package/dist/notifications/BrowserNotifications.d.ts.map +1 -0
  49. package/dist/notifications/BrowserNotifications.js +147 -0
  50. package/dist/notifications/BrowserNotifications.js.map +1 -0
  51. package/dist/preact/index.d.ts +37 -0
  52. package/dist/preact/index.d.ts.map +1 -0
  53. package/dist/preact/index.js +150 -0
  54. package/dist/preact/index.js.map +1 -0
  55. package/dist/preact/style.css +12 -0
  56. package/dist/react/index.d.ts +52 -0
  57. package/dist/react/index.d.ts.map +1 -0
  58. package/dist/react/index.js +165 -0
  59. package/dist/react/index.js.map +1 -0
  60. package/dist/react/style.css +12 -0
  61. package/dist/safeStorage.d.ts +26 -0
  62. package/dist/safeStorage.d.ts.map +1 -0
  63. package/dist/safeStorage.js +78 -0
  64. package/dist/safeStorage.js.map +1 -0
  65. package/dist/solid/index.d.ts +40 -0
  66. package/dist/solid/index.d.ts.map +1 -0
  67. package/dist/solid/index.jsx +134 -0
  68. package/dist/solid/index.jsx.map +1 -0
  69. package/dist/solid/style.css +12 -0
  70. package/package.json +85 -0
@@ -0,0 +1,512 @@
1
+ import { RpcError } from '@pageloop/shared';
2
+ import { EventBus } from './EventBus.js';
3
+ /**
4
+ * Every server-event name, as a runtime array. The SSE transport must
5
+ * register one `addEventListener` per named event (EventSource has no
6
+ * generic catch-all for named events), so it needs the names at runtime
7
+ * — whereas `ServerEvent['name']` is a type that's erased at compile.
8
+ *
9
+ * `satisfies readonly ServerEvent['name'][]` makes this list authoritative:
10
+ * adding a new event to the `ServerEvent` union without listing it here is
11
+ * still allowed (the array stays a valid subset), but the exhaustiveness
12
+ * guard below turns a *missing* name into a compile error, so SSE can't
13
+ * silently drop a freshly-added server event (CW-8). Done entirely
14
+ * client-side; no server change required.
15
+ */
16
+ const SERVER_EVENT_NAMES = [
17
+ 'projects.updated',
18
+ 'memberships.updated',
19
+ 'comments.created',
20
+ 'comments.updated',
21
+ 'comments.deleted',
22
+ 'comments.bulkDeleted',
23
+ 'replies.created',
24
+ 'replies.updated',
25
+ 'replies.deleted',
26
+ 'reactions.changed',
27
+ 'versions.refreshed',
28
+ 'repo.sync.started',
29
+ 'repo.sync.finished',
30
+ 'repo.sync.failed',
31
+ 'integration.sync.started',
32
+ 'integration.sync.finished',
33
+ 'integration.sync.failed',
34
+ 'notifications.new',
35
+ 'media.processed',
36
+ 'pages.changed',
37
+ 'pageGroups.changed',
38
+ 'managedDocs.changed',
39
+ 'chat.message',
40
+ 'chat.presence',
41
+ 'chat.read',
42
+ 'chat.typing',
43
+ 'notification.show',
44
+ // Server-internal admin signals — never broadcast to clients (the
45
+ // server's topicsForEvent returns [] for them). Listed only to satisfy
46
+ // the exhaustiveness guard below.
47
+ 'user.registered',
48
+ 'org.member.joined',
49
+ ];
50
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
51
+ const _assertNoMissingServerEventName = true;
52
+ /**
53
+ * Single point of network IO for the SDK. Tries WebSocket first, falls back
54
+ * to SSE for events + REST for commands. The same `call()` API works
55
+ * regardless of transport — callers don't know whether their command went
56
+ * over a WS frame or a fetch.
57
+ */
58
+ export class ApiClient {
59
+ config;
60
+ bus;
61
+ socket = null;
62
+ sse = null;
63
+ nextId = 1;
64
+ pending = new Map();
65
+ connected = false;
66
+ currentTransport = null;
67
+ /** Topics this client wants to be on. Re-sent on every WS reconnect
68
+ * so a transport bounce doesn't silently drop the user out of
69
+ * chat rooms / other topic streams. */
70
+ desiredTopics = new Set();
71
+ /** Resolver bookkeeping for `subscribeTopic` calls awaiting an
72
+ * `{type:'subscribed', topic, ok, error?}` ack. Keyed by topic. */
73
+ pendingTopicAcks = new Map();
74
+ /** Extra context to send with each subscribe frame (e.g. guestName
75
+ * for anonymous chat presence). Keyed by topic; re-sent on every
76
+ * reconnect via the same desiredTopics flush. */
77
+ topicExtras = new Map();
78
+ /** Set by `disconnect()` so a deliberate teardown (logout, token swap)
79
+ * does NOT trigger the auto-reconnect loop. Cleared on the next
80
+ * `connect()`. */
81
+ intentionalClose = false;
82
+ /** Current backoff attempt count for the WS auto-reconnect loop.
83
+ * Reset to 0 on a clean open. */
84
+ reconnectAttempts = 0;
85
+ /** Pending reconnect timer handle so we never stack two loops. */
86
+ reconnectTimer = null;
87
+ /** Base + cap for the exponential backoff (with jitter). */
88
+ static RECONNECT_BASE_MS = 1000;
89
+ static RECONNECT_MAX_MS = 30_000;
90
+ constructor(config, bus) {
91
+ this.config = config;
92
+ this.bus = bus;
93
+ }
94
+ /** The base URL the API client talks to. Renderers read this to build
95
+ * out-of-band URLs (e.g. the OAuth `/auth/:provider/start` redirect). */
96
+ getEndpoint() {
97
+ return this.config.endpoint;
98
+ }
99
+ /**
100
+ * Re-emit the current connection state on the bus. Used after the renderer
101
+ * mounts (and therefore registers its connection-state listener) to make
102
+ * sure it sees connections that completed during pre-mount bootstrap.
103
+ */
104
+ replayConnectionState() {
105
+ if (this.connected && this.currentTransport) {
106
+ this.bus.emit('connection:open', { transport: this.currentTransport });
107
+ }
108
+ else {
109
+ this.bus.emit('connection:closed', {});
110
+ }
111
+ }
112
+ setToken(token) {
113
+ this.config.token = token;
114
+ // Force a reconnect so the new token is used in the WS query string.
115
+ this.disconnect();
116
+ void this.connect();
117
+ }
118
+ /** Currently-attached bearer token, or null when anonymous. */
119
+ getToken() {
120
+ return this.config.token;
121
+ }
122
+ async connect() {
123
+ // A fresh connect cancels any in-flight backoff loop and re-opens
124
+ // the auto-reconnect gate that `disconnect()` may have closed.
125
+ this.intentionalClose = false;
126
+ if (this.reconnectTimer) {
127
+ clearTimeout(this.reconnectTimer);
128
+ this.reconnectTimer = null;
129
+ }
130
+ const kind = this.config.transport ?? 'auto';
131
+ if (kind === 'ws' || kind === 'auto') {
132
+ try {
133
+ await this.connectWs();
134
+ return;
135
+ }
136
+ catch {
137
+ if (kind === 'ws')
138
+ throw new Error('WebSocket connect failed');
139
+ // fall through to SSE
140
+ }
141
+ }
142
+ if (kind === 'sse' || kind === 'auto') {
143
+ try {
144
+ this.connectSse();
145
+ return;
146
+ }
147
+ catch {
148
+ // fall through to REST-only
149
+ }
150
+ }
151
+ // REST-only mode: no persistent connection. We're effectively done.
152
+ this.connected = true;
153
+ this.currentTransport = 'rest';
154
+ this.bus.emit('connection:open', { transport: 'rest' });
155
+ }
156
+ disconnect() {
157
+ // Deliberate teardown: suppress the auto-reconnect loop and cancel
158
+ // any pending backoff timer so a logout / token swap doesn't
159
+ // immediately re-dial the old session.
160
+ this.intentionalClose = true;
161
+ if (this.reconnectTimer) {
162
+ clearTimeout(this.reconnectTimer);
163
+ this.reconnectTimer = null;
164
+ }
165
+ if (this.socket) {
166
+ this.socket.close();
167
+ this.socket = null;
168
+ }
169
+ if (this.sse) {
170
+ this.sse.close();
171
+ this.sse = null;
172
+ }
173
+ this.connected = false;
174
+ }
175
+ /**
176
+ * Force an immediate reconnect attempt, bypassing the backoff delay.
177
+ * Used by the connection pill's click-to-retry while offline (CR-8).
178
+ * No-op when a healthy socket is already open or a deliberate
179
+ * teardown is in effect.
180
+ */
181
+ retryNow() {
182
+ if (this.intentionalClose)
183
+ return;
184
+ if (this.socket && this.socket.readyState === this.socket.OPEN)
185
+ return;
186
+ if (this.reconnectTimer) {
187
+ clearTimeout(this.reconnectTimer);
188
+ this.reconnectTimer = null;
189
+ }
190
+ this.reconnectAttempts = 0;
191
+ void this.connect();
192
+ }
193
+ /**
194
+ * Schedule a WebSocket re-dial with exponential backoff + jitter
195
+ * (1s → 2s → 4s … capped at 30s). Gated on `intentionalClose` so a
196
+ * deliberate teardown never reconnects. Emits `connection:reconnecting`
197
+ * before each attempt so the connection pill can show the
198
+ * reconnecting state. On a successful re-open the existing
199
+ * `desiredTopics` flush (in `connectWs`'s onopen) re-subscribes every
200
+ * topic, and `reconnectAttempts` resets to 0.
201
+ */
202
+ scheduleReconnect() {
203
+ if (this.intentionalClose)
204
+ return;
205
+ // Only the WS transport auto-reconnects; SSE has its own browser-
206
+ // managed retry, and REST has no persistent connection.
207
+ if (this.config.transport && this.config.transport !== 'auto' && this.config.transport !== 'ws') {
208
+ return;
209
+ }
210
+ if (this.reconnectTimer)
211
+ return; // a loop is already armed
212
+ const attempt = this.reconnectAttempts + 1;
213
+ const expo = ApiClient.RECONNECT_BASE_MS * 2 ** (attempt - 1);
214
+ const capped = Math.min(expo, ApiClient.RECONNECT_MAX_MS);
215
+ // Full jitter on the upper half of the window keeps a fleet of
216
+ // widgets from reconnecting in lockstep after a server restart.
217
+ const delayMs = Math.round(capped / 2 + Math.random() * (capped / 2));
218
+ this.bus.emit('connection:reconnecting', { attempt, delayMs });
219
+ this.reconnectTimer = setTimeout(() => {
220
+ this.reconnectTimer = null;
221
+ this.reconnectAttempts = attempt;
222
+ this.connect().catch(() => {
223
+ // connect() already falls WS→SSE→REST internally; if it
224
+ // still rejected, arm the next backoff step.
225
+ this.scheduleReconnect();
226
+ });
227
+ }, delayMs);
228
+ }
229
+ /**
230
+ * Send a typed command/query. Goes over WS when available, otherwise REST.
231
+ * Always returns a Promise; subscriptions are managed separately.
232
+ */
233
+ async call(method, params) {
234
+ if (this.socket && this.socket.readyState === this.socket.OPEN) {
235
+ return this.callWs(method, params);
236
+ }
237
+ return this.callRest(method, params);
238
+ }
239
+ /**
240
+ * Subscribe this socket to a server-side broadcast topic (e.g.
241
+ * `chat:page:<pageId>`). Returns when the server acks the
242
+ * subscribe; rejects on FORBIDDEN.
243
+ *
244
+ * The topic is remembered in `desiredTopics` so a transport bounce
245
+ * automatically re-subscribes after reconnect — callers don't have
246
+ * to re-arm anything on `connection:open`. Likewise, calling
247
+ * subscribeTopic BEFORE the initial WS handshake completes is
248
+ * fine: the topic queues here and the on-open handler flushes it
249
+ * once the socket is up. The returned Promise resolves whenever
250
+ * the eventual ack lands (or times out after ~30s).
251
+ *
252
+ * Events still arrive via the `EventBus`; callers filter by event
253
+ * name + scope/id. This method only handles the server-side
254
+ * permission handshake.
255
+ */
256
+ async subscribeTopic(topic, extras) {
257
+ if (!topic)
258
+ throw new Error('subscribeTopic: empty topic');
259
+ this.desiredTopics.add(topic);
260
+ if (extras)
261
+ this.topicExtras.set(topic, extras);
262
+ else
263
+ this.topicExtras.delete(topic);
264
+ if (this.socket && this.socket.readyState === this.socket.OPEN) {
265
+ return this.sendSubscribeFrame(topic);
266
+ }
267
+ // WS not connected yet (or has dropped). Register an ack waiter;
268
+ // the on-open handler will send the subscribe frame and the
269
+ // `subscribed` reply will resolve us. Use a longer timeout than
270
+ // the per-frame one since we're also waiting for the initial
271
+ // transport handshake.
272
+ return this.queueTopicAck(topic, 30_000);
273
+ }
274
+ /** Drop this socket from a topic. Idempotent. */
275
+ unsubscribeTopic(topic) {
276
+ this.desiredTopics.delete(topic);
277
+ if (this.socket && this.socket.readyState === this.socket.OPEN) {
278
+ this.socket.send(JSON.stringify({ type: 'unsubscribe', topic }));
279
+ }
280
+ // Reject any still-pending ack waiters for this topic — they'll
281
+ // never receive a subscribed ack after an unsubscribe.
282
+ const waiters = this.pendingTopicAcks.get(topic);
283
+ if (waiters) {
284
+ this.pendingTopicAcks.delete(topic);
285
+ for (const w of waiters) {
286
+ clearTimeout(w.timer);
287
+ w.reject(new Error(`unsubscribed before ack: ${topic}`));
288
+ }
289
+ }
290
+ }
291
+ // --- WS ---
292
+ connectWs() {
293
+ return new Promise((resolve, reject) => {
294
+ const wsUrl = this.config.endpoint.replace(/^http/, 'ws') +
295
+ '/ws' +
296
+ `?projectId=${encodeURIComponent(this.config.projectId)}` +
297
+ (this.config.token ? `&token=${encodeURIComponent(this.config.token)}` : '');
298
+ const sock = new WebSocket(wsUrl);
299
+ const t = setTimeout(() => {
300
+ sock.close();
301
+ reject(new Error('WS connect timeout'));
302
+ }, 4000);
303
+ sock.onopen = () => {
304
+ clearTimeout(t);
305
+ this.socket = sock;
306
+ this.connected = true;
307
+ this.currentTransport = 'ws';
308
+ // Clean open — reset the backoff ladder so the *next* drop
309
+ // starts again at the 1s base rather than wherever the last
310
+ // reconnect storm left off.
311
+ this.reconnectAttempts = 0;
312
+ this.bus.emit('connection:open', { transport: 'ws' });
313
+ // Re-arm topic subscriptions across reconnects. We swallow
314
+ // per-topic failures (server may legitimately deny after a
315
+ // permission change) — the topic will simply drop out.
316
+ for (const topic of this.desiredTopics) {
317
+ void this.sendSubscribeFrame(topic).catch(() => undefined);
318
+ }
319
+ resolve();
320
+ };
321
+ sock.onerror = () => {
322
+ clearTimeout(t);
323
+ reject(new Error('WS connect error'));
324
+ };
325
+ sock.onclose = () => {
326
+ this.connected = false;
327
+ // Drop the dead socket reference so `call()` routes over REST
328
+ // (and a stale `this.socket` can't be re-used) until the
329
+ // reconnect loop installs a fresh one.
330
+ if (this.socket === sock)
331
+ this.socket = null;
332
+ this.bus.emit('connection:closed', { transport: 'ws' });
333
+ // Re-arm pending topic-ack timers with a generous reconnect
334
+ // window. Their original 8s per-frame timers were anchored
335
+ // to a now-dead socket; without this they'd fire mid-
336
+ // reconnect and reject as "subscribe timeout" — which
337
+ // surfaces in the chat panel as "Could not reach the chat
338
+ // server" even though the next WS open would have flushed
339
+ // the subscribe cleanly.
340
+ for (const [topic, waiters] of this.pendingTopicAcks) {
341
+ for (const w of waiters) {
342
+ clearTimeout(w.timer);
343
+ w.timer = this.armAckTimer(topic, 60_000, w.reject);
344
+ }
345
+ }
346
+ // Auto-reconnect with exponential backoff unless this was a
347
+ // deliberate teardown. The reconnected socket's onopen
348
+ // flushes desiredTopics so chat rooms / topic streams
349
+ // resume without any caller re-arming anything.
350
+ this.scheduleReconnect();
351
+ };
352
+ sock.onmessage = (e) => this.handleWsMessage(e.data);
353
+ });
354
+ }
355
+ callWs(method, params) {
356
+ return new Promise((resolve, reject) => {
357
+ const id = String(this.nextId++);
358
+ this.pending.set(id, { resolve, reject });
359
+ this.socket.send(JSON.stringify({ type: 'request', id, method, params }));
360
+ // 30s timeout — tunable later.
361
+ setTimeout(() => {
362
+ if (this.pending.has(id)) {
363
+ this.pending.delete(id);
364
+ reject(new Error(`Request timeout: ${method}`));
365
+ }
366
+ }, 30_000);
367
+ });
368
+ }
369
+ sendSubscribeFrame(topic) {
370
+ return new Promise((resolve, reject) => {
371
+ const timer = this.armAckTimer(topic, 8000, reject);
372
+ const waiters = this.pendingTopicAcks.get(topic) ?? [];
373
+ waiters.push({ resolve, reject, timer });
374
+ this.pendingTopicAcks.set(topic, waiters);
375
+ const extras = this.topicExtras.get(topic);
376
+ this.socket.send(JSON.stringify({ type: 'subscribe', topic, ...(extras ?? {}) }));
377
+ });
378
+ }
379
+ /** Park a topic-subscribe in the pending map without sending a
380
+ * frame yet — the next `connection:open` handler will send the
381
+ * subscribe and the `subscribed` ack resolves us. */
382
+ queueTopicAck(topic, timeoutMs) {
383
+ return new Promise((resolve, reject) => {
384
+ const timer = this.armAckTimer(topic, timeoutMs, reject);
385
+ const waiters = this.pendingTopicAcks.get(topic) ?? [];
386
+ waiters.push({ resolve, reject, timer });
387
+ this.pendingTopicAcks.set(topic, waiters);
388
+ });
389
+ }
390
+ /** Shared timer-cleanup for both immediate-send and queued
391
+ * subscribe paths. */
392
+ armAckTimer(topic, timeoutMs, reject) {
393
+ const timer = setTimeout(() => {
394
+ const list = this.pendingTopicAcks.get(topic);
395
+ if (!list)
396
+ return;
397
+ const idx = list.findIndex((w) => w.timer === timer);
398
+ if (idx >= 0) {
399
+ list.splice(idx, 1);
400
+ if (list.length === 0)
401
+ this.pendingTopicAcks.delete(topic);
402
+ }
403
+ reject(new Error(`subscribe timeout: ${topic}`));
404
+ }, timeoutMs);
405
+ return timer;
406
+ }
407
+ resolveTopicAck(topic, ok, error) {
408
+ const waiters = this.pendingTopicAcks.get(topic);
409
+ if (!waiters)
410
+ return;
411
+ this.pendingTopicAcks.delete(topic);
412
+ for (const w of waiters) {
413
+ clearTimeout(w.timer);
414
+ if (ok)
415
+ w.resolve();
416
+ else
417
+ w.reject(new RpcError(error?.code ?? 'FORBIDDEN', error?.message ?? 'subscribe denied'));
418
+ }
419
+ }
420
+ handleWsMessage(raw) {
421
+ let msg;
422
+ try {
423
+ msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
424
+ }
425
+ catch {
426
+ return;
427
+ }
428
+ if (msg.type === 'response') {
429
+ const pending = this.pending.get(msg.id);
430
+ if (!pending)
431
+ return;
432
+ this.pending.delete(msg.id);
433
+ if (msg.error)
434
+ pending.reject(new RpcError(msg.error.code, msg.error.message));
435
+ else
436
+ pending.resolve(msg.result);
437
+ return;
438
+ }
439
+ if (msg.type === 'subscribed') {
440
+ this.resolveTopicAck(msg.topic, !!msg.ok, msg.error);
441
+ // On a denied subscribe, drop the topic from desiredTopics so
442
+ // we don't keep re-trying after every reconnect.
443
+ if (!msg.ok)
444
+ this.desiredTopics.delete(msg.topic);
445
+ return;
446
+ }
447
+ if (msg.type === 'unsubscribed') {
448
+ // Silent — desiredTopics already updated client-side.
449
+ return;
450
+ }
451
+ if (msg.type === 'event') {
452
+ const event = msg.event;
453
+ this.bus.emit(event.name, event);
454
+ }
455
+ }
456
+ // --- SSE ---
457
+ connectSse() {
458
+ const url = `${this.config.endpoint}/api/v1/events?projectId=${encodeURIComponent(this.config.projectId)}`;
459
+ this.sse = new EventSource(url, { withCredentials: true });
460
+ this.sse.onopen = () => {
461
+ this.connected = true;
462
+ this.currentTransport = 'sse';
463
+ this.bus.emit('connection:open', { transport: 'sse' });
464
+ };
465
+ this.sse.onerror = () => {
466
+ this.connected = false;
467
+ this.bus.emit('connection:closed', { transport: 'sse' });
468
+ };
469
+ // Each named server event arrives as its own SSE `event:` line, so
470
+ // we register one listener per name. The list is derived from the
471
+ // shared `ServerEvent['name']` union (SERVER_EVENT_NAMES, guarded
472
+ // for exhaustiveness at compile time) — the WS path re-emits any
473
+ // event generically, and this keeps SSE-only clients (WS-blocking
474
+ // proxies, some mobile webviews) at parity without a server change.
475
+ const reemit = (e) => {
476
+ try {
477
+ const event = JSON.parse(e.data);
478
+ this.bus.emit(event.name, event);
479
+ }
480
+ catch {
481
+ // bad frame — ignore
482
+ }
483
+ };
484
+ for (const name of SERVER_EVENT_NAMES) {
485
+ this.sse.addEventListener(name, (e) => reemit(e));
486
+ }
487
+ // Belt-and-suspenders: if the server ever emits a bare `data:` frame
488
+ // (no `event:` line) or a name we haven't listed, the default
489
+ // `message` handler still re-emits by the payload's own `name`.
490
+ this.sse.onmessage = (e) => reemit(e);
491
+ }
492
+ // --- REST ---
493
+ async callRest(method, params) {
494
+ const headers = {
495
+ 'content-type': 'application/json',
496
+ 'x-pageloop-project': this.config.projectId,
497
+ };
498
+ if (this.config.token)
499
+ headers.authorization = `Bearer ${this.config.token}`;
500
+ const res = await fetch(`${this.config.endpoint}/api/v1/rpc`, {
501
+ method: 'POST',
502
+ headers,
503
+ body: JSON.stringify({ method, params, projectId: this.config.projectId }),
504
+ });
505
+ const json = await res.json();
506
+ if (!res.ok || json.error) {
507
+ throw new RpcError(json.error?.code ?? 'HTTP', json.error?.message ?? res.statusText, res.status);
508
+ }
509
+ return json.result;
510
+ }
511
+ }
512
+ //# sourceMappingURL=ApiClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiClient.js","sourceRoot":"","sources":["../src/ApiClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import type { Comment, Reply, Reaction, ReactionTargetKind, Anchor } from '@pageloop/shared';
2
+ import { ApiClient } from './ApiClient.js';
3
+ import { EventBus } from './EventBus.js';
4
+ /**
5
+ * Local comment store + CRUD API. Keeps in-memory maps keyed by ids, listens
6
+ * to server events to stay in sync, and emits client-side `comment:*` /
7
+ * `reply:*` / `reaction:*` events for UI modules.
8
+ *
9
+ * Replies and reactions are loaded lazily — most comments don't have either,
10
+ * and pre-fetching them all on bootstrap would inflate the payload.
11
+ */
12
+ export declare class CommentEngine {
13
+ private readonly api;
14
+ private readonly bus;
15
+ private readonly byPage;
16
+ /** commentId → replies. Populated on demand by loadReplies. */
17
+ private readonly repliesByComment;
18
+ /** "${kind}:${id}" → reactions. Populated on demand or via the reactions.changed event. */
19
+ private readonly reactionsByTarget;
20
+ constructor(api: ApiClient, bus: EventBus);
21
+ load(pageId: string): Comment[];
22
+ loadFromServer(pageId: string): Promise<Comment[]>;
23
+ /** Create a comment + immediately fold the response into the local
24
+ * cache so the sidebar / bubble layer paint without waiting for
25
+ * a WS broadcast echo (comments.* events currently route to no
26
+ * topic, so the echo never arrives — the local-emit closes the
27
+ * refresh-required gap). */
28
+ create(input: {
29
+ trackedPageId: string;
30
+ /** Null/omitted for a "general" (non-anchored, page-level) comment. */
31
+ anchor?: Anchor | null;
32
+ bodyMd: string;
33
+ /** Required when the user is anonymous and the project allows public comments. */
34
+ guestName?: string;
35
+ }): Promise<Comment>;
36
+ /** Append a new comment to the per-page cache + fire the derived
37
+ * `comment:created` event. De-dupes against the WS-arrival path
38
+ * so a future fan-out wouldn't double-render. */
39
+ private applyLocalCreate;
40
+ /** Edit a comment + fold the updated row into the local cache so the
41
+ * editor's own change repaints immediately. Same rationale as
42
+ * `create`/`delete`: `comments.updated` doesn't route back to the
43
+ * editing connection today, so without this local-emit the edited
44
+ * body/status sits stale until an unrelated refresh. */
45
+ update(input: {
46
+ commentId: string;
47
+ bodyMd?: string;
48
+ status?: Comment['status'];
49
+ }): Promise<Comment>;
50
+ /** Promote/demote a comment to/from a task, or toggle its completion.
51
+ * Gated server-side on write access to the page (any commenter). Folds
52
+ * the result into the local cache so the sidebar repaints immediately. */
53
+ setTask(input: {
54
+ commentId: string;
55
+ isTask?: boolean;
56
+ resolved?: boolean;
57
+ }): Promise<Comment>;
58
+ /** Replace a cached comment in place + fire `comment:updated`. Locates
59
+ * the page bucket by the comment's own `trackedPageId`; de-dupes so a
60
+ * later WS echo re-renders with identical data instead of duplicating. */
61
+ private applyLocalUpdate;
62
+ delete(commentId: string): Promise<void>;
63
+ /** Mirror of `applyLocalCreate` for deletes. The WS `comments.deleted`
64
+ * fan-out doesn't route to the deleter's connection right now, so
65
+ * without this local-emit the sidebar + bubble layer hold the
66
+ * stale row until a page reload. Walks every cached page bucket
67
+ * to find the comment because `delete()` doesn't take a page id. */
68
+ private applyLocalDelete;
69
+ /** Post a reply + fold it into the cached reply list so the thread
70
+ * paints without waiting for a (currently-unrouted) `replies.created`
71
+ * echo. Mirrors the comment-create local-emit pattern. */
72
+ reply(commentId: string, bodyMd: string, guestName?: string): Promise<Reply>;
73
+ /** Edit a reply + repaint locally. */
74
+ updateReply(replyId: string, bodyMd: string): Promise<Reply>;
75
+ /** Delete a reply + drop it from the cached list locally. `replyId` is
76
+ * all we have, so walk every cached comment's reply list to find it. */
77
+ deleteReply(replyId: string): Promise<void>;
78
+ /** Append a reply to its comment's cached list (de-duped) + emit
79
+ * `reply:created`. Only mutates if the list was already loaded; an
80
+ * unloaded thread will fetch fresh on next open. */
81
+ private applyLocalReplyCreate;
82
+ /** Replace a reply in place within its comment's cached list + emit
83
+ * `reply:updated`. */
84
+ private applyLocalReplyUpdate;
85
+ /** Remove a reply from whichever comment bucket holds it + emit
86
+ * `reply:deleted` with the owning commentId so the sidebar can target
87
+ * the right thread. */
88
+ private applyLocalReplyDelete;
89
+ /** Cached replies for a comment (or `null` if never loaded). */
90
+ loadedReplies(commentId: string): Reply[] | null;
91
+ /** Force-fetch replies for a comment from the server. */
92
+ fetchReplies(commentId: string): Promise<Reply[]>;
93
+ /** Cached reactions for a comment or reply (may be stale; use `fetchReactions` to refresh). */
94
+ loadedReactions(targetKind: ReactionTargetKind, targetId: string): Reaction[];
95
+ fetchReactions(targetKind: ReactionTargetKind, targetId: string): Promise<Reaction[]>;
96
+ /**
97
+ * Toggle a reaction on a comment or reply. Returns the new reaction list
98
+ * so callers can update UI without waiting for the WS broadcast (the
99
+ * event also fires asynchronously).
100
+ */
101
+ toggleReaction(targetKind: ReactionTargetKind, targetId: string, kind: Reaction['kind']): Promise<Reaction[]>;
102
+ /** Bulk-clear all comments on a page (editor+ only). Locally drops
103
+ * the cached list + fires `comment:bulkDeleted` so the renderer
104
+ * repaints without waiting for a (currently-unrouted) WS echo. */
105
+ bulkDeleteForPage(trackedPageId: string): Promise<{
106
+ deleted: number;
107
+ }>;
108
+ /** Replace the cached list (used from bootstrap). */
109
+ prime(pageId: string, comments: Comment[]): void;
110
+ }
111
+ //# sourceMappingURL=CommentEngine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentEngine.d.ts","sourceRoot":"","sources":["../src/CommentEngine.ts"],"names":[],"mappings":"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"}