@rool-dev/client 0.3.1-dev.ff91c85 → 0.4.0-dev.22f8ef0

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/dist/space.js ADDED
@@ -0,0 +1,730 @@
1
+ import { immutableJSONPatch } from 'immutable-json-patch';
2
+ import { EventEmitter } from './event-emitter.js';
3
+ const MAX_UNDO_STACK_SIZE = 50;
4
+ // 6-character alphanumeric ID (62^6 = 56.8 billion possible values)
5
+ const ID_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
6
+ export function generateEntityId() {
7
+ let result = '';
8
+ for (let i = 0; i < 6; i++) {
9
+ result += ID_CHARS[Math.floor(Math.random() * ID_CHARS.length)];
10
+ }
11
+ return result;
12
+ }
13
+ /**
14
+ * First-class Space object.
15
+ *
16
+ * Features:
17
+ * - High-level object/link operations
18
+ * - Built-in undo/redo with checkpoints
19
+ * - Metadata management
20
+ * - Event emission for state changes
21
+ * - Real-time subscription support
22
+ */
23
+ export class RoolSpace extends EventEmitter {
24
+ _id;
25
+ _name;
26
+ _role;
27
+ _data;
28
+ graphqlClient;
29
+ mediaClient;
30
+ onRegisterForEvents;
31
+ onUnregisterFromEvents;
32
+ // Undo/redo stacks
33
+ undoStack = [];
34
+ redoStack = [];
35
+ // Subscription state
36
+ _isSubscribed = false;
37
+ constructor(config) {
38
+ super();
39
+ this._id = config.id;
40
+ this._name = config.name;
41
+ this._role = config.role;
42
+ this._data = config.initialData;
43
+ this.graphqlClient = config.graphqlClient;
44
+ this.mediaClient = config.mediaClient;
45
+ this.onRegisterForEvents = config.onRegisterForEvents;
46
+ this.onUnregisterFromEvents = config.onUnregisterFromEvents;
47
+ }
48
+ // ===========================================================================
49
+ // Properties
50
+ // ===========================================================================
51
+ get id() {
52
+ return this._id;
53
+ }
54
+ get name() {
55
+ return this._name;
56
+ }
57
+ get role() {
58
+ return this._role;
59
+ }
60
+ get isReadOnly() {
61
+ return this._role === 'viewer';
62
+ }
63
+ get isSubscribed() {
64
+ return this._isSubscribed;
65
+ }
66
+ // ===========================================================================
67
+ // Space Lifecycle
68
+ // ===========================================================================
69
+ /**
70
+ * Rename this space.
71
+ */
72
+ async rename(newName) {
73
+ const oldName = this._name;
74
+ this._name = newName;
75
+ try {
76
+ await this.graphqlClient.renameSpace(this._id, newName);
77
+ }
78
+ catch (error) {
79
+ this._name = oldName;
80
+ throw error;
81
+ }
82
+ }
83
+ /**
84
+ * Subscribe to real-time updates for this space.
85
+ * Registers with the client for event routing.
86
+ */
87
+ subscribe() {
88
+ if (this._isSubscribed)
89
+ return;
90
+ this._isSubscribed = true;
91
+ this.onRegisterForEvents(this._id, this);
92
+ }
93
+ /**
94
+ * Unsubscribe from real-time updates.
95
+ */
96
+ unsubscribe() {
97
+ if (!this._isSubscribed)
98
+ return;
99
+ this._isSubscribed = false;
100
+ this.onUnregisterFromEvents(this._id);
101
+ }
102
+ /**
103
+ * Close this space and clean up resources.
104
+ */
105
+ close() {
106
+ this.unsubscribe();
107
+ this.undoStack = [];
108
+ this.redoStack = [];
109
+ this.removeAllListeners();
110
+ }
111
+ // ===========================================================================
112
+ // Undo / Redo
113
+ // ===========================================================================
114
+ /**
115
+ * Create a checkpoint for undo.
116
+ * Call this before a user action to capture the current state.
117
+ */
118
+ checkpoint(label = 'Change') {
119
+ const entry = {
120
+ timestamp: Date.now(),
121
+ label,
122
+ data: JSON.parse(JSON.stringify(this._data)),
123
+ };
124
+ this.undoStack.push(entry);
125
+ // Limit stack size
126
+ if (this.undoStack.length > MAX_UNDO_STACK_SIZE) {
127
+ this.undoStack.shift();
128
+ }
129
+ // Clear redo stack (new action invalidates redo)
130
+ this.redoStack = [];
131
+ }
132
+ /**
133
+ * Check if undo is available.
134
+ */
135
+ canUndo() {
136
+ return this.undoStack.length > 0;
137
+ }
138
+ /**
139
+ * Check if redo is available.
140
+ */
141
+ canRedo() {
142
+ return this.redoStack.length > 0;
143
+ }
144
+ /**
145
+ * Undo to the previous checkpoint.
146
+ * @returns true if undo was performed
147
+ */
148
+ async undo() {
149
+ if (!this.canUndo())
150
+ return false;
151
+ // Save current state to redo stack
152
+ const currentEntry = {
153
+ timestamp: Date.now(),
154
+ label: 'Redo point',
155
+ data: JSON.parse(JSON.stringify(this._data)),
156
+ };
157
+ this.redoStack.push(currentEntry);
158
+ // Restore previous state
159
+ const previousEntry = this.undoStack.pop();
160
+ this._data = previousEntry.data;
161
+ // Sync to server - resync on failure
162
+ try {
163
+ await this.graphqlClient.setSpace(this._id, this._data);
164
+ this.emit('reset', { source: 'local_user' });
165
+ }
166
+ catch (error) {
167
+ console.error('[Space] Failed to sync undo to server:', error);
168
+ await this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
169
+ }
170
+ return true;
171
+ }
172
+ /**
173
+ * Redo a previously undone action.
174
+ * @returns true if redo was performed
175
+ */
176
+ async redo() {
177
+ if (!this.canRedo())
178
+ return false;
179
+ // Save current state to undo stack
180
+ const currentEntry = {
181
+ timestamp: Date.now(),
182
+ label: 'Undo point',
183
+ data: JSON.parse(JSON.stringify(this._data)),
184
+ };
185
+ this.undoStack.push(currentEntry);
186
+ // Restore next state
187
+ const nextEntry = this.redoStack.pop();
188
+ this._data = nextEntry.data;
189
+ // Sync to server - resync on failure
190
+ try {
191
+ await this.graphqlClient.setSpace(this._id, this._data);
192
+ this.emit('reset', { source: 'local_user' });
193
+ }
194
+ catch (error) {
195
+ console.error('[Space] Failed to sync redo to server:', error);
196
+ await this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
197
+ }
198
+ return true;
199
+ }
200
+ /**
201
+ * Clear undo/redo history.
202
+ * Called when external changes invalidate local history.
203
+ */
204
+ clearHistory() {
205
+ this.undoStack = [];
206
+ this.redoStack = [];
207
+ }
208
+ // ===========================================================================
209
+ // Object Operations
210
+ // ===========================================================================
211
+ /**
212
+ * Get an object's data by ID.
213
+ * Returns just the data portion (RoolObject), not the full entry with meta/links.
214
+ * @throws Error if object not found
215
+ */
216
+ getObject(objectId) {
217
+ const entry = this._data.objects[objectId];
218
+ if (!entry) {
219
+ throw new Error(`Object ${objectId} not found`);
220
+ }
221
+ return entry.data;
222
+ }
223
+ /**
224
+ * Get an object's data by ID, or undefined if not found.
225
+ */
226
+ getObjectOrUndefined(objectId) {
227
+ return this._data.objects[objectId]?.data;
228
+ }
229
+ /**
230
+ * Get an object's metadata (position, UI state, etc).
231
+ */
232
+ getObjectMeta(objectId) {
233
+ const entry = this._data.objects[objectId];
234
+ if (!entry) {
235
+ throw new Error(`Object ${objectId} not found`);
236
+ }
237
+ return entry.meta;
238
+ }
239
+ /**
240
+ * Query objects by field values.
241
+ * Returns all objects where all specified fields match the given values.
242
+ *
243
+ * @example
244
+ * space.queryObjects({ type: 'article' })
245
+ * space.queryObjects({ type: 'task', status: 'done' })
246
+ */
247
+ queryObjects(where) {
248
+ return Object.values(this._data.objects)
249
+ .filter(entry => {
250
+ return Object.entries(where).every(([key, value]) => entry.data[key] === value);
251
+ })
252
+ .map(entry => entry.data);
253
+ }
254
+ /**
255
+ * Get all object IDs.
256
+ */
257
+ getObjectIds() {
258
+ return Object.keys(this._data.objects);
259
+ }
260
+ /**
261
+ * Create a new object with optional AI generation.
262
+ * @param options.data - Object data fields (any key-value pairs). Use {{placeholder}} for AI-generated content.
263
+ * @param options.meta - Client-private metadata (optional). Hidden from AI operations.
264
+ * @param options.prompt - AI prompt for content generation (optional).
265
+ * @returns The generated object ID
266
+ */
267
+ async createObject(options) {
268
+ const objectId = generateEntityId();
269
+ const { data, meta, prompt } = options;
270
+ // Build the entry for local state
271
+ const entry = {
272
+ meta: meta ?? {},
273
+ links: {},
274
+ data,
275
+ };
276
+ // Update local state immediately (optimistic)
277
+ this._data.objects[objectId] = entry;
278
+ this.emit('objectCreated', { objectId, object: entry.data, source: 'local_user' });
279
+ // Await server call
280
+ try {
281
+ const message = await this.graphqlClient.createObject(this.id, objectId, data, meta, prompt);
282
+ return { id: objectId, message };
283
+ }
284
+ catch (error) {
285
+ console.error('[Space] Failed to create object:', error);
286
+ this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
287
+ throw error;
288
+ }
289
+ }
290
+ /**
291
+ * Update an existing object.
292
+ * @param objectId - The ID of the object to update
293
+ * @param options.data - Fields to add or update. Use {{placeholder}} for AI-generated content.
294
+ * @param options.meta - Client-private metadata to merge. Hidden from AI operations.
295
+ * @param options.prompt - AI prompt for content editing (optional).
296
+ */
297
+ async updateObject(objectId, options) {
298
+ const entry = this._data.objects[objectId];
299
+ if (!entry) {
300
+ throw new Error(`Object ${objectId} not found for update`);
301
+ }
302
+ const { data, meta } = options;
303
+ // Build local updates
304
+ if (data) {
305
+ Object.assign(entry.data, data);
306
+ }
307
+ if (meta) {
308
+ entry.meta = { ...entry.meta, ...meta };
309
+ }
310
+ // Emit semantic event with updated object
311
+ if (data || meta) {
312
+ this.emit('objectUpdated', { objectId, object: entry.data, source: 'local_user' });
313
+ }
314
+ // Await server call
315
+ try {
316
+ return await this.graphqlClient.updateObject(this.id, objectId, data, meta, options.prompt);
317
+ }
318
+ catch (error) {
319
+ console.error('[Space] Failed to update object:', error);
320
+ this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
321
+ throw error;
322
+ }
323
+ }
324
+ /**
325
+ * Delete objects by IDs.
326
+ * Outbound links are automatically deleted with the object.
327
+ * Inbound links become orphans (tolerated).
328
+ */
329
+ async deleteObjects(objectIds) {
330
+ if (objectIds.length === 0)
331
+ return;
332
+ const deletedObjectIds = [];
333
+ // Collect links that will be orphaned (for events)
334
+ const deletedLinks = [];
335
+ for (const objectId of objectIds) {
336
+ const entry = this._data.objects[objectId];
337
+ if (entry) {
338
+ // Collect outbound links for deletion events
339
+ for (const [linkType, targets] of Object.entries(entry.links)) {
340
+ for (const targetId of Object.keys(targets)) {
341
+ deletedLinks.push({ sourceId: objectId, targetId, linkType });
342
+ }
343
+ }
344
+ }
345
+ }
346
+ // Remove objects (local state)
347
+ for (const objectId of objectIds) {
348
+ if (this._data.objects[objectId]) {
349
+ delete this._data.objects[objectId];
350
+ deletedObjectIds.push(objectId);
351
+ }
352
+ }
353
+ // Emit semantic events
354
+ for (const link of deletedLinks) {
355
+ this.emit('unlinked', { ...link, source: 'local_user' });
356
+ }
357
+ for (const objectId of deletedObjectIds) {
358
+ this.emit('objectDeleted', { objectId, source: 'local_user' });
359
+ }
360
+ // Await server call
361
+ try {
362
+ await this.graphqlClient.deleteObjects(this.id, objectIds);
363
+ }
364
+ catch (error) {
365
+ console.error('[Space] Failed to delete objects:', error);
366
+ this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
367
+ throw error;
368
+ }
369
+ }
370
+ // ===========================================================================
371
+ // Link Operations
372
+ // ===========================================================================
373
+ /**
374
+ * Create a link between objects.
375
+ * Links are stored on the source object.
376
+ */
377
+ async link(sourceId, targetId, linkType) {
378
+ const entry = this._data.objects[sourceId];
379
+ if (!entry) {
380
+ throw new Error(`Source object ${sourceId} not found`);
381
+ }
382
+ // Update local state immediately
383
+ if (!entry.links[linkType]) {
384
+ entry.links[linkType] = {};
385
+ }
386
+ entry.links[linkType][targetId] = {};
387
+ const linkData = entry.links[linkType][targetId];
388
+ this.emit('linked', { sourceId, targetId, linkType, linkData, source: 'local_user' });
389
+ // Await server call
390
+ try {
391
+ await this.graphqlClient.link(this.id, sourceId, targetId, linkType);
392
+ }
393
+ catch (error) {
394
+ console.error('[Space] Failed to create link:', error);
395
+ this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
396
+ throw error;
397
+ }
398
+ }
399
+ /**
400
+ * Remove a link between two objects.
401
+ * @param linkType - Optional: if provided, only removes that type; otherwise removes all links between the objects
402
+ * @returns true if any links were removed
403
+ */
404
+ async unlink(sourceId, targetId, linkType) {
405
+ const entry = this._data.objects[sourceId];
406
+ if (!entry) {
407
+ throw new Error(`Source object ${sourceId} not found`);
408
+ }
409
+ const deletedLinks = [];
410
+ // Update local state
411
+ if (linkType) {
412
+ // Remove specific link type
413
+ if (entry.links[linkType]?.[targetId] !== undefined) {
414
+ delete entry.links[linkType][targetId];
415
+ deletedLinks.push({ linkType });
416
+ }
417
+ }
418
+ else {
419
+ // Remove all links from source to target
420
+ for (const [type, targets] of Object.entries(entry.links)) {
421
+ if (targets[targetId] !== undefined) {
422
+ delete targets[targetId];
423
+ deletedLinks.push({ linkType: type });
424
+ }
425
+ }
426
+ }
427
+ // Emit semantic events
428
+ for (const link of deletedLinks) {
429
+ this.emit('unlinked', { sourceId, targetId, linkType: link.linkType, source: 'local_user' });
430
+ }
431
+ // Await server call
432
+ try {
433
+ await this.graphqlClient.unlink(this.id, sourceId, targetId);
434
+ }
435
+ catch (error) {
436
+ console.error('[Space] Failed to remove link:', error);
437
+ this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
438
+ throw error;
439
+ }
440
+ return deletedLinks.length > 0;
441
+ }
442
+ /**
443
+ * Get parent object IDs (objects that have links pointing TO this object).
444
+ * @param linkType - Optional filter by link type
445
+ */
446
+ getParents(objectId, linkType) {
447
+ const parents = [];
448
+ for (const [entryId, entry] of Object.entries(this._data.objects)) {
449
+ for (const [type, targets] of Object.entries(entry.links)) {
450
+ if ((!linkType || type === linkType) && objectId in targets) {
451
+ parents.push(entryId);
452
+ break; // Found a link, move to next object
453
+ }
454
+ }
455
+ }
456
+ return parents;
457
+ }
458
+ /**
459
+ * Get child object IDs (objects that this object has links pointing TO).
460
+ * Filters out orphan targets (targets that don't exist).
461
+ * @param linkType - Optional filter by link type
462
+ */
463
+ getChildren(objectId, linkType) {
464
+ const entry = this._data.objects[objectId];
465
+ if (!entry)
466
+ return [];
467
+ const children = [];
468
+ for (const [type, targets] of Object.entries(entry.links)) {
469
+ if (!linkType || type === linkType) {
470
+ for (const targetId of Object.keys(targets)) {
471
+ // Filter orphans - only return existing targets
472
+ if (this._data.objects[targetId]) {
473
+ children.push(targetId);
474
+ }
475
+ }
476
+ }
477
+ }
478
+ return children;
479
+ }
480
+ /**
481
+ * Get all child object IDs including orphans (targets that may not exist).
482
+ * @param linkType - Optional filter by link type
483
+ */
484
+ getChildrenIncludingOrphans(objectId, linkType) {
485
+ const entry = this._data.objects[objectId];
486
+ if (!entry)
487
+ return [];
488
+ const children = [];
489
+ for (const [type, targets] of Object.entries(entry.links)) {
490
+ if (!linkType || type === linkType) {
491
+ children.push(...Object.keys(targets));
492
+ }
493
+ }
494
+ return children;
495
+ }
496
+ // ===========================================================================
497
+ // Metadata Operations
498
+ // ===========================================================================
499
+ /**
500
+ * Set a space-level metadata value.
501
+ * Metadata is stored in meta and hidden from AI operations.
502
+ */
503
+ setMetadata(key, value) {
504
+ if (!this._data.meta) {
505
+ this._data.meta = {};
506
+ }
507
+ this._data.meta[key] = value;
508
+ this.emit('metaUpdated', { meta: this._data.meta, source: 'local_user' });
509
+ // Fire-and-forget server call - errors trigger resync
510
+ this.graphqlClient.setSpaceMeta(this.id, this._data.meta)
511
+ .catch((error) => {
512
+ console.error('[Space] Failed to set graph meta:', error);
513
+ this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
514
+ });
515
+ }
516
+ /**
517
+ * Get a space-level metadata value.
518
+ */
519
+ getMetadata(key) {
520
+ return this._data.meta?.[key];
521
+ }
522
+ /**
523
+ * Get all space-level metadata.
524
+ */
525
+ getAllMetadata() {
526
+ return this._data.meta ?? {};
527
+ }
528
+ // ===========================================================================
529
+ // AI Operations
530
+ // ===========================================================================
531
+ /**
532
+ * Send a prompt to the AI agent for space manipulation.
533
+ */
534
+ async prompt(prompt, options) {
535
+ return this.graphqlClient.prompt(this._id, prompt, options);
536
+ }
537
+ // ===========================================================================
538
+ // Collaboration
539
+ // ===========================================================================
540
+ /**
541
+ * List users with access to this space.
542
+ */
543
+ async listUsers() {
544
+ return this.graphqlClient.listSpaceUsers(this._id);
545
+ }
546
+ /**
547
+ * Add a user to this space with specified role.
548
+ */
549
+ async addUser(userId, role) {
550
+ return this.graphqlClient.addSpaceUser(this._id, userId, role);
551
+ }
552
+ /**
553
+ * Remove a user from this space.
554
+ */
555
+ async removeUser(userId) {
556
+ return this.graphqlClient.removeSpaceUser(this._id, userId);
557
+ }
558
+ // ===========================================================================
559
+ // Media Operations
560
+ // ===========================================================================
561
+ /**
562
+ * List all media files for this space.
563
+ */
564
+ async listMedia() {
565
+ return this.mediaClient.list(this._id);
566
+ }
567
+ /**
568
+ * Upload a file to this space.
569
+ */
570
+ async uploadMedia(file) {
571
+ return this.mediaClient.upload(this._id, file);
572
+ }
573
+ /**
574
+ * Get the URL for a media file.
575
+ */
576
+ getMediaUrl(uuid) {
577
+ return this.mediaClient.getUrl(this._id, uuid);
578
+ }
579
+ /**
580
+ * Download a media file as a Blob.
581
+ */
582
+ async downloadMedia(uuid) {
583
+ return this.mediaClient.download(this._id, uuid);
584
+ }
585
+ /**
586
+ * Delete a media file.
587
+ */
588
+ async deleteMedia(uuid) {
589
+ return this.mediaClient.delete(this._id, uuid);
590
+ }
591
+ // ===========================================================================
592
+ // Low-level Operations
593
+ // ===========================================================================
594
+ /**
595
+ * Get the full space data.
596
+ * Use sparingly - prefer specific operations.
597
+ */
598
+ getData() {
599
+ return this._data;
600
+ }
601
+ // ===========================================================================
602
+ // Event Handlers (called by RoolClient for routing)
603
+ // ===========================================================================
604
+ /**
605
+ * Handle a patch event from another client.
606
+ * @internal
607
+ */
608
+ handleRemotePatch(patch, source = 'user') {
609
+ try {
610
+ this._data = immutableJSONPatch(this._data, patch);
611
+ }
612
+ catch (error) {
613
+ console.error('[Space] Failed to apply remote patch:', error);
614
+ // Force resync on patch error
615
+ this.resyncFromServer(error instanceof Error ? error : new Error(String(error))).catch(() => { });
616
+ return;
617
+ }
618
+ // Parse patch operations and emit semantic events
619
+ const changeSource = source === 'agent' ? 'remote_agent' : 'remote_user';
620
+ this.emitSemanticEventsFromPatch(patch, changeSource);
621
+ }
622
+ /**
623
+ * Parse JSON patch operations and emit semantic events.
624
+ * @internal
625
+ */
626
+ emitSemanticEventsFromPatch(patch, source) {
627
+ // Track which objects have been updated (to avoid duplicate events)
628
+ const updatedObjects = new Set();
629
+ for (const op of patch) {
630
+ const { path } = op;
631
+ // Object operations: /objects/{objectId}/...
632
+ if (path.startsWith('/objects/')) {
633
+ const parts = path.split('/');
634
+ const objectId = parts[2];
635
+ if (parts.length === 3) {
636
+ // /objects/{objectId} - full object add or remove
637
+ if (op.op === 'add') {
638
+ const entry = this._data.objects[objectId];
639
+ if (entry) {
640
+ this.emit('objectCreated', { objectId, object: entry.data, source });
641
+ }
642
+ }
643
+ else if (op.op === 'remove') {
644
+ this.emit('objectDeleted', { objectId, source });
645
+ }
646
+ }
647
+ else if (parts[3] === 'data') {
648
+ // /objects/{objectId}/data/... - data field update
649
+ if (!updatedObjects.has(objectId)) {
650
+ const entry = this._data.objects[objectId];
651
+ if (entry) {
652
+ this.emit('objectUpdated', { objectId, object: entry.data, source });
653
+ updatedObjects.add(objectId);
654
+ }
655
+ }
656
+ }
657
+ else if (parts[3] === 'links') {
658
+ // /objects/{objectId}/links/{type}/{targetId}
659
+ if (parts.length >= 6) {
660
+ const linkType = parts[4];
661
+ const targetId = parts[5];
662
+ if (op.op === 'add') {
663
+ const linkData = this._data.objects[objectId]?.links[linkType]?.[targetId] ?? {};
664
+ this.emit('linked', { sourceId: objectId, targetId, linkType, linkData, source });
665
+ }
666
+ else if (op.op === 'remove') {
667
+ this.emit('unlinked', { sourceId: objectId, targetId, linkType, source });
668
+ }
669
+ }
670
+ else if (parts.length === 5 && op.op === 'add') {
671
+ // /objects/{objectId}/links/{type} - new link type object added
672
+ const linkType = parts[4];
673
+ const targets = this._data.objects[objectId]?.links[linkType] ?? {};
674
+ for (const [targetId, linkData] of Object.entries(targets)) {
675
+ this.emit('linked', { sourceId: objectId, targetId, linkType, linkData: linkData, source });
676
+ }
677
+ }
678
+ }
679
+ else if (parts[3] === 'meta') {
680
+ // /objects/{objectId}/meta/... - object meta update
681
+ // Could emit an objectMeta event if needed, but for now treat as object update
682
+ if (!updatedObjects.has(objectId)) {
683
+ const entry = this._data.objects[objectId];
684
+ if (entry) {
685
+ this.emit('objectUpdated', { objectId, object: entry.data, source });
686
+ updatedObjects.add(objectId);
687
+ }
688
+ }
689
+ }
690
+ }
691
+ else if (path === '/meta' || path.startsWith('/meta/')) {
692
+ this.emit('metaUpdated', { meta: this._data.meta, source });
693
+ }
694
+ }
695
+ }
696
+ /**
697
+ * Handle a full reload from server.
698
+ * @internal
699
+ */
700
+ handleRemoteChange(newData) {
701
+ this._data = newData;
702
+ this.emit('reset', { source: 'remote_user' });
703
+ }
704
+ /**
705
+ * Update the name from external source.
706
+ * @internal
707
+ */
708
+ handleRemoteRename(newName) {
709
+ this._name = newName;
710
+ }
711
+ // ===========================================================================
712
+ // Private Methods
713
+ // ===========================================================================
714
+ async resyncFromServer(originalError) {
715
+ console.warn('[Space] Resyncing from server after sync failure');
716
+ try {
717
+ const serverData = await this.graphqlClient.getSpace(this._id);
718
+ this._data = serverData;
719
+ this.clearHistory();
720
+ this.emit('syncError', originalError ?? new Error('Sync failed'));
721
+ this.emit('reset', { source: 'system' });
722
+ }
723
+ catch (error) {
724
+ console.error('[Space] Failed to resync from server:', error);
725
+ // Still emit syncError with the original error
726
+ this.emit('syncError', originalError ?? new Error('Sync failed'));
727
+ }
728
+ }
729
+ }
730
+ //# sourceMappingURL=space.js.map