@rool-dev/sdk 0.10.2-dev.425fd65 → 0.10.2-dev.a9e71cd

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