@rool-dev/client 0.3.0 → 0.3.1-dev.34258b6
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 +189 -82
- package/dist/auth-browser.d.ts +71 -0
- package/dist/auth-browser.d.ts.map +1 -0
- package/dist/auth-browser.js +346 -0
- package/dist/auth-browser.js.map +1 -0
- package/dist/auth-node.d.ts +33 -0
- package/dist/auth-node.d.ts.map +1 -0
- package/dist/auth-node.js +271 -0
- package/dist/auth-node.js.map +1 -0
- package/dist/auth.d.ts +7 -35
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +34 -326
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -13
- package/dist/client.js.map +1 -1
- package/dist/graph.d.ts +71 -56
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +336 -184
- package/dist/graph.js.map +1 -1
- package/dist/graphql.d.ts +8 -5
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +86 -49
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/media.d.ts +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +1 -1
- package/dist/media.js.map +1 -1
- package/dist/subscription.d.ts +1 -1
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +1 -1
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +94 -47
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -3
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/link 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/link operations
|
|
22
22
|
* - Built-in undo/redo with checkpoints
|
|
23
23
|
* - Metadata management
|
|
24
24
|
* - Event emission for state changes
|
|
@@ -36,9 +36,6 @@ export class RoolGraph extends EventEmitter {
|
|
|
36
36
|
// Undo/redo stacks
|
|
37
37
|
undoStack = [];
|
|
38
38
|
redoStack = [];
|
|
39
|
-
// Patch batching
|
|
40
|
-
pendingPatch = [];
|
|
41
|
-
patchScheduled = false;
|
|
42
39
|
// Subscription state
|
|
43
40
|
_isSubscribed = false;
|
|
44
41
|
constructor(config) {
|
|
@@ -113,7 +110,6 @@ export class RoolGraph extends EventEmitter {
|
|
|
113
110
|
this.unsubscribe();
|
|
114
111
|
this.undoStack = [];
|
|
115
112
|
this.redoStack = [];
|
|
116
|
-
this.pendingPatch = [];
|
|
117
113
|
this.removeAllListeners();
|
|
118
114
|
}
|
|
119
115
|
// ===========================================================================
|
|
@@ -169,7 +165,7 @@ export class RoolGraph extends EventEmitter {
|
|
|
169
165
|
// Sync to server - resync on failure
|
|
170
166
|
try {
|
|
171
167
|
await this.graphqlClient.setGraph(this._id, this._data);
|
|
172
|
-
this.emit('reset', { source: '
|
|
168
|
+
this.emit('reset', { source: 'local_user' });
|
|
173
169
|
}
|
|
174
170
|
catch (error) {
|
|
175
171
|
console.error('[Graph] Failed to sync undo to server:', error);
|
|
@@ -197,7 +193,7 @@ export class RoolGraph extends EventEmitter {
|
|
|
197
193
|
// Sync to server - resync on failure
|
|
198
194
|
try {
|
|
199
195
|
await this.graphqlClient.setGraph(this._id, this._data);
|
|
200
|
-
this.emit('reset', { source: '
|
|
196
|
+
this.emit('reset', { source: 'local_user' });
|
|
201
197
|
}
|
|
202
198
|
catch (error) {
|
|
203
199
|
console.error('[Graph] Failed to sync redo to server:', error);
|
|
@@ -214,198 +210,322 @@ export class RoolGraph extends EventEmitter {
|
|
|
214
210
|
this.redoStack = [];
|
|
215
211
|
}
|
|
216
212
|
// ===========================================================================
|
|
217
|
-
//
|
|
213
|
+
// Object Operations
|
|
218
214
|
// ===========================================================================
|
|
219
215
|
/**
|
|
220
|
-
* Get
|
|
221
|
-
*
|
|
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
|
|
222
219
|
*/
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
226
|
-
throw new Error(`
|
|
220
|
+
getObject(objectId) {
|
|
221
|
+
const entry = this._data.objects[objectId];
|
|
222
|
+
if (!entry) {
|
|
223
|
+
throw new Error(`Object ${objectId} not found`);
|
|
227
224
|
}
|
|
228
|
-
return
|
|
225
|
+
return entry.data;
|
|
229
226
|
}
|
|
230
227
|
/**
|
|
231
|
-
* Get
|
|
228
|
+
* Get an object's data by ID, or undefined if not found.
|
|
232
229
|
*/
|
|
233
|
-
|
|
234
|
-
return this._data.
|
|
230
|
+
getObjectOrUndefined(objectId) {
|
|
231
|
+
return this._data.objects[objectId]?.data;
|
|
235
232
|
}
|
|
236
233
|
/**
|
|
237
|
-
* Get
|
|
234
|
+
* Get an object's metadata (position, UI state, etc).
|
|
238
235
|
*/
|
|
239
|
-
|
|
240
|
-
|
|
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;
|
|
241
242
|
}
|
|
242
243
|
/**
|
|
243
|
-
* Get all
|
|
244
|
+
* Get all objects of a specific type.
|
|
244
245
|
*/
|
|
245
|
-
|
|
246
|
-
return Object.
|
|
246
|
+
getObjectsByType(type) {
|
|
247
|
+
return Object.values(this._data.objects)
|
|
248
|
+
.filter(entry => entry.data.type === type)
|
|
249
|
+
.map(entry => entry.data);
|
|
247
250
|
}
|
|
248
251
|
/**
|
|
249
|
-
*
|
|
252
|
+
* Get all object IDs.
|
|
250
253
|
*/
|
|
251
|
-
|
|
252
|
-
this._data.
|
|
253
|
-
const ops = [{ op: 'add', path: `/nodes/${nodeId}`, value: node }];
|
|
254
|
-
this.sendPatch(ops);
|
|
255
|
-
this.emit('patch', { ops, source: 'local' });
|
|
254
|
+
getObjectIds() {
|
|
255
|
+
return Object.keys(this._data.objects);
|
|
256
256
|
}
|
|
257
257
|
/**
|
|
258
|
-
*
|
|
259
|
-
* @param
|
|
260
|
-
* @param
|
|
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.
|
|
261
|
+
* @param options.meta - Client-private metadata (optional). Hidden from AI operations.
|
|
262
|
+
* @param options.prompt - AI prompt for content generation (optional).
|
|
263
|
+
* @returns The generated object ID
|
|
261
264
|
*/
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
async createObject(options) {
|
|
266
|
+
const objectId = generateEntityId();
|
|
267
|
+
const { type, fields, meta, prompt } = options;
|
|
268
|
+
// Build the entry for local state
|
|
269
|
+
const entry = {
|
|
270
|
+
meta: meta ?? {},
|
|
271
|
+
links: {},
|
|
272
|
+
data: { type, ...fields },
|
|
273
|
+
};
|
|
274
|
+
// Update local state immediately (optimistic)
|
|
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 };
|
|
266
281
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let ops;
|
|
272
|
-
if (updateKeys.length === 1 && updateKeys[0] === '_meta' && updates._meta) {
|
|
273
|
-
// "add" is an upsert operation, so it works regardless of whether the _meta exists or not
|
|
274
|
-
ops = [{ op: 'add', path: `/nodes/${nodeId}/_meta`, value: updates._meta }];
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.error('[Graph] Failed to create object:', error);
|
|
284
|
+
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
285
|
+
throw error;
|
|
275
286
|
}
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
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
|
|
292
|
+
* @param options.fields - Fields to add or update. Use {{placeholder}} for AI-generated content.
|
|
293
|
+
* @param options.meta - Client-private metadata to merge. Hidden from AI operations.
|
|
294
|
+
* @param options.prompt - AI prompt for content editing (optional).
|
|
295
|
+
*/
|
|
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`);
|
|
300
|
+
}
|
|
301
|
+
const { type, fields, meta } = options;
|
|
302
|
+
// Build local updates
|
|
303
|
+
if (type) {
|
|
304
|
+
entry.data.type = type;
|
|
305
|
+
}
|
|
306
|
+
if (fields) {
|
|
307
|
+
Object.assign(entry.data, fields);
|
|
308
|
+
}
|
|
309
|
+
if (meta) {
|
|
310
|
+
entry.meta = { ...entry.meta, ...meta };
|
|
311
|
+
}
|
|
312
|
+
// Emit semantic event with updated object
|
|
313
|
+
if (type || fields || meta) {
|
|
314
|
+
this.emit('objectUpdated', { objectId, object: entry.data, source: 'local_user' });
|
|
315
|
+
}
|
|
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);
|
|
322
|
+
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
323
|
+
throw error;
|
|
278
324
|
}
|
|
279
|
-
this.sendPatch(ops);
|
|
280
|
-
this.emit('patch', { ops, source: 'local' });
|
|
281
325
|
}
|
|
282
326
|
/**
|
|
283
|
-
* Delete
|
|
284
|
-
*
|
|
327
|
+
* Delete objects by IDs.
|
|
328
|
+
* Outbound links are automatically deleted with the object.
|
|
329
|
+
* Inbound links become orphans (tolerated).
|
|
285
330
|
*/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
331
|
+
async deleteObjects(objectIds) {
|
|
332
|
+
if (objectIds.length === 0)
|
|
333
|
+
return;
|
|
334
|
+
const deletedObjectIds = [];
|
|
335
|
+
// Collect links that will be orphaned (for events)
|
|
336
|
+
const deletedLinks = [];
|
|
337
|
+
for (const objectId of objectIds) {
|
|
338
|
+
const entry = this._data.objects[objectId];
|
|
339
|
+
if (entry) {
|
|
340
|
+
// Collect outbound links for deletion events
|
|
341
|
+
for (const [linkType, targets] of Object.entries(entry.links)) {
|
|
342
|
+
for (const targetId of Object.keys(targets)) {
|
|
343
|
+
deletedLinks.push({ sourceId: objectId, targetId, linkType });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
296
346
|
}
|
|
297
347
|
}
|
|
298
|
-
// Remove
|
|
299
|
-
for (const
|
|
300
|
-
if (this._data.
|
|
301
|
-
delete this._data.
|
|
302
|
-
|
|
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);
|
|
303
353
|
}
|
|
304
354
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
this.emit('
|
|
355
|
+
// Emit semantic events
|
|
356
|
+
for (const link of deletedLinks) {
|
|
357
|
+
this.emit('unlinked', { ...link, source: 'local_user' });
|
|
358
|
+
}
|
|
359
|
+
for (const objectId of deletedObjectIds) {
|
|
360
|
+
this.emit('objectDeleted', { objectId, source: 'local_user' });
|
|
361
|
+
}
|
|
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);
|
|
368
|
+
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
369
|
+
throw error;
|
|
308
370
|
}
|
|
309
371
|
}
|
|
310
372
|
// ===========================================================================
|
|
311
|
-
//
|
|
373
|
+
// Link Operations
|
|
312
374
|
// ===========================================================================
|
|
313
375
|
/**
|
|
314
|
-
* Create
|
|
315
|
-
*
|
|
376
|
+
* Create a link between objects.
|
|
377
|
+
* Links are stored on the source object.
|
|
316
378
|
*/
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
targets: [targetId],
|
|
322
|
-
type: edgeType,
|
|
323
|
-
_meta: {},
|
|
324
|
-
};
|
|
325
|
-
this._data.edges[edgeId] = edge;
|
|
326
|
-
const ops = [{ op: 'add', path: `/edges/${edgeId}`, value: edge }];
|
|
327
|
-
this.sendPatch(ops);
|
|
328
|
-
this.emit('patch', { ops, source: 'local' });
|
|
329
|
-
return edgeId;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Remove edges between two nodes.
|
|
333
|
-
* @returns true if any edges were removed
|
|
334
|
-
*/
|
|
335
|
-
unlinkNodes(sourceId, targetId) {
|
|
336
|
-
const patch = [];
|
|
337
|
-
const removedEdgeIds = [];
|
|
338
|
-
for (const [edgeId, edge] of Object.entries(this._data.edges)) {
|
|
339
|
-
if (edge.sources.includes(sourceId) && edge.targets.includes(targetId)) {
|
|
340
|
-
delete this._data.edges[edgeId];
|
|
341
|
-
patch.push({ op: 'remove', path: `/edges/${edgeId}` });
|
|
342
|
-
removedEdgeIds.push(edgeId);
|
|
343
|
-
}
|
|
379
|
+
async link(sourceId, targetId, linkType) {
|
|
380
|
+
const entry = this._data.objects[sourceId];
|
|
381
|
+
if (!entry) {
|
|
382
|
+
throw new Error(`Source object ${sourceId} not found`);
|
|
344
383
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
384
|
+
// Update local state immediately
|
|
385
|
+
if (!entry.links[linkType]) {
|
|
386
|
+
entry.links[linkType] = {};
|
|
387
|
+
}
|
|
388
|
+
entry.links[linkType][targetId] = {};
|
|
389
|
+
const linkData = entry.links[linkType][targetId];
|
|
390
|
+
this.emit('linked', { sourceId, targetId, linkType, linkData, source: 'local_user' });
|
|
391
|
+
// Await server call
|
|
392
|
+
try {
|
|
393
|
+
await this.graphqlClient.link(this.id, sourceId, targetId, linkType);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
console.error('[Graph] Failed to create link:', error);
|
|
397
|
+
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
398
|
+
throw error;
|
|
349
399
|
}
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Get parent node IDs (nodes that have edges pointing TO this node).
|
|
354
|
-
* @param edgeType - Optional filter by edge type
|
|
355
|
-
*/
|
|
356
|
-
getParents(nodeId, edgeType) {
|
|
357
|
-
return Object.values(this._data.edges)
|
|
358
|
-
.filter(edge => edge.targets.includes(nodeId) &&
|
|
359
|
-
(!edgeType || edge.type === edgeType))
|
|
360
|
-
.flatMap(edge => edge.sources);
|
|
361
400
|
}
|
|
362
401
|
/**
|
|
363
|
-
*
|
|
364
|
-
* @param
|
|
402
|
+
* Remove a link between two objects.
|
|
403
|
+
* @param linkType - Optional: if provided, only removes that type; otherwise removes all links between the objects
|
|
404
|
+
* @returns true if any links were removed
|
|
365
405
|
*/
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
(
|
|
370
|
-
|
|
406
|
+
async unlink(sourceId, targetId, linkType) {
|
|
407
|
+
const entry = this._data.objects[sourceId];
|
|
408
|
+
if (!entry) {
|
|
409
|
+
throw new Error(`Source object ${sourceId} not found`);
|
|
410
|
+
}
|
|
411
|
+
const deletedLinks = [];
|
|
412
|
+
// Update local state
|
|
413
|
+
if (linkType) {
|
|
414
|
+
// Remove specific link type
|
|
415
|
+
if (entry.links[linkType]?.[targetId] !== undefined) {
|
|
416
|
+
delete entry.links[linkType][targetId];
|
|
417
|
+
deletedLinks.push({ linkType });
|
|
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
|
+
deletedLinks.push({ linkType: type });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Emit semantic events
|
|
430
|
+
for (const link of deletedLinks) {
|
|
431
|
+
this.emit('unlinked', { sourceId, targetId, linkType: link.linkType, source: 'local_user' });
|
|
432
|
+
}
|
|
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);
|
|
439
|
+
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
return deletedLinks.length > 0;
|
|
371
443
|
}
|
|
372
444
|
/**
|
|
373
|
-
* Get
|
|
445
|
+
* Get parent object IDs (objects that have links pointing TO this object).
|
|
446
|
+
* @param linkType - Optional filter by link type
|
|
374
447
|
*/
|
|
375
|
-
|
|
376
|
-
|
|
448
|
+
getParents(objectId, linkType) {
|
|
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 ((!linkType || type === linkType) && objectId in targets) {
|
|
453
|
+
parents.push(entryId);
|
|
454
|
+
break; // Found a link, move to next object
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return parents;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get child object IDs (objects that this object has links pointing TO).
|
|
462
|
+
* Filters out orphan targets (targets that don't exist).
|
|
463
|
+
* @param linkType - Optional filter by link type
|
|
464
|
+
*/
|
|
465
|
+
getChildren(objectId, linkType) {
|
|
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 (!linkType || type === linkType) {
|
|
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;
|
|
377
481
|
}
|
|
378
482
|
/**
|
|
379
|
-
* Get all
|
|
483
|
+
* Get all child object IDs including orphans (targets that may not exist).
|
|
484
|
+
* @param linkType - Optional filter by link type
|
|
380
485
|
*/
|
|
381
|
-
|
|
382
|
-
|
|
486
|
+
getChildrenIncludingOrphans(objectId, linkType) {
|
|
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 (!linkType || type === linkType) {
|
|
493
|
+
children.push(...Object.keys(targets));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return children;
|
|
383
497
|
}
|
|
384
498
|
// ===========================================================================
|
|
385
499
|
// Metadata Operations
|
|
386
500
|
// ===========================================================================
|
|
387
501
|
/**
|
|
388
|
-
* Set a metadata value.
|
|
389
|
-
* Metadata is stored in
|
|
502
|
+
* Set a graph-level metadata value.
|
|
503
|
+
* Metadata is stored in meta and hidden from AI operations.
|
|
390
504
|
*/
|
|
391
505
|
setMetadata(key, value) {
|
|
392
|
-
if (!this._data.
|
|
393
|
-
this._data.
|
|
506
|
+
if (!this._data.meta) {
|
|
507
|
+
this._data.meta = {};
|
|
394
508
|
}
|
|
395
|
-
this._data.
|
|
396
|
-
this.
|
|
509
|
+
this._data.meta[key] = value;
|
|
510
|
+
this.emit('metaUpdated', { meta: this._data.meta, source: 'local_user' });
|
|
511
|
+
// Fire-and-forget server call - errors trigger resync
|
|
512
|
+
this.graphqlClient.setGraphMeta(this.id, this._data.meta)
|
|
513
|
+
.catch((error) => {
|
|
514
|
+
console.error('[Graph] Failed to set graph meta:', error);
|
|
515
|
+
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
516
|
+
});
|
|
397
517
|
}
|
|
398
518
|
/**
|
|
399
|
-
* Get a metadata value.
|
|
519
|
+
* Get a graph-level metadata value.
|
|
400
520
|
*/
|
|
401
521
|
getMetadata(key) {
|
|
402
|
-
return this._data.
|
|
522
|
+
return this._data.meta?.[key];
|
|
403
523
|
}
|
|
404
524
|
/**
|
|
405
|
-
* Get all metadata.
|
|
525
|
+
* Get all graph-level metadata.
|
|
406
526
|
*/
|
|
407
527
|
getAllMetadata() {
|
|
408
|
-
return this._data.
|
|
528
|
+
return this._data.meta ?? {};
|
|
409
529
|
}
|
|
410
530
|
// ===========================================================================
|
|
411
531
|
// AI Operations
|
|
@@ -416,18 +536,6 @@ export class RoolGraph extends EventEmitter {
|
|
|
416
536
|
async prompt(prompt, options) {
|
|
417
537
|
return this.graphqlClient.promptGraph(this._id, prompt, options);
|
|
418
538
|
}
|
|
419
|
-
/**
|
|
420
|
-
* Generate an AI image.
|
|
421
|
-
*/
|
|
422
|
-
async generateImage(prompt, aspectRatio) {
|
|
423
|
-
return this.graphqlClient.generateImage(this._id, prompt, aspectRatio);
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Edit an existing image using AI.
|
|
427
|
-
*/
|
|
428
|
-
async editImage(prompt, url) {
|
|
429
|
-
return this.graphqlClient.editImage(this._id, prompt, url);
|
|
430
|
-
}
|
|
431
539
|
// ===========================================================================
|
|
432
540
|
// Collaboration
|
|
433
541
|
// ===========================================================================
|
|
@@ -492,21 +600,6 @@ export class RoolGraph extends EventEmitter {
|
|
|
492
600
|
getData() {
|
|
493
601
|
return this._data;
|
|
494
602
|
}
|
|
495
|
-
/**
|
|
496
|
-
* Apply a raw JSON patch.
|
|
497
|
-
* Use sparingly - prefer semantic operations.
|
|
498
|
-
*/
|
|
499
|
-
patch(operations) {
|
|
500
|
-
try {
|
|
501
|
-
this._data = immutableJSONPatch(this._data, operations);
|
|
502
|
-
}
|
|
503
|
-
catch (error) {
|
|
504
|
-
console.error('[Graph] Failed to apply patch:', error);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
this.sendPatch(operations);
|
|
508
|
-
this.emit('patch', { ops: operations, source: 'local' });
|
|
509
|
-
}
|
|
510
603
|
// ===========================================================================
|
|
511
604
|
// Event Handlers (called by RoolClient for routing)
|
|
512
605
|
// ===========================================================================
|
|
@@ -524,8 +617,84 @@ export class RoolGraph extends EventEmitter {
|
|
|
524
617
|
this.resyncFromServer(error instanceof Error ? error : new Error(String(error))).catch(() => { });
|
|
525
618
|
return;
|
|
526
619
|
}
|
|
527
|
-
|
|
528
|
-
|
|
620
|
+
// Parse patch operations and emit semantic events
|
|
621
|
+
const changeSource = source === 'agent' ? 'remote_agent' : 'remote_user';
|
|
622
|
+
this.emitSemanticEventsFromPatch(patch, changeSource);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Parse JSON patch operations and emit semantic events.
|
|
626
|
+
* @internal
|
|
627
|
+
*/
|
|
628
|
+
emitSemanticEventsFromPatch(patch, source) {
|
|
629
|
+
// Track which objects have been updated (to avoid duplicate events)
|
|
630
|
+
const updatedObjects = new Set();
|
|
631
|
+
for (const op of patch) {
|
|
632
|
+
const { path } = op;
|
|
633
|
+
// Object operations: /objects/{objectId}/...
|
|
634
|
+
if (path.startsWith('/objects/')) {
|
|
635
|
+
const parts = path.split('/');
|
|
636
|
+
const objectId = parts[2];
|
|
637
|
+
if (parts.length === 3) {
|
|
638
|
+
// /objects/{objectId} - full object add or remove
|
|
639
|
+
if (op.op === 'add') {
|
|
640
|
+
const entry = this._data.objects[objectId];
|
|
641
|
+
if (entry) {
|
|
642
|
+
this.emit('objectCreated', { objectId, object: entry.data, source });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else if (op.op === 'remove') {
|
|
646
|
+
this.emit('objectDeleted', { objectId, source });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
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
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else if (parts[3] === 'links') {
|
|
660
|
+
// /objects/{objectId}/links/{type}/{targetId}
|
|
661
|
+
if (parts.length >= 6) {
|
|
662
|
+
const linkType = parts[4];
|
|
663
|
+
const targetId = parts[5];
|
|
664
|
+
if (op.op === 'add') {
|
|
665
|
+
const linkData = this._data.objects[objectId]?.links[linkType]?.[targetId] ?? {};
|
|
666
|
+
this.emit('linked', { sourceId: objectId, targetId, linkType, linkData, source });
|
|
667
|
+
}
|
|
668
|
+
else if (op.op === 'remove') {
|
|
669
|
+
this.emit('unlinked', { sourceId: objectId, targetId, linkType, source });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else if (parts.length === 5 && op.op === 'add') {
|
|
673
|
+
// /objects/{objectId}/links/{type} - new link type object added
|
|
674
|
+
const linkType = parts[4];
|
|
675
|
+
const targets = this._data.objects[objectId]?.links[linkType] ?? {};
|
|
676
|
+
for (const [targetId, linkData] of Object.entries(targets)) {
|
|
677
|
+
this.emit('linked', { sourceId: objectId, targetId, linkType, 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
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// Graph metadata
|
|
694
|
+
else if (path === '/meta' || path.startsWith('/meta/')) {
|
|
695
|
+
this.emit('metaUpdated', { meta: this._data.meta, source });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
529
698
|
}
|
|
530
699
|
/**
|
|
531
700
|
* Handle a full graph reload from server.
|
|
@@ -545,23 +714,6 @@ export class RoolGraph extends EventEmitter {
|
|
|
545
714
|
// ===========================================================================
|
|
546
715
|
// Private Methods
|
|
547
716
|
// ===========================================================================
|
|
548
|
-
sendPatch(patch) {
|
|
549
|
-
this.pendingPatch.push(...patch);
|
|
550
|
-
if (!this.patchScheduled) {
|
|
551
|
-
this.patchScheduled = true;
|
|
552
|
-
queueMicrotask(() => {
|
|
553
|
-
const batch = this.pendingPatch;
|
|
554
|
-
this.pendingPatch = [];
|
|
555
|
-
this.patchScheduled = false;
|
|
556
|
-
if (batch.length > 0) {
|
|
557
|
-
this.graphqlClient.patch(this._id, batch).catch((error) => {
|
|
558
|
-
console.error('[Graph] Failed to send patch:', error);
|
|
559
|
-
this.resyncFromServer(error instanceof Error ? error : new Error(String(error)));
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
717
|
async resyncFromServer(originalError) {
|
|
566
718
|
console.warn('[Graph] Resyncing from server after sync failure');
|
|
567
719
|
try {
|