@rool-dev/sdk 0.6.0-dev.4595e79 → 0.6.0-dev.adc819f
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 +32 -8
- package/dist/client.d.ts +11 -36
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +61 -228
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/space.d.ts +62 -15
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +259 -19
- package/dist/space.js.map +1 -1
- package/dist/types.d.ts +18 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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`** —
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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('
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
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
|
|
113
|
-
* Returns a
|
|
114
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
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
|
|
7
|
+
import { ClientSubscriptionManager } from './subscription.js';
|
|
8
8
|
import { MediaClient } from './media.js';
|
|
9
9
|
import { ExtensionsClient } from './apps.js';
|
|
10
|
-
import {
|
|
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
|
-
//
|
|
33
|
-
|
|
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
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
286
|
-
* Returns a
|
|
287
|
-
*
|
|
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
|
-
*
|
|
232
|
+
* Call space.close() when done to stop the subscription.
|
|
290
233
|
*/
|
|
291
234
|
async openSpace(spaceId) {
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|