@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/dist/channel.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { EventEmitter } from './event-emitter.js';
2
- import { generateBasename, loc, normalizeLocation, parseLocation } from './locations.js';
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, the live object location list, and the channel's own
129
- * history are cached locally. Object bodies are fetched on demand. Changes
130
- * arrive via SSE semantic events and are emitted as SDK events.
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 locations, stats)
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 this.graphqlClient.getObject(this._id, normalizeLocation(location));
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. Use `{{placeholder}}` for AI-generated content.
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 ?? generateBasename();
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
- const { message, object } = await this.graphqlClient.createObject(this._id, location, body, this._channelId, conversationId, interactionId, { ephemeral: options?.ephemeral, parentInteractionId: options?.parentInteractionId });
483
- const fresh = object ?? await this._collectObject(location);
484
- return { object: fresh, message };
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. Use `{{placeholder}}` for AI-generated content.
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 { data } = options;
510
- // Normalize undefined to null (for JSON serialization) and build server patch
511
- let serverPatch;
512
- if (data) {
513
- serverPatch = {};
514
- for (const [key, value] of Object.entries(data)) {
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
- const { message, object } = await this.graphqlClient.updateObject(this._id, canonical, this._channelId, conversationId, interactionId, {
528
- patch: serverPatch,
529
- prompt: options.prompt,
530
- ephemeral: options.ephemeral,
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 = object ?? await this._collectObject(canonical);
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
- const { message, object } = await this.graphqlClient.moveObject(this._id, fromLoc, toLoc, this._channelId, conversationId, interactionId, {
574
- body: options?.body,
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
- const fresh = object ?? await this._collectObject(toLoc);
579
- return { object: fresh, message };
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
- await this.graphqlClient.deleteObjects(this._id, canonical, this._channelId, conversationId, interactionId);
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 = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
639
+ const optimisticDef = collectionDef(fields, options);
636
640
  this._schema[name] = optimisticDef;
637
641
  try {
638
- return await this.graphqlClient.createCollection(this._id, name, fields, this._channelId, conversationId);
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] = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
666
+ this._schema[name] = collectionDef(fields, options);
657
667
  try {
658
- return await this.graphqlClient.alterCollection(this._id, name, fields, this._channelId, conversationId);
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.graphqlClient.dropCollection(this._id, name, this._channelId, conversationId);
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 missing = [];
857
- for (const location of result.modifiedObjectLocations) {
858
- const buffered = this._objectBuffer.get(location);
859
- if (buffered) {
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) {