@crowdedkingdoms/crowdyjs 1.0.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/MIGRATION.md +247 -0
  3. package/README.md +303 -0
  4. package/dist/auth-state.d.ts +11 -0
  5. package/dist/auth-state.d.ts.map +1 -0
  6. package/dist/auth-state.js +13 -0
  7. package/dist/client.d.ts +135 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +150 -0
  10. package/dist/crowdy-client.d.ts +182 -0
  11. package/dist/crowdy-client.d.ts.map +1 -0
  12. package/dist/crowdy-client.js +146 -0
  13. package/dist/domains/actors.d.ts +117 -0
  14. package/dist/domains/actors.d.ts.map +1 -0
  15. package/dist/domains/actors.js +140 -0
  16. package/dist/domains/admin.d.ts +61 -0
  17. package/dist/domains/admin.d.ts.map +1 -0
  18. package/dist/domains/admin.js +33 -0
  19. package/dist/domains/appAccess.d.ts +141 -0
  20. package/dist/domains/appAccess.d.ts.map +1 -0
  21. package/dist/domains/appAccess.js +198 -0
  22. package/dist/domains/apps.d.ts +192 -0
  23. package/dist/domains/apps.d.ts.map +1 -0
  24. package/dist/domains/apps.js +217 -0
  25. package/dist/domains/auth.d.ts +163 -0
  26. package/dist/domains/auth.d.ts.map +1 -0
  27. package/dist/domains/auth.js +208 -0
  28. package/dist/domains/avatars.d.ts +94 -0
  29. package/dist/domains/avatars.d.ts.map +1 -0
  30. package/dist/domains/avatars.js +137 -0
  31. package/dist/domains/billing.d.ts +97 -0
  32. package/dist/domains/billing.d.ts.map +1 -0
  33. package/dist/domains/billing.js +131 -0
  34. package/dist/domains/channels.d.ts +293 -0
  35. package/dist/domains/channels.d.ts.map +1 -0
  36. package/dist/domains/channels.js +353 -0
  37. package/dist/domains/chunks.d.ts +133 -0
  38. package/dist/domains/chunks.d.ts.map +1 -0
  39. package/dist/domains/chunks.js +153 -0
  40. package/dist/domains/controlPlane.d.ts +174 -0
  41. package/dist/domains/controlPlane.d.ts.map +1 -0
  42. package/dist/domains/controlPlane.js +252 -0
  43. package/dist/domains/environments.d.ts +155 -0
  44. package/dist/domains/environments.d.ts.map +1 -0
  45. package/dist/domains/environments.js +223 -0
  46. package/dist/domains/gameApps.d.ts +114 -0
  47. package/dist/domains/gameApps.d.ts.map +1 -0
  48. package/dist/domains/gameApps.js +169 -0
  49. package/dist/domains/gameModel.d.ts +668 -0
  50. package/dist/domains/gameModel.d.ts.map +1 -0
  51. package/dist/domains/gameModel.js +816 -0
  52. package/dist/domains/host.d.ts +35 -0
  53. package/dist/domains/host.d.ts.map +1 -0
  54. package/dist/domains/host.js +40 -0
  55. package/dist/domains/organizations.d.ts +179 -0
  56. package/dist/domains/organizations.d.ts.map +1 -0
  57. package/dist/domains/organizations.js +269 -0
  58. package/dist/domains/payments.d.ts +104 -0
  59. package/dist/domains/payments.d.ts.map +1 -0
  60. package/dist/domains/payments.js +129 -0
  61. package/dist/domains/platform.d.ts +49 -0
  62. package/dist/domains/platform.d.ts.map +1 -0
  63. package/dist/domains/platform.js +50 -0
  64. package/dist/domains/quotas.d.ts +62 -0
  65. package/dist/domains/quotas.d.ts.map +1 -0
  66. package/dist/domains/quotas.js +79 -0
  67. package/dist/domains/serverStatus.d.ts +90 -0
  68. package/dist/domains/serverStatus.d.ts.map +1 -0
  69. package/dist/domains/serverStatus.js +104 -0
  70. package/dist/domains/sharedEnvironment.d.ts +133 -0
  71. package/dist/domains/sharedEnvironment.d.ts.map +1 -0
  72. package/dist/domains/sharedEnvironment.js +179 -0
  73. package/dist/domains/state.d.ts +64 -0
  74. package/dist/domains/state.d.ts.map +1 -0
  75. package/dist/domains/state.js +75 -0
  76. package/dist/domains/teams.d.ts +292 -0
  77. package/dist/domains/teams.d.ts.map +1 -0
  78. package/dist/domains/teams.js +352 -0
  79. package/dist/domains/teleport.d.ts +41 -0
  80. package/dist/domains/teleport.d.ts.map +1 -0
  81. package/dist/domains/teleport.js +43 -0
  82. package/dist/domains/udp.d.ts +405 -0
  83. package/dist/domains/udp.d.ts.map +1 -0
  84. package/dist/domains/udp.js +457 -0
  85. package/dist/domains/usage.d.ts +76 -0
  86. package/dist/domains/usage.d.ts.map +1 -0
  87. package/dist/domains/usage.js +110 -0
  88. package/dist/domains/users.d.ts +147 -0
  89. package/dist/domains/users.d.ts.map +1 -0
  90. package/dist/domains/users.js +195 -0
  91. package/dist/domains/voxels.d.ts +136 -0
  92. package/dist/domains/voxels.d.ts.map +1 -0
  93. package/dist/domains/voxels.js +153 -0
  94. package/dist/errors.d.ts +158 -0
  95. package/dist/errors.d.ts.map +1 -0
  96. package/dist/errors.js +142 -0
  97. package/dist/generated/graphql.d.ts +12206 -0
  98. package/dist/generated/graphql.d.ts.map +1 -0
  99. package/dist/generated/graphql.js +474 -0
  100. package/dist/index.d.ts +84 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +85 -0
  103. package/dist/logger.d.ts +8 -0
  104. package/dist/logger.d.ts.map +1 -0
  105. package/dist/logger.js +1 -0
  106. package/dist/realtime.d.ts +319 -0
  107. package/dist/realtime.d.ts.map +1 -0
  108. package/dist/realtime.js +390 -0
  109. package/dist/session.d.ts +73 -0
  110. package/dist/session.d.ts.map +1 -0
  111. package/dist/session.js +96 -0
  112. package/dist/subscriptions.d.ts +2 -0
  113. package/dist/subscriptions.d.ts.map +1 -0
  114. package/dist/subscriptions.js +1 -0
  115. package/dist/types.d.ts +658 -0
  116. package/dist/types.d.ts.map +1 -0
  117. package/dist/types.js +61 -0
  118. package/dist/utils.d.ts +98 -0
  119. package/dist/utils.d.ts.map +1 -0
  120. package/dist/utils.js +136 -0
  121. package/dist/world.d.ts +236 -0
  122. package/dist/world.d.ts.map +1 -0
  123. package/dist/world.js +275 -0
  124. package/package.json +73 -0
@@ -0,0 +1,390 @@
1
+ import { print } from 'graphql';
2
+ import { createClient } from 'graphql-ws';
3
+ import { silentLogger } from './logger.js';
4
+ import { CrowdyRealtimeError } from './errors.js';
5
+ import { UdpNotificationsDocument, } from './generated/graphql.js';
6
+ /**
7
+ * Manages the single WebSocket subscription to the game-api's
8
+ * `udpNotifications` stream — the realtime layer behind `client.udp` and
9
+ * `client.realtime`. It opens the socket lazily on the first {@link subscribe},
10
+ * authenticates with the shared session token, scopes the session to one
11
+ * `appId`, reconnects with jittered exponential backoff, re-reads the token and
12
+ * resubscribes on reconnect, fans each notification out to the registered
13
+ * {@link UdpNotificationHandlers}, and resolves `...AndWait` sends via
14
+ * {@link waitForSequence}.
15
+ *
16
+ * The connection lifecycle is observable through {@link status} /
17
+ * {@link onStatus} ({@link RealtimeStatus}). A realtime session is scoped to a
18
+ * single app, so run one client per app (sharing the same token store) for a
19
+ * player who is in multiple apps at once.
20
+ *
21
+ * You normally interact with this through `client.udp` / `client.realtime`
22
+ * rather than constructing it directly.
23
+ */
24
+ export class RealtimeClient {
25
+ /**
26
+ * @param config - Reconnect/timeout/endpoint tuning; see
27
+ * {@link RealtimeConfig}.
28
+ * @param session - Shared session store. The client reads the Bearer token
29
+ * from it for the connection handshake and watches it for changes: clearing
30
+ * the token tears the connection down (emitting an `AUTH_CLEARED`
31
+ * {@link CrowdyRealtimeError}), while a token change made while connected
32
+ * forces a reconnect using the new token.
33
+ */
34
+ constructor(config = {}, session) {
35
+ this.session = session;
36
+ this.client = null;
37
+ this.release = null;
38
+ this.desired = false;
39
+ this.statusValue = 'idle';
40
+ this.statusListeners = new Set();
41
+ this.subscribers = new Map();
42
+ this.pending = new Map();
43
+ this.nextSubscriberId = 1;
44
+ // App this realtime session is scoped to. Sent in connectionParams so the
45
+ // game-api only fans this app's spatial notifications to this subscription.
46
+ // The game-api rejects subscriptions that arrive without it.
47
+ this.subscribedAppId = null;
48
+ this.wsUrl = config.wsUrl || config.wsEndpoint || 'ws://localhost:3000/graphql';
49
+ this.logger = config.logger ?? silentLogger;
50
+ this.retryAttempts = config.retryAttempts ?? 8;
51
+ this.retryInitialDelayMs = config.retryInitialDelayMs ?? 250;
52
+ this.retryMaxDelayMs = config.retryMaxDelayMs ?? 5000;
53
+ this.waitTimeoutMs = config.waitTimeoutMs ?? 5000;
54
+ this.session.onChange((token) => {
55
+ if (!this.desired)
56
+ return;
57
+ if (!token) {
58
+ this.disconnect();
59
+ this.dispatchError(new CrowdyRealtimeError('Realtime disconnected because the session token was cleared', {
60
+ code: 'AUTH_CLEARED',
61
+ retryable: false,
62
+ }));
63
+ return;
64
+ }
65
+ this.restart();
66
+ });
67
+ }
68
+ /**
69
+ * The current connection state.
70
+ *
71
+ * @returns The latest {@link RealtimeStatus}.
72
+ */
73
+ status() {
74
+ return this.statusValue;
75
+ }
76
+ /**
77
+ * Subscribe to connection-state changes. The listener is invoked
78
+ * **immediately** with the current status, then again on every transition.
79
+ *
80
+ * @param listener - Called with each new {@link RealtimeStatus}.
81
+ * @returns An unsubscribe function that removes the listener.
82
+ */
83
+ onStatus(listener) {
84
+ this.statusListeners.add(listener);
85
+ listener(this.statusValue);
86
+ return () => {
87
+ this.statusListeners.delete(listener);
88
+ };
89
+ }
90
+ /**
91
+ * Mark the connection as desired and open the subscription if it isn't
92
+ * already open. You usually don't call this directly — {@link subscribe}
93
+ * calls it for you; use it (or `client.realtime.connect()`) only to pre-warm
94
+ * the socket.
95
+ *
96
+ * @throws {CrowdyRealtimeError} `AUTH_REQUIRED` if there is no session token.
97
+ */
98
+ connect() {
99
+ this.desired = true;
100
+ this.ensureSubscription();
101
+ }
102
+ /**
103
+ * Close the socket and stop wanting a connection. Outstanding
104
+ * {@link waitForSequence} promises are left intact (they will time out on
105
+ * their own); use {@link close} to also reject those and drop all
106
+ * subscribers. Safe to call when already disconnected.
107
+ */
108
+ disconnect() {
109
+ this.desired = false;
110
+ this.release?.();
111
+ this.release = null;
112
+ this.client?.dispose();
113
+ this.client = null;
114
+ this.setStatus('disconnected');
115
+ }
116
+ /**
117
+ * Fully tear down the client: {@link disconnect}, drop all notification
118
+ * subscribers, and reject every outstanding {@link waitForSequence} promise
119
+ * with a non-retryable {@link CrowdyRealtimeError}. Call this when disposing
120
+ * the SDK instance.
121
+ */
122
+ close() {
123
+ this.disconnect();
124
+ this.subscribers.clear();
125
+ this.rejectAllPending(new CrowdyRealtimeError('Realtime client closed', { retryable: false }));
126
+ }
127
+ /**
128
+ * Register a set of {@link UdpNotificationHandlers} and ensure the realtime
129
+ * connection is open, scoping the session to `appId`. The game-api requires
130
+ * an app id and rejects an app-agnostic subscription with a
131
+ * `RealtimeConnectionEvent` (`code: 'APP_ID_REQUIRED'`).
132
+ *
133
+ * Multiple handler sets can be registered at once; the returned function
134
+ * unregisters this one, and the socket closes automatically once the last
135
+ * subscriber unsubscribes.
136
+ *
137
+ * @param handlers - Callbacks for the notification types you care about.
138
+ * @param appId - The app to scope this realtime session to (decimal id;
139
+ * coerced to a string). Required.
140
+ * @returns An unsubscribe function that removes these handlers (and
141
+ * disconnects when none remain).
142
+ */
143
+ subscribe(handlers, appId) {
144
+ // appId is required by the type; guard for JS callers so a missing value
145
+ // is sent as "no app" (cleanly rejected by the game-api) rather than the
146
+ // literal string "undefined".
147
+ this.subscribedAppId = appId != null ? String(appId) : null;
148
+ const id = `s${this.nextSubscriberId++}`;
149
+ this.subscribers.set(id, handlers);
150
+ this.connect();
151
+ return () => {
152
+ this.subscribers.delete(id);
153
+ if (this.subscribers.size === 0 && this.desired) {
154
+ this.disconnect();
155
+ }
156
+ };
157
+ }
158
+ /**
159
+ * Return a promise that resolves when a notification carrying the given
160
+ * `sequenceNumber` arrives — the mechanism behind the `...AndWait` spatial
161
+ * sends. Resolves with the matching {@link SpatialNotification}, or rejects
162
+ * if that match is a `GenericErrorResponse` or the wait times out.
163
+ *
164
+ * @param sequenceNumber - The sequence number to wait for (as allocated by
165
+ * {@link SequenceAllocator} and stamped on the send).
166
+ * @param timeoutMs - How long to wait before rejecting, in milliseconds.
167
+ * Defaults to the configured {@link RealtimeConfig.waitTimeoutMs}.
168
+ * @returns The matching spatial notification.
169
+ * @throws {CrowdyRealtimeError} `UDP_SEQUENCE_TIMEOUT` (retryable) on timeout,
170
+ * or carrying the server `errorCode` when the match is a
171
+ * `GenericErrorResponse`.
172
+ */
173
+ waitForSequence(sequenceNumber, timeoutMs = this.waitTimeoutMs) {
174
+ return new Promise((resolve, reject) => {
175
+ const timer = setTimeout(() => {
176
+ this.removePending(sequenceNumber, wait);
177
+ reject(new CrowdyRealtimeError(`Timed out waiting for UDP response sequence ${sequenceNumber}`, { code: 'UDP_SEQUENCE_TIMEOUT', retryable: true }));
178
+ }, timeoutMs);
179
+ const wait = { resolve, reject, timer };
180
+ const waits = this.pending.get(sequenceNumber) ?? [];
181
+ waits.push(wait);
182
+ this.pending.set(sequenceNumber, waits);
183
+ });
184
+ }
185
+ ensureSubscription() {
186
+ if (this.release)
187
+ return;
188
+ const token = this.session.getToken();
189
+ if (!token) {
190
+ const error = new CrowdyRealtimeError('Must be authenticated to subscribe', {
191
+ code: 'AUTH_REQUIRED',
192
+ retryable: false,
193
+ });
194
+ this.setStatus('failed');
195
+ this.dispatchError(error);
196
+ throw error;
197
+ }
198
+ this.setStatus('connecting');
199
+ this.client = createClient({
200
+ url: this.wsUrl,
201
+ lazy: true,
202
+ retryAttempts: this.retryAttempts,
203
+ connectionParams: () => {
204
+ const currentToken = this.session.getToken();
205
+ if (!currentToken)
206
+ return {};
207
+ const params = {
208
+ Authorization: `Bearer ${currentToken}`,
209
+ };
210
+ if (this.subscribedAppId != null)
211
+ params.appId = this.subscribedAppId;
212
+ return params;
213
+ },
214
+ retryWait: async (retries) => {
215
+ this.setStatus('reconnecting');
216
+ const delay = Math.min(this.retryMaxDelayMs, this.retryInitialDelayMs * 2 ** retries);
217
+ const jitter = Math.floor(Math.random() * this.retryInitialDelayMs);
218
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
219
+ },
220
+ on: {
221
+ connected: () => this.setStatus('connected'),
222
+ closed: () => {
223
+ if (this.desired) {
224
+ this.setStatus('reconnecting');
225
+ }
226
+ else {
227
+ this.setStatus('disconnected');
228
+ }
229
+ },
230
+ error: (error) => {
231
+ this.logger.error?.('Realtime WebSocket error', error);
232
+ this.dispatchError(new CrowdyRealtimeError('Realtime WebSocket error', {
233
+ code: 'WEBSOCKET_ERROR',
234
+ retryable: true,
235
+ cause: error,
236
+ }));
237
+ },
238
+ },
239
+ });
240
+ this.release = this.client.subscribe({ query: print(UdpNotificationsDocument) }, {
241
+ next: (message) => {
242
+ const data = message.data;
243
+ const notification = data?.udpNotifications;
244
+ if (notification)
245
+ this.dispatch(notification);
246
+ if (message.errors?.length) {
247
+ this.dispatchError(new CrowdyRealtimeError(message.errors[0]?.message ?? 'Subscription error', {
248
+ code: 'SUBSCRIPTION_ERROR',
249
+ retryable: true,
250
+ cause: message.errors,
251
+ }));
252
+ }
253
+ },
254
+ error: (error) => {
255
+ this.setStatus('failed');
256
+ this.dispatchError(new CrowdyRealtimeError('Realtime subscription failed', {
257
+ code: 'SUBSCRIPTION_FAILED',
258
+ retryable: true,
259
+ cause: error,
260
+ }));
261
+ },
262
+ complete: () => {
263
+ this.release = null;
264
+ if (this.desired) {
265
+ this.setStatus('reconnecting');
266
+ this.ensureSubscription();
267
+ }
268
+ },
269
+ });
270
+ }
271
+ restart() {
272
+ this.release?.();
273
+ this.release = null;
274
+ this.client?.dispose();
275
+ this.client = null;
276
+ this.ensureSubscription();
277
+ }
278
+ dispatch(notification) {
279
+ this.resolvePending(notification);
280
+ // A non-retryable connection event (e.g. APP_ID_REQUIRED, AUTH_REQUIRED)
281
+ // means the server completed the subscription and resubscribing would just
282
+ // be rejected again. Stop wanting the connection so the `complete` handler
283
+ // doesn't immediately reopen it (lazy graphql-ws then closes the socket).
284
+ if (notification.__typename === 'RealtimeConnectionEvent' &&
285
+ notification.retryable === false) {
286
+ this.desired = false;
287
+ this.setStatus('failed');
288
+ }
289
+ for (const handlers of [...this.subscribers.values()]) {
290
+ try {
291
+ handlers.any?.(notification);
292
+ switch (notification.__typename) {
293
+ case 'ActorUpdateNotification':
294
+ handlers.actorUpdate?.(notification);
295
+ break;
296
+ case 'ActorUpdateResponse':
297
+ handlers.actorUpdateResponse?.(notification);
298
+ break;
299
+ case 'VoxelUpdateNotification':
300
+ handlers.voxelUpdate?.(notification);
301
+ break;
302
+ case 'VoxelUpdateResponse':
303
+ handlers.voxelUpdateResponse?.(notification);
304
+ break;
305
+ case 'ClientAudioNotification':
306
+ handlers.audio?.(notification);
307
+ break;
308
+ case 'ClientTextNotification':
309
+ handlers.text?.(notification);
310
+ break;
311
+ case 'ClientEventNotification':
312
+ handlers.clientEvent?.(notification);
313
+ break;
314
+ case 'ServerEventNotification':
315
+ handlers.serverEvent?.(notification);
316
+ break;
317
+ case 'SingleActorMessageNotification':
318
+ handlers.singleActorMessage?.(notification);
319
+ break;
320
+ case 'ChannelMessageNotification':
321
+ handlers.channelMessage?.(notification);
322
+ break;
323
+ case 'GenericErrorResponse':
324
+ handlers.genericError?.(notification);
325
+ break;
326
+ case 'RealtimeConnectionEvent':
327
+ handlers.connectionEvent?.(notification);
328
+ break;
329
+ }
330
+ }
331
+ catch (error) {
332
+ this.logger.error?.('Realtime notification handler threw', error);
333
+ }
334
+ }
335
+ }
336
+ resolvePending(notification) {
337
+ if (!('sequenceNumber' in notification))
338
+ return;
339
+ const waits = this.pending.get(notification.sequenceNumber);
340
+ if (!waits?.length)
341
+ return;
342
+ this.pending.delete(notification.sequenceNumber);
343
+ for (const wait of waits) {
344
+ clearTimeout(wait.timer);
345
+ if (notification.__typename === 'GenericErrorResponse') {
346
+ wait.reject(new CrowdyRealtimeError(`UDP request failed: ${notification.errorCode}`, {
347
+ code: notification.errorCode,
348
+ retryable: false,
349
+ }));
350
+ }
351
+ else {
352
+ wait.resolve(notification);
353
+ }
354
+ }
355
+ }
356
+ removePending(sequenceNumber, wait) {
357
+ const waits = this.pending.get(sequenceNumber);
358
+ if (!waits)
359
+ return;
360
+ const next = waits.filter((candidate) => candidate !== wait);
361
+ if (next.length) {
362
+ this.pending.set(sequenceNumber, next);
363
+ }
364
+ else {
365
+ this.pending.delete(sequenceNumber);
366
+ }
367
+ }
368
+ rejectAllPending(error) {
369
+ for (const waits of this.pending.values()) {
370
+ for (const wait of waits) {
371
+ clearTimeout(wait.timer);
372
+ wait.reject(error);
373
+ }
374
+ }
375
+ this.pending.clear();
376
+ }
377
+ dispatchError(error) {
378
+ for (const handlers of [...this.subscribers.values()]) {
379
+ handlers.error?.(error);
380
+ }
381
+ }
382
+ setStatus(status) {
383
+ if (status === this.statusValue)
384
+ return;
385
+ this.statusValue = status;
386
+ for (const listener of [...this.statusListeners]) {
387
+ listener(status);
388
+ }
389
+ }
390
+ }
@@ -0,0 +1,73 @@
1
+ /** Callback notified whenever the active token changes (`null` on sign-out). */
2
+ export type SessionListener = (token: string | null) => void;
3
+ /**
4
+ * Pluggable persistence for the Bearer token. Implement this to back the
5
+ * session with whatever storage your runtime offers (cookies, secure storage,
6
+ * a database for SSR, etc.). All three methods may be sync or async.
7
+ *
8
+ * `BrowserLocalStorageTokenStore` is provided for browser apps.
9
+ */
10
+ export interface TokenStore {
11
+ /** Return the persisted token, or `null`/`undefined` if none. */
12
+ get(): string | null | Promise<string | null>;
13
+ /** Persist a token (called on login and token refresh). */
14
+ set(token: string): void | Promise<void>;
15
+ /** Remove the persisted token (called on logout). */
16
+ clear(): void | Promise<void>;
17
+ }
18
+ /**
19
+ * {@link TokenStore} backed by the browser `localStorage`. No-ops gracefully
20
+ * when `localStorage` is unavailable (e.g. SSR), so it's safe to construct
21
+ * unconditionally.
22
+ */
23
+ export declare class BrowserLocalStorageTokenStore implements TokenStore {
24
+ private readonly key;
25
+ /** @param key - localStorage key under which the token is stored. */
26
+ constructor(key?: string);
27
+ get(): string | null;
28
+ set(token: string): void;
29
+ clear(): void;
30
+ }
31
+ /**
32
+ * In-memory token holder with change notifications and optional persistence via
33
+ * a {@link TokenStore}. Setting the token fans out to every {@link onChange}
34
+ * listener, which is how the HTTP client and the WebSocket stay in lock-step
35
+ * (their auth can never drift).
36
+ */
37
+ export declare class SessionStore {
38
+ private readonly tokenStore?;
39
+ private token;
40
+ private readonly listeners;
41
+ /** @param tokenStore - Optional persistence; when omitted the token is memory-only. */
42
+ constructor(tokenStore?: TokenStore | undefined);
43
+ /**
44
+ * Load the token from the {@link TokenStore} into memory (without re-persisting)
45
+ * and notify listeners. Call once on startup to resume a saved session.
46
+ *
47
+ * @returns The restored token, or `null` if none was stored.
48
+ */
49
+ restore(): Promise<string | null>;
50
+ /** The current in-memory token, or `null` if there's no active session. */
51
+ getToken(): string | null;
52
+ /**
53
+ * Set (or clear, with `null`) the active token. Persists to the
54
+ * {@link TokenStore} unless `options.persist` is `false`, then notifies all
55
+ * listeners. A no-op if the token is unchanged.
56
+ *
57
+ * @param token - The new Bearer token, or `null` to sign out.
58
+ * @param options - `persist: false` updates memory + listeners only.
59
+ */
60
+ setToken(token: string | null, options?: {
61
+ persist?: boolean;
62
+ }): void;
63
+ /** Clear the active token (equivalent to `setToken(null)`). */
64
+ clear(): void;
65
+ /**
66
+ * Subscribe to token changes. The listener fires immediately with the current
67
+ * token, then on every change.
68
+ *
69
+ * @returns An unsubscribe function.
70
+ */
71
+ onChange(listener: SessionListener): () => void;
72
+ }
73
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,GAAG,IAAI,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,2DAA2D;IAC3D,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,qDAAqD;IACrD,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED;;;;GAIG;AACH,qBAAa,6BAA8B,YAAW,UAAU;IAElD,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,qEAAqE;gBACxC,GAAG,SAAmB;IAEnD,GAAG,IAAI,MAAM,GAAG,IAAI;IAKpB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAKxB,KAAK,IAAI,IAAI;CAId;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IAKX,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAJxC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IAExD,uFAAuF;gBAC1D,UAAU,CAAC,EAAE,UAAU,YAAA;IAEpD;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAMvC,2EAA2E;IAC3E,QAAQ,IAAI,MAAM,GAAG,IAAI;IAIzB;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,IAAI;IAiBzE,+DAA+D;IAC/D,KAAK,IAAI,IAAI;IAIb;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,IAAI;CAOhD"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * {@link TokenStore} backed by the browser `localStorage`. No-ops gracefully
3
+ * when `localStorage` is unavailable (e.g. SSR), so it's safe to construct
4
+ * unconditionally.
5
+ */
6
+ export class BrowserLocalStorageTokenStore {
7
+ /** @param key - localStorage key under which the token is stored. */
8
+ constructor(key = 'crowdyjs:token') {
9
+ this.key = key;
10
+ }
11
+ get() {
12
+ if (typeof localStorage === 'undefined')
13
+ return null;
14
+ return localStorage.getItem(this.key);
15
+ }
16
+ set(token) {
17
+ if (typeof localStorage === 'undefined')
18
+ return;
19
+ localStorage.setItem(this.key, token);
20
+ }
21
+ clear() {
22
+ if (typeof localStorage === 'undefined')
23
+ return;
24
+ localStorage.removeItem(this.key);
25
+ }
26
+ }
27
+ /**
28
+ * In-memory token holder with change notifications and optional persistence via
29
+ * a {@link TokenStore}. Setting the token fans out to every {@link onChange}
30
+ * listener, which is how the HTTP client and the WebSocket stay in lock-step
31
+ * (their auth can never drift).
32
+ */
33
+ export class SessionStore {
34
+ /** @param tokenStore - Optional persistence; when omitted the token is memory-only. */
35
+ constructor(tokenStore) {
36
+ this.tokenStore = tokenStore;
37
+ this.token = null;
38
+ this.listeners = new Set();
39
+ }
40
+ /**
41
+ * Load the token from the {@link TokenStore} into memory (without re-persisting)
42
+ * and notify listeners. Call once on startup to resume a saved session.
43
+ *
44
+ * @returns The restored token, or `null` if none was stored.
45
+ */
46
+ async restore() {
47
+ const token = (await this.tokenStore?.get()) ?? null;
48
+ this.setToken(token, { persist: false });
49
+ return token;
50
+ }
51
+ /** The current in-memory token, or `null` if there's no active session. */
52
+ getToken() {
53
+ return this.token;
54
+ }
55
+ /**
56
+ * Set (or clear, with `null`) the active token. Persists to the
57
+ * {@link TokenStore} unless `options.persist` is `false`, then notifies all
58
+ * listeners. A no-op if the token is unchanged.
59
+ *
60
+ * @param token - The new Bearer token, or `null` to sign out.
61
+ * @param options - `persist: false` updates memory + listeners only.
62
+ */
63
+ setToken(token, options = {}) {
64
+ if (token === this.token)
65
+ return;
66
+ this.token = token;
67
+ if (options.persist !== false) {
68
+ if (token) {
69
+ void this.tokenStore?.set(token);
70
+ }
71
+ else {
72
+ void this.tokenStore?.clear();
73
+ }
74
+ }
75
+ for (const listener of [...this.listeners]) {
76
+ listener(token);
77
+ }
78
+ }
79
+ /** Clear the active token (equivalent to `setToken(null)`). */
80
+ clear() {
81
+ this.setToken(null);
82
+ }
83
+ /**
84
+ * Subscribe to token changes. The listener fires immediately with the current
85
+ * token, then on every change.
86
+ *
87
+ * @returns An unsubscribe function.
88
+ */
89
+ onChange(listener) {
90
+ this.listeners.add(listener);
91
+ listener(this.token);
92
+ return () => {
93
+ this.listeners.delete(listener);
94
+ };
95
+ }
96
+ }
@@ -0,0 +1,2 @@
1
+ export { RealtimeClient as SubscriptionManager, type RealtimeConfig as SubscriptionManagerConfig, type UdpNotificationHandlers, } from './realtime.js';
2
+ //# sourceMappingURL=subscriptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../src/subscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,IAAI,mBAAmB,EACrC,KAAK,cAAc,IAAI,yBAAyB,EAChD,KAAK,uBAAuB,GAC7B,MAAM,eAAe,CAAC"}
@@ -0,0 +1 @@
1
+ export { RealtimeClient as SubscriptionManager, } from './realtime.js';