@rool-dev/sdk 0.6.0-dev.4595e79 → 0.6.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.
package/README.md CHANGED
@@ -83,15 +83,20 @@ 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`** — Lightweight admin handle for user management, link access, channel management, and export. No real-time subscription.
86
+ - **`RoolSpace`** — Live handle with SSE subscription for user management, link access, channel management, export, and channel lifecycle events. Extends `EventEmitter`.
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 for admin operations
90
+ // Open a space live handle with SSE subscription
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
+
95
100
  // Open a channel for object and AI operations
96
101
  const channel = await client.openChannel('space-id', 'my-channel');
97
102
  await channel.prompt('Create some planets');
@@ -99,6 +104,9 @@ await channel.prompt('Create some planets');
99
104
  // Or open a channel via the space handle
100
105
  const channel2 = await space.openChannel('research');
101
106
  await channel2.prompt('Analyze the data'); // Independent channel
107
+
108
+ // Clean up — stops subscription and closes all open channels
109
+ space.close();
102
110
  ```
103
111
 
104
112
  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.
@@ -568,9 +576,9 @@ const client = new RoolClient({
568
576
  | Method | Description |
569
577
  |--------|-------------|
570
578
  | `listSpaces(): Promise<RoolSpaceInfo[]>` | List available spaces |
571
- | `openSpace(spaceId): Promise<RoolSpace>` | Open a space for admin operations (no real-time subscription) |
579
+ | `openSpace(spaceId): Promise<RoolSpace>` | Open a space with live SSE subscription. Caches and reuses open spaces. |
572
580
  | `openChannel(spaceId, channelId): Promise<RoolChannel>` | Open a channel on a space |
573
- | `createSpace(name): Promise<RoolSpace>` | Create a new space, returns admin handle |
581
+ | `createSpace(name): Promise<RoolSpace>` | Create a new space, returns live handle with SSE subscription |
574
582
  | `deleteSpace(id): Promise<void>` | Permanently delete a space (cannot be undone) |
575
583
  | `importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
576
584
 
@@ -582,7 +590,8 @@ Manage channels within a space. Available on both the client and space handles:
582
590
  |--------|-------------|
583
591
  | `client.renameChannel(spaceId, channelId, name): Promise<void>` | Rename a channel |
584
592
  | `client.deleteChannel(spaceId, channelId): Promise<void>` | Delete a channel and its interaction history |
585
- | `space.getChannels(): ChannelInfo[]` | List channels (from cached snapshot) |
593
+ | `space.channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
594
+ | `space.getChannels(): ChannelInfo[]` | List channels (deprecated — use `space.channels` instead) |
586
595
  | `space.deleteChannel(channelId): Promise<void>` | Delete a channel |
587
596
  | `channel.rename(name): Promise<void>` | Rename the current channel |
588
597
 
@@ -668,13 +677,15 @@ client.on('spaceAdded', (space: RoolSpaceInfo) => void) // Space created or
668
677
  client.on('spaceRemoved', (spaceId: string) => void) // Space deleted or access revoked
669
678
  client.on('spaceRenamed', (spaceId: string, newName: string) => void)
670
679
  client.on('channelCreated', (spaceId: string, channel: ChannelInfo) => void)
671
- client.on('channelRenamed', (spaceId: string, channelId: string, newName: string) => void)
680
+ client.on('channelUpdated', (spaceId: string, channel: ChannelInfo) => void)
672
681
  client.on('channelDeleted', (spaceId: string, channelId: string) => void)
673
682
  client.on('userStorageChanged', ({ key, value, source }: UserStorageChangedEvent) => void)
674
683
  client.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
675
684
  client.on('error', (error: Error, context?: string) => void)
676
685
  ```
677
686
 
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
+
678
689
  **Space list management pattern:**
679
690
  ```typescript
680
691
  const spaces = new Map<string, RoolSpaceInfo>();
@@ -689,7 +700,9 @@ client.on('spaceRenamed', (id, name) => {
689
700
 
690
701
  ## RoolSpace API
691
702
 
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.
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.
693
706
 
694
707
  ### Properties
695
708
 
@@ -700,23 +713,34 @@ A space is a lightweight admin handle for space-level operations. It does not ha
700
713
  | `role: RoolUserRole` | User's role |
701
714
  | `linkAccess: LinkAccess` | URL sharing level |
702
715
  | `memberCount: number` | Number of users with access to the space |
716
+ | `channels: ChannelInfo[]` | Live channel list (auto-updates via SSE) |
703
717
 
704
718
  ### Methods
705
719
 
706
720
  | Method | Description |
707
721
  |--------|-------------|
708
722
  | `openChannel(channelId): Promise<RoolChannel>` | Open a channel on this space |
723
+ | `close(): void` | Stop SSE subscription and close all open channels |
709
724
  | `rename(newName): Promise<void>` | Rename this space |
710
725
  | `delete(): Promise<void>` | Permanently delete this space |
711
726
  | `listUsers(): Promise<SpaceMember[]>` | List users with access |
712
727
  | `addUser(userId, role): Promise<void>` | Add user to space |
713
728
  | `removeUser(userId): Promise<void>` | Remove user from space |
714
729
  | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level |
715
- | `getChannels(): ChannelInfo[]` | List channels (from cached snapshot) |
730
+ | `getChannels(): ChannelInfo[]` | List channels (deprecated use `channels` property instead) |
716
731
  | `deleteChannel(channelId): Promise<void>` | Delete a channel |
717
732
  | `exportArchive(): Promise<Blob>` | Export space as zip archive |
718
733
  | `refresh(): Promise<void>` | Refresh space data from server |
719
734
 
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
+
720
744
  ## RoolChannel API
721
745
 
722
746
  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';
3
2
  import { RoolSpace } from './space.js';
4
3
  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 RoolChannel instances.
8
+ * The client is lightweight - most operations happen on RoolSpace and RoolChannel instances.
9
9
  *
10
10
  * Features:
11
11
  * - Authentication (login, logout, token management)
@@ -21,9 +21,7 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
21
21
  private graphqlClient;
22
22
  private subscriptionManager;
23
23
  private logger;
24
- private openChannels;
25
- private spaceSubscriptions;
26
- private spaceDataCache;
24
+ private openSpaces;
27
25
  private _storageCache;
28
26
  private _currentUser;
29
27
  constructor(config?: RoolClientConfig);
@@ -101,24 +99,23 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
101
99
  listSpaces(): Promise<RoolSpaceInfo[]>;
102
100
  /**
103
101
  * Open a channel on a space.
104
- * Fetches full space data, ensures the channel exists, and starts the
105
- * shared space subscription if not already active.
102
+ * Convenience method that opens (or reuses) the space, then opens the channel.
106
103
  *
107
104
  * @param spaceId - The ID of the space
108
105
  * @param channelId - The channel ID (created if it doesn't exist)
109
106
  */
110
107
  openChannel(spaceId: string, channelId: string): Promise<RoolChannel>;
111
108
  /**
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.
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.
115
112
  *
116
- * To work with objects and AI, call space.openChannel(channelId).
113
+ * Call space.close() when done to stop the subscription.
117
114
  */
118
115
  openSpace(spaceId: string): Promise<RoolSpace>;
119
116
  /**
120
117
  * Create a new space.
121
- * Returns a RoolSpace handle for admin operations.
118
+ * Returns a RoolSpace handle with a real-time subscription.
122
119
  * Call space.openChannel(channelId) to start working with objects.
123
120
  */
124
121
  createSpace(name: string): Promise<RoolSpace>;
@@ -210,7 +207,6 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
210
207
  /**
211
208
  * Ensure the client-level event subscription is active.
212
209
  * Called automatically when opening spaces.
213
- * Also fetches and caches the current user ID.
214
210
  * @internal
215
211
  */
216
212
  private ensureSubscribed;
@@ -230,31 +226,10 @@ export declare class RoolClient extends EventEmitter<RoolClientEvents> {
230
226
  * @internal Not part of the public API — use typed methods instead.
231
227
  */
232
228
  _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;
256
229
  /**
257
230
  * 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.
258
233
  * @internal
259
234
  */
260
235
  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, SpaceSubscriptionManager } from './subscription.js';
7
+ import { ClientSubscriptionManager } from './subscription.js';
8
8
  import { MediaClient } from './media.js';
9
9
  import { ExtensionsClient } from './apps.js';
10
- import { RoolChannel, generateEntityId } from './channel.js';
10
+ import { 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 RoolChannel instances.
16
+ * The client is lightweight - most operations happen on RoolSpace and RoolChannel instances.
17
17
  *
18
18
  * Features:
19
19
  * - Authentication (login, logout, token management)
@@ -29,12 +29,8 @@ export class RoolClient extends EventEmitter {
29
29
  graphqlClient;
30
30
  subscriptionManager = null;
31
31
  logger;
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();
32
+ // Open spaces (cached for reuse by openChannel)
33
+ openSpaces = new Map();
38
34
  // User storage cache (synced to localStorage)
39
35
  _storageCache = {};
40
36
  // Current user (fetched during initialize)
@@ -113,16 +109,10 @@ export class RoolClient extends EventEmitter {
113
109
  destroy() {
114
110
  this.authManager.destroy();
115
111
  this.subscriptionManager?.destroy();
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();
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();
126
116
  this.removeAllListeners();
127
117
  }
128
118
  // ===========================================================================
@@ -164,16 +154,10 @@ export class RoolClient extends EventEmitter {
164
154
  logout() {
165
155
  this.authManager.logout();
166
156
  this.unsubscribe();
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();
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();
177
161
  }
178
162
  /**
179
163
  * Process auth callback from URL fragment.
@@ -229,99 +213,70 @@ export class RoolClient extends EventEmitter {
229
213
  }
230
214
  /**
231
215
  * Open a channel on a space.
232
- * Fetches full space data, ensures the channel exists, and starts the
233
- * shared space subscription if not already active.
216
+ * Convenience method that opens (or reuses) the space, then opens the channel.
234
217
  *
235
218
  * @param spaceId - The ID of the space
236
219
  * @param channelId - The channel ID (created if it doesn't exist)
237
220
  */
238
221
  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
- }
242
222
  // Ensure client subscription is active (for lifecycle events)
243
223
  void this.ensureSubscribed();
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;
224
+ const space = await this.openSpace(spaceId);
225
+ return space.openChannel(channelId);
283
226
  }
284
227
  /**
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.
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.
288
231
  *
289
- * To work with objects and AI, call space.openChannel(channelId).
232
+ * Call space.close() when done to stop the subscription.
290
233
  */
291
234
  async openSpace(spaceId) {
292
- const { name, role, linkAccess, memberCount, channels } = await this.graphqlClient.openSpace(spaceId);
293
- return new RoolSpace({
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({
294
243
  id: spaceId,
295
- name,
296
- role: role,
297
- linkAccess,
298
- memberCount,
299
- channels,
244
+ name: fullData.name,
245
+ role: fullData.role,
246
+ userId: fullData.userId,
247
+ linkAccess: fullData.linkAccess,
248
+ memberCount: fullData.memberCount,
249
+ fullData,
300
250
  graphqlClient: this.graphqlClient,
301
251
  mediaClient: this.mediaClient,
302
- openChannelFn: (sid, cid) => this.openChannel(sid, cid),
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);
303
264
  });
265
+ space.on('channelDeleted', (channelId) => {
266
+ this.emit('channelDeleted', spaceId, channelId);
267
+ });
268
+ return space;
304
269
  }
305
270
  /**
306
271
  * Create a new space.
307
- * Returns a RoolSpace handle for admin operations.
272
+ * Returns a RoolSpace handle with a real-time subscription.
308
273
  * Call space.openChannel(channelId) to start working with objects.
309
274
  */
310
275
  async createSpace(name) {
311
276
  // Ensure client subscription is active (for lifecycle events)
312
277
  void this.ensureSubscribed();
313
278
  const { spaceId } = await this.graphqlClient.createSpace(name);
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
- });
279
+ return this.openSpace(spaceId);
325
280
  }
326
281
  /**
327
282
  * Rename a channel in a space.
@@ -343,6 +298,12 @@ export class RoolClient extends EventEmitter {
343
298
  */
344
299
  async deleteSpace(spaceId) {
345
300
  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
+ }
346
307
  // Client-level event will be emitted via SSE subscription
347
308
  }
348
309
  /**
@@ -504,7 +465,6 @@ export class RoolClient extends EventEmitter {
504
465
  /**
505
466
  * Ensure the client-level event subscription is active.
506
467
  * Called automatically when opening spaces.
507
- * Also fetches and caches the current user ID.
508
468
  * @internal
509
469
  */
510
470
  async ensureSubscribed() {
@@ -553,125 +513,12 @@ export class RoolClient extends EventEmitter {
553
513
  return this.graphqlClient.query(query, variables);
554
514
  }
555
515
  // ===========================================================================
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
- // ===========================================================================
671
516
  // Private Methods - Event Handling
672
517
  // ===========================================================================
673
518
  /**
674
519
  * 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.
675
522
  * @internal
676
523
  */
677
524
  handleClientEvent(event) {
@@ -716,24 +563,10 @@ export class RoolClient extends EventEmitter {
716
563
  case 'user_storage_changed':
717
564
  this.handleUserStorageChanged(event.key, event.value);
718
565
  break;
566
+ // Channel events from client SSE are ignored — handled by RoolSpace
719
567
  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;
732
568
  case 'channel_renamed':
733
- this.emit('channelRenamed', event.spaceId, event.channelId, event.name);
734
- break;
735
569
  case 'channel_deleted':
736
- this.emit('channelDeleted', event.spaceId, event.channelId);
737
570
  break;
738
571
  }
739
572
  }