@rool-dev/sdk 0.10.2-dev.47747e3 → 0.10.2-dev.d6bf9eb

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