@fastrelay/js-sdk 0.1.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.
@@ -0,0 +1,521 @@
1
+ import { parseJsonSafely, toAbsoluteUrl } from "./utils.js";
2
+ const MAX_RECENT_EVENT_IDS = 1000;
3
+ export class FastrelayRealtime {
4
+ client;
5
+ token;
6
+ tokenProvider;
7
+ socketFactory;
8
+ subscribeDebounceMs;
9
+ deadConnectionTimeoutMs;
10
+ reconnectInitialDelayMs;
11
+ reconnectMaxDelayMs;
12
+ feedListeners = new Map();
13
+ acknowledgedFeeds = new Set();
14
+ pendingSubscribes = new Set();
15
+ pendingUnsubscribes = new Set();
16
+ recentEventIds = new Set();
17
+ eventListeners = new Set();
18
+ stateListeners = new Set();
19
+ errorListeners = new Set();
20
+ videoListeners = new Set();
21
+ socket = null;
22
+ subscribeBatchTimer = null;
23
+ reconnectTimer = null;
24
+ deadConnectionTimer = null;
25
+ lastServerMessageAt = null;
26
+ state = 'disconnected';
27
+ shouldBeConnected = false;
28
+ disposed = false;
29
+ awaitingTokenRefresh = false;
30
+ hasConnectedAtLeastOnce = false;
31
+ generation = 0;
32
+ reconnectAttempt = 0;
33
+ constructor(options) {
34
+ this.client = options.client;
35
+ this.token = options.token;
36
+ this.tokenProvider = options.tokenProvider;
37
+ this.socketFactory =
38
+ options.socketFactory ??
39
+ ((url) => new WebSocket(url));
40
+ this.subscribeDebounceMs = options.subscribeDebounceMs ?? 50;
41
+ this.deadConnectionTimeoutMs = options.deadConnectionTimeoutMs ?? 90_000;
42
+ this.reconnectInitialDelayMs = options.reconnectInitialDelayMs ?? 1_000;
43
+ this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? 30_000;
44
+ }
45
+ get connectionState() {
46
+ return this.state;
47
+ }
48
+ onEvent(callback) {
49
+ this.eventListeners.add(callback);
50
+ return () => this.eventListeners.delete(callback);
51
+ }
52
+ onStateChange(callback) {
53
+ this.stateListeners.add(callback);
54
+ return () => this.stateListeners.delete(callback);
55
+ }
56
+ onError(callback) {
57
+ this.errorListeners.add(callback);
58
+ return () => this.errorListeners.delete(callback);
59
+ }
60
+ onVideoStatus(callback) {
61
+ this.videoListeners.add(callback);
62
+ return () => this.videoListeners.delete(callback);
63
+ }
64
+ /**
65
+ * Listen to events for one feed. Subscribes over the socket on the first
66
+ * listener and unsubscribes when the last one is removed.
67
+ */
68
+ subscribeToFeed(feedId, callback, { type } = {}) {
69
+ if (this.disposed) {
70
+ throw new Error('FastrelayRealtime is disposed.');
71
+ }
72
+ const normalizedFeedId = feedId.trim();
73
+ if (normalizedFeedId === '') {
74
+ throw new TypeError('feedId must not be empty.');
75
+ }
76
+ let listeners = this.feedListeners.get(normalizedFeedId);
77
+ if (!listeners) {
78
+ listeners = new Set();
79
+ this.feedListeners.set(normalizedFeedId, listeners);
80
+ this.pendingUnsubscribes.delete(normalizedFeedId);
81
+ this.pendingSubscribes.add(normalizedFeedId);
82
+ this.scheduleSubscribeBatch();
83
+ }
84
+ const listener = { type: type?.trim() || undefined, callback };
85
+ listeners.add(listener);
86
+ return () => {
87
+ const current = this.feedListeners.get(normalizedFeedId);
88
+ if (!current)
89
+ return;
90
+ current.delete(listener);
91
+ if (current.size === 0) {
92
+ this.feedListeners.delete(normalizedFeedId);
93
+ this.acknowledgedFeeds.delete(normalizedFeedId);
94
+ this.pendingSubscribes.delete(normalizedFeedId);
95
+ this.pendingUnsubscribes.add(normalizedFeedId);
96
+ this.scheduleSubscribeBatch();
97
+ }
98
+ };
99
+ }
100
+ updateToken(token) {
101
+ this.token = token;
102
+ }
103
+ onBaseUrlChanged() {
104
+ if (!this.shouldBeConnected || this.disposed)
105
+ return;
106
+ this.scheduleReconnect({ immediate: true });
107
+ }
108
+ connect() {
109
+ if (this.disposed)
110
+ return;
111
+ this.shouldBeConnected = true;
112
+ this.cancelReconnectTimer();
113
+ void this.openSocket(this.hasConnectedAtLeastOnce);
114
+ }
115
+ disconnect({ clearSubscriptions = false } = {}) {
116
+ if (this.disposed)
117
+ return;
118
+ this.shouldBeConnected = false;
119
+ this.awaitingTokenRefresh = false;
120
+ this.cancelReconnectTimer();
121
+ this.cancelSubscribeBatch();
122
+ this.pendingSubscribes.clear();
123
+ this.pendingUnsubscribes.clear();
124
+ this.closeSocketResources();
125
+ this.acknowledgedFeeds.clear();
126
+ if (clearSubscriptions) {
127
+ this.feedListeners.clear();
128
+ }
129
+ this.setState('disconnected');
130
+ }
131
+ dispose() {
132
+ if (this.disposed)
133
+ return;
134
+ this.disposed = true;
135
+ this.shouldBeConnected = false;
136
+ this.cancelReconnectTimer();
137
+ this.cancelSubscribeBatch();
138
+ this.stopDeadConnectionMonitor();
139
+ this.closeSocketResources();
140
+ this.feedListeners.clear();
141
+ this.eventListeners.clear();
142
+ this.stateListeners.clear();
143
+ this.errorListeners.clear();
144
+ this.videoListeners.clear();
145
+ }
146
+ // ---- internals ---------------------------------------------------------
147
+ async openSocket(isReconnect) {
148
+ if (this.disposed || !this.shouldBeConnected)
149
+ return;
150
+ if (this.awaitingTokenRefresh) {
151
+ const refreshed = await this.refreshToken();
152
+ if (this.disposed || !this.shouldBeConnected)
153
+ return;
154
+ if (!refreshed) {
155
+ this.scheduleReconnect();
156
+ return;
157
+ }
158
+ this.awaitingTokenRefresh = false;
159
+ }
160
+ const token = this.token.trim();
161
+ if (token === '') {
162
+ this.emitError({
163
+ code: 'MISSING_TOKEN',
164
+ message: 'Realtime connection requires a non-empty token.',
165
+ retryable: false,
166
+ });
167
+ this.setState('disconnected');
168
+ return;
169
+ }
170
+ this.closeSocketResources();
171
+ const generation = ++this.generation;
172
+ this.setState(isReconnect ? 'reconnecting' : 'connecting');
173
+ const httpUrl = toAbsoluteUrl(this.client.baseUrl, '/v1/realtime', { token });
174
+ const wsUrl = httpUrl.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
175
+ let socket;
176
+ try {
177
+ socket = this.socketFactory(wsUrl);
178
+ }
179
+ catch (error) {
180
+ this.emitError({
181
+ code: 'CONNECTION_DROPPED',
182
+ message: 'Failed to open realtime socket.',
183
+ retryable: true,
184
+ cause: error,
185
+ });
186
+ this.scheduleReconnect();
187
+ return;
188
+ }
189
+ this.socket = socket;
190
+ this.lastServerMessageAt = Date.now();
191
+ this.startDeadConnectionMonitor();
192
+ socket.onopen = () => this.onConnected(generation, isReconnect);
193
+ socket.onmessage = (event) => {
194
+ if (typeof event.data === 'string') {
195
+ this.handleRawMessage(event.data, generation);
196
+ }
197
+ };
198
+ socket.onclose = (event) => this.onDisconnected(generation, event.code ?? null, event.reason ?? null);
199
+ socket.onerror = () => {
200
+ // The paired close event carries the actionable detail; nothing to do here.
201
+ };
202
+ }
203
+ onConnected(generation, _wasReconnect) {
204
+ if (this.disposed || generation !== this.generation)
205
+ return;
206
+ this.reconnectAttempt = 0;
207
+ this.cancelReconnectTimer();
208
+ this.setState('connected');
209
+ this.hasConnectedAtLeastOnce = true;
210
+ for (const feed of this.feedListeners.keys()) {
211
+ if (!this.acknowledgedFeeds.has(feed)) {
212
+ this.pendingSubscribes.add(feed);
213
+ }
214
+ }
215
+ this.flushSubscriptionBatch();
216
+ }
217
+ onDisconnected(generation, closeCode, reason) {
218
+ if (this.disposed || generation !== this.generation)
219
+ return;
220
+ this.stopDeadConnectionMonitor();
221
+ this.acknowledgedFeeds.clear();
222
+ this.setState('disconnected');
223
+ if (!this.shouldBeConnected)
224
+ return;
225
+ if (closeCode === 4029 || closeCode === 4002) {
226
+ this.shouldBeConnected = false;
227
+ this.emitError({
228
+ code: closeCode === 4029 ? 'CONNECTION_LIMIT_EXCEEDED' : 'INVALID_TOKEN',
229
+ message: closeCode === 4029
230
+ ? 'Realtime connection limit exceeded for this user or app.'
231
+ : 'Realtime token is invalid.',
232
+ retryable: false,
233
+ closeCode,
234
+ details: reason,
235
+ });
236
+ return;
237
+ }
238
+ if (closeCode === 4003) {
239
+ if (!this.tokenProvider) {
240
+ this.shouldBeConnected = false;
241
+ this.emitError({
242
+ code: 'TOKEN_EXPIRED',
243
+ message: 'Realtime token expired and no tokenProvider was configured.',
244
+ retryable: false,
245
+ closeCode,
246
+ details: reason,
247
+ });
248
+ return;
249
+ }
250
+ this.awaitingTokenRefresh = true;
251
+ this.scheduleReconnect({ immediate: true });
252
+ return;
253
+ }
254
+ this.emitError({
255
+ code: 'CONNECTION_DROPPED',
256
+ message: 'Realtime connection dropped.',
257
+ retryable: true,
258
+ closeCode,
259
+ details: reason,
260
+ });
261
+ this.scheduleReconnect();
262
+ }
263
+ handleRawMessage(rawMessage, generation) {
264
+ if (this.disposed || generation !== this.generation)
265
+ return;
266
+ this.lastServerMessageAt = Date.now();
267
+ const decoded = parseJsonSafely(rawMessage);
268
+ if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded)) {
269
+ return;
270
+ }
271
+ const payload = decoded;
272
+ const type = String(payload.type ?? '');
273
+ switch (type) {
274
+ case 'connection.established':
275
+ case 'heartbeat':
276
+ case 'server.going_away':
277
+ return;
278
+ case 'subscribe.success':
279
+ for (const feed of toStringArray(payload.feeds)) {
280
+ this.acknowledgedFeeds.add(feed);
281
+ }
282
+ return;
283
+ case 'unsubscribe.success':
284
+ for (const feed of toStringArray(payload.feeds)) {
285
+ this.acknowledgedFeeds.delete(feed);
286
+ }
287
+ return;
288
+ case 'subscribe.error':
289
+ case 'error':
290
+ this.emitControlError(type, payload);
291
+ return;
292
+ case 'video.ready':
293
+ case 'video.failed': {
294
+ const videoId = String(payload.videoId ?? '');
295
+ if (videoId === '')
296
+ return;
297
+ const event = payload;
298
+ for (const listener of [...this.videoListeners])
299
+ listener(event);
300
+ return;
301
+ }
302
+ }
303
+ const event = {
304
+ type,
305
+ feedId: String(payload.feedId ?? ''),
306
+ eventId: String(payload.eventId ?? ''),
307
+ seq: typeof payload.seq === 'number' ? payload.seq : null,
308
+ createdAt: payload.createdAt !== undefined ? String(payload.createdAt) : undefined,
309
+ data: payload.data && typeof payload.data === 'object'
310
+ ? payload.data
311
+ : {},
312
+ };
313
+ if (event.type === '' || event.feedId === '')
314
+ return;
315
+ if (event.eventId !== '' && !this.trackEventId(event.eventId))
316
+ return;
317
+ this.dispatchEvent(event);
318
+ }
319
+ dispatchEvent(event) {
320
+ for (const listener of [...this.eventListeners])
321
+ listener(event);
322
+ const feedListeners = this.feedListeners.get(event.feedId);
323
+ if (!feedListeners)
324
+ return;
325
+ for (const listener of [...feedListeners]) {
326
+ if (!listener.type || listener.type === event.type) {
327
+ listener.callback(event);
328
+ }
329
+ }
330
+ }
331
+ emitControlError(type, payload) {
332
+ const errorMap = payload.error && typeof payload.error === 'object'
333
+ ? payload.error
334
+ : {};
335
+ const code = String(errorMap.code ??
336
+ (type === 'subscribe.error' ? 'SUBSCRIBE_ERROR' : 'REALTIME_ERROR'));
337
+ this.emitError({
338
+ code,
339
+ message: String(errorMap.message ??
340
+ (type === 'subscribe.error'
341
+ ? 'Feed subscription failed.'
342
+ : 'Realtime error received from server.')),
343
+ retryable: code !== 'CONNECTION_LIMIT_EXCEEDED' && code !== 'INVALID_TOKEN',
344
+ details: errorMap.details,
345
+ hint: errorMap.hint !== undefined ? String(errorMap.hint) : undefined,
346
+ });
347
+ }
348
+ trackEventId(eventId) {
349
+ if (this.recentEventIds.has(eventId))
350
+ return false;
351
+ this.recentEventIds.add(eventId);
352
+ if (this.recentEventIds.size > MAX_RECENT_EVENT_IDS) {
353
+ const oldest = this.recentEventIds.values().next().value;
354
+ if (oldest !== undefined)
355
+ this.recentEventIds.delete(oldest);
356
+ }
357
+ return true;
358
+ }
359
+ scheduleSubscribeBatch() {
360
+ if (this.disposed || this.subscribeBatchTimer !== null)
361
+ return;
362
+ this.subscribeBatchTimer = setTimeout(() => {
363
+ this.subscribeBatchTimer = null;
364
+ this.flushSubscriptionBatch();
365
+ }, this.subscribeDebounceMs);
366
+ }
367
+ flushSubscriptionBatch() {
368
+ if (this.disposed || this.state !== 'connected')
369
+ return;
370
+ if (this.pendingSubscribes.size > 0) {
371
+ const feeds = [...this.pendingSubscribes].sort();
372
+ this.pendingSubscribes.clear();
373
+ this.send({ type: 'subscribe', feeds });
374
+ }
375
+ if (this.pendingUnsubscribes.size > 0) {
376
+ const feeds = [...this.pendingUnsubscribes].sort();
377
+ this.pendingUnsubscribes.clear();
378
+ this.send({ type: 'unsubscribe', feeds });
379
+ }
380
+ }
381
+ send(payload) {
382
+ if (!this.socket || this.disposed)
383
+ return;
384
+ try {
385
+ this.socket.send(JSON.stringify(payload));
386
+ }
387
+ catch (error) {
388
+ this.emitError({
389
+ code: 'SEND_FAILED',
390
+ message: 'Failed to send realtime message.',
391
+ retryable: true,
392
+ cause: error,
393
+ details: payload,
394
+ });
395
+ }
396
+ }
397
+ setState(state) {
398
+ if (this.state === state)
399
+ return;
400
+ this.state = state;
401
+ for (const listener of [...this.stateListeners])
402
+ listener(state);
403
+ }
404
+ emitError(error) {
405
+ if (this.disposed)
406
+ return;
407
+ for (const listener of [...this.errorListeners])
408
+ listener(error);
409
+ }
410
+ startDeadConnectionMonitor() {
411
+ this.stopDeadConnectionMonitor();
412
+ const frequency = Math.max(1000, Math.round(this.deadConnectionTimeoutMs / 3));
413
+ this.deadConnectionTimer = setInterval(() => {
414
+ if (this.disposed || !this.shouldBeConnected)
415
+ return;
416
+ if (this.lastServerMessageAt === null)
417
+ return;
418
+ if (Date.now() - this.lastServerMessageAt < this.deadConnectionTimeoutMs) {
419
+ return;
420
+ }
421
+ this.emitError({
422
+ code: 'DEAD_CONNECTION_TIMEOUT',
423
+ message: 'No realtime heartbeat/messages received within the dead connection timeout.',
424
+ retryable: true,
425
+ });
426
+ this.scheduleReconnect({ immediate: true });
427
+ }, frequency);
428
+ }
429
+ stopDeadConnectionMonitor() {
430
+ if (this.deadConnectionTimer !== null) {
431
+ clearInterval(this.deadConnectionTimer);
432
+ this.deadConnectionTimer = null;
433
+ }
434
+ }
435
+ scheduleReconnect({ immediate = false } = {}) {
436
+ if (this.disposed || !this.shouldBeConnected)
437
+ return;
438
+ if (this.reconnectTimer !== null)
439
+ return;
440
+ const delay = immediate ? 0 : this.nextReconnectDelay();
441
+ this.setState('reconnecting');
442
+ this.reconnectTimer = setTimeout(() => {
443
+ this.reconnectTimer = null;
444
+ void this.openSocket(true);
445
+ }, delay);
446
+ }
447
+ nextReconnectDelay() {
448
+ const exponent = Math.min(this.reconnectAttempt, 10);
449
+ const rawDelay = this.reconnectInitialDelayMs * 2 ** exponent;
450
+ const cappedDelay = Math.min(rawDelay, this.reconnectMaxDelayMs);
451
+ const jitter = 0.8 + Math.random() * 0.4;
452
+ this.reconnectAttempt += 1;
453
+ return Math.max(1, Math.round(cappedDelay * jitter));
454
+ }
455
+ cancelReconnectTimer() {
456
+ if (this.reconnectTimer !== null) {
457
+ clearTimeout(this.reconnectTimer);
458
+ this.reconnectTimer = null;
459
+ }
460
+ }
461
+ cancelSubscribeBatch() {
462
+ if (this.subscribeBatchTimer !== null) {
463
+ clearTimeout(this.subscribeBatchTimer);
464
+ this.subscribeBatchTimer = null;
465
+ }
466
+ }
467
+ closeSocketResources() {
468
+ this.stopDeadConnectionMonitor();
469
+ if (this.socket) {
470
+ this.socket.onopen = null;
471
+ this.socket.onmessage = null;
472
+ this.socket.onclose = null;
473
+ this.socket.onerror = null;
474
+ try {
475
+ this.socket.close();
476
+ }
477
+ catch {
478
+ // Already closed.
479
+ }
480
+ this.socket = null;
481
+ }
482
+ }
483
+ async refreshToken() {
484
+ if (!this.tokenProvider) {
485
+ this.emitError({
486
+ code: 'TOKEN_PROVIDER_MISSING',
487
+ message: 'Realtime token refresh requested but tokenProvider is null.',
488
+ retryable: false,
489
+ });
490
+ return false;
491
+ }
492
+ const generation = this.generation;
493
+ try {
494
+ const token = await this.tokenProvider();
495
+ // A dispose or newer connection attempt during the await means this
496
+ // token belongs to a dead session; never write it into the client.
497
+ if (this.disposed || generation !== this.generation)
498
+ return false;
499
+ if (token.trim() === '') {
500
+ throw new Error('tokenProvider returned an empty token.');
501
+ }
502
+ this.token = token;
503
+ this.client.setToken(token);
504
+ return true;
505
+ }
506
+ catch (error) {
507
+ this.emitError({
508
+ code: 'TOKEN_REFRESH_FAILED',
509
+ message: 'Failed to refresh realtime token.',
510
+ retryable: true,
511
+ cause: error,
512
+ });
513
+ return false;
514
+ }
515
+ }
516
+ }
517
+ function toStringArray(value) {
518
+ if (!Array.isArray(value))
519
+ return [];
520
+ return value.map((entry) => String(entry ?? '')).filter((entry) => entry !== '');
521
+ }
@@ -0,0 +1,190 @@
1
+ export interface FastrelayUser {
2
+ id: string;
3
+ displayName?: string | null;
4
+ profileData?: Record<string, unknown> | null;
5
+ role?: string | null;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ }
9
+ export interface FastrelayReaction {
10
+ id: string;
11
+ activityId: string;
12
+ userId: string;
13
+ user?: FastrelayUser | null;
14
+ type: string;
15
+ createdAt: string;
16
+ }
17
+ export interface FastrelayActivity {
18
+ id: string;
19
+ type: string;
20
+ text?: string | null;
21
+ userId: string;
22
+ user?: FastrelayUser | null;
23
+ feeds: string[];
24
+ visibility: string;
25
+ custom?: Record<string, unknown> | null;
26
+ popularity: number;
27
+ reactionCounts: Record<string, number>;
28
+ commentCount: number;
29
+ bookmarkCount: number;
30
+ expiresAt?: string | null;
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ ownReactions?: FastrelayReaction[] | null;
34
+ }
35
+ export interface FastrelayComment {
36
+ id: string;
37
+ activityId: string;
38
+ userId: string;
39
+ user?: FastrelayUser | null;
40
+ text: string;
41
+ parentId?: string | null;
42
+ mentionedUsers: string[];
43
+ reactionCounts: Record<string, number>;
44
+ score: number;
45
+ createdAt: string;
46
+ updatedAt: string;
47
+ }
48
+ export interface FastrelayCommentReaction {
49
+ id: string;
50
+ commentId: string;
51
+ userId: string;
52
+ type: string;
53
+ createdAt: string;
54
+ }
55
+ export interface FastrelayBookmark {
56
+ id: string;
57
+ activityId: string;
58
+ userId: string;
59
+ createdAt: string;
60
+ }
61
+ export interface FastrelayFeedActivityPin {
62
+ appId: string;
63
+ feedId: string;
64
+ activityId: string;
65
+ pinnedAt: string;
66
+ pinnedBy: string;
67
+ }
68
+ export interface FastrelayFeedback {
69
+ id: string;
70
+ activityId: string;
71
+ userId: string;
72
+ type: 'show_more' | 'show_less';
73
+ createdAt: string;
74
+ }
75
+ export interface FastrelayFile {
76
+ id: string;
77
+ url: string;
78
+ type: string;
79
+ mimeType: string;
80
+ size: number;
81
+ metadata: Record<string, unknown>;
82
+ createdAt: string;
83
+ }
84
+ export interface FastrelayModerationFlag {
85
+ id: string;
86
+ reporterId: string;
87
+ targetType: string;
88
+ targetId: string;
89
+ reason: string;
90
+ description?: string | null;
91
+ status: string;
92
+ resolvedBy?: string | null;
93
+ resolvedAt?: string | null;
94
+ createdAt: string;
95
+ }
96
+ export interface FastrelayPollOption {
97
+ id: string;
98
+ text: string;
99
+ voteCount: number;
100
+ }
101
+ export interface FastrelayPoll {
102
+ id: string;
103
+ question: string;
104
+ options: FastrelayPollOption[];
105
+ totalVotes: number;
106
+ userVote?: string | null;
107
+ expiresAt?: string | null;
108
+ isClosed: boolean;
109
+ }
110
+ export interface FastrelayUserMute {
111
+ id: string;
112
+ muterId?: string | null;
113
+ mutedUserId: string;
114
+ type: string;
115
+ mutedBy?: string | null;
116
+ expiresAt?: string | null;
117
+ createdAt: string;
118
+ }
119
+ export interface FastrelayVideo {
120
+ id: string;
121
+ mimeType: string;
122
+ sizeBytes: number;
123
+ status: 'uploading' | 'processing' | 'ready' | 'failed' | string;
124
+ provider: string;
125
+ createdAt: string;
126
+ hlsUrl?: string | null;
127
+ thumbnailUrl?: string | null;
128
+ durationSeconds?: number | null;
129
+ width?: number | null;
130
+ height?: number | null;
131
+ errorCode?: string | null;
132
+ errorMessage?: string | null;
133
+ }
134
+ export interface FastrelayVideoUploadUrl {
135
+ videoId: string;
136
+ uploadUrl: string;
137
+ protocol: string;
138
+ }
139
+ export interface FastrelayVideoStatusEvent {
140
+ type: 'video.ready' | 'video.failed';
141
+ videoId: string;
142
+ video?: FastrelayVideo | null;
143
+ errorCode?: string | null;
144
+ errorMessage?: string | null;
145
+ }
146
+ export interface CursorPage<T> {
147
+ data: T[];
148
+ nextCursor?: string | null;
149
+ hasMore: boolean;
150
+ pinned?: T[];
151
+ }
152
+ export interface NotificationGroup<T> {
153
+ groupKey: string;
154
+ activities: T[];
155
+ activityCount: number;
156
+ createdAt?: string | null;
157
+ updatedAt?: string | null;
158
+ }
159
+ export interface NotificationPage<T> extends CursorPage<T> {
160
+ unseenCount?: number;
161
+ unreadCount?: number;
162
+ groups?: NotificationGroup<T>[];
163
+ }
164
+ export interface FastrelayRealtimeEvent {
165
+ type: string;
166
+ feedId: string;
167
+ eventId: string;
168
+ seq?: number | null;
169
+ createdAt?: string;
170
+ data: Record<string, unknown>;
171
+ }
172
+ export type FastrelayConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
173
+ export interface FastrelayRealtimeError {
174
+ code: string;
175
+ message: string;
176
+ retryable: boolean;
177
+ closeCode?: number | null;
178
+ details?: unknown;
179
+ hint?: string | null;
180
+ cause?: unknown;
181
+ }
182
+ export interface FeedActivityQuery {
183
+ limit?: number;
184
+ cursor?: string;
185
+ view?: string;
186
+ markSeen?: boolean | string;
187
+ markRead?: boolean | string | string[];
188
+ filter?: Record<string, unknown>;
189
+ [key: string]: unknown;
190
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // Wire models. The fastrelay API speaks camelCase JSON, so responses are used
2
+ // as-is; date fields are ISO-8601 strings.
3
+ export {};