@rool-dev/sdk 0.6.0-dev.1cbaafa → 0.6.0-dev.4595e79

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.
package/README.md CHANGED
@@ -83,20 +83,15 @@ channel.close();
83
83
  A **space** is a container that holds objects, schema, metadata, and channels. A **channel** is a named context within a space — it's the handle you use for all object and AI operations. Each channel contains one or more **conversations**, each with independent interaction history.
84
84
 
85
85
  There are two main handles:
86
- - **`RoolSpace`** — Live handle with SSE subscription for user management, link access, channel management, export, and channel lifecycle events. Extends `EventEmitter`.
86
+ - **`RoolSpace`** — Lightweight admin handle for user management, link access, channel management, and export. No real-time subscription.
87
87
  - **`RoolChannel`** — Full real-time handle for objects, AI prompts, media, schema, and undo/redo.
88
88
 
89
89
  ```typescript
90
- // Open a space live handle with SSE subscription
90
+ // Open a space for admin operations
91
91
  const space = await client.openSpace('space-id');
92
92
  await space.addUser(userId, 'editor');
93
93
  await space.setLinkAccess('viewer');
94
94
 
95
- // React to channel changes in real-time
96
- space.on('channelCreated', (channel) => console.log('New channel:', channel.id));
97
- space.on('channelUpdated', (channel) => console.log('Updated:', channel.id));
98
- space.on('channelDeleted', (channelId) => console.log('Deleted:', channelId));
99
-
100
95
  // Open a channel for object and AI operations
101
96
  const channel = await client.openChannel('space-id', 'my-channel');
102
97
  await channel.prompt('Create some planets');
@@ -104,9 +99,6 @@ await channel.prompt('Create some planets');
104
99
  // Or open a channel via the space handle
105
100
  const channel2 = await space.openChannel('research');
106
101
  await channel2.prompt('Analyze the data'); // Independent channel
107
-
108
- // Clean up — stops subscription and closes all open channels
109
- space.close();
110
102
  ```
111
103
 
112
104
  The `channelId` is fixed when you open a channel and cannot be changed. To use a different channel, open a new one. Channels to the same space share the same objects and schema.
@@ -576,9 +568,9 @@ const client = new RoolClient({
576
568
  | Method | Description |
577
569
  |--------|-------------|
578
570
  | `listSpaces(): Promise<RoolSpaceInfo[]>` | List available spaces |
579
- | `openSpace(spaceId): Promise<RoolSpace>` | Open a space with live SSE subscription. Caches and reuses open spaces. |
571
+ | `openSpace(spaceId): Promise<RoolSpace>` | Open a space for admin operations (no real-time subscription) |
580
572
  | `openChannel(spaceId, channelId): Promise<RoolChannel>` | Open a channel on a space |
581
- | `createSpace(name): Promise<RoolSpace>` | Create a new space, returns live handle with SSE subscription |
573
+ | `createSpace(name): Promise<RoolSpace>` | Create a new space, returns admin handle |
582
574
  | `deleteSpace(id): Promise<void>` | Permanently delete a space (cannot be undone) |
583
575
  | `importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
584
576
 
@@ -590,8 +582,7 @@ Manage channels within a space. Available on both the client and space handles:
590
582
  |--------|-------------|
591
583
  | `client.renameChannel(spaceId, channelId, name): Promise<void>` | Rename a channel |
592
584
  | `client.deleteChannel(spaceId, channelId): Promise<void>` | Delete a channel and its interaction history |
593
- | `space.channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
594
- | `space.getChannels(): ChannelInfo[]` | List channels (deprecated — use `space.channels` instead) |
585
+ | `space.getChannels(): ChannelInfo[]` | List channels (from cached snapshot) |
595
586
  | `space.deleteChannel(channelId): Promise<void>` | Delete a channel |
596
587
  | `channel.rename(name): Promise<void>` | Rename the current channel |
597
588
 
@@ -677,15 +668,13 @@ client.on('spaceAdded', (space: RoolSpaceInfo) => void) // Space created or
677
668
  client.on('spaceRemoved', (spaceId: string) => void) // Space deleted or access revoked
678
669
  client.on('spaceRenamed', (spaceId: string, newName: string) => void)
679
670
  client.on('channelCreated', (spaceId: string, channel: ChannelInfo) => void)
680
- client.on('channelUpdated', (spaceId: string, channel: ChannelInfo) => void)
671
+ client.on('channelRenamed', (spaceId: string, channelId: string, newName: string) => void)
681
672
  client.on('channelDeleted', (spaceId: string, channelId: string) => void)
682
673
  client.on('userStorageChanged', ({ key, value, source }: UserStorageChangedEvent) => void)
683
674
  client.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
684
675
  client.on('error', (error: Error, context?: string) => void)
685
676
  ```
686
677
 
687
- Channel events on the client (`channelCreated`, `channelUpdated`, `channelDeleted`) are pass-throughs from space events for backwards compatibility. Prefer listening on the space handle directly for new code.
688
-
689
678
  **Space list management pattern:**
690
679
  ```typescript
691
680
  const spaces = new Map<string, RoolSpaceInfo>();
@@ -700,9 +689,7 @@ client.on('spaceRenamed', (id, name) => {
700
689
 
701
690
  ## RoolSpace API
702
691
 
703
- A space handle with a live SSE subscription. Extends `EventEmitter`. Manages user access, link sharing, channels, and export. The `channels` property auto-updates via SSE, and channel lifecycle events fire in real-time.
704
-
705
- `openSpace()` caches and reuses open spaces — calling it twice with the same ID returns the same instance. Call `close()` when done to stop the subscription and close all open channels.
692
+ A space is a lightweight admin handle for space-level operations. It does not have a real-time subscription use channels for live data and object operations.
706
693
 
707
694
  ### Properties
708
695
 
@@ -713,34 +700,23 @@ A space handle with a live SSE subscription. Extends `EventEmitter`. Manages use
713
700
  | `role: RoolUserRole` | User's role |
714
701
  | `linkAccess: LinkAccess` | URL sharing level |
715
702
  | `memberCount: number` | Number of users with access to the space |
716
- | `channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
717
703
 
718
704
  ### Methods
719
705
 
720
706
  | Method | Description |
721
707
  |--------|-------------|
722
708
  | `openChannel(channelId): Promise<RoolChannel>` | Open a channel on this space |
723
- | `close(): void` | Stop SSE subscription and close all open channels |
724
709
  | `rename(newName): Promise<void>` | Rename this space |
725
710
  | `delete(): Promise<void>` | Permanently delete this space |
726
711
  | `listUsers(): Promise<SpaceMember[]>` | List users with access |
727
712
  | `addUser(userId, role): Promise<void>` | Add user to space |
728
713
  | `removeUser(userId): Promise<void>` | Remove user from space |
729
714
  | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level |
730
- | `getChannels(): ChannelInfo[]` | List channels (deprecated use `channels` property instead) |
715
+ | `getChannels(): ChannelInfo[]` | List channels (from cached snapshot) |
731
716
  | `deleteChannel(channelId): Promise<void>` | Delete a channel |
732
717
  | `exportArchive(): Promise<Blob>` | Export space as zip archive |
733
718
  | `refresh(): Promise<void>` | Refresh space data from server |
734
719
 
735
- ### Space Events
736
-
737
- ```typescript
738
- space.on('channelCreated', (channel: ChannelInfo) => void) // New channel added
739
- space.on('channelUpdated', (channel: ChannelInfo) => void) // Channel metadata changed (name, extension, manifest)
740
- space.on('channelDeleted', (channelId: string) => void) // Channel removed
741
- space.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
742
- ```
743
-
744
720
  ## RoolChannel API
745
721
 
746
722
  A channel is a named context within a space. All object operations, AI prompts, and real-time sync go through a channel. The `channelId` is fixed at open time — to use a different channel, open a new one.
package/dist/client.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { EventEmitter } from './event-emitter.js';
2
+ import { RoolChannel } from './channel.js';
2
3
  import { RoolSpace } from './space.js';
3
4
  import type { RoolClientConfig, RoolClientEvents, RoolSpaceInfo, CurrentUser, UserResult, AuthUser, ExtensionInfo, PublishedExtensionInfo, UploadExtensionOptions, FindExtensionsOptions } from './types.js';
4
- import type { RoolChannel } from './channel.js';
5
5
  /**
6
6
  * Rool Client - Manages authentication, space lifecycle, and shared infrastructure.
7
7
  *
8
- * The client is lightweight - most operations happen on RoolSpace and RoolChannel instances.
8
+ * The client is lightweight - most operations happen on RoolChannel instances.
9
9
  *
10
10
  * Features:
11
11
  * - Authentication (login, logout, token management)
@@ -21,7 +21,9 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
21
21
  private graphqlClient;
22
22
  private subscriptionManager;
23
23
  private logger;
24
- private openSpaces;
24
+ private openChannels;
25
+ private spaceSubscriptions;
26
+ private spaceDataCache;
25
27
  private _storageCache;
26
28
  private _currentUser;
27
29
  constructor(config?: RoolClientConfig);
@@ -99,23 +101,24 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
99
101
  listSpaces(): Promise<RoolSpaceInfo[]>;
100
102
  /**
101
103
  * Open a channel on a space.
102
- * Convenience method that opens (or reuses) the space, then opens the channel.
104
+ * Fetches full space data, ensures the channel exists, and starts the
105
+ * shared space subscription if not already active.
103
106
  *
104
107
  * @param spaceId - The ID of the space
105
108
  * @param channelId - The channel ID (created if it doesn't exist)
106
109
  */
107
110
  openChannel(spaceId: string, channelId: string): Promise<RoolChannel>;
108
111
  /**
109
- * Open a space with a real-time subscription.
110
- * Returns a live RoolSpace handle with channel lifecycle events.
111
- * Reuses an existing handle if the space is already open.
112
+ * Open a space for admin operations.
113
+ * Returns a lightweight handle for user management, link access,
114
+ * channel management, and export. Does not start a real-time subscription.
112
115
  *
113
- * Call space.close() when done to stop the subscription.
116
+ * To work with objects and AI, call space.openChannel(channelId).
114
117
  */
115
118
  openSpace(spaceId: string): Promise<RoolSpace>;
116
119
  /**
117
120
  * Create a new space.
118
- * Returns a RoolSpace handle with a real-time subscription.
121
+ * Returns a RoolSpace handle for admin operations.
119
122
  * Call space.openChannel(channelId) to start working with objects.
120
123
  */
121
124
  createSpace(name: string): Promise<RoolSpace>;
@@ -207,6 +210,7 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
207
210
  /**
208
211
  * Ensure the client-level event subscription is active.
209
212
  * Called automatically when opening spaces.
213
+ * Also fetches and caches the current user ID.
210
214
  * @internal
211
215
  */
212
216
  private ensureSubscribed;
@@ -226,10 +230,31 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
226
230
  * @internal Not part of the public API — use typed methods instead.
227
231
  */
228
232
  _graphql<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
233
+ private registerChannel;
234
+ private unregisterChannel;
235
+ /**
236
+ * Get space data, using a short-lived cache so concurrent openChannel calls
237
+ * for the same space share one fetch.
238
+ */
239
+ private getSpaceData;
240
+ /**
241
+ * Ensure a shared space subscription exists for the given spaceId.
242
+ * Creates one if it doesn't exist yet. Returns when connected.
243
+ */
244
+ private ensureSpaceSubscription;
245
+ /**
246
+ * Route a space event to the appropriate channel(s).
247
+ * Space-wide events go to all channels on the space.
248
+ * Channel-specific events go only to the matching channel.
249
+ * The `connected` event triggers a single resync for the whole space.
250
+ */
251
+ private routeSpaceEvent;
252
+ /**
253
+ * Handle reconnection for a space: fetch full state once, distribute to all channels.
254
+ */
255
+ private handleSpaceResync;
229
256
  /**
230
257
  * Handle a client-level event from the subscription.
231
- * Channel events from the client SSE are ignored — they're handled by
232
- * the space subscription via RoolSpace instead.
233
258
  * @internal
234
259
  */
235
260
  private handleClientEvent;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -4,16 +4,16 @@
4
4
  import { EventEmitter } from './event-emitter.js';
5
5
  import { AuthManager } from './auth.js';
6
6
  import { GraphQLClient } from './graphql.js';
7
- import { ClientSubscriptionManager } from './subscription.js';
7
+ import { ClientSubscriptionManager, SpaceSubscriptionManager } from './subscription.js';
8
8
  import { MediaClient } from './media.js';
9
9
  import { ExtensionsClient } from './apps.js';
10
- import { generateEntityId } from './channel.js';
10
+ import { RoolChannel, generateEntityId } from './channel.js';
11
11
  import { RoolSpace } from './space.js';
12
12
  import { defaultLogger } from './logger.js';
13
13
  /**
14
14
  * Rool Client - Manages authentication, space lifecycle, and shared infrastructure.
15
15
  *
16
- * The client is lightweight - most operations happen on RoolSpace and RoolChannel instances.
16
+ * The client is lightweight - most operations happen on RoolChannel instances.
17
17
  *
18
18
  * Features:
19
19
  * - Authentication (login, logout, token management)
@@ -29,8 +29,12 @@ export class RoolClient extends EventEmitter {
29
29
  graphqlClient;
30
30
  subscriptionManager = null;
31
31
  logger;
32
- // Open spaces (cached for reuse by openChannel)
33
- openSpaces = new Map();
32
+ // Registry of open channels (for cleanup on logout/destroy)
33
+ openChannels = new Map();
34
+ // Shared space subscriptions: one SSE connection per space, shared by all channels
35
+ spaceSubscriptions = new Map();
36
+ // Cached space data: avoids redundant openSpaceFull calls when opening multiple channels
37
+ spaceDataCache = new Map();
34
38
  // User storage cache (synced to localStorage)
35
39
  _storageCache = {};
36
40
  // Current user (fetched during initialize)
@@ -109,10 +113,16 @@ export class RoolClient extends EventEmitter {
109
113
  destroy() {
110
114
  this.authManager.destroy();
111
115
  this.subscriptionManager?.destroy();
112
- // Close all open spaces (which close their channels and subscriptions)
113
- for (const space of this.openSpaces.values())
114
- space.close();
115
- this.openSpaces.clear();
116
+ // Close all open channels snapshot first to avoid mutating during iteration
117
+ const channels = [...this.openChannels.values()];
118
+ for (const channel of channels)
119
+ channel.close();
120
+ this.openChannels.clear();
121
+ // Clean up space subscriptions
122
+ for (const sub of this.spaceSubscriptions.values())
123
+ sub.manager.destroy();
124
+ this.spaceSubscriptions.clear();
125
+ this.spaceDataCache.clear();
116
126
  this.removeAllListeners();
117
127
  }
118
128
  // ===========================================================================
@@ -154,10 +164,16 @@ export class RoolClient extends EventEmitter {
154
164
  logout() {
155
165
  this.authManager.logout();
156
166
  this.unsubscribe();
157
- // Close all open spaces (which close their channels and subscriptions)
158
- for (const space of this.openSpaces.values())
159
- space.close();
160
- this.openSpaces.clear();
167
+ // Close all open channels snapshot first to avoid mutating during iteration
168
+ const channels = [...this.openChannels.values()];
169
+ for (const channel of channels)
170
+ channel.close();
171
+ this.openChannels.clear();
172
+ // Clean up space subscriptions
173
+ for (const sub of this.spaceSubscriptions.values())
174
+ sub.manager.destroy();
175
+ this.spaceSubscriptions.clear();
176
+ this.spaceDataCache.clear();
161
177
  }
162
178
  /**
163
179
  * Process auth callback from URL fragment.
@@ -213,70 +229,99 @@ export class RoolClient extends EventEmitter {
213
229
  }
214
230
  /**
215
231
  * Open a channel on a space.
216
- * Convenience method that opens (or reuses) the space, then opens the channel.
232
+ * Fetches full space data, ensures the channel exists, and starts the
233
+ * shared space subscription if not already active.
217
234
  *
218
235
  * @param spaceId - The ID of the space
219
236
  * @param channelId - The channel ID (created if it doesn't exist)
220
237
  */
221
238
  async openChannel(spaceId, channelId) {
239
+ if (!channelId || channelId.length > 32 || !/^[a-zA-Z0-9_-]+$/.test(channelId)) {
240
+ throw new Error('channelId must be 1–32 characters containing only alphanumeric characters, hyphens, and underscores');
241
+ }
222
242
  // Ensure client subscription is active (for lifecycle events)
223
243
  void this.ensureSubscribed();
224
- const space = await this.openSpace(spaceId);
225
- return space.openChannel(channelId);
244
+ // Fetch full space data (cached per space to avoid redundant fetches)
245
+ const result = await this.getSpaceData(spaceId);
246
+ // Ensure channel exists — create if missing
247
+ let channelData = result.channels[channelId];
248
+ if (!channelData) {
249
+ try {
250
+ channelData = await this.graphqlClient.createChannel(spaceId, channelId);
251
+ }
252
+ catch {
253
+ // Race: another client may have created it. Re-fetch.
254
+ this.spaceDataCache.delete(spaceId);
255
+ const refreshed = await this.getSpaceData(spaceId);
256
+ channelData = refreshed.channels[channelId];
257
+ if (!channelData)
258
+ throw new Error(`Failed to create channel "${channelId}"`);
259
+ }
260
+ }
261
+ const channel = new RoolChannel({
262
+ id: spaceId,
263
+ name: result.name,
264
+ role: result.role,
265
+ linkAccess: result.linkAccess,
266
+ userId: result.userId,
267
+ objectIds: result.objectIds,
268
+ objectStats: result.objectStats,
269
+ schema: result.schema,
270
+ meta: result.meta,
271
+ channel: channelData,
272
+ channelId,
273
+ graphqlClient: this.graphqlClient,
274
+ mediaClient: this.mediaClient,
275
+ logger: this.logger,
276
+ onClose: () => this.unregisterChannel(spaceId, channelId),
277
+ });
278
+ // Register for cleanup (before awaiting subscription so close() works if it fails)
279
+ this.registerChannel(spaceId, channelId, channel);
280
+ // Ensure shared space subscription is active
281
+ await this.ensureSpaceSubscription(spaceId);
282
+ return channel;
226
283
  }
227
284
  /**
228
- * Open a space with a real-time subscription.
229
- * Returns a live RoolSpace handle with channel lifecycle events.
230
- * Reuses an existing handle if the space is already open.
285
+ * Open a space for admin operations.
286
+ * Returns a lightweight handle for user management, link access,
287
+ * channel management, and export. Does not start a real-time subscription.
231
288
  *
232
- * Call space.close() when done to stop the subscription.
289
+ * To work with objects and AI, call space.openChannel(channelId).
233
290
  */
234
291
  async openSpace(spaceId) {
235
- // Reuse existing open space
236
- const existing = this.openSpaces.get(spaceId);
237
- if (existing)
238
- return existing;
239
- // Ensure client subscription is active (for lifecycle events)
240
- void this.ensureSubscribed();
241
- const fullData = await this.graphqlClient.openSpaceFull(spaceId);
242
- const space = new RoolSpace({
292
+ const { name, role, linkAccess, memberCount, channels } = await this.graphqlClient.openSpace(spaceId);
293
+ return new RoolSpace({
243
294
  id: spaceId,
244
- name: fullData.name,
245
- role: fullData.role,
246
- userId: fullData.userId,
247
- linkAccess: fullData.linkAccess,
248
- memberCount: fullData.memberCount,
249
- fullData,
295
+ name,
296
+ role: role,
297
+ linkAccess,
298
+ memberCount,
299
+ channels,
250
300
  graphqlClient: this.graphqlClient,
251
301
  mediaClient: this.mediaClient,
252
- authManager: this.authManager,
253
- graphqlUrl: this.urls.graphql,
254
- logger: this.logger,
255
- onClose: () => this.openSpaces.delete(spaceId),
256
- });
257
- this.openSpaces.set(spaceId, space);
258
- // Forward space channel events to client-level events (backwards compat)
259
- space.on('channelCreated', (channel) => {
260
- this.emit('channelCreated', spaceId, channel);
261
- });
262
- space.on('channelUpdated', (channel) => {
263
- this.emit('channelUpdated', spaceId, channel);
302
+ openChannelFn: (sid, cid) => this.openChannel(sid, cid),
264
303
  });
265
- space.on('channelDeleted', (channelId) => {
266
- this.emit('channelDeleted', spaceId, channelId);
267
- });
268
- return space;
269
304
  }
270
305
  /**
271
306
  * Create a new space.
272
- * Returns a RoolSpace handle with a real-time subscription.
307
+ * Returns a RoolSpace handle for admin operations.
273
308
  * Call space.openChannel(channelId) to start working with objects.
274
309
  */
275
310
  async createSpace(name) {
276
311
  // Ensure client subscription is active (for lifecycle events)
277
312
  void this.ensureSubscribed();
278
313
  const { spaceId } = await this.graphqlClient.createSpace(name);
279
- return this.openSpace(spaceId);
314
+ return new RoolSpace({
315
+ id: spaceId,
316
+ name,
317
+ role: 'owner',
318
+ linkAccess: 'none',
319
+ memberCount: 1,
320
+ channels: [],
321
+ graphqlClient: this.graphqlClient,
322
+ mediaClient: this.mediaClient,
323
+ openChannelFn: (sid, cid) => this.openChannel(sid, cid),
324
+ });
280
325
  }
281
326
  /**
282
327
  * Rename a channel in a space.
@@ -298,12 +343,6 @@ export class RoolClient extends EventEmitter {
298
343
  */
299
344
  async deleteSpace(spaceId) {
300
345
  await this.graphqlClient.deleteSpace(spaceId);
301
- // Close and remove the cached space if open
302
- const space = this.openSpaces.get(spaceId);
303
- if (space) {
304
- space.close();
305
- this.openSpaces.delete(spaceId);
306
- }
307
346
  // Client-level event will be emitted via SSE subscription
308
347
  }
309
348
  /**
@@ -465,6 +504,7 @@ export class RoolClient extends EventEmitter {
465
504
  /**
466
505
  * Ensure the client-level event subscription is active.
467
506
  * Called automatically when opening spaces.
507
+ * Also fetches and caches the current user ID.
468
508
  * @internal
469
509
  */
470
510
  async ensureSubscribed() {
@@ -513,12 +553,125 @@ export class RoolClient extends EventEmitter {
513
553
  return this.graphqlClient.query(query, variables);
514
554
  }
515
555
  // ===========================================================================
556
+ // Private Methods - Channel Registry
557
+ // ===========================================================================
558
+ registerChannel(spaceId, channelId, channel) {
559
+ this.openChannels.set(`${spaceId}:${channelId}`, channel);
560
+ }
561
+ unregisterChannel(spaceId, channelId) {
562
+ this.openChannels.delete(`${spaceId}:${channelId}`);
563
+ // Tear down space subscription if no more channels on this space
564
+ const hasChannelsOnSpace = [...this.openChannels.keys()].some(key => key.startsWith(`${spaceId}:`));
565
+ if (!hasChannelsOnSpace) {
566
+ const sub = this.spaceSubscriptions.get(spaceId);
567
+ if (sub) {
568
+ sub.manager.destroy();
569
+ this.spaceSubscriptions.delete(spaceId);
570
+ }
571
+ }
572
+ }
573
+ // ===========================================================================
574
+ // Private Methods - Space Subscriptions
575
+ // ===========================================================================
576
+ /**
577
+ * Get space data, using a short-lived cache so concurrent openChannel calls
578
+ * for the same space share one fetch.
579
+ */
580
+ getSpaceData(spaceId) {
581
+ const cached = this.spaceDataCache.get(spaceId);
582
+ if (cached)
583
+ return cached;
584
+ const promise = this.graphqlClient.openSpaceFull(spaceId).finally(() => {
585
+ // Clear cache once resolved — data is now in the channels and kept current via SSE
586
+ this.spaceDataCache.delete(spaceId);
587
+ });
588
+ this.spaceDataCache.set(spaceId, promise);
589
+ return promise;
590
+ }
591
+ /**
592
+ * Ensure a shared space subscription exists for the given spaceId.
593
+ * Creates one if it doesn't exist yet. Returns when connected.
594
+ */
595
+ ensureSpaceSubscription(spaceId) {
596
+ const existing = this.spaceSubscriptions.get(spaceId);
597
+ if (existing)
598
+ return existing.ready;
599
+ const manager = new SpaceSubscriptionManager({
600
+ graphqlUrl: this.urls.graphql,
601
+ authManager: this.authManager,
602
+ logger: this.logger,
603
+ spaceId,
604
+ onEvent: (event) => this.routeSpaceEvent(spaceId, event),
605
+ onConnectionStateChanged: () => { },
606
+ onError: (error) => {
607
+ this.logger.error(`[RoolClient] Space ${spaceId} subscription error:`, error);
608
+ },
609
+ });
610
+ const ready = manager.subscribe();
611
+ this.spaceSubscriptions.set(spaceId, { manager, ready });
612
+ return ready;
613
+ }
614
+ /**
615
+ * Route a space event to the appropriate channel(s).
616
+ * Space-wide events go to all channels on the space.
617
+ * Channel-specific events go only to the matching channel.
618
+ * The `connected` event triggers a single resync for the whole space.
619
+ */
620
+ routeSpaceEvent(spaceId, event) {
621
+ // Reconnect or full state change: single fetch, distribute to all channels
622
+ if (event.type === 'connected' || event.type === 'space_changed') {
623
+ this.handleSpaceResync(spaceId);
624
+ return;
625
+ }
626
+ // Channel-specific events: route to the matching channel only
627
+ if ('channelId' in event && event.channelId) {
628
+ const channel = this.openChannels.get(`${spaceId}:${event.channelId}`);
629
+ if (channel)
630
+ channel._handleEvent(event);
631
+ return;
632
+ }
633
+ // Space-wide events (objects, schema, metadata, space_changed):
634
+ // broadcast to all channels on this space
635
+ for (const [key, channel] of this.openChannels) {
636
+ if (key.startsWith(`${spaceId}:`)) {
637
+ channel._handleEvent(event);
638
+ }
639
+ }
640
+ }
641
+ /**
642
+ * Handle reconnection for a space: fetch full state once, distribute to all channels.
643
+ */
644
+ handleSpaceResync(spaceId) {
645
+ // Collect channels on this space
646
+ const channels = [];
647
+ for (const [key, channel] of this.openChannels) {
648
+ if (key.startsWith(`${spaceId}:`))
649
+ channels.push(channel);
650
+ }
651
+ if (channels.length === 0)
652
+ return;
653
+ this.logger.info(`[RoolClient] Space ${spaceId} reconnected, resyncing ${channels.length} channel(s)...`);
654
+ void this.graphqlClient.openSpaceFull(spaceId).then((result) => {
655
+ for (const channel of channels) {
656
+ const channelData = result.channels[channel.channelId];
657
+ channel._applyResyncData({
658
+ meta: result.meta,
659
+ schema: result.schema,
660
+ objectIds: result.objectIds,
661
+ objectStats: result.objectStats,
662
+ channel: channelData,
663
+ });
664
+ }
665
+ this.logger.info(`[RoolClient] Space ${spaceId} resync complete (${result.objectIds.length} objects)`);
666
+ }).catch((error) => {
667
+ this.logger.error(`[RoolClient] Space ${spaceId} resync failed:`, error);
668
+ });
669
+ }
670
+ // ===========================================================================
516
671
  // Private Methods - Event Handling
517
672
  // ===========================================================================
518
673
  /**
519
674
  * Handle a client-level event from the subscription.
520
- * Channel events from the client SSE are ignored — they're handled by
521
- * the space subscription via RoolSpace instead.
522
675
  * @internal
523
676
  */
524
677
  handleClientEvent(event) {
@@ -563,10 +716,24 @@ export class RoolClient extends EventEmitter {
563
716
  case 'user_storage_changed':
564
717
  this.handleUserStorageChanged(event.key, event.value);
565
718
  break;
566
- // Channel events from client SSE are ignored — handled by RoolSpace
567
719
  case 'channel_created':
720
+ this.emit('channelCreated', event.spaceId, {
721
+ id: event.channelId,
722
+ name: event.name ?? null,
723
+ createdAt: event.channelCreatedAt ?? Date.now(),
724
+ createdBy: event.channelCreatedBy ?? '',
725
+ createdByName: event.channelCreatedByName ?? null,
726
+ interactionCount: 0,
727
+ extensionUrl: event.channelExtensionUrl ?? null,
728
+ extensionId: event.channelExtensionId ?? null,
729
+ manifest: event.channelManifest ?? null,
730
+ });
731
+ break;
568
732
  case 'channel_renamed':
733
+ this.emit('channelRenamed', event.spaceId, event.channelId, event.name);
734
+ break;
569
735
  case 'channel_deleted':
736
+ this.emit('channelDeleted', event.spaceId, event.channelId);
570
737
  break;
571
738
  }
572
739
  }