@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 +8 -32
- package/dist/client.d.ts +36 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +228 -61
- 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 +15 -62
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +19 -259
- package/dist/space.js.map +1 -1
- package/dist/types.d.ts +2 -18
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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`** —
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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('
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
110
|
-
* Returns a
|
|
111
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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;
|
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 } 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
|
|
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
|
-
//
|
|
33
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
225
|
-
|
|
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
|
|
229
|
-
* Returns a
|
|
230
|
-
*
|
|
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
|
-
*
|
|
289
|
+
* To work with objects and AI, call space.openChannel(channelId).
|
|
233
290
|
*/
|
|
234
291
|
async openSpace(spaceId) {
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
245
|
-
role:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
fullData,
|
|
295
|
+
name,
|
|
296
|
+
role: role,
|
|
297
|
+
linkAccess,
|
|
298
|
+
memberCount,
|
|
299
|
+
channels,
|
|
250
300
|
graphqlClient: this.graphqlClient,
|
|
251
301
|
mediaClient: this.mediaClient,
|
|
252
|
-
|
|
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
|
|
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
|
|
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
|
}
|