@rool-dev/sdk 0.10.2-dev.425fd65 → 0.10.2-dev.a9e71cd
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 +31 -115
- package/dist/channel.d.ts +15 -67
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +145 -288
- package/dist/channel.js.map +1 -1
- package/dist/client.d.ts +1 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -19
- package/dist/client.js.map +1 -1
- package/dist/graphql.d.ts +3 -39
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +5 -157
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/space.d.ts +2 -4
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +19 -16
- package/dist/space.js.map +1 -1
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +25 -28
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +17 -77
- package/dist/types.d.ts.map +1 -1
- package/dist/webdav.d.ts +14 -4
- package/dist/webdav.d.ts.map +1 -1
- package/dist/webdav.js +92 -27
- package/dist/webdav.js.map +1 -1
- package/package.json +2 -1
package/dist/channel.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from './event-emitter.js';
|
|
2
|
+
import { WebDAVError } from './webdav.js';
|
|
2
3
|
import { generateBasename, loc, normalizeLocation, parseLocation } from './locations.js';
|
|
3
4
|
import { resolveMachineResource } from './machine.js';
|
|
4
5
|
// 6-character alphanumeric ID — used for interactionIds, conversationIds, etc.
|
|
@@ -42,6 +43,44 @@ function findDefaultLeaf(interactions) {
|
|
|
42
43
|
}
|
|
43
44
|
return best?.id;
|
|
44
45
|
}
|
|
46
|
+
function objectDavPath(location) {
|
|
47
|
+
parseLocation(location);
|
|
48
|
+
return location;
|
|
49
|
+
}
|
|
50
|
+
function collectionDavPath(name) {
|
|
51
|
+
parseLocation(loc(name, 'schema')); // Reuse collection validation.
|
|
52
|
+
return `/space/${name}/`;
|
|
53
|
+
}
|
|
54
|
+
function schemaDavPath(name) {
|
|
55
|
+
return `${collectionDavPath(name)}.schema.json`;
|
|
56
|
+
}
|
|
57
|
+
function objectFromBody(location, body) {
|
|
58
|
+
const { collection, basename } = parseLocation(location);
|
|
59
|
+
return { location, collection, basename, body };
|
|
60
|
+
}
|
|
61
|
+
function jsonObject(value, label) {
|
|
62
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
63
|
+
throw new Error(`${label} must be a JSON object`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function patchBody(current, patch) {
|
|
68
|
+
const next = { ...current };
|
|
69
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
70
|
+
if (value === null || value === undefined)
|
|
71
|
+
delete next[key];
|
|
72
|
+
else
|
|
73
|
+
next[key] = value;
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
function collectionDef(input, options) {
|
|
78
|
+
const base = Array.isArray(input)
|
|
79
|
+
? { fields: input }
|
|
80
|
+
: { fields: input.fields, schemaOrgType: input.schemaOrgType };
|
|
81
|
+
const schemaOrgType = options?.schemaOrgType ?? base.schemaOrgType;
|
|
82
|
+
return schemaOrgType ? { fields: base.fields, schemaOrgType } : { fields: base.fields };
|
|
83
|
+
}
|
|
45
84
|
function attachmentBody(file) {
|
|
46
85
|
if (isFile(file)) {
|
|
47
86
|
return {
|
|
@@ -115,8 +154,6 @@ function base64Body(data) {
|
|
|
115
154
|
bytes[i] = binary.charCodeAt(i);
|
|
116
155
|
return bytes.buffer;
|
|
117
156
|
}
|
|
118
|
-
// Default timeout for waiting on SSE object events (30 seconds)
|
|
119
|
-
const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
120
157
|
/**
|
|
121
158
|
* A channel is a space + channelId pair.
|
|
122
159
|
*
|
|
@@ -125,9 +162,9 @@ const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
|
125
162
|
* open a second one.
|
|
126
163
|
*
|
|
127
164
|
* Objects are addressed by location (`/space/<collection>/<basename>.json`).
|
|
128
|
-
* Only schema, metadata,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
165
|
+
* Only schema, metadata, object stats, and the channel's own history are cached
|
|
166
|
+
* locally. Object bodies are fetched on demand. Object/file reactivity is
|
|
167
|
+
* exposed at the space level via WebDAV sync notifications.
|
|
131
168
|
*/
|
|
132
169
|
export class RoolChannel extends EventEmitter {
|
|
133
170
|
_id;
|
|
@@ -143,21 +180,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
143
180
|
webdav;
|
|
144
181
|
onCloseCallback;
|
|
145
182
|
logger;
|
|
146
|
-
// Local cache for bounded data (schema, metadata, own channel, object
|
|
183
|
+
// Local cache for bounded data (schema, metadata, own channel, object stats)
|
|
147
184
|
_meta;
|
|
148
185
|
_schema;
|
|
149
186
|
_channel;
|
|
150
|
-
_objectLocations;
|
|
151
187
|
_objectStats;
|
|
152
188
|
// Active leaf per conversation (client-side tree cursor)
|
|
153
189
|
_activeLeaves = new Map();
|
|
154
|
-
// Object collection: tracks pending local mutations (by location) for dedup
|
|
155
|
-
// Maps location → optimistic object (for create/update) or null (for delete)
|
|
156
|
-
_pendingMutations = new Map();
|
|
157
|
-
// Resolvers waiting for object data from SSE events, keyed by location
|
|
158
|
-
_objectResolvers = new Map();
|
|
159
|
-
// Buffer for object data that arrived before a collector was registered, keyed by location
|
|
160
|
-
_objectBuffer = new Map();
|
|
161
190
|
constructor(config) {
|
|
162
191
|
super();
|
|
163
192
|
this._id = config.id;
|
|
@@ -177,7 +206,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
177
206
|
this._meta = config.meta;
|
|
178
207
|
this._schema = config.schema;
|
|
179
208
|
this._channel = config.channel;
|
|
180
|
-
this._objectLocations = config.objectLocations;
|
|
181
209
|
this._objectStats = new Map(Object.entries(config.objectStats));
|
|
182
210
|
}
|
|
183
211
|
/**
|
|
@@ -198,7 +226,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
198
226
|
return;
|
|
199
227
|
this._meta = data.meta;
|
|
200
228
|
this._schema = data.schema;
|
|
201
|
-
this._objectLocations = data.objectLocations;
|
|
202
229
|
this._objectStats = new Map(Object.entries(data.objectStats));
|
|
203
230
|
if (data.channel)
|
|
204
231
|
this._channel = data.channel;
|
|
@@ -376,10 +403,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
376
403
|
close() {
|
|
377
404
|
this._closed = true;
|
|
378
405
|
this.onCloseCallback();
|
|
379
|
-
// Clean up pending object collectors
|
|
380
|
-
this._objectResolvers.clear();
|
|
381
|
-
this._objectBuffer.clear();
|
|
382
|
-
this._pendingMutations.clear();
|
|
383
406
|
this.removeAllListeners();
|
|
384
407
|
}
|
|
385
408
|
/**
|
|
@@ -413,6 +436,30 @@ export class RoolChannel extends EventEmitter {
|
|
|
413
436
|
async clearHistory() {
|
|
414
437
|
await this.graphqlClient.clearCheckpointHistory(this._id, this._channelId);
|
|
415
438
|
}
|
|
439
|
+
davHeaders(conversationId, interactionId) {
|
|
440
|
+
const headers = new Headers({
|
|
441
|
+
'X-Rool-Channel-Id': this._channelId,
|
|
442
|
+
'X-Rool-Conversation-Id': conversationId,
|
|
443
|
+
});
|
|
444
|
+
if (interactionId)
|
|
445
|
+
headers.set('X-Rool-Interaction-Id', interactionId);
|
|
446
|
+
return headers;
|
|
447
|
+
}
|
|
448
|
+
async readObject(location) {
|
|
449
|
+
const canonical = normalizeLocation(location);
|
|
450
|
+
try {
|
|
451
|
+
const response = await this.webdav.get(objectDavPath(canonical));
|
|
452
|
+
const body = jsonObject(await response.json(), `Object ${canonical}`);
|
|
453
|
+
return { object: objectFromBody(canonical, body), etag: response.headers.get('ETag') };
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (error instanceof WebDAVError && error.status === 404)
|
|
457
|
+
return undefined;
|
|
458
|
+
if (error instanceof SyntaxError)
|
|
459
|
+
throw new Error(`Object ${canonical} did not contain valid JSON`);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
416
463
|
/**
|
|
417
464
|
* Get an object by location. Fetches from the server on each call.
|
|
418
465
|
*
|
|
@@ -420,7 +467,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
420
467
|
* or the short form (`<collection>/<basename>`).
|
|
421
468
|
*/
|
|
422
469
|
async getObject(location) {
|
|
423
|
-
return
|
|
470
|
+
return (await this.readObject(location))?.object;
|
|
424
471
|
}
|
|
425
472
|
/**
|
|
426
473
|
* Get an object's stat (audit information).
|
|
@@ -429,42 +476,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
429
476
|
stat(location) {
|
|
430
477
|
return this._objectStats.get(normalizeLocation(location));
|
|
431
478
|
}
|
|
432
|
-
/**
|
|
433
|
-
* Find objects using structured filters and/or natural language.
|
|
434
|
-
*/
|
|
435
|
-
async findObjects(options) {
|
|
436
|
-
return this._findObjectsImpl(options, this._conversationId);
|
|
437
|
-
}
|
|
438
|
-
/** @internal */
|
|
439
|
-
_findObjectsImpl(options, conversationId) {
|
|
440
|
-
const normalized = {
|
|
441
|
-
...options,
|
|
442
|
-
locations: options.locations?.map(normalizeLocation),
|
|
443
|
-
};
|
|
444
|
-
return this.graphqlClient.findObjects(this._id, normalized, this._channelId, conversationId);
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Get all object locations (sync, from local cache).
|
|
448
|
-
* The list is loaded on open and kept current via SSE events.
|
|
449
|
-
*/
|
|
450
|
-
getObjectLocations(options) {
|
|
451
|
-
let locs = this._objectLocations;
|
|
452
|
-
if (options?.order === 'asc') {
|
|
453
|
-
locs = [...locs].reverse();
|
|
454
|
-
}
|
|
455
|
-
if (options?.limit !== undefined) {
|
|
456
|
-
locs = locs.slice(0, options.limit);
|
|
457
|
-
}
|
|
458
|
-
return locs;
|
|
459
|
-
}
|
|
460
479
|
/**
|
|
461
480
|
* Create a new object in the given collection.
|
|
462
481
|
*
|
|
463
482
|
* @param collection - The collection (must exist in the schema)
|
|
464
|
-
* @param body - Object body fields.
|
|
465
|
-
* Fields prefixed with `_` are hidden from AI.
|
|
483
|
+
* @param body - Object body fields. Fields prefixed with `_` are hidden from AI.
|
|
466
484
|
* @param options.basename - Specific basename to use. If omitted, the SDK generates a random one.
|
|
467
|
-
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
468
485
|
* @returns The created object and a status message.
|
|
469
486
|
*/
|
|
470
487
|
async createObject(collection, body, options) {
|
|
@@ -475,20 +492,19 @@ export class RoolChannel extends EventEmitter {
|
|
|
475
492
|
const basename = options?.basename ?? generateBasename();
|
|
476
493
|
const location = loc(collection, basename);
|
|
477
494
|
const optimistic = { location, collection, basename, body };
|
|
478
|
-
this._pendingMutations.set(location, optimistic);
|
|
479
|
-
this.emit('objectCreated', { location, object: optimistic, source: 'local_user' });
|
|
480
495
|
try {
|
|
481
496
|
const interactionId = generateEntityId();
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
497
|
+
await this.webdav.put(objectDavPath(location), JSON.stringify(body), {
|
|
498
|
+
contentType: 'application/json',
|
|
499
|
+
ifNoneMatch: '*',
|
|
500
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
501
|
+
});
|
|
502
|
+
const fresh = await this.getObject(location) ?? optimistic;
|
|
503
|
+
return { object: fresh, message: `Created ${location}` };
|
|
485
504
|
}
|
|
486
505
|
catch (error) {
|
|
487
506
|
this.logger.error('[RoolChannel] Failed to create object:', error);
|
|
488
|
-
this._pendingMutations.delete(location);
|
|
489
|
-
this._cancelCollector(location);
|
|
490
507
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
491
|
-
this.emit('reset', { source: 'system' });
|
|
492
508
|
throw error;
|
|
493
509
|
}
|
|
494
510
|
}
|
|
@@ -496,9 +512,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
496
512
|
* Update an existing object.
|
|
497
513
|
*
|
|
498
514
|
* @param location - The object's location (canonical or short form)
|
|
499
|
-
* @param options.data - Fields to add or update. Pass `null` to delete a field.
|
|
500
|
-
* @param options.prompt - AI prompt to drive the update.
|
|
501
|
-
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
515
|
+
* @param options.data - Fields to add or update. Pass `null` to delete a field.
|
|
502
516
|
*/
|
|
503
517
|
async updateObject(location, options) {
|
|
504
518
|
return this._updateObjectImpl(location, options, this._conversationId);
|
|
@@ -506,39 +520,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
506
520
|
/** @internal */
|
|
507
521
|
async _updateObjectImpl(location, options, conversationId) {
|
|
508
522
|
const canonical = normalizeLocation(location);
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
serverPatch[key] = value === undefined ? null : value;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
// Emit optimistic event if we have data changes
|
|
519
|
-
if (data) {
|
|
520
|
-
const { collection, basename } = parseLocation(canonical);
|
|
521
|
-
const optimistic = { location: canonical, collection, basename, body: data };
|
|
522
|
-
this._pendingMutations.set(canonical, optimistic);
|
|
523
|
-
this.emit('objectUpdated', { location: canonical, object: optimistic, source: 'local_user' });
|
|
524
|
-
}
|
|
523
|
+
const data = options.data ?? {};
|
|
524
|
+
const current = await this.readObject(canonical);
|
|
525
|
+
if (!current)
|
|
526
|
+
throw new Error(`Object ${canonical} not found`);
|
|
527
|
+
const body = patchBody(current.object.body, data);
|
|
528
|
+
const optimistic = objectFromBody(canonical, body);
|
|
525
529
|
try {
|
|
526
530
|
const interactionId = generateEntityId();
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
parentInteractionId: options.parentInteractionId,
|
|
531
|
+
await this.webdav.put(objectDavPath(canonical), JSON.stringify(body), {
|
|
532
|
+
contentType: 'application/json',
|
|
533
|
+
ifMatch: current.etag ?? undefined,
|
|
534
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
532
535
|
});
|
|
533
|
-
const fresh =
|
|
534
|
-
return { object: fresh, message };
|
|
536
|
+
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
537
|
+
return { object: fresh, message: `Updated ${canonical}` };
|
|
535
538
|
}
|
|
536
539
|
catch (error) {
|
|
537
540
|
this.logger.error('[RoolChannel] Failed to update object:', error);
|
|
538
|
-
this._pendingMutations.delete(canonical);
|
|
539
|
-
this._cancelCollector(canonical);
|
|
540
541
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
541
|
-
this.emit('reset', { source: 'system' });
|
|
542
542
|
throw error;
|
|
543
543
|
}
|
|
544
544
|
}
|
|
@@ -549,7 +549,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
549
549
|
* @param from - Current location
|
|
550
550
|
* @param to - New location
|
|
551
551
|
* @param options.body - Replace the body atomically as part of the move.
|
|
552
|
-
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
553
552
|
*/
|
|
554
553
|
async moveObject(from, to, options) {
|
|
555
554
|
return this._moveObjectImpl(from, to, options, this._conversationId);
|
|
@@ -566,24 +565,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
566
565
|
basename,
|
|
567
566
|
body: options?.body ?? {},
|
|
568
567
|
};
|
|
569
|
-
this._pendingMutations.set(toLoc, optimistic);
|
|
570
|
-
this.emit('objectMoved', { from: fromLoc, to: toLoc, object: optimistic, source: 'local_user' });
|
|
571
568
|
try {
|
|
572
569
|
const interactionId = generateEntityId();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
ephemeral: options?.ephemeral,
|
|
576
|
-
parentInteractionId: options?.parentInteractionId,
|
|
570
|
+
await this.webdav.move(objectDavPath(fromLoc), objectDavPath(toLoc), {
|
|
571
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
577
572
|
});
|
|
578
|
-
|
|
579
|
-
|
|
573
|
+
if (options?.body) {
|
|
574
|
+
await this.webdav.put(objectDavPath(toLoc), JSON.stringify(options.body), {
|
|
575
|
+
contentType: 'application/json',
|
|
576
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
this._objectStats.delete(fromLoc);
|
|
580
|
+
const fresh = await this.getObject(toLoc) ?? optimistic;
|
|
581
|
+
return { object: fresh, message: `Moved ${fromLoc} to ${toLoc}` };
|
|
580
582
|
}
|
|
581
583
|
catch (error) {
|
|
582
584
|
this.logger.error('[RoolChannel] Failed to move object:', error);
|
|
583
|
-
this._pendingMutations.delete(toLoc);
|
|
584
|
-
this._cancelCollector(toLoc);
|
|
585
585
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
586
|
-
this.emit('reset', { source: 'system' });
|
|
587
586
|
throw error;
|
|
588
587
|
}
|
|
589
588
|
}
|
|
@@ -599,22 +598,18 @@ export class RoolChannel extends EventEmitter {
|
|
|
599
598
|
if (locations.length === 0)
|
|
600
599
|
return;
|
|
601
600
|
const canonical = locations.map(normalizeLocation);
|
|
602
|
-
// Track for dedup and emit optimistic events
|
|
603
|
-
for (const location of canonical) {
|
|
604
|
-
this._pendingMutations.set(location, null);
|
|
605
|
-
this.emit('objectDeleted', { location, source: 'local_user' });
|
|
606
|
-
}
|
|
607
601
|
try {
|
|
608
602
|
const interactionId = generateEntityId();
|
|
609
|
-
|
|
603
|
+
for (const location of canonical) {
|
|
604
|
+
await this.webdav.delete(objectDavPath(location), {
|
|
605
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
606
|
+
});
|
|
607
|
+
this._objectStats.delete(location);
|
|
608
|
+
}
|
|
610
609
|
}
|
|
611
610
|
catch (error) {
|
|
612
611
|
this.logger.error('[RoolChannel] Failed to delete objects:', error);
|
|
613
|
-
for (const location of canonical) {
|
|
614
|
-
this._pendingMutations.delete(location);
|
|
615
|
-
}
|
|
616
612
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
617
|
-
this.emit('reset', { source: 'system' });
|
|
618
613
|
throw error;
|
|
619
614
|
}
|
|
620
615
|
}
|
|
@@ -623,19 +618,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
623
618
|
return this._schema;
|
|
624
619
|
}
|
|
625
620
|
/** Create a new collection schema. */
|
|
626
|
-
async createCollection(name, fields) {
|
|
627
|
-
return this._createCollectionImpl(name, fields, this._conversationId);
|
|
621
|
+
async createCollection(name, fields, options) {
|
|
622
|
+
return this._createCollectionImpl(name, fields, options, this._conversationId);
|
|
628
623
|
}
|
|
629
624
|
/** @internal */
|
|
630
|
-
async _createCollectionImpl(name, fields, conversationId) {
|
|
625
|
+
async _createCollectionImpl(name, fields, options, conversationId) {
|
|
631
626
|
if (this._schema[name]) {
|
|
632
627
|
throw new Error(`Collection "${name}" already exists`);
|
|
633
628
|
}
|
|
634
629
|
// Optimistic local update
|
|
635
|
-
const optimisticDef =
|
|
630
|
+
const optimisticDef = collectionDef(fields, options);
|
|
636
631
|
this._schema[name] = optimisticDef;
|
|
637
632
|
try {
|
|
638
|
-
|
|
633
|
+
await this.webdav.mkcol(collectionDavPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
634
|
+
await this.webdav.put(schemaDavPath(name), JSON.stringify(optimisticDef), {
|
|
635
|
+
contentType: 'application/json',
|
|
636
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
637
|
+
});
|
|
638
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
639
|
+
return optimisticDef;
|
|
639
640
|
}
|
|
640
641
|
catch (error) {
|
|
641
642
|
this.logger.error('[RoolChannel] Failed to create collection:', error);
|
|
@@ -644,18 +645,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
644
645
|
}
|
|
645
646
|
}
|
|
646
647
|
/** Alter an existing collection schema, replacing its field definitions. */
|
|
647
|
-
async alterCollection(name, fields) {
|
|
648
|
-
return this._alterCollectionImpl(name, fields, this._conversationId);
|
|
648
|
+
async alterCollection(name, fields, options) {
|
|
649
|
+
return this._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
649
650
|
}
|
|
650
651
|
/** @internal */
|
|
651
|
-
async _alterCollectionImpl(name, fields, conversationId) {
|
|
652
|
+
async _alterCollectionImpl(name, fields, options, conversationId) {
|
|
652
653
|
if (!this._schema[name]) {
|
|
653
654
|
throw new Error(`Collection "${name}" not found`);
|
|
654
655
|
}
|
|
655
656
|
const previous = this._schema[name];
|
|
656
|
-
this._schema[name] =
|
|
657
|
+
this._schema[name] = collectionDef(fields, options);
|
|
657
658
|
try {
|
|
658
|
-
|
|
659
|
+
const updated = this._schema[name];
|
|
660
|
+
await this.webdav.put(schemaDavPath(name), JSON.stringify(updated), {
|
|
661
|
+
contentType: 'application/json',
|
|
662
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
663
|
+
});
|
|
664
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
665
|
+
return updated;
|
|
659
666
|
}
|
|
660
667
|
catch (error) {
|
|
661
668
|
this.logger.error('[RoolChannel] Failed to alter collection:', error);
|
|
@@ -675,7 +682,8 @@ export class RoolChannel extends EventEmitter {
|
|
|
675
682
|
const previous = this._schema[name];
|
|
676
683
|
delete this._schema[name];
|
|
677
684
|
try {
|
|
678
|
-
await this.
|
|
685
|
+
await this.webdav.delete(collectionDavPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
686
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
679
687
|
}
|
|
680
688
|
catch (error) {
|
|
681
689
|
this.logger.error('[RoolChannel] Failed to drop collection:', error);
|
|
@@ -851,25 +859,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
851
859
|
if (onAbort)
|
|
852
860
|
signal.removeEventListener('abort', onAbort);
|
|
853
861
|
}
|
|
854
|
-
// Collect modified objects — they arrive via SSE events during/after the mutation.
|
|
855
862
|
const objects = [];
|
|
856
|
-
const
|
|
857
|
-
for (const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
this._objectBuffer.delete(location);
|
|
861
|
-
objects.push(buffered);
|
|
862
|
-
}
|
|
863
|
-
else {
|
|
864
|
-
missing.push(location);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
if (missing.length > 0) {
|
|
868
|
-
const fetched = await Promise.all(missing.map(location => this.graphqlClient.getObject(this._id, location)));
|
|
869
|
-
for (const obj of fetched) {
|
|
870
|
-
if (obj)
|
|
871
|
-
objects.push(obj);
|
|
872
|
-
}
|
|
863
|
+
const fetched = await Promise.all(result.modifiedObjectLocations.map((location) => this.getObject(location)));
|
|
864
|
+
for (const object of fetched) {
|
|
865
|
+
if (object)
|
|
866
|
+
objects.push(object);
|
|
873
867
|
}
|
|
874
868
|
return {
|
|
875
869
|
message: result.message,
|
|
@@ -919,52 +913,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
919
913
|
return;
|
|
920
914
|
throw new Error(`Failed to create collection ${path}: ${response.status} ${await response.text()}`);
|
|
921
915
|
}
|
|
922
|
-
/**
|
|
923
|
-
* Register a collector that resolves when the object arrives via SSE.
|
|
924
|
-
* @internal
|
|
925
|
-
*/
|
|
926
|
-
_collectObject(location) {
|
|
927
|
-
return new Promise((resolve, reject) => {
|
|
928
|
-
const buffered = this._objectBuffer.get(location);
|
|
929
|
-
if (buffered) {
|
|
930
|
-
this._objectBuffer.delete(location);
|
|
931
|
-
resolve(buffered);
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
const timer = setTimeout(() => {
|
|
935
|
-
this._objectResolvers.delete(location);
|
|
936
|
-
// Fallback: try to fetch from server
|
|
937
|
-
this.graphqlClient.getObject(this._id, location).then(obj => {
|
|
938
|
-
if (obj) {
|
|
939
|
-
resolve(obj);
|
|
940
|
-
}
|
|
941
|
-
else {
|
|
942
|
-
reject(new Error(`Timeout waiting for object ${location} from SSE`));
|
|
943
|
-
}
|
|
944
|
-
}).catch(reject);
|
|
945
|
-
}, OBJECT_COLLECT_TIMEOUT);
|
|
946
|
-
this._objectResolvers.set(location, (obj) => {
|
|
947
|
-
clearTimeout(timer);
|
|
948
|
-
resolve(obj);
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
/** @internal */
|
|
953
|
-
_cancelCollector(location) {
|
|
954
|
-
this._objectResolvers.delete(location);
|
|
955
|
-
this._objectBuffer.delete(location);
|
|
956
|
-
}
|
|
957
|
-
/** @internal */
|
|
958
|
-
_deliverObject(location, object) {
|
|
959
|
-
const resolver = this._objectResolvers.get(location);
|
|
960
|
-
if (resolver) {
|
|
961
|
-
resolver(object);
|
|
962
|
-
this._objectResolvers.delete(location);
|
|
963
|
-
}
|
|
964
|
-
else {
|
|
965
|
-
this._objectBuffer.set(location, object);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
916
|
/**
|
|
969
917
|
* Handle a channel event from the subscription.
|
|
970
918
|
* @internal
|
|
@@ -977,34 +925,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
977
925
|
case 'connected':
|
|
978
926
|
// Resync is handled by the client via _applyResyncData.
|
|
979
927
|
break;
|
|
980
|
-
case 'object_created':
|
|
981
|
-
if (event.location && event.object) {
|
|
982
|
-
if (event.objectStat)
|
|
983
|
-
this._objectStats.set(event.location, event.objectStat);
|
|
984
|
-
this._handleObjectCreated(event.location, event.object, changeSource);
|
|
985
|
-
}
|
|
986
|
-
break;
|
|
987
|
-
case 'object_updated':
|
|
988
|
-
if (event.location && event.object) {
|
|
989
|
-
if (event.objectStat)
|
|
990
|
-
this._objectStats.set(event.location, event.objectStat);
|
|
991
|
-
this._handleObjectUpdated(event.location, event.object, changeSource);
|
|
992
|
-
}
|
|
993
|
-
break;
|
|
994
|
-
case 'object_deleted':
|
|
995
|
-
if (event.location) {
|
|
996
|
-
this._objectStats.delete(event.location);
|
|
997
|
-
this._handleObjectDeleted(event.location, changeSource);
|
|
998
|
-
}
|
|
999
|
-
break;
|
|
1000
|
-
case 'object_moved':
|
|
1001
|
-
if (event.from && event.to && event.object) {
|
|
1002
|
-
this._objectStats.delete(event.from);
|
|
1003
|
-
if (event.objectStat)
|
|
1004
|
-
this._objectStats.set(event.to, event.objectStat);
|
|
1005
|
-
this._handleObjectMoved(event.from, event.to, event.object, changeSource);
|
|
1006
|
-
}
|
|
1007
|
-
break;
|
|
1008
928
|
case 'schema_updated':
|
|
1009
929
|
if (event.schema) {
|
|
1010
930
|
this._schema = event.schema;
|
|
@@ -1026,6 +946,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
1026
946
|
}
|
|
1027
947
|
}
|
|
1028
948
|
break;
|
|
949
|
+
case 'channel_deleted':
|
|
950
|
+
if (event.channelId === this._channelId) {
|
|
951
|
+
this._channel = undefined;
|
|
952
|
+
this._activeLeaves.clear();
|
|
953
|
+
this.emit('reset', { source: changeSource });
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
1029
956
|
case 'conversation_updated':
|
|
1030
957
|
if (event.channelId === this._channelId && event.conversationId) {
|
|
1031
958
|
if (!this._channel) {
|
|
@@ -1070,72 +997,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1070
997
|
break;
|
|
1071
998
|
}
|
|
1072
999
|
}
|
|
1073
|
-
/** @internal */
|
|
1074
|
-
_handleObjectCreated(location, object, source) {
|
|
1075
|
-
this._deliverObject(location, object);
|
|
1076
|
-
// Maintain local location list — prepend (most recently modified first)
|
|
1077
|
-
this._objectLocations = [location, ...this._objectLocations.filter(l => l !== location)];
|
|
1078
|
-
const pending = this._pendingMutations.get(location);
|
|
1079
|
-
if (pending !== undefined) {
|
|
1080
|
-
this._pendingMutations.delete(location);
|
|
1081
|
-
if (pending !== null) {
|
|
1082
|
-
// Already emitted objectCreated optimistically.
|
|
1083
|
-
// Emit objectUpdated only if AI resolved placeholders (data changed).
|
|
1084
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1085
|
-
this.emit('objectUpdated', { location, object, source });
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
else {
|
|
1090
|
-
this.emit('objectCreated', { location, object, source });
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
/** @internal */
|
|
1094
|
-
_handleObjectUpdated(location, object, source) {
|
|
1095
|
-
this._deliverObject(location, object);
|
|
1096
|
-
this._objectLocations = [location, ...this._objectLocations.filter(l => l !== location)];
|
|
1097
|
-
const pending = this._pendingMutations.get(location);
|
|
1098
|
-
if (pending !== undefined) {
|
|
1099
|
-
this._pendingMutations.delete(location);
|
|
1100
|
-
if (pending !== null) {
|
|
1101
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1102
|
-
this.emit('objectUpdated', { location, object, source });
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
else {
|
|
1107
|
-
this.emit('objectUpdated', { location, object, source });
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
/** @internal */
|
|
1111
|
-
_handleObjectDeleted(location, source) {
|
|
1112
|
-
this._objectLocations = this._objectLocations.filter(l => l !== location);
|
|
1113
|
-
const pending = this._pendingMutations.get(location);
|
|
1114
|
-
if (pending !== undefined) {
|
|
1115
|
-
this._pendingMutations.delete(location);
|
|
1116
|
-
}
|
|
1117
|
-
else {
|
|
1118
|
-
this.emit('objectDeleted', { location, source });
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
/** @internal */
|
|
1122
|
-
_handleObjectMoved(from, to, object, source) {
|
|
1123
|
-
this._deliverObject(to, object);
|
|
1124
|
-
// Drop old location, insert new one at the front.
|
|
1125
|
-
this._objectLocations = [to, ...this._objectLocations.filter(l => l !== from && l !== to)];
|
|
1126
|
-
const pending = this._pendingMutations.get(to);
|
|
1127
|
-
if (pending !== undefined) {
|
|
1128
|
-
this._pendingMutations.delete(to);
|
|
1129
|
-
if (pending !== null) {
|
|
1130
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1131
|
-
this.emit('objectUpdated', { location: to, object, source });
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
else {
|
|
1136
|
-
this.emit('objectMoved', { from, to, object, source });
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
1000
|
}
|
|
1140
1001
|
/**
|
|
1141
1002
|
* A lightweight handle for a specific conversation within a channel.
|
|
@@ -1179,10 +1040,6 @@ export class ConversationHandle {
|
|
|
1179
1040
|
async rename(name) {
|
|
1180
1041
|
return this._channel._renameConversationImpl(name, this._conversationId);
|
|
1181
1042
|
}
|
|
1182
|
-
/** Find objects using structured filters and/or natural language. */
|
|
1183
|
-
async findObjects(options) {
|
|
1184
|
-
return this._channel._findObjectsImpl(options, this._conversationId);
|
|
1185
|
-
}
|
|
1186
1043
|
/** Create a new object. */
|
|
1187
1044
|
async createObject(collection, body, options) {
|
|
1188
1045
|
return this._channel._createObjectImpl(collection, body, options, this._conversationId);
|
|
@@ -1204,12 +1061,12 @@ export class ConversationHandle {
|
|
|
1204
1061
|
return this._channel._promptImpl(text, options, this._conversationId);
|
|
1205
1062
|
}
|
|
1206
1063
|
/** Create a new collection schema. */
|
|
1207
|
-
async createCollection(name, fields) {
|
|
1208
|
-
return this._channel._createCollectionImpl(name, fields, this._conversationId);
|
|
1064
|
+
async createCollection(name, fields, options) {
|
|
1065
|
+
return this._channel._createCollectionImpl(name, fields, options, this._conversationId);
|
|
1209
1066
|
}
|
|
1210
1067
|
/** Alter an existing collection schema. */
|
|
1211
|
-
async alterCollection(name, fields) {
|
|
1212
|
-
return this._channel._alterCollectionImpl(name, fields, this._conversationId);
|
|
1068
|
+
async alterCollection(name, fields, options) {
|
|
1069
|
+
return this._channel._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
1213
1070
|
}
|
|
1214
1071
|
/** Drop a collection schema. */
|
|
1215
1072
|
async dropCollection(name) {
|