@rool-dev/sdk 0.10.2-dev.47747e3 → 0.10.2-dev.d6bf9eb
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 +23 -67
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +172 -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/rest.d.ts +2 -0
- package/dist/rest.d.ts.map +1 -1
- package/dist/rest.js +12 -0
- package/dist/rest.js.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 +20 -21
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +22 -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,8 +1,10 @@
|
|
|
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.
|
|
5
6
|
const ENTITY_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
7
|
+
const GET_OBJECTS_CHUNK_SIZE = 500;
|
|
6
8
|
export function generateEntityId() {
|
|
7
9
|
let result = '';
|
|
8
10
|
for (let i = 0; i < 6; i++) {
|
|
@@ -42,6 +44,44 @@ function findDefaultLeaf(interactions) {
|
|
|
42
44
|
}
|
|
43
45
|
return best?.id;
|
|
44
46
|
}
|
|
47
|
+
function objectDavPath(location) {
|
|
48
|
+
parseLocation(location);
|
|
49
|
+
return location;
|
|
50
|
+
}
|
|
51
|
+
function collectionDavPath(name) {
|
|
52
|
+
parseLocation(loc(name, 'schema')); // Reuse collection validation.
|
|
53
|
+
return `/space/${name}/`;
|
|
54
|
+
}
|
|
55
|
+
function schemaDavPath(name) {
|
|
56
|
+
return `${collectionDavPath(name)}.schema.json`;
|
|
57
|
+
}
|
|
58
|
+
function objectFromBody(location, body) {
|
|
59
|
+
const { collection, basename } = parseLocation(location);
|
|
60
|
+
return { location, collection, basename, body };
|
|
61
|
+
}
|
|
62
|
+
function jsonObject(value, label) {
|
|
63
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
64
|
+
throw new Error(`${label} must be a JSON object`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function patchBody(current, patch) {
|
|
69
|
+
const next = { ...current };
|
|
70
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
71
|
+
if (value === null || value === undefined)
|
|
72
|
+
delete next[key];
|
|
73
|
+
else
|
|
74
|
+
next[key] = value;
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
function collectionDef(input, options) {
|
|
79
|
+
const base = Array.isArray(input)
|
|
80
|
+
? { fields: input }
|
|
81
|
+
: { fields: input.fields, schemaOrgType: input.schemaOrgType };
|
|
82
|
+
const schemaOrgType = options?.schemaOrgType ?? base.schemaOrgType;
|
|
83
|
+
return schemaOrgType ? { fields: base.fields, schemaOrgType } : { fields: base.fields };
|
|
84
|
+
}
|
|
45
85
|
function attachmentBody(file) {
|
|
46
86
|
if (isFile(file)) {
|
|
47
87
|
return {
|
|
@@ -115,8 +155,6 @@ function base64Body(data) {
|
|
|
115
155
|
bytes[i] = binary.charCodeAt(i);
|
|
116
156
|
return bytes.buffer;
|
|
117
157
|
}
|
|
118
|
-
// Default timeout for waiting on SSE object events (30 seconds)
|
|
119
|
-
const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
120
158
|
/**
|
|
121
159
|
* A channel is a space + channelId pair.
|
|
122
160
|
*
|
|
@@ -125,9 +163,9 @@ const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
|
125
163
|
* open a second one.
|
|
126
164
|
*
|
|
127
165
|
* Objects are addressed by location (`/space/<collection>/<basename>.json`).
|
|
128
|
-
* Only schema, metadata,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
166
|
+
* Only schema, metadata, object stats, and the channel's own history are cached
|
|
167
|
+
* locally. Object bodies are fetched on demand. Object/file reactivity is
|
|
168
|
+
* exposed at the space level via WebDAV sync notifications.
|
|
131
169
|
*/
|
|
132
170
|
export class RoolChannel extends EventEmitter {
|
|
133
171
|
_id;
|
|
@@ -143,21 +181,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
143
181
|
webdav;
|
|
144
182
|
onCloseCallback;
|
|
145
183
|
logger;
|
|
146
|
-
// Local cache for bounded data (schema, metadata, own channel, object
|
|
184
|
+
// Local cache for bounded data (schema, metadata, own channel, object stats)
|
|
147
185
|
_meta;
|
|
148
186
|
_schema;
|
|
149
187
|
_channel;
|
|
150
|
-
_objectLocations;
|
|
151
188
|
_objectStats;
|
|
152
189
|
// Active leaf per conversation (client-side tree cursor)
|
|
153
190
|
_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
191
|
constructor(config) {
|
|
162
192
|
super();
|
|
163
193
|
this._id = config.id;
|
|
@@ -177,7 +207,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
177
207
|
this._meta = config.meta;
|
|
178
208
|
this._schema = config.schema;
|
|
179
209
|
this._channel = config.channel;
|
|
180
|
-
this._objectLocations = config.objectLocations;
|
|
181
210
|
this._objectStats = new Map(Object.entries(config.objectStats));
|
|
182
211
|
}
|
|
183
212
|
/**
|
|
@@ -198,7 +227,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
198
227
|
return;
|
|
199
228
|
this._meta = data.meta;
|
|
200
229
|
this._schema = data.schema;
|
|
201
|
-
this._objectLocations = data.objectLocations;
|
|
202
230
|
this._objectStats = new Map(Object.entries(data.objectStats));
|
|
203
231
|
if (data.channel)
|
|
204
232
|
this._channel = data.channel;
|
|
@@ -376,10 +404,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
376
404
|
close() {
|
|
377
405
|
this._closed = true;
|
|
378
406
|
this.onCloseCallback();
|
|
379
|
-
// Clean up pending object collectors
|
|
380
|
-
this._objectResolvers.clear();
|
|
381
|
-
this._objectBuffer.clear();
|
|
382
|
-
this._pendingMutations.clear();
|
|
383
407
|
this.removeAllListeners();
|
|
384
408
|
}
|
|
385
409
|
/**
|
|
@@ -413,6 +437,30 @@ export class RoolChannel extends EventEmitter {
|
|
|
413
437
|
async clearHistory() {
|
|
414
438
|
await this.graphqlClient.clearCheckpointHistory(this._id, this._channelId);
|
|
415
439
|
}
|
|
440
|
+
davHeaders(conversationId, interactionId) {
|
|
441
|
+
const headers = new Headers({
|
|
442
|
+
'X-Rool-Channel-Id': this._channelId,
|
|
443
|
+
'X-Rool-Conversation-Id': conversationId,
|
|
444
|
+
});
|
|
445
|
+
if (interactionId)
|
|
446
|
+
headers.set('X-Rool-Interaction-Id', interactionId);
|
|
447
|
+
return headers;
|
|
448
|
+
}
|
|
449
|
+
async readObject(location) {
|
|
450
|
+
const canonical = normalizeLocation(location);
|
|
451
|
+
try {
|
|
452
|
+
const response = await this.webdav.get(objectDavPath(canonical));
|
|
453
|
+
const body = jsonObject(await response.json(), `Object ${canonical}`);
|
|
454
|
+
return { object: objectFromBody(canonical, body), etag: response.headers.get('ETag') };
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (error instanceof WebDAVError && error.status === 404)
|
|
458
|
+
return undefined;
|
|
459
|
+
if (error instanceof SyntaxError)
|
|
460
|
+
throw new Error(`Object ${canonical} did not contain valid JSON`);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
416
464
|
/**
|
|
417
465
|
* Get an object by location. Fetches from the server on each call.
|
|
418
466
|
*
|
|
@@ -420,7 +468,33 @@ export class RoolChannel extends EventEmitter {
|
|
|
420
468
|
* or the short form (`<collection>/<basename>`).
|
|
421
469
|
*/
|
|
422
470
|
async getObject(location) {
|
|
423
|
-
return
|
|
471
|
+
return (await this.readObject(location))?.object;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get objects by location in bulk.
|
|
475
|
+
*
|
|
476
|
+
* Accepts either canonical locations (`/space/<collection>/<basename>.json`)
|
|
477
|
+
* or short locations (`<collection>/<basename>`). Duplicate locations are
|
|
478
|
+
* fetched once, preserving their first requested order.
|
|
479
|
+
*/
|
|
480
|
+
async getObjects(locations) {
|
|
481
|
+
const canonical = [];
|
|
482
|
+
const seen = new Set();
|
|
483
|
+
for (const location of locations) {
|
|
484
|
+
const normalized = normalizeLocation(location);
|
|
485
|
+
if (seen.has(normalized))
|
|
486
|
+
continue;
|
|
487
|
+
seen.add(normalized);
|
|
488
|
+
canonical.push(normalized);
|
|
489
|
+
}
|
|
490
|
+
const result = { objects: [], missing: [] };
|
|
491
|
+
for (let i = 0; i < canonical.length; i += GET_OBJECTS_CHUNK_SIZE) {
|
|
492
|
+
const chunk = canonical.slice(i, i + GET_OBJECTS_CHUNK_SIZE);
|
|
493
|
+
const partial = await this.restClient.getObjects(this._id, chunk);
|
|
494
|
+
result.objects.push(...partial.objects);
|
|
495
|
+
result.missing.push(...partial.missing);
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
424
498
|
}
|
|
425
499
|
/**
|
|
426
500
|
* Get an object's stat (audit information).
|
|
@@ -429,42 +503,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
429
503
|
stat(location) {
|
|
430
504
|
return this._objectStats.get(normalizeLocation(location));
|
|
431
505
|
}
|
|
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
506
|
/**
|
|
461
507
|
* Create a new object in the given collection.
|
|
462
508
|
*
|
|
463
509
|
* @param collection - The collection (must exist in the schema)
|
|
464
|
-
* @param body - Object body fields.
|
|
465
|
-
* Fields prefixed with `_` are hidden from AI.
|
|
510
|
+
* @param body - Object body fields. Fields prefixed with `_` are hidden from AI.
|
|
466
511
|
* @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
512
|
* @returns The created object and a status message.
|
|
469
513
|
*/
|
|
470
514
|
async createObject(collection, body, options) {
|
|
@@ -475,20 +519,19 @@ export class RoolChannel extends EventEmitter {
|
|
|
475
519
|
const basename = options?.basename ?? generateBasename();
|
|
476
520
|
const location = loc(collection, basename);
|
|
477
521
|
const optimistic = { location, collection, basename, body };
|
|
478
|
-
this._pendingMutations.set(location, optimistic);
|
|
479
|
-
this.emit('objectCreated', { location, object: optimistic, source: 'local_user' });
|
|
480
522
|
try {
|
|
481
523
|
const interactionId = generateEntityId();
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
524
|
+
await this.webdav.put(objectDavPath(location), JSON.stringify(body), {
|
|
525
|
+
contentType: 'application/json',
|
|
526
|
+
ifNoneMatch: '*',
|
|
527
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
528
|
+
});
|
|
529
|
+
const fresh = await this.getObject(location) ?? optimistic;
|
|
530
|
+
return { object: fresh, message: `Created ${location}` };
|
|
485
531
|
}
|
|
486
532
|
catch (error) {
|
|
487
533
|
this.logger.error('[RoolChannel] Failed to create object:', error);
|
|
488
|
-
this._pendingMutations.delete(location);
|
|
489
|
-
this._cancelCollector(location);
|
|
490
534
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
491
|
-
this.emit('reset', { source: 'system' });
|
|
492
535
|
throw error;
|
|
493
536
|
}
|
|
494
537
|
}
|
|
@@ -496,9 +539,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
496
539
|
* Update an existing object.
|
|
497
540
|
*
|
|
498
541
|
* @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.
|
|
542
|
+
* @param options.data - Fields to add or update. Pass `null` to delete a field.
|
|
502
543
|
*/
|
|
503
544
|
async updateObject(location, options) {
|
|
504
545
|
return this._updateObjectImpl(location, options, this._conversationId);
|
|
@@ -506,39 +547,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
506
547
|
/** @internal */
|
|
507
548
|
async _updateObjectImpl(location, options, conversationId) {
|
|
508
549
|
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
|
-
}
|
|
550
|
+
const data = options.data ?? {};
|
|
551
|
+
const current = await this.readObject(canonical);
|
|
552
|
+
if (!current)
|
|
553
|
+
throw new Error(`Object ${canonical} not found`);
|
|
554
|
+
const body = patchBody(current.object.body, data);
|
|
555
|
+
const optimistic = objectFromBody(canonical, body);
|
|
525
556
|
try {
|
|
526
557
|
const interactionId = generateEntityId();
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
parentInteractionId: options.parentInteractionId,
|
|
558
|
+
await this.webdav.put(objectDavPath(canonical), JSON.stringify(body), {
|
|
559
|
+
contentType: 'application/json',
|
|
560
|
+
ifMatch: current.etag ?? undefined,
|
|
561
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
532
562
|
});
|
|
533
|
-
const fresh =
|
|
534
|
-
return { object: fresh, message };
|
|
563
|
+
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
564
|
+
return { object: fresh, message: `Updated ${canonical}` };
|
|
535
565
|
}
|
|
536
566
|
catch (error) {
|
|
537
567
|
this.logger.error('[RoolChannel] Failed to update object:', error);
|
|
538
|
-
this._pendingMutations.delete(canonical);
|
|
539
|
-
this._cancelCollector(canonical);
|
|
540
568
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
541
|
-
this.emit('reset', { source: 'system' });
|
|
542
569
|
throw error;
|
|
543
570
|
}
|
|
544
571
|
}
|
|
@@ -549,7 +576,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
549
576
|
* @param from - Current location
|
|
550
577
|
* @param to - New location
|
|
551
578
|
* @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
579
|
*/
|
|
554
580
|
async moveObject(from, to, options) {
|
|
555
581
|
return this._moveObjectImpl(from, to, options, this._conversationId);
|
|
@@ -566,24 +592,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
566
592
|
basename,
|
|
567
593
|
body: options?.body ?? {},
|
|
568
594
|
};
|
|
569
|
-
this._pendingMutations.set(toLoc, optimistic);
|
|
570
|
-
this.emit('objectMoved', { from: fromLoc, to: toLoc, object: optimistic, source: 'local_user' });
|
|
571
595
|
try {
|
|
572
596
|
const interactionId = generateEntityId();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
ephemeral: options?.ephemeral,
|
|
576
|
-
parentInteractionId: options?.parentInteractionId,
|
|
597
|
+
await this.webdav.move(objectDavPath(fromLoc), objectDavPath(toLoc), {
|
|
598
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
577
599
|
});
|
|
578
|
-
|
|
579
|
-
|
|
600
|
+
if (options?.body) {
|
|
601
|
+
await this.webdav.put(objectDavPath(toLoc), JSON.stringify(options.body), {
|
|
602
|
+
contentType: 'application/json',
|
|
603
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
this._objectStats.delete(fromLoc);
|
|
607
|
+
const fresh = await this.getObject(toLoc) ?? optimistic;
|
|
608
|
+
return { object: fresh, message: `Moved ${fromLoc} to ${toLoc}` };
|
|
580
609
|
}
|
|
581
610
|
catch (error) {
|
|
582
611
|
this.logger.error('[RoolChannel] Failed to move object:', error);
|
|
583
|
-
this._pendingMutations.delete(toLoc);
|
|
584
|
-
this._cancelCollector(toLoc);
|
|
585
612
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
586
|
-
this.emit('reset', { source: 'system' });
|
|
587
613
|
throw error;
|
|
588
614
|
}
|
|
589
615
|
}
|
|
@@ -599,22 +625,18 @@ export class RoolChannel extends EventEmitter {
|
|
|
599
625
|
if (locations.length === 0)
|
|
600
626
|
return;
|
|
601
627
|
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
628
|
try {
|
|
608
629
|
const interactionId = generateEntityId();
|
|
609
|
-
|
|
630
|
+
for (const location of canonical) {
|
|
631
|
+
await this.webdav.delete(objectDavPath(location), {
|
|
632
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
633
|
+
});
|
|
634
|
+
this._objectStats.delete(location);
|
|
635
|
+
}
|
|
610
636
|
}
|
|
611
637
|
catch (error) {
|
|
612
638
|
this.logger.error('[RoolChannel] Failed to delete objects:', error);
|
|
613
|
-
for (const location of canonical) {
|
|
614
|
-
this._pendingMutations.delete(location);
|
|
615
|
-
}
|
|
616
639
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
617
|
-
this.emit('reset', { source: 'system' });
|
|
618
640
|
throw error;
|
|
619
641
|
}
|
|
620
642
|
}
|
|
@@ -623,19 +645,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
623
645
|
return this._schema;
|
|
624
646
|
}
|
|
625
647
|
/** Create a new collection schema. */
|
|
626
|
-
async createCollection(name, fields) {
|
|
627
|
-
return this._createCollectionImpl(name, fields, this._conversationId);
|
|
648
|
+
async createCollection(name, fields, options) {
|
|
649
|
+
return this._createCollectionImpl(name, fields, options, this._conversationId);
|
|
628
650
|
}
|
|
629
651
|
/** @internal */
|
|
630
|
-
async _createCollectionImpl(name, fields, conversationId) {
|
|
652
|
+
async _createCollectionImpl(name, fields, options, conversationId) {
|
|
631
653
|
if (this._schema[name]) {
|
|
632
654
|
throw new Error(`Collection "${name}" already exists`);
|
|
633
655
|
}
|
|
634
656
|
// Optimistic local update
|
|
635
|
-
const optimisticDef =
|
|
657
|
+
const optimisticDef = collectionDef(fields, options);
|
|
636
658
|
this._schema[name] = optimisticDef;
|
|
637
659
|
try {
|
|
638
|
-
|
|
660
|
+
await this.webdav.mkcol(collectionDavPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
661
|
+
await this.webdav.put(schemaDavPath(name), JSON.stringify(optimisticDef), {
|
|
662
|
+
contentType: 'application/json',
|
|
663
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
664
|
+
});
|
|
665
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
666
|
+
return optimisticDef;
|
|
639
667
|
}
|
|
640
668
|
catch (error) {
|
|
641
669
|
this.logger.error('[RoolChannel] Failed to create collection:', error);
|
|
@@ -644,18 +672,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
644
672
|
}
|
|
645
673
|
}
|
|
646
674
|
/** Alter an existing collection schema, replacing its field definitions. */
|
|
647
|
-
async alterCollection(name, fields) {
|
|
648
|
-
return this._alterCollectionImpl(name, fields, this._conversationId);
|
|
675
|
+
async alterCollection(name, fields, options) {
|
|
676
|
+
return this._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
649
677
|
}
|
|
650
678
|
/** @internal */
|
|
651
|
-
async _alterCollectionImpl(name, fields, conversationId) {
|
|
679
|
+
async _alterCollectionImpl(name, fields, options, conversationId) {
|
|
652
680
|
if (!this._schema[name]) {
|
|
653
681
|
throw new Error(`Collection "${name}" not found`);
|
|
654
682
|
}
|
|
655
683
|
const previous = this._schema[name];
|
|
656
|
-
this._schema[name] =
|
|
684
|
+
this._schema[name] = collectionDef(fields, options);
|
|
657
685
|
try {
|
|
658
|
-
|
|
686
|
+
const updated = this._schema[name];
|
|
687
|
+
await this.webdav.put(schemaDavPath(name), JSON.stringify(updated), {
|
|
688
|
+
contentType: 'application/json',
|
|
689
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
690
|
+
});
|
|
691
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
692
|
+
return updated;
|
|
659
693
|
}
|
|
660
694
|
catch (error) {
|
|
661
695
|
this.logger.error('[RoolChannel] Failed to alter collection:', error);
|
|
@@ -675,7 +709,8 @@ export class RoolChannel extends EventEmitter {
|
|
|
675
709
|
const previous = this._schema[name];
|
|
676
710
|
delete this._schema[name];
|
|
677
711
|
try {
|
|
678
|
-
await this.
|
|
712
|
+
await this.webdav.delete(collectionDavPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
713
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
679
714
|
}
|
|
680
715
|
catch (error) {
|
|
681
716
|
this.logger.error('[RoolChannel] Failed to drop collection:', error);
|
|
@@ -851,25 +886,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
851
886
|
if (onAbort)
|
|
852
887
|
signal.removeEventListener('abort', onAbort);
|
|
853
888
|
}
|
|
854
|
-
// Collect modified objects — they arrive via SSE events during/after the mutation.
|
|
855
889
|
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
|
-
}
|
|
890
|
+
const fetched = await Promise.all(result.modifiedObjectLocations.map((location) => this.getObject(location)));
|
|
891
|
+
for (const object of fetched) {
|
|
892
|
+
if (object)
|
|
893
|
+
objects.push(object);
|
|
873
894
|
}
|
|
874
895
|
return {
|
|
875
896
|
message: result.message,
|
|
@@ -919,52 +940,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
919
940
|
return;
|
|
920
941
|
throw new Error(`Failed to create collection ${path}: ${response.status} ${await response.text()}`);
|
|
921
942
|
}
|
|
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
943
|
/**
|
|
969
944
|
* Handle a channel event from the subscription.
|
|
970
945
|
* @internal
|
|
@@ -977,34 +952,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
977
952
|
case 'connected':
|
|
978
953
|
// Resync is handled by the client via _applyResyncData.
|
|
979
954
|
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
955
|
case 'schema_updated':
|
|
1009
956
|
if (event.schema) {
|
|
1010
957
|
this._schema = event.schema;
|
|
@@ -1026,6 +973,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
1026
973
|
}
|
|
1027
974
|
}
|
|
1028
975
|
break;
|
|
976
|
+
case 'channel_deleted':
|
|
977
|
+
if (event.channelId === this._channelId) {
|
|
978
|
+
this._channel = undefined;
|
|
979
|
+
this._activeLeaves.clear();
|
|
980
|
+
this.emit('reset', { source: changeSource });
|
|
981
|
+
}
|
|
982
|
+
break;
|
|
1029
983
|
case 'conversation_updated':
|
|
1030
984
|
if (event.channelId === this._channelId && event.conversationId) {
|
|
1031
985
|
if (!this._channel) {
|
|
@@ -1070,72 +1024,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1070
1024
|
break;
|
|
1071
1025
|
}
|
|
1072
1026
|
}
|
|
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
1027
|
}
|
|
1140
1028
|
/**
|
|
1141
1029
|
* A lightweight handle for a specific conversation within a channel.
|
|
@@ -1179,10 +1067,6 @@ export class ConversationHandle {
|
|
|
1179
1067
|
async rename(name) {
|
|
1180
1068
|
return this._channel._renameConversationImpl(name, this._conversationId);
|
|
1181
1069
|
}
|
|
1182
|
-
/** Find objects using structured filters and/or natural language. */
|
|
1183
|
-
async findObjects(options) {
|
|
1184
|
-
return this._channel._findObjectsImpl(options, this._conversationId);
|
|
1185
|
-
}
|
|
1186
1070
|
/** Create a new object. */
|
|
1187
1071
|
async createObject(collection, body, options) {
|
|
1188
1072
|
return this._channel._createObjectImpl(collection, body, options, this._conversationId);
|
|
@@ -1204,12 +1088,12 @@ export class ConversationHandle {
|
|
|
1204
1088
|
return this._channel._promptImpl(text, options, this._conversationId);
|
|
1205
1089
|
}
|
|
1206
1090
|
/** Create a new collection schema. */
|
|
1207
|
-
async createCollection(name, fields) {
|
|
1208
|
-
return this._channel._createCollectionImpl(name, fields, this._conversationId);
|
|
1091
|
+
async createCollection(name, fields, options) {
|
|
1092
|
+
return this._channel._createCollectionImpl(name, fields, options, this._conversationId);
|
|
1209
1093
|
}
|
|
1210
1094
|
/** Alter an existing collection schema. */
|
|
1211
|
-
async alterCollection(name, fields) {
|
|
1212
|
-
return this._channel._alterCollectionImpl(name, fields, this._conversationId);
|
|
1095
|
+
async alterCollection(name, fields, options) {
|
|
1096
|
+
return this._channel._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
1213
1097
|
}
|
|
1214
1098
|
/** Drop a collection schema. */
|
|
1215
1099
|
async dropCollection(name) {
|