@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/dist/graph.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // =============================================================================
2
2
  // Graph
3
- // First-class Graph object with node/edge operations, undo/redo, and events
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 node/edge operations
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
- // Node Operations
213
+ // Object Operations
214
214
  // ===========================================================================
215
215
  /**
216
- * Get a node by ID.
217
- * @throws Error if node not found
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
- getNode(nodeId) {
220
- const node = this._data.nodes[nodeId];
221
- if (!node) {
222
- throw new Error(`Node ${nodeId} not found`);
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 node;
225
+ return entry.data;
225
226
  }
226
227
  /**
227
- * Get a node by ID, or undefined if not found.
228
+ * Get an object's data by ID, or undefined if not found.
228
229
  */
229
- getNodeOrUndefined(nodeId) {
230
- return this._data.nodes[nodeId];
230
+ getObjectOrUndefined(objectId) {
231
+ return this._data.objects[objectId]?.data;
231
232
  }
232
233
  /**
233
- * Get all nodes of a specific type.
234
+ * Get an object's metadata (position, UI state, etc).
234
235
  */
235
- getNodesByType(type) {
236
- return Object.values(this._data.nodes).filter(node => node.type === type);
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 node IDs.
244
+ * Get all objects of a specific type.
240
245
  */
241
- getNodeIds() {
242
- return Object.keys(this._data.nodes);
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
- * Create a new node with optional AI generation.
246
- * @param options.type - The node type (required)
247
- * @param options.fields - Node fields (optional). Use {{placeholder}} for AI-generated content.
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 node ID
263
+ * @returns The generated object ID
251
264
  */
252
- createNode(options) {
253
- const nodeId = generateEntityId();
265
+ async createObject(options) {
266
+ const objectId = generateEntityId();
254
267
  const { type, fields, meta, prompt } = options;
255
- // Build the node for local state
256
- const node = { type, _meta: meta ?? {}, ...fields };
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.nodes[nodeId] = node;
259
- this.emit('nodeCreated', { nodeId, node, source: 'local_user' });
260
- // Fire-and-forget server call - errors trigger resync
261
- this.graphqlClient.createNode(this.id, nodeId, type, fields, meta, prompt)
262
- .catch((error) => {
263
- console.error('[Graph] Failed to create node:', error);
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
- return nodeId;
285
+ throw error;
286
+ }
267
287
  }
268
288
  /**
269
- * Update an existing node.
270
- * @param nodeId - The ID of the node to update
271
- * @param options.type - Optional new type for the node
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
- updateNode(nodeId, options) {
277
- const node = this._data.nodes[nodeId];
278
- if (!node) {
279
- throw new Error(`Node ${nodeId} not found for update`);
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
- node.type = type;
304
+ entry.data.type = type;
285
305
  }
286
306
  if (fields) {
287
- Object.assign(node, fields);
307
+ Object.assign(entry.data, fields);
288
308
  }
289
309
  if (meta) {
290
- node._meta = { ...node._meta, ...meta };
310
+ entry.meta = { ...entry.meta, ...meta };
291
311
  }
292
- // Emit semantic event with updated node
312
+ // Emit semantic event with updated object
293
313
  if (type || fields || meta) {
294
- this.emit('nodeUpdated', { nodeId, node, source: 'local_user' });
314
+ this.emit('objectUpdated', { objectId, object: entry.data, source: 'local_user' });
295
315
  }
296
- // Fire-and-forget server call - errors trigger resync
297
- this.graphqlClient.updateNode(this.id, nodeId, type, fields, meta, options.prompt)
298
- .catch((error) => {
299
- console.error('[Graph] Failed to update node:', error);
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 nodes by IDs.
305
- * Also removes any edges connected to the deleted nodes.
327
+ * Delete objects by IDs.
328
+ * Outbound links are automatically deleted with the object.
329
+ * Inbound links become orphans (tolerated).
306
330
  */
307
- deleteNodes(nodeIds) {
308
- if (nodeIds.length === 0)
331
+ async deleteObjects(objectIds) {
332
+ if (objectIds.length === 0)
309
333
  return;
310
- const nodeIdSet = new Set(nodeIds);
311
- const deletedEdgeIds = [];
312
- const deletedNodeIds = [];
313
- // Remove edges connected to any deleted node (local state)
314
- for (const [edgeId, edge] of Object.entries(this._data.edges)) {
315
- const hasDeletedSource = edge.sources.some(s => nodeIdSet.has(s));
316
- const hasDeletedTarget = edge.targets.some(t => nodeIdSet.has(t));
317
- if (hasDeletedSource || hasDeletedTarget) {
318
- delete this._data.edges[edgeId];
319
- deletedEdgeIds.push(edgeId);
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 nodes (local state)
323
- for (const nodeId of nodeIds) {
324
- if (this._data.nodes[nodeId]) {
325
- delete this._data.nodes[nodeId];
326
- deletedNodeIds.push(nodeId);
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 edgeId of deletedEdgeIds) {
331
- this.emit('edgeDeleted', { edgeId, source: 'local_user' });
356
+ for (const edge of deletedEdges) {
357
+ this.emit('edgeDeleted', { ...edge, source: 'local_user' });
332
358
  }
333
- for (const nodeId of deletedNodeIds) {
334
- this.emit('nodeDeleted', { nodeId, source: 'local_user' });
359
+ for (const objectId of deletedObjectIds) {
360
+ this.emit('objectDeleted', { objectId, source: 'local_user' });
335
361
  }
336
- // Fire-and-forget server call - errors trigger resync
337
- this.graphqlClient.deleteNodes(this.id, nodeIds)
338
- .catch((error) => {
339
- console.error('[Graph] Failed to delete nodes:', error);
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 an edge between nodes.
348
- * @returns The generated edge ID
376
+ * Create a link between objects.
377
+ * Links are stored on the source object.
349
378
  */
350
- createEdge(sourceId, targetId, edgeType) {
351
- const edgeId = generateEntityId();
352
- const edge = {
353
- sources: [sourceId],
354
- targets: [targetId],
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
- this._data.edges[edgeId] = edge;
360
- this.emit('edgeCreated', { edgeId, edge, source: 'local_user' });
361
- // Fire-and-forget server call - errors trigger resync
362
- this.graphqlClient.createEdge(this.id, edgeId, sourceId, targetId, edgeType)
363
- .catch((error) => {
364
- console.error('[Graph] Failed to create edge:', error);
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
- return edgeId;
398
+ throw error;
399
+ }
368
400
  }
369
401
  /**
370
- * Remove edges between two nodes.
371
- * @returns true if any edges were removed
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
- deleteEdge(sourceId, targetId) {
374
- const deletedEdgeIds = [];
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
- for (const [edgeId, edge] of Object.entries(this._data.edges)) {
377
- if (edge.sources.includes(sourceId) && edge.targets.includes(targetId)) {
378
- delete this._data.edges[edgeId];
379
- deletedEdgeIds.push(edgeId);
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 edgeId of deletedEdgeIds) {
384
- this.emit('edgeDeleted', { edgeId, source: 'local_user' });
430
+ for (const edge of deletedEdges) {
431
+ this.emit('edgeDeleted', { sourceId, targetId, edgeType: edge.edgeType, source: 'local_user' });
385
432
  }
386
- // Fire-and-forget server call - errors trigger resync
387
- this.graphqlClient.deleteEdge(this.id, sourceId, targetId)
388
- .catch((error) => {
389
- console.error('[Graph] Failed to delete edge:', error);
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
- return deletedEdgeIds.length > 0;
440
+ throw error;
441
+ }
442
+ return deletedEdges.length > 0;
393
443
  }
394
444
  /**
395
- * Get parent node IDs (nodes that have edges pointing TO this node).
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(nodeId, edgeType) {
399
- return Object.values(this._data.edges)
400
- .filter(edge => edge.targets.includes(nodeId) &&
401
- (!edgeType || edge.type === edgeType))
402
- .flatMap(edge => edge.sources);
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 node IDs (nodes that this node has edges pointing TO).
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(nodeId, edgeType) {
409
- return Object.values(this._data.edges)
410
- .filter(edge => edge.sources.includes(nodeId) &&
411
- (!edgeType || edge.type === edgeType))
412
- .flatMap(edge => edge.targets);
413
- }
414
- /**
415
- * Get an edge by ID.
416
- */
417
- getEdge(edgeId) {
418
- return this._data.edges[edgeId];
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 edge IDs.
483
+ * Get all child object IDs including orphans (targets that may not exist).
484
+ * @param edgeType - Optional filter by edge type
422
485
  */
423
- getEdgeIds() {
424
- return Object.keys(this._data.edges);
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 _meta and hidden from AI operations.
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._meta) {
435
- this._data._meta = {};
506
+ if (!this._data.meta) {
507
+ this._data.meta = {};
436
508
  }
437
- this._data._meta[key] = value;
438
- this.emit('metaUpdated', { meta: this._data._meta, source: 'local_user' });
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._meta)
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._meta?.[key];
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._meta ?? {};
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 nodes have been updated (to avoid duplicate events)
558
- const updatedNodes = new Set();
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
- // Node operations
562
- if (path.startsWith('/nodes/')) {
633
+ // Object operations: /objects/{objectId}/...
634
+ if (path.startsWith('/objects/')) {
563
635
  const parts = path.split('/');
564
- const nodeId = parts[2];
636
+ const objectId = parts[2];
565
637
  if (parts.length === 3) {
566
- // /nodes/{nodeId} - full node add or remove
638
+ // /objects/{objectId} - full object add or remove
567
639
  if (op.op === 'add') {
568
- const node = this._data.nodes[nodeId];
569
- if (node) {
570
- this.emit('nodeCreated', { nodeId, node, source });
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('nodeDeleted', { nodeId, source });
646
+ this.emit('objectDeleted', { objectId, source });
575
647
  }
576
648
  }
577
- else if (parts.length > 3 && !updatedNodes.has(nodeId)) {
578
- // /nodes/{nodeId}/{field} - field update
579
- const node = this._data.nodes[nodeId];
580
- if (node) {
581
- this.emit('nodeUpdated', { nodeId, node, source });
582
- updatedNodes.add(nodeId);
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
- // Edge operations
587
- else if (path.startsWith('/edges/')) {
588
- const parts = path.split('/');
589
- const edgeId = parts[2];
590
- if (parts.length === 3) {
591
- // /edges/{edgeId} - full edge add or remove
592
- if (op.op === 'add') {
593
- const edge = this._data.edges[edgeId];
594
- if (edge) {
595
- this.emit('edgeCreated', { edgeId, edge, source });
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 === 'remove') {
599
- this.emit('edgeDeleted', { edgeId, source });
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 === '/_meta' || path.startsWith('/_meta/')) {
605
- this.emit('metaUpdated', { meta: this._data._meta, source });
694
+ else if (path === '/meta' || path.startsWith('/meta/')) {
695
+ this.emit('metaUpdated', { meta: this._data.meta, source });
606
696
  }
607
697
  }
608
698
  }