@rool-dev/client 0.3.1-dev.de3060f → 0.3.1-dev.ff89f50
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 +47 -48
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/graph.d.ts +54 -46
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +267 -177
- package/dist/graph.js.map +1 -1
- package/dist/graphql.d.ts +5 -5
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +24 -23
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +42 -39
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
2
|
// Graph
|
|
3
|
-
// First-class Graph object with
|
|
3
|
+
// First-class Graph object with object/edge operations, undo/redo, and events
|
|
4
4
|
// =============================================================================
|
|
5
5
|
import { immutableJSONPatch } from 'immutable-json-patch';
|
|
6
6
|
import { EventEmitter } from './event-emitter.js';
|
|
@@ -18,7 +18,7 @@ export function generateEntityId() {
|
|
|
18
18
|
* First-class Graph object.
|
|
19
19
|
*
|
|
20
20
|
* Features:
|
|
21
|
-
* - High-level
|
|
21
|
+
* - High-level object/edge operations
|
|
22
22
|
* - Built-in undo/redo with checkpoints
|
|
23
23
|
* - Metadata management
|
|
24
24
|
* - Event emission for state changes
|
|
@@ -210,250 +210,322 @@ export class RoolGraph extends EventEmitter {
|
|
|
210
210
|
this.redoStack = [];
|
|
211
211
|
}
|
|
212
212
|
// ===========================================================================
|
|
213
|
-
//
|
|
213
|
+
// Object Operations
|
|
214
214
|
// ===========================================================================
|
|
215
215
|
/**
|
|
216
|
-
* Get
|
|
217
|
-
*
|
|
216
|
+
* Get an object's data by ID.
|
|
217
|
+
* Returns just the data portion (RoolObject), not the full entry with meta/links.
|
|
218
|
+
* @throws Error if object not found
|
|
218
219
|
*/
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
if (!
|
|
222
|
-
throw new Error(`
|
|
220
|
+
getObject(objectId) {
|
|
221
|
+
const entry = this._data.objects[objectId];
|
|
222
|
+
if (!entry) {
|
|
223
|
+
throw new Error(`Object ${objectId} not found`);
|
|
223
224
|
}
|
|
224
|
-
return
|
|
225
|
+
return entry.data;
|
|
225
226
|
}
|
|
226
227
|
/**
|
|
227
|
-
* Get
|
|
228
|
+
* Get an object's data by ID, or undefined if not found.
|
|
228
229
|
*/
|
|
229
|
-
|
|
230
|
-
return this._data.
|
|
230
|
+
getObjectOrUndefined(objectId) {
|
|
231
|
+
return this._data.objects[objectId]?.data;
|
|
231
232
|
}
|
|
232
233
|
/**
|
|
233
|
-
* Get
|
|
234
|
+
* Get an object's metadata (position, UI state, etc).
|
|
234
235
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
getObjectMeta(objectId) {
|
|
237
|
+
const entry = this._data.objects[objectId];
|
|
238
|
+
if (!entry) {
|
|
239
|
+
throw new Error(`Object ${objectId} not found`);
|
|
240
|
+
}
|
|
241
|
+
return entry.meta;
|
|
237
242
|
}
|
|
238
243
|
/**
|
|
239
|
-
* Get all
|
|
244
|
+
* Get all objects of a specific type.
|
|
240
245
|
*/
|
|
241
|
-
|
|
242
|
-
return Object.
|
|
246
|
+
getObjectsByType(type) {
|
|
247
|
+
return Object.values(this._data.objects)
|
|
248
|
+
.filter(entry => entry.data.type === type)
|
|
249
|
+
.map(entry => entry.data);
|
|
243
250
|
}
|
|
244
251
|
/**
|
|
245
|
-
*
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
* Get all object IDs.
|
|
253
|
+
*/
|
|
254
|
+
getObjectIds() {
|
|
255
|
+
return Object.keys(this._data.objects);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Create a new object with optional AI generation.
|
|
259
|
+
* @param options.type - The object type (required)
|
|
260
|
+
* @param options.fields - Object fields (optional). Use {{placeholder}} for AI-generated content.
|
|
248
261
|
* @param options.meta - Client-private metadata (optional). Hidden from AI operations.
|
|
249
262
|
* @param options.prompt - AI prompt for content generation (optional).
|
|
250
|
-
* @returns The generated
|
|
263
|
+
* @returns The generated object ID
|
|
251
264
|
*/
|
|
252
|
-
|
|
253
|
-
const
|
|
265
|
+
async createObject(options) {
|
|
266
|
+
const objectId = generateEntityId();
|
|
254
267
|
const { type, fields, meta, prompt } = options;
|
|
255
|
-
// Build the
|
|
256
|
-
const
|
|
268
|
+
// Build the entry for local state
|
|
269
|
+
const entry = {
|
|
270
|
+
meta: meta ?? {},
|
|
271
|
+
links: {},
|
|
272
|
+
data: { type, ...fields },
|
|
273
|
+
};
|
|
257
274
|
// Update local state immediately (optimistic)
|
|
258
|
-
this._data.
|
|
259
|
-
this.emit('
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
.
|
|
263
|
-
|
|
275
|
+
this._data.objects[objectId] = entry;
|
|
276
|
+
this.emit('objectCreated', { objectId, object: entry.data, source: 'local_user' });
|
|
277
|
+
// Await server call
|
|
278
|
+
try {
|
|
279
|
+
const message = await this.graphqlClient.createObject(this.id, objectId, type, fields, meta, prompt);
|
|
280
|
+
return { id: objectId, message };
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.error('[Graph] Failed to create object:', error);
|
|
264
284
|
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
265
|
-
|
|
266
|
-
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
267
287
|
}
|
|
268
288
|
/**
|
|
269
|
-
* Update an existing
|
|
270
|
-
* @param
|
|
271
|
-
* @param options.type - Optional new type for the
|
|
289
|
+
* Update an existing object.
|
|
290
|
+
* @param objectId - The ID of the object to update
|
|
291
|
+
* @param options.type - Optional new type for the object
|
|
272
292
|
* @param options.fields - Fields to add or update. Use {{placeholder}} for AI-generated content.
|
|
273
293
|
* @param options.meta - Client-private metadata to merge. Hidden from AI operations.
|
|
274
294
|
* @param options.prompt - AI prompt for content editing (optional).
|
|
275
295
|
*/
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
if (!
|
|
279
|
-
throw new Error(`
|
|
296
|
+
async updateObject(objectId, options) {
|
|
297
|
+
const entry = this._data.objects[objectId];
|
|
298
|
+
if (!entry) {
|
|
299
|
+
throw new Error(`Object ${objectId} not found for update`);
|
|
280
300
|
}
|
|
281
301
|
const { type, fields, meta } = options;
|
|
282
302
|
// Build local updates
|
|
283
303
|
if (type) {
|
|
284
|
-
|
|
304
|
+
entry.data.type = type;
|
|
285
305
|
}
|
|
286
306
|
if (fields) {
|
|
287
|
-
Object.assign(
|
|
307
|
+
Object.assign(entry.data, fields);
|
|
288
308
|
}
|
|
289
309
|
if (meta) {
|
|
290
|
-
|
|
310
|
+
entry.meta = { ...entry.meta, ...meta };
|
|
291
311
|
}
|
|
292
|
-
// Emit semantic event with updated
|
|
312
|
+
// Emit semantic event with updated object
|
|
293
313
|
if (type || fields || meta) {
|
|
294
|
-
this.emit('
|
|
314
|
+
this.emit('objectUpdated', { objectId, object: entry.data, source: 'local_user' });
|
|
295
315
|
}
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
.
|
|
299
|
-
|
|
316
|
+
// Await server call
|
|
317
|
+
try {
|
|
318
|
+
return await this.graphqlClient.updateObject(this.id, objectId, type, fields, meta, options.prompt);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
console.error('[Graph] Failed to update object:', error);
|
|
300
322
|
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
301
|
-
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
302
325
|
}
|
|
303
326
|
/**
|
|
304
|
-
* Delete
|
|
305
|
-
*
|
|
327
|
+
* Delete objects by IDs.
|
|
328
|
+
* Outbound links are automatically deleted with the object.
|
|
329
|
+
* Inbound links become orphans (tolerated).
|
|
306
330
|
*/
|
|
307
|
-
|
|
308
|
-
if (
|
|
331
|
+
async deleteObjects(objectIds) {
|
|
332
|
+
if (objectIds.length === 0)
|
|
309
333
|
return;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
334
|
+
const deletedObjectIds = [];
|
|
335
|
+
// Collect edges that will be orphaned (for events)
|
|
336
|
+
const deletedEdges = [];
|
|
337
|
+
for (const objectId of objectIds) {
|
|
338
|
+
const entry = this._data.objects[objectId];
|
|
339
|
+
if (entry) {
|
|
340
|
+
// Collect outbound edges for deletion events
|
|
341
|
+
for (const [edgeType, targets] of Object.entries(entry.links)) {
|
|
342
|
+
for (const targetId of Object.keys(targets)) {
|
|
343
|
+
deletedEdges.push({ sourceId: objectId, targetId, edgeType });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
320
346
|
}
|
|
321
347
|
}
|
|
322
|
-
// Remove
|
|
323
|
-
for (const
|
|
324
|
-
if (this._data.
|
|
325
|
-
delete this._data.
|
|
326
|
-
|
|
348
|
+
// Remove objects (local state)
|
|
349
|
+
for (const objectId of objectIds) {
|
|
350
|
+
if (this._data.objects[objectId]) {
|
|
351
|
+
delete this._data.objects[objectId];
|
|
352
|
+
deletedObjectIds.push(objectId);
|
|
327
353
|
}
|
|
328
354
|
}
|
|
329
355
|
// Emit semantic events
|
|
330
|
-
for (const
|
|
331
|
-
this.emit('edgeDeleted', {
|
|
356
|
+
for (const edge of deletedEdges) {
|
|
357
|
+
this.emit('edgeDeleted', { ...edge, source: 'local_user' });
|
|
332
358
|
}
|
|
333
|
-
for (const
|
|
334
|
-
this.emit('
|
|
359
|
+
for (const objectId of deletedObjectIds) {
|
|
360
|
+
this.emit('objectDeleted', { objectId, source: 'local_user' });
|
|
335
361
|
}
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
.
|
|
339
|
-
|
|
362
|
+
// Await server call
|
|
363
|
+
try {
|
|
364
|
+
await this.graphqlClient.deleteObjects(this.id, objectIds);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
console.error('[Graph] Failed to delete objects:', error);
|
|
340
368
|
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
341
|
-
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
342
371
|
}
|
|
343
372
|
// ===========================================================================
|
|
344
373
|
// Edge Operations
|
|
345
374
|
// ===========================================================================
|
|
346
375
|
/**
|
|
347
|
-
* Create
|
|
348
|
-
*
|
|
376
|
+
* Create a link between objects.
|
|
377
|
+
* Links are stored on the source object.
|
|
349
378
|
*/
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
type: edgeType,
|
|
356
|
-
_meta: {},
|
|
357
|
-
};
|
|
379
|
+
async link(sourceId, targetId, edgeType) {
|
|
380
|
+
const entry = this._data.objects[sourceId];
|
|
381
|
+
if (!entry) {
|
|
382
|
+
throw new Error(`Source object ${sourceId} not found`);
|
|
383
|
+
}
|
|
358
384
|
// Update local state immediately
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
385
|
+
if (!entry.links[edgeType]) {
|
|
386
|
+
entry.links[edgeType] = {};
|
|
387
|
+
}
|
|
388
|
+
entry.links[edgeType][targetId] = {};
|
|
389
|
+
const linkData = entry.links[edgeType][targetId];
|
|
390
|
+
this.emit('edgeCreated', { sourceId, targetId, edgeType, linkData, source: 'local_user' });
|
|
391
|
+
// Await server call
|
|
392
|
+
try {
|
|
393
|
+
await this.graphqlClient.link(this.id, sourceId, targetId, edgeType);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
console.error('[Graph] Failed to create link:', error);
|
|
365
397
|
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
366
|
-
|
|
367
|
-
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
368
400
|
}
|
|
369
401
|
/**
|
|
370
|
-
* Remove
|
|
371
|
-
* @
|
|
402
|
+
* Remove a link between two objects.
|
|
403
|
+
* @param edgeType - Optional: if provided, only removes that type; otherwise removes all links between the objects
|
|
404
|
+
* @returns true if any links were removed
|
|
372
405
|
*/
|
|
373
|
-
|
|
374
|
-
const
|
|
406
|
+
async unlink(sourceId, targetId, edgeType) {
|
|
407
|
+
const entry = this._data.objects[sourceId];
|
|
408
|
+
if (!entry) {
|
|
409
|
+
throw new Error(`Source object ${sourceId} not found`);
|
|
410
|
+
}
|
|
411
|
+
const deletedEdges = [];
|
|
375
412
|
// Update local state
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
413
|
+
if (edgeType) {
|
|
414
|
+
// Remove specific edge type
|
|
415
|
+
if (entry.links[edgeType]?.[targetId] !== undefined) {
|
|
416
|
+
delete entry.links[edgeType][targetId];
|
|
417
|
+
deletedEdges.push({ edgeType });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// Remove all links from source to target
|
|
422
|
+
for (const [type, targets] of Object.entries(entry.links)) {
|
|
423
|
+
if (targets[targetId] !== undefined) {
|
|
424
|
+
delete targets[targetId];
|
|
425
|
+
deletedEdges.push({ edgeType: type });
|
|
426
|
+
}
|
|
380
427
|
}
|
|
381
428
|
}
|
|
382
429
|
// Emit semantic events
|
|
383
|
-
for (const
|
|
384
|
-
this.emit('edgeDeleted', {
|
|
430
|
+
for (const edge of deletedEdges) {
|
|
431
|
+
this.emit('edgeDeleted', { sourceId, targetId, edgeType: edge.edgeType, source: 'local_user' });
|
|
385
432
|
}
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
.
|
|
389
|
-
|
|
433
|
+
// Await server call
|
|
434
|
+
try {
|
|
435
|
+
await this.graphqlClient.unlink(this.id, sourceId, targetId);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
console.error('[Graph] Failed to remove link:', error);
|
|
390
439
|
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
391
|
-
|
|
392
|
-
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
return deletedEdges.length > 0;
|
|
393
443
|
}
|
|
394
444
|
/**
|
|
395
|
-
* Get parent
|
|
445
|
+
* Get parent object IDs (objects that have edges pointing TO this object).
|
|
396
446
|
* @param edgeType - Optional filter by edge type
|
|
397
447
|
*/
|
|
398
|
-
getParents(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
(
|
|
402
|
-
|
|
448
|
+
getParents(objectId, edgeType) {
|
|
449
|
+
const parents = [];
|
|
450
|
+
for (const [entryId, entry] of Object.entries(this._data.objects)) {
|
|
451
|
+
for (const [type, targets] of Object.entries(entry.links)) {
|
|
452
|
+
if ((!edgeType || type === edgeType) && objectId in targets) {
|
|
453
|
+
parents.push(entryId);
|
|
454
|
+
break; // Found a link, move to next object
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return parents;
|
|
403
459
|
}
|
|
404
460
|
/**
|
|
405
|
-
* Get child
|
|
461
|
+
* Get child object IDs (objects that this object has edges pointing TO).
|
|
462
|
+
* Filters out orphan targets (targets that don't exist).
|
|
406
463
|
* @param edgeType - Optional filter by edge type
|
|
407
464
|
*/
|
|
408
|
-
getChildren(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
465
|
+
getChildren(objectId, edgeType) {
|
|
466
|
+
const entry = this._data.objects[objectId];
|
|
467
|
+
if (!entry)
|
|
468
|
+
return [];
|
|
469
|
+
const children = [];
|
|
470
|
+
for (const [type, targets] of Object.entries(entry.links)) {
|
|
471
|
+
if (!edgeType || type === edgeType) {
|
|
472
|
+
for (const targetId of Object.keys(targets)) {
|
|
473
|
+
// Filter orphans - only return existing targets
|
|
474
|
+
if (this._data.objects[targetId]) {
|
|
475
|
+
children.push(targetId);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return children;
|
|
419
481
|
}
|
|
420
482
|
/**
|
|
421
|
-
* Get all
|
|
483
|
+
* Get all child object IDs including orphans (targets that may not exist).
|
|
484
|
+
* @param edgeType - Optional filter by edge type
|
|
422
485
|
*/
|
|
423
|
-
|
|
424
|
-
|
|
486
|
+
getChildrenIncludingOrphans(objectId, edgeType) {
|
|
487
|
+
const entry = this._data.objects[objectId];
|
|
488
|
+
if (!entry)
|
|
489
|
+
return [];
|
|
490
|
+
const children = [];
|
|
491
|
+
for (const [type, targets] of Object.entries(entry.links)) {
|
|
492
|
+
if (!edgeType || type === edgeType) {
|
|
493
|
+
children.push(...Object.keys(targets));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return children;
|
|
425
497
|
}
|
|
426
498
|
// ===========================================================================
|
|
427
499
|
// Metadata Operations
|
|
428
500
|
// ===========================================================================
|
|
429
501
|
/**
|
|
430
|
-
* Set a metadata value.
|
|
431
|
-
* Metadata is stored in
|
|
502
|
+
* Set a graph-level metadata value.
|
|
503
|
+
* Metadata is stored in meta and hidden from AI operations.
|
|
432
504
|
*/
|
|
433
505
|
setMetadata(key, value) {
|
|
434
|
-
if (!this._data.
|
|
435
|
-
this._data.
|
|
506
|
+
if (!this._data.meta) {
|
|
507
|
+
this._data.meta = {};
|
|
436
508
|
}
|
|
437
|
-
this._data.
|
|
438
|
-
this.emit('metaUpdated', { meta: this._data.
|
|
509
|
+
this._data.meta[key] = value;
|
|
510
|
+
this.emit('metaUpdated', { meta: this._data.meta, source: 'local_user' });
|
|
439
511
|
// Fire-and-forget server call - errors trigger resync
|
|
440
|
-
this.graphqlClient.setGraphMeta(this.id, this._data.
|
|
512
|
+
this.graphqlClient.setGraphMeta(this.id, this._data.meta)
|
|
441
513
|
.catch((error) => {
|
|
442
514
|
console.error('[Graph] Failed to set graph meta:', error);
|
|
443
515
|
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
444
516
|
});
|
|
445
517
|
}
|
|
446
518
|
/**
|
|
447
|
-
* Get a metadata value.
|
|
519
|
+
* Get a graph-level metadata value.
|
|
448
520
|
*/
|
|
449
521
|
getMetadata(key) {
|
|
450
|
-
return this._data.
|
|
522
|
+
return this._data.meta?.[key];
|
|
451
523
|
}
|
|
452
524
|
/**
|
|
453
|
-
* Get all metadata.
|
|
525
|
+
* Get all graph-level metadata.
|
|
454
526
|
*/
|
|
455
527
|
getAllMetadata() {
|
|
456
|
-
return this._data.
|
|
528
|
+
return this._data.meta ?? {};
|
|
457
529
|
}
|
|
458
530
|
// ===========================================================================
|
|
459
531
|
// AI Operations
|
|
@@ -554,55 +626,73 @@ export class RoolGraph extends EventEmitter {
|
|
|
554
626
|
* @internal
|
|
555
627
|
*/
|
|
556
628
|
emitSemanticEventsFromPatch(patch, source) {
|
|
557
|
-
// Track which
|
|
558
|
-
const
|
|
629
|
+
// Track which objects have been updated (to avoid duplicate events)
|
|
630
|
+
const updatedObjects = new Set();
|
|
559
631
|
for (const op of patch) {
|
|
560
632
|
const { path } = op;
|
|
561
|
-
//
|
|
562
|
-
if (path.startsWith('/
|
|
633
|
+
// Object operations: /objects/{objectId}/...
|
|
634
|
+
if (path.startsWith('/objects/')) {
|
|
563
635
|
const parts = path.split('/');
|
|
564
|
-
const
|
|
636
|
+
const objectId = parts[2];
|
|
565
637
|
if (parts.length === 3) {
|
|
566
|
-
// /
|
|
638
|
+
// /objects/{objectId} - full object add or remove
|
|
567
639
|
if (op.op === 'add') {
|
|
568
|
-
const
|
|
569
|
-
if (
|
|
570
|
-
this.emit('
|
|
640
|
+
const entry = this._data.objects[objectId];
|
|
641
|
+
if (entry) {
|
|
642
|
+
this.emit('objectCreated', { objectId, object: entry.data, source });
|
|
571
643
|
}
|
|
572
644
|
}
|
|
573
645
|
else if (op.op === 'remove') {
|
|
574
|
-
this.emit('
|
|
646
|
+
this.emit('objectDeleted', { objectId, source });
|
|
575
647
|
}
|
|
576
648
|
}
|
|
577
|
-
else if (parts
|
|
578
|
-
// /
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
649
|
+
else if (parts[3] === 'data') {
|
|
650
|
+
// /objects/{objectId}/data/... - data field update
|
|
651
|
+
if (!updatedObjects.has(objectId)) {
|
|
652
|
+
const entry = this._data.objects[objectId];
|
|
653
|
+
if (entry) {
|
|
654
|
+
this.emit('objectUpdated', { objectId, object: entry.data, source });
|
|
655
|
+
updatedObjects.add(objectId);
|
|
656
|
+
}
|
|
583
657
|
}
|
|
584
658
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
this.emit('
|
|
659
|
+
else if (parts[3] === 'links') {
|
|
660
|
+
// /objects/{objectId}/links/{type}/{targetId}
|
|
661
|
+
if (parts.length >= 6) {
|
|
662
|
+
const edgeType = parts[4];
|
|
663
|
+
const targetId = parts[5];
|
|
664
|
+
if (op.op === 'add') {
|
|
665
|
+
const linkData = this._data.objects[objectId]?.links[edgeType]?.[targetId] ?? {};
|
|
666
|
+
this.emit('edgeCreated', { sourceId: objectId, targetId, edgeType, linkData, source });
|
|
667
|
+
}
|
|
668
|
+
else if (op.op === 'remove') {
|
|
669
|
+
this.emit('edgeDeleted', { sourceId: objectId, targetId, edgeType, source });
|
|
596
670
|
}
|
|
597
671
|
}
|
|
598
|
-
else if (op.op === '
|
|
599
|
-
|
|
672
|
+
else if (parts.length === 5 && op.op === 'add') {
|
|
673
|
+
// /objects/{objectId}/links/{type} - new link type object added
|
|
674
|
+
const edgeType = parts[4];
|
|
675
|
+
const targets = this._data.objects[objectId]?.links[edgeType] ?? {};
|
|
676
|
+
for (const [targetId, linkData] of Object.entries(targets)) {
|
|
677
|
+
this.emit('edgeCreated', { sourceId: objectId, targetId, edgeType, linkData: linkData, source });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else if (parts[3] === 'meta') {
|
|
682
|
+
// /objects/{objectId}/meta/... - object meta update
|
|
683
|
+
// Could emit an objectMeta event if needed, but for now treat as object update
|
|
684
|
+
if (!updatedObjects.has(objectId)) {
|
|
685
|
+
const entry = this._data.objects[objectId];
|
|
686
|
+
if (entry) {
|
|
687
|
+
this.emit('objectUpdated', { objectId, object: entry.data, source });
|
|
688
|
+
updatedObjects.add(objectId);
|
|
689
|
+
}
|
|
600
690
|
}
|
|
601
691
|
}
|
|
602
692
|
}
|
|
603
693
|
// Graph metadata
|
|
604
|
-
else if (path === '/
|
|
605
|
-
this.emit('metaUpdated', { meta: this._data.
|
|
694
|
+
else if (path === '/meta' || path.startsWith('/meta/')) {
|
|
695
|
+
this.emit('metaUpdated', { meta: this._data.meta, source });
|
|
606
696
|
}
|
|
607
697
|
}
|
|
608
698
|
}
|