@rool-dev/sdk 0.2.0-dev.64c2b97 → 0.2.0-dev.6769bdc
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 +190 -141
- package/dist/channel.d.ts +316 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +793 -0
- package/dist/channel.js.map +1 -0
- package/dist/client.d.ts +39 -32
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +108 -70
- package/dist/client.js.map +1 -1
- package/dist/graphql.d.ts +17 -8
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +43 -28
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/space.d.ts +28 -295
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +48 -879
- package/dist/space.js.map +1 -1
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +18 -7
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +18 -36
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/space.js
CHANGED
|
@@ -1,690 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
const ID_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
5
|
-
export function generateEntityId() {
|
|
6
|
-
let result = '';
|
|
7
|
-
for (let i = 0; i < 6; i++) {
|
|
8
|
-
result += ID_CHARS[Math.floor(Math.random() * ID_CHARS.length)];
|
|
9
|
-
}
|
|
10
|
-
return result;
|
|
11
|
-
}
|
|
12
|
-
// Default timeout for waiting on SSE object events (30 seconds)
|
|
13
|
-
const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// RoolSpace — Lightweight handle for space-level admin operations
|
|
3
|
+
// =============================================================================
|
|
14
4
|
/**
|
|
15
|
-
*
|
|
5
|
+
* A space is a container for objects, schema, metadata, and channels.
|
|
16
6
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
7
|
+
* RoolSpace is a lightweight handle for space-level admin operations:
|
|
8
|
+
* user management, link access, channel management, and export.
|
|
9
|
+
* It does not have a real-time subscription — use channels for live data.
|
|
20
10
|
*
|
|
21
|
-
*
|
|
22
|
-
* - High-level object operations
|
|
23
|
-
* - Built-in undo/redo with checkpoints
|
|
24
|
-
* - Metadata management
|
|
25
|
-
* - Event emission for state changes
|
|
26
|
-
* - Real-time updates via space-specific subscription
|
|
11
|
+
* To work with objects and AI, open a channel on the space.
|
|
27
12
|
*/
|
|
28
|
-
export class RoolSpace
|
|
13
|
+
export class RoolSpace {
|
|
29
14
|
_id;
|
|
30
15
|
_name;
|
|
31
16
|
_role;
|
|
32
17
|
_linkAccess;
|
|
33
|
-
|
|
34
|
-
_conversationId;
|
|
35
|
-
_closed = false;
|
|
18
|
+
_channels;
|
|
36
19
|
graphqlClient;
|
|
37
20
|
mediaClient;
|
|
38
|
-
|
|
39
|
-
onCloseCallback;
|
|
40
|
-
_subscriptionReady;
|
|
41
|
-
logger;
|
|
42
|
-
// Local cache for bounded data (schema, metadata, conversations, object IDs)
|
|
43
|
-
_meta;
|
|
44
|
-
_schema;
|
|
45
|
-
_conversations;
|
|
46
|
-
_objectIds;
|
|
47
|
-
// Object collection: tracks pending local mutations for dedup
|
|
48
|
-
// Maps objectId → optimistic object data (for create/update) or null (for delete)
|
|
49
|
-
_pendingMutations = new Map();
|
|
50
|
-
// Resolvers waiting for object data from SSE events
|
|
51
|
-
_objectResolvers = new Map();
|
|
52
|
-
// Buffer for object data that arrived before a collector was registered
|
|
53
|
-
_objectBuffer = new Map();
|
|
21
|
+
_openChannelFn;
|
|
54
22
|
constructor(config) {
|
|
55
|
-
super();
|
|
56
23
|
this._id = config.id;
|
|
57
24
|
this._name = config.name;
|
|
58
25
|
this._role = config.role;
|
|
59
26
|
this._linkAccess = config.linkAccess;
|
|
60
|
-
this.
|
|
61
|
-
this._emitterLogger = config.logger;
|
|
62
|
-
this._conversationId = config.conversationId ?? generateEntityId();
|
|
27
|
+
this._channels = config.channels;
|
|
63
28
|
this.graphqlClient = config.graphqlClient;
|
|
64
29
|
this.mediaClient = config.mediaClient;
|
|
65
|
-
this.
|
|
66
|
-
this.onCloseCallback = config.onClose;
|
|
67
|
-
// Initialize local cache from server data
|
|
68
|
-
this._meta = config.initialData.meta ?? {};
|
|
69
|
-
this._schema = config.initialData.schema ?? {};
|
|
70
|
-
this._conversations = config.initialData.conversations ?? {};
|
|
71
|
-
this._objectIds = config.initialData.objectIds ?? [];
|
|
72
|
-
// Create space-level subscription
|
|
73
|
-
this.subscriptionManager = new SpaceSubscriptionManager({
|
|
74
|
-
graphqlUrl: config.graphqlUrl,
|
|
75
|
-
authManager: config.authManager,
|
|
76
|
-
logger: this.logger,
|
|
77
|
-
spaceId: this._id,
|
|
78
|
-
conversationId: this._conversationId,
|
|
79
|
-
onEvent: (event) => this.handleSpaceEvent(event),
|
|
80
|
-
onConnectionStateChanged: () => {
|
|
81
|
-
// Space-level connection state (could emit events if needed)
|
|
82
|
-
},
|
|
83
|
-
onError: (error) => {
|
|
84
|
-
this.logger.error(`[RoolSpace ${this._id}] Subscription error:`, error);
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
// Start subscription - store promise for openSpace/createSpace to await
|
|
88
|
-
this._subscriptionReady = this.subscriptionManager.subscribe();
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Wait for the real-time subscription to be established.
|
|
92
|
-
* Called internally by openSpace/createSpace before returning the space.
|
|
93
|
-
* @internal
|
|
94
|
-
*/
|
|
95
|
-
_waitForSubscription() {
|
|
96
|
-
return this._subscriptionReady;
|
|
30
|
+
this._openChannelFn = config.openChannelFn;
|
|
97
31
|
}
|
|
98
32
|
// ===========================================================================
|
|
99
33
|
// Properties
|
|
100
34
|
// ===========================================================================
|
|
101
|
-
get id() {
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
get
|
|
105
|
-
return this._name;
|
|
106
|
-
}
|
|
107
|
-
get role() {
|
|
108
|
-
return this._role;
|
|
109
|
-
}
|
|
110
|
-
get linkAccess() {
|
|
111
|
-
return this._linkAccess;
|
|
112
|
-
}
|
|
113
|
-
/** Current user's ID (for identifying own interactions) */
|
|
114
|
-
get userId() {
|
|
115
|
-
return this._userId;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Get the conversation ID for this space instance.
|
|
119
|
-
* Used for AI context tracking and echo suppression.
|
|
120
|
-
*/
|
|
121
|
-
get conversationId() {
|
|
122
|
-
return this._conversationId;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Set the conversation ID for AI context tracking.
|
|
126
|
-
* Emits 'conversationIdChanged' event.
|
|
127
|
-
*/
|
|
128
|
-
set conversationId(value) {
|
|
129
|
-
if (value === this._conversationId)
|
|
130
|
-
return;
|
|
131
|
-
const previous = this._conversationId;
|
|
132
|
-
this._conversationId = value;
|
|
133
|
-
this.emit('conversationIdChanged', {
|
|
134
|
-
previousConversationId: previous,
|
|
135
|
-
newConversationId: value,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
get isReadOnly() {
|
|
139
|
-
return this._role === 'viewer';
|
|
140
|
-
}
|
|
35
|
+
get id() { return this._id; }
|
|
36
|
+
get name() { return this._name; }
|
|
37
|
+
get role() { return this._role; }
|
|
38
|
+
get linkAccess() { return this._linkAccess; }
|
|
141
39
|
// ===========================================================================
|
|
142
|
-
//
|
|
40
|
+
// Channel Lifecycle
|
|
143
41
|
// ===========================================================================
|
|
144
42
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*/
|
|
148
|
-
getInteractions() {
|
|
149
|
-
return this._conversations[this._conversationId]?.interactions ?? [];
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Get interactions for a specific conversation ID.
|
|
153
|
-
* Useful for viewing other conversations in the space.
|
|
154
|
-
*/
|
|
155
|
-
getInteractionsById(conversationId) {
|
|
156
|
-
return this._conversations[conversationId]?.interactions ?? [];
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Get all conversation IDs that have conversations in this space.
|
|
43
|
+
* Open a channel on this space with a specific conversation.
|
|
44
|
+
* If the conversation doesn't exist, the server creates it.
|
|
160
45
|
*/
|
|
161
|
-
|
|
162
|
-
return
|
|
46
|
+
async openChannel(conversationId) {
|
|
47
|
+
return this._openChannelFn(this._id, conversationId);
|
|
163
48
|
}
|
|
164
49
|
// ===========================================================================
|
|
165
|
-
// Space
|
|
50
|
+
// Space Admin
|
|
166
51
|
// ===========================================================================
|
|
167
52
|
/**
|
|
168
53
|
* Rename this space.
|
|
169
54
|
*/
|
|
170
55
|
async rename(newName) {
|
|
171
|
-
|
|
56
|
+
await this.graphqlClient.renameSpace(this._id, newName);
|
|
172
57
|
this._name = newName;
|
|
173
|
-
try {
|
|
174
|
-
await this.graphqlClient.renameSpace(this._id, newName);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
this._name = oldName;
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Close this space and clean up resources.
|
|
183
|
-
* Stops real-time subscription and unregisters from client.
|
|
184
|
-
*/
|
|
185
|
-
close() {
|
|
186
|
-
this._closed = true;
|
|
187
|
-
this.subscriptionManager.destroy();
|
|
188
|
-
this.onCloseCallback(this._id);
|
|
189
|
-
// Clean up pending object collectors
|
|
190
|
-
this._objectResolvers.clear();
|
|
191
|
-
this._objectBuffer.clear();
|
|
192
|
-
this._pendingMutations.clear();
|
|
193
|
-
this.removeAllListeners();
|
|
194
|
-
}
|
|
195
|
-
// ===========================================================================
|
|
196
|
-
// Undo / Redo (Server-managed checkpoints)
|
|
197
|
-
// ===========================================================================
|
|
198
|
-
/**
|
|
199
|
-
* Create a checkpoint (seal current batch of changes).
|
|
200
|
-
* @returns The checkpoint ID
|
|
201
|
-
*/
|
|
202
|
-
async checkpoint(label = 'Change') {
|
|
203
|
-
const result = await this.graphqlClient.checkpoint(this._id, label, this._conversationId);
|
|
204
|
-
return result.checkpointId;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Check if undo is available.
|
|
208
|
-
*/
|
|
209
|
-
async canUndo() {
|
|
210
|
-
const status = await this.graphqlClient.checkpointStatus(this._id, this._conversationId);
|
|
211
|
-
return status.canUndo;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Check if redo is available.
|
|
215
|
-
*/
|
|
216
|
-
async canRedo() {
|
|
217
|
-
const status = await this.graphqlClient.checkpointStatus(this._id, this._conversationId);
|
|
218
|
-
return status.canRedo;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Undo the most recent batch of changes.
|
|
222
|
-
* Reverses your most recent batch (sealed or open).
|
|
223
|
-
* Conflicting patches (modified by others) are silently skipped.
|
|
224
|
-
* @returns true if undo was performed
|
|
225
|
-
*/
|
|
226
|
-
async undo() {
|
|
227
|
-
const result = await this.graphqlClient.undo(this._id, this._conversationId);
|
|
228
|
-
// Server broadcasts space_changed, which triggers reset event
|
|
229
|
-
return result.success;
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Redo a previously undone batch of changes.
|
|
233
|
-
* @returns true if redo was performed
|
|
234
|
-
*/
|
|
235
|
-
async redo() {
|
|
236
|
-
const result = await this.graphqlClient.redo(this._id, this._conversationId);
|
|
237
|
-
// Server broadcasts space_changed, which triggers reset event
|
|
238
|
-
return result.success;
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Clear checkpoint history for this conversation.
|
|
242
|
-
*/
|
|
243
|
-
async clearHistory() {
|
|
244
|
-
await this.graphqlClient.clearCheckpointHistory(this._id, this._conversationId);
|
|
245
|
-
}
|
|
246
|
-
// ===========================================================================
|
|
247
|
-
// Object Operations
|
|
248
|
-
// ===========================================================================
|
|
249
|
-
/**
|
|
250
|
-
* Get an object's data by ID.
|
|
251
|
-
* Fetches from the server on each call.
|
|
252
|
-
*/
|
|
253
|
-
async getObject(objectId) {
|
|
254
|
-
return this.graphqlClient.getObject(this._id, objectId);
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Get an object's stat (audit information).
|
|
258
|
-
* Returns modification timestamp and author, or undefined if object not found.
|
|
259
|
-
*/
|
|
260
|
-
async stat(_objectId) {
|
|
261
|
-
// TODO: Requires a dedicated server endpoint for object audit info
|
|
262
|
-
this.logger.warn('[RoolSpace] stat() not yet supported in stateless mode');
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Find objects using structured filters and/or natural language.
|
|
267
|
-
*
|
|
268
|
-
* `where` provides exact-match filtering — values must match literally (no placeholders or operators).
|
|
269
|
-
* `prompt` enables AI-powered semantic queries. When both are provided, `where` and `objectIds`
|
|
270
|
-
* constrain the data set before the AI sees it.
|
|
271
|
-
*
|
|
272
|
-
* @param options.where - Exact-match field filter (e.g. `{ type: 'article' }`). Constrains which objects the AI can see when combined with `prompt`.
|
|
273
|
-
* @param options.prompt - Natural language query. Triggers AI evaluation (uses credits).
|
|
274
|
-
* @param options.limit - Maximum number of results to return (applies to structured filtering only; the AI controls its own result size).
|
|
275
|
-
* @param options.objectIds - Scope search to specific object IDs. Constrains the candidate set in both structured and AI queries.
|
|
276
|
-
* @param options.order - Sort order by modifiedAt: `'asc'` or `'desc'` (default: `'desc'`). Only applies to structured filtering (no `prompt`).
|
|
277
|
-
* @param options.ephemeral - If true, the query won't be recorded in conversation history.
|
|
278
|
-
* @returns The matching objects and a descriptive message.
|
|
279
|
-
*/
|
|
280
|
-
async findObjects(options) {
|
|
281
|
-
return this.graphqlClient.findObjects(this._id, options, this._conversationId);
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Get all object IDs (sync, from local cache).
|
|
285
|
-
* The list is loaded on open and kept current via SSE events.
|
|
286
|
-
* @param options.limit - Maximum number of IDs to return
|
|
287
|
-
* @param options.order - Sort order by modifiedAt ('asc' or 'desc', default: 'desc')
|
|
288
|
-
*/
|
|
289
|
-
getObjectIds(options) {
|
|
290
|
-
let ids = this._objectIds;
|
|
291
|
-
if (options?.order === 'asc') {
|
|
292
|
-
ids = [...ids].reverse();
|
|
293
|
-
}
|
|
294
|
-
if (options?.limit !== undefined) {
|
|
295
|
-
ids = ids.slice(0, options.limit);
|
|
296
|
-
}
|
|
297
|
-
return ids;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Create a new object with optional AI generation.
|
|
301
|
-
* @param options.data - Object data fields (any key-value pairs). Optionally include `id` to use a custom ID. Use {{placeholder}} for AI-generated content. Fields prefixed with _ are hidden from AI.
|
|
302
|
-
* @param options.ephemeral - If true, the operation won't be recorded in conversation history.
|
|
303
|
-
* @returns The created object (with AI-filled content) and message
|
|
304
|
-
*/
|
|
305
|
-
async createObject(options) {
|
|
306
|
-
const { data, ephemeral } = options;
|
|
307
|
-
// Use data.id if provided (string), otherwise generate
|
|
308
|
-
const objectId = typeof data.id === 'string' ? data.id : generateEntityId();
|
|
309
|
-
// Validate ID format: alphanumeric, hyphens, underscores only
|
|
310
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(objectId)) {
|
|
311
|
-
throw new Error(`Invalid object ID "${objectId}". IDs must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
312
|
-
}
|
|
313
|
-
const dataWithId = { ...data, id: objectId };
|
|
314
|
-
// Emit optimistic event and track for dedup
|
|
315
|
-
this._pendingMutations.set(objectId, dataWithId);
|
|
316
|
-
this.emit('objectCreated', { objectId, object: dataWithId, source: 'local_user' });
|
|
317
|
-
try {
|
|
318
|
-
// Await mutation — server processes AI placeholders before responding.
|
|
319
|
-
// SSE events arrive during the await and are buffered via _deliverObject.
|
|
320
|
-
const { message } = await this.graphqlClient.createObject(this.id, dataWithId, this._conversationId, ephemeral);
|
|
321
|
-
// Collect resolved object from buffer (or wait if not yet arrived)
|
|
322
|
-
const object = await this._collectObject(objectId);
|
|
323
|
-
return { object, message };
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
this.logger.error('[RoolSpace] Failed to create object:', error);
|
|
327
|
-
this._pendingMutations.delete(objectId);
|
|
328
|
-
this._cancelCollector(objectId);
|
|
329
|
-
// Emit reset so UI can recover from the optimistic event
|
|
330
|
-
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
331
|
-
this.emit('reset', { source: 'system' });
|
|
332
|
-
throw error;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Update an existing object.
|
|
337
|
-
* @param objectId - The ID of the object to update
|
|
338
|
-
* @param options.data - Fields to add or update. Pass null or undefined to delete a field. Use {{placeholder}} for AI-generated content. Fields prefixed with _ are hidden from AI.
|
|
339
|
-
* @param options.prompt - AI prompt for content editing (optional).
|
|
340
|
-
* @param options.ephemeral - If true, the operation won't be recorded in conversation history.
|
|
341
|
-
* @returns The updated object (with AI-filled content) and message
|
|
342
|
-
*/
|
|
343
|
-
async updateObject(objectId, options) {
|
|
344
|
-
const { data, ephemeral } = options;
|
|
345
|
-
// id is immutable after creation (but null/undefined means delete attempt, which we also reject)
|
|
346
|
-
if (data?.id !== undefined && data.id !== null) {
|
|
347
|
-
throw new Error('Cannot change id in updateObject. The id field is immutable after creation.');
|
|
348
|
-
}
|
|
349
|
-
if (data && ('id' in data)) {
|
|
350
|
-
throw new Error('Cannot delete id field. The id field is immutable after creation.');
|
|
351
|
-
}
|
|
352
|
-
// Normalize undefined to null (for JSON serialization) and build server data
|
|
353
|
-
let serverData;
|
|
354
|
-
if (data) {
|
|
355
|
-
serverData = {};
|
|
356
|
-
for (const [key, value] of Object.entries(data)) {
|
|
357
|
-
// Convert undefined to null for wire protocol
|
|
358
|
-
serverData[key] = value === undefined ? null : value;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
// Emit optimistic event if we have data changes
|
|
362
|
-
if (data) {
|
|
363
|
-
// Build optimistic object (best effort — we may not have the current state)
|
|
364
|
-
const optimistic = { id: objectId, ...data };
|
|
365
|
-
this._pendingMutations.set(objectId, optimistic);
|
|
366
|
-
this.emit('objectUpdated', { objectId, object: optimistic, source: 'local_user' });
|
|
367
|
-
}
|
|
368
|
-
try {
|
|
369
|
-
const { message } = await this.graphqlClient.updateObject(this.id, objectId, this._conversationId, serverData, options.prompt, ephemeral);
|
|
370
|
-
const object = await this._collectObject(objectId);
|
|
371
|
-
return { object, message };
|
|
372
|
-
}
|
|
373
|
-
catch (error) {
|
|
374
|
-
this.logger.error('[RoolSpace] Failed to update object:', error);
|
|
375
|
-
this._pendingMutations.delete(objectId);
|
|
376
|
-
this._cancelCollector(objectId);
|
|
377
|
-
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
378
|
-
this.emit('reset', { source: 'system' });
|
|
379
|
-
throw error;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Delete objects by IDs.
|
|
384
|
-
* Other objects that reference deleted objects via data fields will retain stale ref values.
|
|
385
|
-
*/
|
|
386
|
-
async deleteObjects(objectIds) {
|
|
387
|
-
if (objectIds.length === 0)
|
|
388
|
-
return;
|
|
389
|
-
// Track for dedup and emit optimistic events
|
|
390
|
-
for (const objectId of objectIds) {
|
|
391
|
-
this._pendingMutations.set(objectId, null);
|
|
392
|
-
this.emit('objectDeleted', { objectId, source: 'local_user' });
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
await this.graphqlClient.deleteObjects(this.id, objectIds, this._conversationId);
|
|
396
|
-
}
|
|
397
|
-
catch (error) {
|
|
398
|
-
this.logger.error('[RoolSpace] Failed to delete objects:', error);
|
|
399
|
-
for (const objectId of objectIds) {
|
|
400
|
-
this._pendingMutations.delete(objectId);
|
|
401
|
-
}
|
|
402
|
-
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
403
|
-
this.emit('reset', { source: 'system' });
|
|
404
|
-
throw error;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
// ===========================================================================
|
|
408
|
-
// Collection Schema Operations
|
|
409
|
-
// ===========================================================================
|
|
410
|
-
/**
|
|
411
|
-
* Get the current schema for this space.
|
|
412
|
-
* Returns a map of collection names to their definitions.
|
|
413
|
-
*/
|
|
414
|
-
getSchema() {
|
|
415
|
-
return this._schema;
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Create a new collection schema.
|
|
419
|
-
* @param name - Collection name (must start with a letter, alphanumeric/hyphens/underscores only)
|
|
420
|
-
* @param fields - Field definitions for the collection
|
|
421
|
-
* @returns The created CollectionDef
|
|
422
|
-
*/
|
|
423
|
-
async createCollection(name, fields) {
|
|
424
|
-
if (this._schema[name]) {
|
|
425
|
-
throw new Error(`Collection "${name}" already exists`);
|
|
426
|
-
}
|
|
427
|
-
// Optimistic local update
|
|
428
|
-
const optimisticDef = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
|
|
429
|
-
this._schema[name] = optimisticDef;
|
|
430
|
-
try {
|
|
431
|
-
return await this.graphqlClient.createCollection(this._id, name, fields, this._conversationId);
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
this.logger.error('[RoolSpace] Failed to create collection:', error);
|
|
435
|
-
delete this._schema[name];
|
|
436
|
-
throw error;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Alter an existing collection schema, replacing its field definitions.
|
|
441
|
-
* @param name - Name of the collection to alter
|
|
442
|
-
* @param fields - New field definitions (replaces all existing fields)
|
|
443
|
-
* @returns The updated CollectionDef
|
|
444
|
-
*/
|
|
445
|
-
async alterCollection(name, fields) {
|
|
446
|
-
if (!this._schema[name]) {
|
|
447
|
-
throw new Error(`Collection "${name}" not found`);
|
|
448
|
-
}
|
|
449
|
-
const previous = this._schema[name];
|
|
450
|
-
// Optimistic local update
|
|
451
|
-
this._schema[name] = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
|
|
452
|
-
try {
|
|
453
|
-
return await this.graphqlClient.alterCollection(this._id, name, fields, this._conversationId);
|
|
454
|
-
}
|
|
455
|
-
catch (error) {
|
|
456
|
-
this.logger.error('[RoolSpace] Failed to alter collection:', error);
|
|
457
|
-
this._schema[name] = previous;
|
|
458
|
-
throw error;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Drop a collection schema.
|
|
463
|
-
* @param name - Name of the collection to drop
|
|
464
|
-
*/
|
|
465
|
-
async dropCollection(name) {
|
|
466
|
-
if (!this._schema[name]) {
|
|
467
|
-
throw new Error(`Collection "${name}" not found`);
|
|
468
|
-
}
|
|
469
|
-
const previous = this._schema[name];
|
|
470
|
-
// Optimistic local update
|
|
471
|
-
delete this._schema[name];
|
|
472
|
-
try {
|
|
473
|
-
await this.graphqlClient.dropCollection(this._id, name, this._conversationId);
|
|
474
|
-
}
|
|
475
|
-
catch (error) {
|
|
476
|
-
this.logger.error('[RoolSpace] Failed to drop collection:', error);
|
|
477
|
-
this._schema[name] = previous;
|
|
478
|
-
throw error;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
// ===========================================================================
|
|
482
|
-
// Conversation Management
|
|
483
|
-
// ===========================================================================
|
|
484
|
-
/**
|
|
485
|
-
* Delete a conversation and its interaction history.
|
|
486
|
-
* Defaults to the current conversation if no conversationId is provided.
|
|
487
|
-
*/
|
|
488
|
-
async deleteConversation(conversationId) {
|
|
489
|
-
const targetConversationId = conversationId ?? this._conversationId;
|
|
490
|
-
// Optimistic local update
|
|
491
|
-
const previous = this._conversations[targetConversationId];
|
|
492
|
-
delete this._conversations[targetConversationId];
|
|
493
|
-
// Emit events
|
|
494
|
-
this.emit('conversationUpdated', {
|
|
495
|
-
conversationId: targetConversationId,
|
|
496
|
-
source: 'local_user',
|
|
497
|
-
});
|
|
498
|
-
this.emit('conversationsChanged', {
|
|
499
|
-
action: 'deleted',
|
|
500
|
-
conversationId: targetConversationId,
|
|
501
|
-
source: 'local_user',
|
|
502
|
-
});
|
|
503
|
-
// Call server
|
|
504
|
-
try {
|
|
505
|
-
await this.graphqlClient.deleteConversation(this.id, targetConversationId);
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
this.logger.error('[RoolSpace] Failed to delete conversation:', error);
|
|
509
|
-
if (previous)
|
|
510
|
-
this._conversations[targetConversationId] = previous;
|
|
511
|
-
throw error;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Rename a conversation.
|
|
516
|
-
* If the conversation doesn't exist, it will be created with the given name.
|
|
517
|
-
*/
|
|
518
|
-
async renameConversation(conversationId, name) {
|
|
519
|
-
// Optimistic local update - auto-create if needed
|
|
520
|
-
const isNew = !this._conversations[conversationId];
|
|
521
|
-
const previous = this._conversations[conversationId];
|
|
522
|
-
if (isNew) {
|
|
523
|
-
this._conversations[conversationId] = {
|
|
524
|
-
name,
|
|
525
|
-
createdAt: Date.now(),
|
|
526
|
-
createdBy: this._userId,
|
|
527
|
-
interactions: [],
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
this._conversations[conversationId] = { ...this._conversations[conversationId], name };
|
|
532
|
-
}
|
|
533
|
-
// Emit events
|
|
534
|
-
this.emit('conversationUpdated', {
|
|
535
|
-
conversationId,
|
|
536
|
-
source: 'local_user',
|
|
537
|
-
});
|
|
538
|
-
this.emit('conversationsChanged', {
|
|
539
|
-
action: isNew ? 'created' : 'renamed',
|
|
540
|
-
conversationId,
|
|
541
|
-
name,
|
|
542
|
-
source: 'local_user',
|
|
543
|
-
});
|
|
544
|
-
// Call server
|
|
545
|
-
try {
|
|
546
|
-
await this.graphqlClient.renameConversation(this.id, conversationId, name);
|
|
547
|
-
}
|
|
548
|
-
catch (error) {
|
|
549
|
-
this.logger.error('[RoolSpace] Failed to rename conversation:', error);
|
|
550
|
-
if (isNew) {
|
|
551
|
-
delete this._conversations[conversationId];
|
|
552
|
-
}
|
|
553
|
-
else if (previous) {
|
|
554
|
-
this._conversations[conversationId] = previous;
|
|
555
|
-
}
|
|
556
|
-
throw error;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* List all conversations in this space with summary info.
|
|
561
|
-
* Returns from local cache (kept in sync via SSE).
|
|
562
|
-
*/
|
|
563
|
-
listConversations() {
|
|
564
|
-
return Object.entries(this._conversations).map(([id, conv]) => ({
|
|
565
|
-
id,
|
|
566
|
-
name: conv.name ?? null,
|
|
567
|
-
createdAt: conv.createdAt,
|
|
568
|
-
createdBy: conv.createdBy,
|
|
569
|
-
createdByName: conv.createdByName ?? null,
|
|
570
|
-
interactionCount: conv.interactions.length,
|
|
571
|
-
}));
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Get the system instruction for the current conversation.
|
|
575
|
-
* Returns undefined if no system instruction is set.
|
|
576
|
-
*/
|
|
577
|
-
getSystemInstruction() {
|
|
578
|
-
return this._conversations[this._conversationId]?.systemInstruction;
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Set the system instruction for the current conversation.
|
|
582
|
-
* Pass null to clear the instruction.
|
|
583
|
-
*/
|
|
584
|
-
async setSystemInstruction(instruction) {
|
|
585
|
-
// Optimistic local update
|
|
586
|
-
if (!this._conversations[this._conversationId]) {
|
|
587
|
-
this._conversations[this._conversationId] = {
|
|
588
|
-
createdAt: Date.now(),
|
|
589
|
-
createdBy: this._userId,
|
|
590
|
-
interactions: [],
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
const previous = this._conversations[this._conversationId];
|
|
594
|
-
if (instruction === null) {
|
|
595
|
-
const { systemInstruction: _, ...rest } = this._conversations[this._conversationId];
|
|
596
|
-
this._conversations[this._conversationId] = rest;
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
this._conversations[this._conversationId] = { ...this._conversations[this._conversationId], systemInstruction: instruction };
|
|
600
|
-
}
|
|
601
|
-
// Emit event
|
|
602
|
-
this.emit('conversationUpdated', {
|
|
603
|
-
conversationId: this._conversationId,
|
|
604
|
-
source: 'local_user',
|
|
605
|
-
});
|
|
606
|
-
// Call server
|
|
607
|
-
try {
|
|
608
|
-
await this.graphqlClient.setSystemInstruction(this.id, this._conversationId, instruction);
|
|
609
|
-
}
|
|
610
|
-
catch (error) {
|
|
611
|
-
this.logger.error('[RoolSpace] Failed to set system instruction:', error);
|
|
612
|
-
this._conversations[this._conversationId] = previous;
|
|
613
|
-
throw error;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// ===========================================================================
|
|
617
|
-
// Metadata Operations
|
|
618
|
-
// ===========================================================================
|
|
619
|
-
/**
|
|
620
|
-
* Set a space-level metadata value.
|
|
621
|
-
* Metadata is stored in meta and hidden from AI operations.
|
|
622
|
-
*/
|
|
623
|
-
setMetadata(key, value) {
|
|
624
|
-
this._meta[key] = value;
|
|
625
|
-
this.emit('metadataUpdated', { metadata: this._meta, source: 'local_user' });
|
|
626
|
-
// Fire-and-forget server call
|
|
627
|
-
this.graphqlClient.setSpaceMeta(this.id, this._meta, this._conversationId)
|
|
628
|
-
.catch((error) => {
|
|
629
|
-
this.logger.error('[RoolSpace] Failed to set meta:', error);
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Get a space-level metadata value.
|
|
634
|
-
*/
|
|
635
|
-
getMetadata(key) {
|
|
636
|
-
return this._meta[key];
|
|
637
58
|
}
|
|
638
59
|
/**
|
|
639
|
-
*
|
|
640
|
-
*/
|
|
641
|
-
getAllMetadata() {
|
|
642
|
-
return this._meta;
|
|
643
|
-
}
|
|
644
|
-
// ===========================================================================
|
|
645
|
-
// AI Operations
|
|
646
|
-
// ===========================================================================
|
|
647
|
-
/**
|
|
648
|
-
* Send a prompt to the AI agent for space manipulation.
|
|
649
|
-
* @returns The message from the AI and the list of objects that were created or modified
|
|
60
|
+
* Delete this space permanently. Cannot be undone.
|
|
650
61
|
*/
|
|
651
|
-
async
|
|
652
|
-
|
|
653
|
-
const { attachments, ...rest } = options ?? {};
|
|
654
|
-
let attachmentUrls;
|
|
655
|
-
if (attachments?.length) {
|
|
656
|
-
attachmentUrls = await Promise.all(attachments.map(file => this.mediaClient.upload(this._id, file)));
|
|
657
|
-
}
|
|
658
|
-
const result = await this.graphqlClient.prompt(this._id, prompt, this._conversationId, { ...rest, attachmentUrls });
|
|
659
|
-
// Collect modified objects — they arrive via SSE events during/after the mutation.
|
|
660
|
-
// Try collecting from buffer first, then fetch any missing from server.
|
|
661
|
-
const objects = [];
|
|
662
|
-
const missing = [];
|
|
663
|
-
for (const id of result.modifiedObjectIds) {
|
|
664
|
-
const buffered = this._objectBuffer.get(id);
|
|
665
|
-
if (buffered) {
|
|
666
|
-
this._objectBuffer.delete(id);
|
|
667
|
-
objects.push(buffered);
|
|
668
|
-
}
|
|
669
|
-
else {
|
|
670
|
-
missing.push(id);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
// Fetch any objects not yet received via SSE
|
|
674
|
-
if (missing.length > 0) {
|
|
675
|
-
const fetched = await Promise.all(missing.map(id => this.graphqlClient.getObject(this._id, id)));
|
|
676
|
-
for (const obj of fetched) {
|
|
677
|
-
if (obj)
|
|
678
|
-
objects.push(obj);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return {
|
|
682
|
-
message: result.message,
|
|
683
|
-
objects,
|
|
684
|
-
};
|
|
62
|
+
async delete() {
|
|
63
|
+
await this.graphqlClient.deleteSpace(this._id);
|
|
685
64
|
}
|
|
686
65
|
// ===========================================================================
|
|
687
|
-
//
|
|
66
|
+
// User Management
|
|
688
67
|
// ===========================================================================
|
|
689
68
|
/**
|
|
690
69
|
* List users with access to this space.
|
|
@@ -707,7 +86,6 @@ export class RoolSpace extends EventEmitter {
|
|
|
707
86
|
/**
|
|
708
87
|
* Set the link sharing level for this space.
|
|
709
88
|
* Requires owner or admin role.
|
|
710
|
-
* @param linkAccess - 'none' (no link access), 'viewer' (read-only), or 'editor' (full edit)
|
|
711
89
|
*/
|
|
712
90
|
async setLinkAccess(linkAccess) {
|
|
713
91
|
const previous = this._linkAccess;
|
|
@@ -721,254 +99,45 @@ export class RoolSpace extends EventEmitter {
|
|
|
721
99
|
}
|
|
722
100
|
}
|
|
723
101
|
// ===========================================================================
|
|
724
|
-
//
|
|
102
|
+
// Channel Management
|
|
725
103
|
// ===========================================================================
|
|
726
104
|
/**
|
|
727
|
-
* List
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
return this.mediaClient.list(this._id);
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Upload a file to this space. Returns the URL.
|
|
734
|
-
*/
|
|
735
|
-
async uploadMedia(file) {
|
|
736
|
-
return this.mediaClient.upload(this._id, file);
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Fetch any URL, returning headers and a blob() method (like fetch Response).
|
|
740
|
-
* Adds auth headers for backend media URLs, fetches external URLs via server proxy if CORS blocks.
|
|
105
|
+
* List channels in this space.
|
|
106
|
+
* Returns from cached snapshot (populated at open time).
|
|
107
|
+
* Call refresh() to update from server.
|
|
741
108
|
*/
|
|
742
|
-
|
|
743
|
-
return this.
|
|
109
|
+
getChannels() {
|
|
110
|
+
return this._channels;
|
|
744
111
|
}
|
|
745
112
|
/**
|
|
746
|
-
* Delete a
|
|
113
|
+
* Delete a channel (conversation) from this space.
|
|
747
114
|
*/
|
|
748
|
-
async
|
|
749
|
-
|
|
115
|
+
async deleteChannel(channelId) {
|
|
116
|
+
await this.graphqlClient.deleteConversation(this._id, channelId);
|
|
117
|
+
this._channels = this._channels.filter(c => c.id !== channelId);
|
|
750
118
|
}
|
|
751
119
|
// ===========================================================================
|
|
752
|
-
//
|
|
753
|
-
// ===========================================================================
|
|
754
|
-
// ===========================================================================
|
|
755
|
-
// Import/Export
|
|
120
|
+
// Export
|
|
756
121
|
// ===========================================================================
|
|
757
122
|
/**
|
|
758
123
|
* Export space data and media as a zip archive.
|
|
759
|
-
* The archive contains a data.json file with objects, relations, metadata,
|
|
760
|
-
* and conversations, plus a media/ folder with all media files.
|
|
761
|
-
* @returns A Blob containing the zip archive
|
|
762
124
|
*/
|
|
763
125
|
async exportArchive() {
|
|
764
126
|
return this.mediaClient.exportArchive(this._id);
|
|
765
127
|
}
|
|
766
128
|
// ===========================================================================
|
|
767
|
-
//
|
|
129
|
+
// Refresh
|
|
768
130
|
// ===========================================================================
|
|
769
131
|
/**
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
* @internal
|
|
773
|
-
*/
|
|
774
|
-
_collectObject(objectId) {
|
|
775
|
-
return new Promise((resolve, reject) => {
|
|
776
|
-
// Check buffer first — SSE event may have arrived before the HTTP response
|
|
777
|
-
const buffered = this._objectBuffer.get(objectId);
|
|
778
|
-
if (buffered) {
|
|
779
|
-
this._objectBuffer.delete(objectId);
|
|
780
|
-
resolve(buffered);
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
const timer = setTimeout(() => {
|
|
784
|
-
this._objectResolvers.delete(objectId);
|
|
785
|
-
// Fallback: try to fetch from server
|
|
786
|
-
this.graphqlClient.getObject(this._id, objectId).then(obj => {
|
|
787
|
-
if (obj) {
|
|
788
|
-
resolve(obj);
|
|
789
|
-
}
|
|
790
|
-
else {
|
|
791
|
-
reject(new Error(`Timeout waiting for object ${objectId} from SSE`));
|
|
792
|
-
}
|
|
793
|
-
}).catch(reject);
|
|
794
|
-
}, OBJECT_COLLECT_TIMEOUT);
|
|
795
|
-
this._objectResolvers.set(objectId, (obj) => {
|
|
796
|
-
clearTimeout(timer);
|
|
797
|
-
resolve(obj);
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* Cancel a pending object collector (e.g., on mutation error).
|
|
803
|
-
* @internal
|
|
132
|
+
* Refresh space data from the server.
|
|
133
|
+
* Updates name, role, linkAccess, and channel list.
|
|
804
134
|
*/
|
|
805
|
-
|
|
806
|
-
this.
|
|
807
|
-
this.
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
* @internal
|
|
812
|
-
*/
|
|
813
|
-
_deliverObject(objectId, object) {
|
|
814
|
-
const resolver = this._objectResolvers.get(objectId);
|
|
815
|
-
if (resolver) {
|
|
816
|
-
resolver(object);
|
|
817
|
-
this._objectResolvers.delete(objectId);
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
// Buffer for prompt() or late collectors
|
|
821
|
-
this._objectBuffer.set(objectId, object);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
// ===========================================================================
|
|
825
|
-
// Event Handlers (internal - handles space subscription events)
|
|
826
|
-
// ===========================================================================
|
|
827
|
-
/**
|
|
828
|
-
* Handle a space event from the subscription.
|
|
829
|
-
* @internal
|
|
830
|
-
*/
|
|
831
|
-
handleSpaceEvent(event) {
|
|
832
|
-
// Ignore events after close - the space is being torn down
|
|
833
|
-
if (this._closed)
|
|
834
|
-
return;
|
|
835
|
-
const changeSource = event.source === 'agent' ? 'remote_agent' : 'remote_user';
|
|
836
|
-
switch (event.type) {
|
|
837
|
-
case 'object_created':
|
|
838
|
-
if (event.objectId && event.object) {
|
|
839
|
-
this._handleObjectCreated(event.objectId, event.object, changeSource);
|
|
840
|
-
}
|
|
841
|
-
break;
|
|
842
|
-
case 'object_updated':
|
|
843
|
-
if (event.objectId && event.object) {
|
|
844
|
-
this._handleObjectUpdated(event.objectId, event.object, changeSource);
|
|
845
|
-
}
|
|
846
|
-
break;
|
|
847
|
-
case 'object_deleted':
|
|
848
|
-
if (event.objectId) {
|
|
849
|
-
this._handleObjectDeleted(event.objectId, changeSource);
|
|
850
|
-
}
|
|
851
|
-
break;
|
|
852
|
-
case 'schema_updated':
|
|
853
|
-
if (event.schema) {
|
|
854
|
-
this._schema = event.schema;
|
|
855
|
-
}
|
|
856
|
-
break;
|
|
857
|
-
case 'metadata_updated':
|
|
858
|
-
if (event.metadata) {
|
|
859
|
-
this._meta = event.metadata;
|
|
860
|
-
this.emit('metadataUpdated', { metadata: this._meta, source: changeSource });
|
|
861
|
-
}
|
|
862
|
-
break;
|
|
863
|
-
case 'conversation_updated':
|
|
864
|
-
if (event.conversationId && event.conversation) {
|
|
865
|
-
this._conversations[event.conversationId] = event.conversation;
|
|
866
|
-
this.emit('conversationUpdated', { conversationId: event.conversationId, source: changeSource });
|
|
867
|
-
// Emit conversationsChanged if this is a new conversation
|
|
868
|
-
this.emit('conversationsChanged', {
|
|
869
|
-
action: 'created',
|
|
870
|
-
conversationId: event.conversationId,
|
|
871
|
-
name: event.conversation.name,
|
|
872
|
-
source: changeSource,
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
break;
|
|
876
|
-
case 'conversation_deleted':
|
|
877
|
-
if (event.conversationId) {
|
|
878
|
-
delete this._conversations[event.conversationId];
|
|
879
|
-
this.emit('conversationUpdated', { conversationId: event.conversationId, source: changeSource });
|
|
880
|
-
this.emit('conversationsChanged', {
|
|
881
|
-
action: 'deleted',
|
|
882
|
-
conversationId: event.conversationId,
|
|
883
|
-
source: changeSource,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
break;
|
|
887
|
-
case 'space_changed':
|
|
888
|
-
// Full reload needed (undo/redo, bulk operations)
|
|
889
|
-
void this.graphqlClient.getSpace(this._id).then(({ data }) => {
|
|
890
|
-
if (this._closed)
|
|
891
|
-
return;
|
|
892
|
-
this._meta = data.meta ?? {};
|
|
893
|
-
this._schema = data.schema ?? {};
|
|
894
|
-
this._conversations = data.conversations ?? {};
|
|
895
|
-
this._objectIds = data.objectIds ?? [];
|
|
896
|
-
this.emit('reset', { source: changeSource });
|
|
897
|
-
});
|
|
898
|
-
break;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* Handle an object_created SSE event.
|
|
903
|
-
* Deduplicates against optimistic local creates.
|
|
904
|
-
* @internal
|
|
905
|
-
*/
|
|
906
|
-
_handleObjectCreated(objectId, object, source) {
|
|
907
|
-
// Deliver to any pending collector (for mutation return values)
|
|
908
|
-
this._deliverObject(objectId, object);
|
|
909
|
-
// Maintain local ID list — prepend (most recently modified first)
|
|
910
|
-
this._objectIds = [objectId, ...this._objectIds.filter(id => id !== objectId)];
|
|
911
|
-
const pending = this._pendingMutations.get(objectId);
|
|
912
|
-
if (pending !== undefined) {
|
|
913
|
-
// This is our own mutation echoed back
|
|
914
|
-
this._pendingMutations.delete(objectId);
|
|
915
|
-
if (pending !== null) {
|
|
916
|
-
// It was a create — already emitted objectCreated optimistically.
|
|
917
|
-
// Emit objectUpdated only if AI resolved placeholders (data changed).
|
|
918
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
919
|
-
this.emit('objectUpdated', { objectId, object, source });
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
else {
|
|
924
|
-
// Remote event — emit normally
|
|
925
|
-
this.emit('objectCreated', { objectId, object, source });
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Handle an object_updated SSE event.
|
|
930
|
-
* Deduplicates against optimistic local updates.
|
|
931
|
-
* @internal
|
|
932
|
-
*/
|
|
933
|
-
_handleObjectUpdated(objectId, object, source) {
|
|
934
|
-
// Deliver to any pending collector
|
|
935
|
-
this._deliverObject(objectId, object);
|
|
936
|
-
// Maintain local ID list — move to front (most recently modified)
|
|
937
|
-
this._objectIds = [objectId, ...this._objectIds.filter(id => id !== objectId)];
|
|
938
|
-
const pending = this._pendingMutations.get(objectId);
|
|
939
|
-
if (pending !== undefined) {
|
|
940
|
-
// This is our own mutation echoed back
|
|
941
|
-
this._pendingMutations.delete(objectId);
|
|
942
|
-
if (pending !== null) {
|
|
943
|
-
// Already emitted objectUpdated optimistically.
|
|
944
|
-
// Emit again only if data changed (AI resolved placeholders).
|
|
945
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
946
|
-
this.emit('objectUpdated', { objectId, object, source });
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
// Remote event
|
|
952
|
-
this.emit('objectUpdated', { objectId, object, source });
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Handle an object_deleted SSE event.
|
|
957
|
-
* Deduplicates against optimistic local deletes.
|
|
958
|
-
* @internal
|
|
959
|
-
*/
|
|
960
|
-
_handleObjectDeleted(objectId, source) {
|
|
961
|
-
// Remove from local ID list
|
|
962
|
-
this._objectIds = this._objectIds.filter(id => id !== objectId);
|
|
963
|
-
const pending = this._pendingMutations.get(objectId);
|
|
964
|
-
if (pending !== undefined) {
|
|
965
|
-
// This is our own delete echoed back — already emitted
|
|
966
|
-
this._pendingMutations.delete(objectId);
|
|
967
|
-
}
|
|
968
|
-
else {
|
|
969
|
-
// Remote event
|
|
970
|
-
this.emit('objectDeleted', { objectId, source });
|
|
971
|
-
}
|
|
135
|
+
async refresh() {
|
|
136
|
+
const { name, role, linkAccess, channels } = await this.graphqlClient.openSpace(this._id);
|
|
137
|
+
this._name = name;
|
|
138
|
+
this._role = role;
|
|
139
|
+
this._linkAccess = linkAccess;
|
|
140
|
+
this._channels = channels;
|
|
972
141
|
}
|
|
973
142
|
}
|
|
974
143
|
//# sourceMappingURL=space.js.map
|