@rool-dev/sdk 0.10.2-dev.47747e3 → 0.10.2-dev.57158ea
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 +35 -160
- package/dist/channel.d.ts +23 -79
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +175 -309
- package/dist/channel.js.map +1 -1
- package/dist/client.d.ts +3 -49
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -85
- package/dist/client.js.map +1 -1
- package/dist/graphql.d.ts +3 -44
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +7 -208
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/locations.d.ts +0 -5
- package/dist/locations.d.ts.map +1 -1
- package/dist/locations.js +1 -13
- package/dist/locations.js.map +1 -1
- package/dist/rest.d.ts +9 -0
- package/dist/rest.d.ts.map +1 -1
- package/dist/rest.js +44 -1
- package/dist/rest.js.map +1 -1
- package/dist/space.d.ts +2 -5
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +25 -29
- package/dist/space.js.map +1 -1
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +19 -25
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +29 -178
- 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/apps.d.ts +0 -30
- package/dist/apps.d.ts.map +0 -1
- package/dist/apps.js +0 -81
- package/dist/apps.js.map +0 -1
package/dist/channel.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { EventEmitter } from './event-emitter.js';
|
|
2
|
-
import {
|
|
2
|
+
import { WebDAVError } from './webdav.js';
|
|
3
|
+
import { loc, normalizeLocation, parseLocation } from './locations.js';
|
|
3
4
|
import { resolveMachineResource } from './machine.js';
|
|
4
|
-
// 6-character alphanumeric ID — used for interactionIds, conversationIds, etc.
|
|
5
|
+
// 6-character alphanumeric ID — used for object names, 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;
|
|
@@ -244,24 +272,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
244
272
|
get isReadOnly() {
|
|
245
273
|
return this._role === 'viewer';
|
|
246
274
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Get the extension URL if this channel was created via installExtension, or null.
|
|
249
|
-
*/
|
|
250
|
-
get extensionUrl() {
|
|
251
|
-
return this._channel?.extensionUrl ?? null;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Get the extension ID if this channel has an installed extension, or null.
|
|
255
|
-
*/
|
|
256
|
-
get extensionId() {
|
|
257
|
-
return this._channel?.extensionId ?? null;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Get the extension manifest if this channel has an installed extension, or null.
|
|
261
|
-
*/
|
|
262
|
-
get manifest() {
|
|
263
|
-
return this._channel?.manifest ?? null;
|
|
264
|
-
}
|
|
265
275
|
/**
|
|
266
276
|
* Get the active branch of the current conversation as a flat array (root → leaf).
|
|
267
277
|
* Walks from the active leaf up through parentId pointers.
|
|
@@ -376,10 +386,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
376
386
|
close() {
|
|
377
387
|
this._closed = true;
|
|
378
388
|
this.onCloseCallback();
|
|
379
|
-
// Clean up pending object collectors
|
|
380
|
-
this._objectResolvers.clear();
|
|
381
|
-
this._objectBuffer.clear();
|
|
382
|
-
this._pendingMutations.clear();
|
|
383
389
|
this.removeAllListeners();
|
|
384
390
|
}
|
|
385
391
|
/**
|
|
@@ -413,6 +419,30 @@ export class RoolChannel extends EventEmitter {
|
|
|
413
419
|
async clearHistory() {
|
|
414
420
|
await this.graphqlClient.clearCheckpointHistory(this._id, this._channelId);
|
|
415
421
|
}
|
|
422
|
+
davHeaders(conversationId, interactionId) {
|
|
423
|
+
const headers = new Headers({
|
|
424
|
+
'X-Rool-Channel-Id': this._channelId,
|
|
425
|
+
'X-Rool-Conversation-Id': conversationId,
|
|
426
|
+
});
|
|
427
|
+
if (interactionId)
|
|
428
|
+
headers.set('X-Rool-Interaction-Id', interactionId);
|
|
429
|
+
return headers;
|
|
430
|
+
}
|
|
431
|
+
async readObject(location) {
|
|
432
|
+
const canonical = normalizeLocation(location);
|
|
433
|
+
try {
|
|
434
|
+
const response = await this.webdav.get(objectDavPath(canonical));
|
|
435
|
+
const body = jsonObject(await response.json(), `Object ${canonical}`);
|
|
436
|
+
return { object: objectFromBody(canonical, body), etag: response.headers.get('ETag') };
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (error instanceof WebDAVError && error.status === 404)
|
|
440
|
+
return undefined;
|
|
441
|
+
if (error instanceof SyntaxError)
|
|
442
|
+
throw new Error(`Object ${canonical} did not contain valid JSON`);
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
416
446
|
/**
|
|
417
447
|
* Get an object by location. Fetches from the server on each call.
|
|
418
448
|
*
|
|
@@ -420,7 +450,33 @@ export class RoolChannel extends EventEmitter {
|
|
|
420
450
|
* or the short form (`<collection>/<basename>`).
|
|
421
451
|
*/
|
|
422
452
|
async getObject(location) {
|
|
423
|
-
return
|
|
453
|
+
return (await this.readObject(location))?.object;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get objects by location in bulk.
|
|
457
|
+
*
|
|
458
|
+
* Accepts either canonical locations (`/space/<collection>/<basename>.json`)
|
|
459
|
+
* or short locations (`<collection>/<basename>`). Duplicate locations are
|
|
460
|
+
* fetched once, preserving their first requested order.
|
|
461
|
+
*/
|
|
462
|
+
async getObjects(locations) {
|
|
463
|
+
const canonical = [];
|
|
464
|
+
const seen = new Set();
|
|
465
|
+
for (const location of locations) {
|
|
466
|
+
const normalized = normalizeLocation(location);
|
|
467
|
+
if (seen.has(normalized))
|
|
468
|
+
continue;
|
|
469
|
+
seen.add(normalized);
|
|
470
|
+
canonical.push(normalized);
|
|
471
|
+
}
|
|
472
|
+
const result = { objects: [], missing: [] };
|
|
473
|
+
for (let i = 0; i < canonical.length; i += GET_OBJECTS_CHUNK_SIZE) {
|
|
474
|
+
const chunk = canonical.slice(i, i + GET_OBJECTS_CHUNK_SIZE);
|
|
475
|
+
const partial = await this.restClient.getObjects(this._id, chunk);
|
|
476
|
+
result.objects.push(...partial.objects);
|
|
477
|
+
result.missing.push(...partial.missing);
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
424
480
|
}
|
|
425
481
|
/**
|
|
426
482
|
* Get an object's stat (audit information).
|
|
@@ -429,42 +485,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
429
485
|
stat(location) {
|
|
430
486
|
return this._objectStats.get(normalizeLocation(location));
|
|
431
487
|
}
|
|
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
488
|
/**
|
|
461
489
|
* Create a new object in the given collection.
|
|
462
490
|
*
|
|
463
491
|
* @param collection - The collection (must exist in the schema)
|
|
464
|
-
* @param body - Object body fields.
|
|
465
|
-
* Fields prefixed with `_` are hidden from AI.
|
|
492
|
+
* @param body - Object body fields. Fields prefixed with `_` are hidden from AI.
|
|
466
493
|
* @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
494
|
* @returns The created object and a status message.
|
|
469
495
|
*/
|
|
470
496
|
async createObject(collection, body, options) {
|
|
@@ -472,23 +498,22 @@ export class RoolChannel extends EventEmitter {
|
|
|
472
498
|
}
|
|
473
499
|
/** @internal */
|
|
474
500
|
async _createObjectImpl(collection, body, options, conversationId) {
|
|
475
|
-
const basename = options?.basename ??
|
|
501
|
+
const basename = options?.basename ?? generateEntityId();
|
|
476
502
|
const location = loc(collection, basename);
|
|
477
503
|
const optimistic = { location, collection, basename, body };
|
|
478
|
-
this._pendingMutations.set(location, optimistic);
|
|
479
|
-
this.emit('objectCreated', { location, object: optimistic, source: 'local_user' });
|
|
480
504
|
try {
|
|
481
505
|
const interactionId = generateEntityId();
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
506
|
+
await this.webdav.put(objectDavPath(location), JSON.stringify(body), {
|
|
507
|
+
contentType: 'application/json',
|
|
508
|
+
ifNoneMatch: '*',
|
|
509
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
510
|
+
});
|
|
511
|
+
const fresh = await this.getObject(location) ?? optimistic;
|
|
512
|
+
return { object: fresh, message: `Created ${location}` };
|
|
485
513
|
}
|
|
486
514
|
catch (error) {
|
|
487
515
|
this.logger.error('[RoolChannel] Failed to create object:', error);
|
|
488
|
-
this._pendingMutations.delete(location);
|
|
489
|
-
this._cancelCollector(location);
|
|
490
516
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
491
|
-
this.emit('reset', { source: 'system' });
|
|
492
517
|
throw error;
|
|
493
518
|
}
|
|
494
519
|
}
|
|
@@ -496,9 +521,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
496
521
|
* Update an existing object.
|
|
497
522
|
*
|
|
498
523
|
* @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.
|
|
524
|
+
* @param options.data - Fields to add or update. Pass `null` to delete a field.
|
|
502
525
|
*/
|
|
503
526
|
async updateObject(location, options) {
|
|
504
527
|
return this._updateObjectImpl(location, options, this._conversationId);
|
|
@@ -506,39 +529,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
506
529
|
/** @internal */
|
|
507
530
|
async _updateObjectImpl(location, options, conversationId) {
|
|
508
531
|
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
|
-
}
|
|
532
|
+
const data = options.data ?? {};
|
|
533
|
+
const current = await this.readObject(canonical);
|
|
534
|
+
if (!current)
|
|
535
|
+
throw new Error(`Object ${canonical} not found`);
|
|
536
|
+
const body = patchBody(current.object.body, data);
|
|
537
|
+
const optimistic = objectFromBody(canonical, body);
|
|
525
538
|
try {
|
|
526
539
|
const interactionId = generateEntityId();
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
parentInteractionId: options.parentInteractionId,
|
|
540
|
+
await this.webdav.put(objectDavPath(canonical), JSON.stringify(body), {
|
|
541
|
+
contentType: 'application/json',
|
|
542
|
+
ifMatch: current.etag ?? undefined,
|
|
543
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
532
544
|
});
|
|
533
|
-
const fresh =
|
|
534
|
-
return { object: fresh, message };
|
|
545
|
+
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
546
|
+
return { object: fresh, message: `Updated ${canonical}` };
|
|
535
547
|
}
|
|
536
548
|
catch (error) {
|
|
537
549
|
this.logger.error('[RoolChannel] Failed to update object:', error);
|
|
538
|
-
this._pendingMutations.delete(canonical);
|
|
539
|
-
this._cancelCollector(canonical);
|
|
540
550
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
541
|
-
this.emit('reset', { source: 'system' });
|
|
542
551
|
throw error;
|
|
543
552
|
}
|
|
544
553
|
}
|
|
@@ -549,7 +558,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
549
558
|
* @param from - Current location
|
|
550
559
|
* @param to - New location
|
|
551
560
|
* @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
561
|
*/
|
|
554
562
|
async moveObject(from, to, options) {
|
|
555
563
|
return this._moveObjectImpl(from, to, options, this._conversationId);
|
|
@@ -566,24 +574,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
566
574
|
basename,
|
|
567
575
|
body: options?.body ?? {},
|
|
568
576
|
};
|
|
569
|
-
this._pendingMutations.set(toLoc, optimistic);
|
|
570
|
-
this.emit('objectMoved', { from: fromLoc, to: toLoc, object: optimistic, source: 'local_user' });
|
|
571
577
|
try {
|
|
572
578
|
const interactionId = generateEntityId();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
ephemeral: options?.ephemeral,
|
|
576
|
-
parentInteractionId: options?.parentInteractionId,
|
|
579
|
+
await this.webdav.move(objectDavPath(fromLoc), objectDavPath(toLoc), {
|
|
580
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
577
581
|
});
|
|
578
|
-
|
|
579
|
-
|
|
582
|
+
if (options?.body) {
|
|
583
|
+
await this.webdav.put(objectDavPath(toLoc), JSON.stringify(options.body), {
|
|
584
|
+
contentType: 'application/json',
|
|
585
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
this._objectStats.delete(fromLoc);
|
|
589
|
+
const fresh = await this.getObject(toLoc) ?? optimistic;
|
|
590
|
+
return { object: fresh, message: `Moved ${fromLoc} to ${toLoc}` };
|
|
580
591
|
}
|
|
581
592
|
catch (error) {
|
|
582
593
|
this.logger.error('[RoolChannel] Failed to move object:', error);
|
|
583
|
-
this._pendingMutations.delete(toLoc);
|
|
584
|
-
this._cancelCollector(toLoc);
|
|
585
594
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
586
|
-
this.emit('reset', { source: 'system' });
|
|
587
595
|
throw error;
|
|
588
596
|
}
|
|
589
597
|
}
|
|
@@ -599,22 +607,18 @@ export class RoolChannel extends EventEmitter {
|
|
|
599
607
|
if (locations.length === 0)
|
|
600
608
|
return;
|
|
601
609
|
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
610
|
try {
|
|
608
611
|
const interactionId = generateEntityId();
|
|
609
|
-
|
|
612
|
+
for (const location of canonical) {
|
|
613
|
+
await this.webdav.delete(objectDavPath(location), {
|
|
614
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
615
|
+
});
|
|
616
|
+
this._objectStats.delete(location);
|
|
617
|
+
}
|
|
610
618
|
}
|
|
611
619
|
catch (error) {
|
|
612
620
|
this.logger.error('[RoolChannel] Failed to delete objects:', error);
|
|
613
|
-
for (const location of canonical) {
|
|
614
|
-
this._pendingMutations.delete(location);
|
|
615
|
-
}
|
|
616
621
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
617
|
-
this.emit('reset', { source: 'system' });
|
|
618
622
|
throw error;
|
|
619
623
|
}
|
|
620
624
|
}
|
|
@@ -623,19 +627,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
623
627
|
return this._schema;
|
|
624
628
|
}
|
|
625
629
|
/** Create a new collection schema. */
|
|
626
|
-
async createCollection(name, fields) {
|
|
627
|
-
return this._createCollectionImpl(name, fields, this._conversationId);
|
|
630
|
+
async createCollection(name, fields, options) {
|
|
631
|
+
return this._createCollectionImpl(name, fields, options, this._conversationId);
|
|
628
632
|
}
|
|
629
633
|
/** @internal */
|
|
630
|
-
async _createCollectionImpl(name, fields, conversationId) {
|
|
634
|
+
async _createCollectionImpl(name, fields, options, conversationId) {
|
|
631
635
|
if (this._schema[name]) {
|
|
632
636
|
throw new Error(`Collection "${name}" already exists`);
|
|
633
637
|
}
|
|
634
638
|
// Optimistic local update
|
|
635
|
-
const optimisticDef =
|
|
639
|
+
const optimisticDef = collectionDef(fields, options);
|
|
636
640
|
this._schema[name] = optimisticDef;
|
|
637
641
|
try {
|
|
638
|
-
|
|
642
|
+
await this.webdav.mkcol(collectionDavPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
643
|
+
await this.webdav.put(schemaDavPath(name), JSON.stringify(optimisticDef), {
|
|
644
|
+
contentType: 'application/json',
|
|
645
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
646
|
+
});
|
|
647
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
648
|
+
return optimisticDef;
|
|
639
649
|
}
|
|
640
650
|
catch (error) {
|
|
641
651
|
this.logger.error('[RoolChannel] Failed to create collection:', error);
|
|
@@ -644,18 +654,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
644
654
|
}
|
|
645
655
|
}
|
|
646
656
|
/** Alter an existing collection schema, replacing its field definitions. */
|
|
647
|
-
async alterCollection(name, fields) {
|
|
648
|
-
return this._alterCollectionImpl(name, fields, this._conversationId);
|
|
657
|
+
async alterCollection(name, fields, options) {
|
|
658
|
+
return this._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
649
659
|
}
|
|
650
660
|
/** @internal */
|
|
651
|
-
async _alterCollectionImpl(name, fields, conversationId) {
|
|
661
|
+
async _alterCollectionImpl(name, fields, options, conversationId) {
|
|
652
662
|
if (!this._schema[name]) {
|
|
653
663
|
throw new Error(`Collection "${name}" not found`);
|
|
654
664
|
}
|
|
655
665
|
const previous = this._schema[name];
|
|
656
|
-
this._schema[name] =
|
|
666
|
+
this._schema[name] = collectionDef(fields, options);
|
|
657
667
|
try {
|
|
658
|
-
|
|
668
|
+
const updated = this._schema[name];
|
|
669
|
+
await this.webdav.put(schemaDavPath(name), JSON.stringify(updated), {
|
|
670
|
+
contentType: 'application/json',
|
|
671
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
672
|
+
});
|
|
673
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
674
|
+
return updated;
|
|
659
675
|
}
|
|
660
676
|
catch (error) {
|
|
661
677
|
this.logger.error('[RoolChannel] Failed to alter collection:', error);
|
|
@@ -675,7 +691,8 @@ export class RoolChannel extends EventEmitter {
|
|
|
675
691
|
const previous = this._schema[name];
|
|
676
692
|
delete this._schema[name];
|
|
677
693
|
try {
|
|
678
|
-
await this.
|
|
694
|
+
await this.webdav.delete(collectionDavPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
695
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
679
696
|
}
|
|
680
697
|
catch (error) {
|
|
681
698
|
this.logger.error('[RoolChannel] Failed to drop collection:', error);
|
|
@@ -851,25 +868,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
851
868
|
if (onAbort)
|
|
852
869
|
signal.removeEventListener('abort', onAbort);
|
|
853
870
|
}
|
|
854
|
-
// Collect modified objects — they arrive via SSE events during/after the mutation.
|
|
855
871
|
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
|
-
}
|
|
872
|
+
const fetched = await Promise.all(result.modifiedObjectLocations.map((location) => this.getObject(location)));
|
|
873
|
+
for (const object of fetched) {
|
|
874
|
+
if (object)
|
|
875
|
+
objects.push(object);
|
|
873
876
|
}
|
|
874
877
|
return {
|
|
875
878
|
message: result.message,
|
|
@@ -919,52 +922,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
919
922
|
return;
|
|
920
923
|
throw new Error(`Failed to create collection ${path}: ${response.status} ${await response.text()}`);
|
|
921
924
|
}
|
|
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
925
|
/**
|
|
969
926
|
* Handle a channel event from the subscription.
|
|
970
927
|
* @internal
|
|
@@ -977,34 +934,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
977
934
|
case 'connected':
|
|
978
935
|
// Resync is handled by the client via _applyResyncData.
|
|
979
936
|
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
937
|
case 'schema_updated':
|
|
1009
938
|
if (event.schema) {
|
|
1010
939
|
this._schema = event.schema;
|
|
@@ -1026,6 +955,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
1026
955
|
}
|
|
1027
956
|
}
|
|
1028
957
|
break;
|
|
958
|
+
case 'channel_deleted':
|
|
959
|
+
if (event.channelId === this._channelId) {
|
|
960
|
+
this._channel = undefined;
|
|
961
|
+
this._activeLeaves.clear();
|
|
962
|
+
this.emit('reset', { source: changeSource });
|
|
963
|
+
}
|
|
964
|
+
break;
|
|
1029
965
|
case 'conversation_updated':
|
|
1030
966
|
if (event.channelId === this._channelId && event.conversationId) {
|
|
1031
967
|
if (!this._channel) {
|
|
@@ -1070,72 +1006,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1070
1006
|
break;
|
|
1071
1007
|
}
|
|
1072
1008
|
}
|
|
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
1009
|
}
|
|
1140
1010
|
/**
|
|
1141
1011
|
* A lightweight handle for a specific conversation within a channel.
|
|
@@ -1179,10 +1049,6 @@ export class ConversationHandle {
|
|
|
1179
1049
|
async rename(name) {
|
|
1180
1050
|
return this._channel._renameConversationImpl(name, this._conversationId);
|
|
1181
1051
|
}
|
|
1182
|
-
/** Find objects using structured filters and/or natural language. */
|
|
1183
|
-
async findObjects(options) {
|
|
1184
|
-
return this._channel._findObjectsImpl(options, this._conversationId);
|
|
1185
|
-
}
|
|
1186
1052
|
/** Create a new object. */
|
|
1187
1053
|
async createObject(collection, body, options) {
|
|
1188
1054
|
return this._channel._createObjectImpl(collection, body, options, this._conversationId);
|
|
@@ -1204,12 +1070,12 @@ export class ConversationHandle {
|
|
|
1204
1070
|
return this._channel._promptImpl(text, options, this._conversationId);
|
|
1205
1071
|
}
|
|
1206
1072
|
/** Create a new collection schema. */
|
|
1207
|
-
async createCollection(name, fields) {
|
|
1208
|
-
return this._channel._createCollectionImpl(name, fields, this._conversationId);
|
|
1073
|
+
async createCollection(name, fields, options) {
|
|
1074
|
+
return this._channel._createCollectionImpl(name, fields, options, this._conversationId);
|
|
1209
1075
|
}
|
|
1210
1076
|
/** Alter an existing collection schema. */
|
|
1211
|
-
async alterCollection(name, fields) {
|
|
1212
|
-
return this._channel._alterCollectionImpl(name, fields, this._conversationId);
|
|
1077
|
+
async alterCollection(name, fields, options) {
|
|
1078
|
+
return this._channel._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
1213
1079
|
}
|
|
1214
1080
|
/** Drop a collection schema. */
|
|
1215
1081
|
async dropCollection(name) {
|