@rool-dev/sdk 0.10.1 → 0.10.2-dev.0bf8edb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -179
- package/dist/channel.d.ts +41 -129
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +220 -394
- package/dist/channel.js.map +1 -1
- package/dist/client.d.ts +3 -55
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -93
- package/dist/client.js.map +1 -1
- package/dist/graphql.d.ts +4 -46
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +28 -233
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +3 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/path.d.ts +6 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +47 -0
- package/dist/path.js.map +1 -0
- package/dist/reroute.d.ts +22 -0
- package/dist/reroute.d.ts.map +1 -0
- package/dist/reroute.js +61 -0
- package/dist/reroute.js.map +1 -0
- package/dist/rest.d.ts +9 -0
- package/dist/rest.d.ts.map +1 -1
- package/dist/rest.js +33 -1
- package/dist/rest.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +25 -10
- package/dist/router.js.map +1 -1
- package/dist/space.d.ts +10 -17
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +79 -55
- package/dist/space.js.map +1 -1
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +40 -32
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +52 -217
- package/dist/types.d.ts.map +1 -1
- package/dist/webdav.d.ts +44 -21
- package/dist/webdav.d.ts.map +1 -1
- package/dist/webdav.js +94 -57
- package/dist/webdav.js.map +1 -1
- package/package.json +2 -1
- package/dist/apps.d.ts +0 -30
- package/dist/apps.d.ts.map +0 -1
- package/dist/apps.js +0 -81
- package/dist/apps.js.map +0 -1
- package/dist/locations.d.ts +0 -34
- package/dist/locations.d.ts.map +0 -1
- package/dist/locations.js +0 -90
- package/dist/locations.js.map +0 -1
- package/dist/machine.d.ts +0 -16
- package/dist/machine.d.ts.map +0 -1
- package/dist/machine.js +0 -51
- package/dist/machine.js.map +0 -1
package/dist/channel.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { EventEmitter } from './event-emitter.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
// 6-character alphanumeric ID — used for interactionIds, conversationIds, etc.
|
|
2
|
+
import { WebDAVError } from './webdav.js';
|
|
3
|
+
import { isObjectPath, machinePath, machineUri } from './path.js';
|
|
4
|
+
// 6-character alphanumeric ID — used for object names, interactionIds, conversationIds, etc.
|
|
5
5
|
const ENTITY_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
6
|
+
const GET_OBJECTS_CHUNK_SIZE = 500;
|
|
6
7
|
export function generateEntityId() {
|
|
7
8
|
let result = '';
|
|
8
9
|
for (let i = 0; i < 6; i++) {
|
|
@@ -42,6 +43,45 @@ function findDefaultLeaf(interactions) {
|
|
|
42
43
|
}
|
|
43
44
|
return best?.id;
|
|
44
45
|
}
|
|
46
|
+
function objectPath(input) {
|
|
47
|
+
const path = machinePath(input);
|
|
48
|
+
if (!isObjectPath(path)) {
|
|
49
|
+
throw new Error(`Object path must be /space/<collection>/<name>.json without dotfiles: ${input}`);
|
|
50
|
+
}
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
53
|
+
function collectionPath(name) {
|
|
54
|
+
return machinePath(`/space/${name}`);
|
|
55
|
+
}
|
|
56
|
+
function schemaPath(name) {
|
|
57
|
+
return `${collectionPath(name)}/.schema.json`;
|
|
58
|
+
}
|
|
59
|
+
function objectFromBody(path, body) {
|
|
60
|
+
return { path, 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 {
|
|
@@ -59,7 +99,7 @@ function attachmentBody(file) {
|
|
|
59
99
|
};
|
|
60
100
|
}
|
|
61
101
|
return {
|
|
62
|
-
filename: safeAttachmentFilename('attachment', file.contentType),
|
|
102
|
+
filename: safeAttachmentFilename(file.filename ?? 'attachment', file.contentType),
|
|
63
103
|
contentType: file.contentType,
|
|
64
104
|
body: base64Body(file.data),
|
|
65
105
|
};
|
|
@@ -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
|
*
|
|
@@ -124,10 +162,10 @@ const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
|
124
162
|
* at open time and cannot be changed. To use a different channel,
|
|
125
163
|
* open a second one.
|
|
126
164
|
*
|
|
127
|
-
* Objects are addressed by
|
|
128
|
-
* Only schema, metadata,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
165
|
+
* Objects are addressed by machine path (`/space/.../*.json`).
|
|
166
|
+
* Only schema, metadata, object stats, and the channel's own history are cached
|
|
167
|
+
* locally. Object bodies are fetched on demand. Object/file reactivity is
|
|
168
|
+
* exposed at the space level via WebDAV sync notifications.
|
|
131
169
|
*/
|
|
132
170
|
export class RoolChannel extends EventEmitter {
|
|
133
171
|
_id;
|
|
@@ -143,21 +181,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
143
181
|
webdav;
|
|
144
182
|
onCloseCallback;
|
|
145
183
|
logger;
|
|
146
|
-
// Local cache for bounded data (schema, metadata, own channel, object
|
|
184
|
+
// Local cache for bounded data (schema, metadata, own channel, object stats)
|
|
147
185
|
_meta;
|
|
148
186
|
_schema;
|
|
149
187
|
_channel;
|
|
150
|
-
_objectLocations;
|
|
151
188
|
_objectStats;
|
|
152
189
|
// Active leaf per conversation (client-side tree cursor)
|
|
153
190
|
_activeLeaves = new Map();
|
|
154
|
-
// Object collection: tracks pending local mutations (by location) for dedup
|
|
155
|
-
// Maps location → optimistic object (for create/update) or null (for delete)
|
|
156
|
-
_pendingMutations = new Map();
|
|
157
|
-
// Resolvers waiting for object data from SSE events, keyed by location
|
|
158
|
-
_objectResolvers = new Map();
|
|
159
|
-
// Buffer for object data that arrived before a collector was registered, keyed by location
|
|
160
|
-
_objectBuffer = new Map();
|
|
161
191
|
constructor(config) {
|
|
162
192
|
super();
|
|
163
193
|
this._id = config.id;
|
|
@@ -176,8 +206,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
176
206
|
// Initialize local cache from server data
|
|
177
207
|
this._meta = config.meta;
|
|
178
208
|
this._schema = config.schema;
|
|
179
|
-
this._channel = config.channel;
|
|
180
|
-
this._objectLocations = config.objectLocations;
|
|
209
|
+
this._channel = config.channel ?? undefined;
|
|
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,208 +419,165 @@ export class RoolChannel extends EventEmitter {
|
|
|
413
419
|
async clearHistory() {
|
|
414
420
|
await this.graphqlClient.clearCheckpointHistory(this._id, this._channelId);
|
|
415
421
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Get an object's stat (audit information).
|
|
427
|
-
* Returns the cached stat or undefined if not known.
|
|
428
|
-
*/
|
|
429
|
-
stat(location) {
|
|
430
|
-
return this._objectStats.get(normalizeLocation(location));
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Find objects using structured filters and/or natural language.
|
|
434
|
-
*/
|
|
435
|
-
async findObjects(options) {
|
|
436
|
-
return this._findObjectsImpl(options, this._conversationId);
|
|
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;
|
|
437
430
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
431
|
+
async readObject(path) {
|
|
432
|
+
const canonical = objectPath(path);
|
|
433
|
+
try {
|
|
434
|
+
const response = await this.webdav.get(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
445
|
}
|
|
446
|
-
/**
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
446
|
+
/** Get an object JSON file by machine path. Fetches from the server on each call. */
|
|
447
|
+
async getObject(path) {
|
|
448
|
+
return (await this.readObject(path))?.object;
|
|
449
|
+
}
|
|
450
|
+
/** Get object JSON files by machine path in bulk. Duplicate paths are fetched once. */
|
|
451
|
+
async getObjects(paths) {
|
|
452
|
+
const canonical = [];
|
|
453
|
+
const seen = new Set();
|
|
454
|
+
for (const path of paths) {
|
|
455
|
+
const normalized = objectPath(path);
|
|
456
|
+
if (seen.has(normalized))
|
|
457
|
+
continue;
|
|
458
|
+
seen.add(normalized);
|
|
459
|
+
canonical.push(normalized);
|
|
454
460
|
}
|
|
455
|
-
|
|
456
|
-
|
|
461
|
+
const result = { objects: [], missing: [] };
|
|
462
|
+
for (let i = 0; i < canonical.length; i += GET_OBJECTS_CHUNK_SIZE) {
|
|
463
|
+
const chunk = canonical.slice(i, i + GET_OBJECTS_CHUNK_SIZE);
|
|
464
|
+
const partial = await this.restClient.getObjects(this._id, chunk);
|
|
465
|
+
result.objects.push(...partial.objects);
|
|
466
|
+
result.missing.push(...partial.missing);
|
|
457
467
|
}
|
|
458
|
-
return
|
|
468
|
+
return result;
|
|
459
469
|
}
|
|
460
|
-
/**
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
468
|
-
* @returns The created object and a status message.
|
|
469
|
-
*/
|
|
470
|
-
async createObject(collection, body, options) {
|
|
471
|
-
return this._createObjectImpl(collection, body, options, this._conversationId);
|
|
470
|
+
/** Get an object's cached audit information. */
|
|
471
|
+
stat(path) {
|
|
472
|
+
return this._objectStats.get(objectPath(path));
|
|
473
|
+
}
|
|
474
|
+
/** Create or replace an object JSON file at an exact machine path. */
|
|
475
|
+
async putObject(path, body) {
|
|
476
|
+
return this._putObjectImpl(path, body, this._conversationId);
|
|
472
477
|
}
|
|
473
478
|
/** @internal */
|
|
474
|
-
async
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
const optimistic = { location, collection, basename, body };
|
|
478
|
-
this._pendingMutations.set(location, optimistic);
|
|
479
|
-
this.emit('objectCreated', { location, object: optimistic, source: 'local_user' });
|
|
479
|
+
async _putObjectImpl(path, body, conversationId) {
|
|
480
|
+
const canonical = objectPath(path);
|
|
481
|
+
const optimistic = objectFromBody(canonical, body);
|
|
480
482
|
try {
|
|
481
483
|
const interactionId = generateEntityId();
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
await this.webdav.put(canonical, JSON.stringify(body), {
|
|
485
|
+
contentType: 'application/json',
|
|
486
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
487
|
+
});
|
|
488
|
+
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
489
|
+
return { object: fresh, message: `Put ${canonical}` };
|
|
485
490
|
}
|
|
486
491
|
catch (error) {
|
|
487
|
-
this.logger.error('[RoolChannel] Failed to
|
|
488
|
-
this._pendingMutations.delete(location);
|
|
489
|
-
this._cancelCollector(location);
|
|
492
|
+
this.logger.error('[RoolChannel] Failed to put object:', error);
|
|
490
493
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
491
|
-
this.emit('reset', { source: 'system' });
|
|
492
494
|
throw error;
|
|
493
495
|
}
|
|
494
496
|
}
|
|
495
|
-
/**
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
* @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.
|
|
502
|
-
*/
|
|
503
|
-
async updateObject(location, options) {
|
|
504
|
-
return this._updateObjectImpl(location, options, this._conversationId);
|
|
497
|
+
/** Patch an existing object. Null or undefined deletes a field. */
|
|
498
|
+
async patchObject(path, options) {
|
|
499
|
+
return this._patchObjectImpl(path, options, this._conversationId);
|
|
505
500
|
}
|
|
506
501
|
/** @internal */
|
|
507
|
-
async
|
|
508
|
-
const canonical =
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
serverPatch[key] = value === undefined ? null : value;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
// Emit optimistic event if we have data changes
|
|
519
|
-
if (data) {
|
|
520
|
-
const { collection, basename } = parseLocation(canonical);
|
|
521
|
-
const optimistic = { location: canonical, collection, basename, body: data };
|
|
522
|
-
this._pendingMutations.set(canonical, optimistic);
|
|
523
|
-
this.emit('objectUpdated', { location: canonical, object: optimistic, source: 'local_user' });
|
|
524
|
-
}
|
|
502
|
+
async _patchObjectImpl(path, options, conversationId) {
|
|
503
|
+
const canonical = objectPath(path);
|
|
504
|
+
const data = options.data ?? {};
|
|
505
|
+
const current = await this.readObject(canonical);
|
|
506
|
+
if (!current)
|
|
507
|
+
throw new Error(`Object ${canonical} not found`);
|
|
508
|
+
const body = patchBody(current.object.body, data);
|
|
509
|
+
const optimistic = objectFromBody(canonical, body);
|
|
525
510
|
try {
|
|
526
511
|
const interactionId = generateEntityId();
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
parentInteractionId: options.parentInteractionId,
|
|
512
|
+
await this.webdav.put(canonical, JSON.stringify(body), {
|
|
513
|
+
contentType: 'application/json',
|
|
514
|
+
ifMatch: current.etag ?? undefined,
|
|
515
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
532
516
|
});
|
|
533
|
-
const fresh =
|
|
534
|
-
return { object: fresh, message };
|
|
517
|
+
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
518
|
+
return { object: fresh, message: `Patched ${canonical}` };
|
|
535
519
|
}
|
|
536
520
|
catch (error) {
|
|
537
|
-
this.logger.error('[RoolChannel] Failed to
|
|
538
|
-
this._pendingMutations.delete(canonical);
|
|
539
|
-
this._cancelCollector(canonical);
|
|
521
|
+
this.logger.error('[RoolChannel] Failed to patch object:', error);
|
|
540
522
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
541
|
-
this.emit('reset', { source: 'system' });
|
|
542
523
|
throw error;
|
|
543
524
|
}
|
|
544
525
|
}
|
|
545
|
-
/**
|
|
546
|
-
* Move (rename or relocate) an object to a new location.
|
|
547
|
-
* Use this to rename, change collection, or atomically rewrite the body.
|
|
548
|
-
*
|
|
549
|
-
* @param from - Current location
|
|
550
|
-
* @param to - New location
|
|
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
|
-
*/
|
|
526
|
+
/** Move an object JSON file to a new machine path, optionally replacing its body. */
|
|
554
527
|
async moveObject(from, to, options) {
|
|
555
528
|
return this._moveObjectImpl(from, to, options, this._conversationId);
|
|
556
529
|
}
|
|
557
530
|
/** @internal */
|
|
558
531
|
async _moveObjectImpl(from, to, options, conversationId) {
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
const { collection, basename } = parseLocation(toLoc);
|
|
563
|
-
const optimistic = {
|
|
564
|
-
location: toLoc,
|
|
565
|
-
collection,
|
|
566
|
-
basename,
|
|
567
|
-
body: options?.body ?? {},
|
|
568
|
-
};
|
|
569
|
-
this._pendingMutations.set(toLoc, optimistic);
|
|
570
|
-
this.emit('objectMoved', { from: fromLoc, to: toLoc, object: optimistic, source: 'local_user' });
|
|
532
|
+
const fromPath = objectPath(from);
|
|
533
|
+
const toPath = objectPath(to);
|
|
534
|
+
const optimistic = objectFromBody(toPath, options?.body ?? {});
|
|
571
535
|
try {
|
|
572
536
|
const interactionId = generateEntityId();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
ephemeral: options?.ephemeral,
|
|
576
|
-
parentInteractionId: options?.parentInteractionId,
|
|
537
|
+
await this.webdav.move(fromPath, toPath, {
|
|
538
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
577
539
|
});
|
|
578
|
-
|
|
579
|
-
|
|
540
|
+
if (options?.body) {
|
|
541
|
+
await this.webdav.put(toPath, JSON.stringify(options.body), {
|
|
542
|
+
contentType: 'application/json',
|
|
543
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
this._objectStats.delete(fromPath);
|
|
547
|
+
const fresh = await this.getObject(toPath) ?? optimistic;
|
|
548
|
+
return { object: fresh, message: `Moved ${fromPath} to ${toPath}` };
|
|
580
549
|
}
|
|
581
550
|
catch (error) {
|
|
582
551
|
this.logger.error('[RoolChannel] Failed to move object:', error);
|
|
583
|
-
this._pendingMutations.delete(toLoc);
|
|
584
|
-
this._cancelCollector(toLoc);
|
|
585
552
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
586
|
-
this.emit('reset', { source: 'system' });
|
|
587
553
|
throw error;
|
|
588
554
|
}
|
|
589
555
|
}
|
|
590
|
-
/**
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
556
|
+
/** Delete object JSON files by machine path. */
|
|
557
|
+
async deleteObjects(paths) {
|
|
558
|
+
return this._deleteObjectsImpl(paths, this._conversationId);
|
|
559
|
+
}
|
|
560
|
+
/** @deprecated Use deleteObjects instead. */
|
|
561
|
+
async deletePaths(paths) {
|
|
562
|
+
return this.deleteObjects(paths);
|
|
596
563
|
}
|
|
597
564
|
/** @internal */
|
|
598
|
-
async _deleteObjectsImpl(
|
|
599
|
-
if (
|
|
565
|
+
async _deleteObjectsImpl(paths, conversationId) {
|
|
566
|
+
if (paths.length === 0)
|
|
600
567
|
return;
|
|
601
|
-
const canonical =
|
|
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
|
-
}
|
|
568
|
+
const canonical = paths.map(objectPath);
|
|
607
569
|
try {
|
|
608
570
|
const interactionId = generateEntityId();
|
|
609
|
-
|
|
571
|
+
for (const path of canonical) {
|
|
572
|
+
await this.webdav.delete(path, {
|
|
573
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
574
|
+
});
|
|
575
|
+
this._objectStats.delete(path);
|
|
576
|
+
}
|
|
610
577
|
}
|
|
611
578
|
catch (error) {
|
|
612
|
-
this.logger.error('[RoolChannel] Failed to delete
|
|
613
|
-
for (const location of canonical) {
|
|
614
|
-
this._pendingMutations.delete(location);
|
|
615
|
-
}
|
|
579
|
+
this.logger.error('[RoolChannel] Failed to delete paths:', error);
|
|
616
580
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
617
|
-
this.emit('reset', { source: 'system' });
|
|
618
581
|
throw error;
|
|
619
582
|
}
|
|
620
583
|
}
|
|
@@ -623,19 +586,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
623
586
|
return this._schema;
|
|
624
587
|
}
|
|
625
588
|
/** Create a new collection schema. */
|
|
626
|
-
async createCollection(name, fields) {
|
|
627
|
-
return this._createCollectionImpl(name, fields, this._conversationId);
|
|
589
|
+
async createCollection(name, fields, options) {
|
|
590
|
+
return this._createCollectionImpl(name, fields, options, this._conversationId);
|
|
628
591
|
}
|
|
629
592
|
/** @internal */
|
|
630
|
-
async _createCollectionImpl(name, fields, conversationId) {
|
|
593
|
+
async _createCollectionImpl(name, fields, options, conversationId) {
|
|
631
594
|
if (this._schema[name]) {
|
|
632
595
|
throw new Error(`Collection "${name}" already exists`);
|
|
633
596
|
}
|
|
634
597
|
// Optimistic local update
|
|
635
|
-
const optimisticDef =
|
|
598
|
+
const optimisticDef = collectionDef(fields, options);
|
|
636
599
|
this._schema[name] = optimisticDef;
|
|
637
600
|
try {
|
|
638
|
-
|
|
601
|
+
await this.webdav.mkcol(collectionPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
602
|
+
await this.webdav.put(schemaPath(name), JSON.stringify(optimisticDef), {
|
|
603
|
+
contentType: 'application/json',
|
|
604
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
605
|
+
});
|
|
606
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
607
|
+
return optimisticDef;
|
|
639
608
|
}
|
|
640
609
|
catch (error) {
|
|
641
610
|
this.logger.error('[RoolChannel] Failed to create collection:', error);
|
|
@@ -644,18 +613,24 @@ export class RoolChannel extends EventEmitter {
|
|
|
644
613
|
}
|
|
645
614
|
}
|
|
646
615
|
/** Alter an existing collection schema, replacing its field definitions. */
|
|
647
|
-
async alterCollection(name, fields) {
|
|
648
|
-
return this._alterCollectionImpl(name, fields, this._conversationId);
|
|
616
|
+
async alterCollection(name, fields, options) {
|
|
617
|
+
return this._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
649
618
|
}
|
|
650
619
|
/** @internal */
|
|
651
|
-
async _alterCollectionImpl(name, fields, conversationId) {
|
|
620
|
+
async _alterCollectionImpl(name, fields, options, conversationId) {
|
|
652
621
|
if (!this._schema[name]) {
|
|
653
622
|
throw new Error(`Collection "${name}" not found`);
|
|
654
623
|
}
|
|
655
624
|
const previous = this._schema[name];
|
|
656
|
-
this._schema[name] =
|
|
625
|
+
this._schema[name] = collectionDef(fields, options);
|
|
657
626
|
try {
|
|
658
|
-
|
|
627
|
+
const updated = this._schema[name];
|
|
628
|
+
await this.webdav.put(schemaPath(name), JSON.stringify(updated), {
|
|
629
|
+
contentType: 'application/json',
|
|
630
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
631
|
+
});
|
|
632
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
633
|
+
return updated;
|
|
659
634
|
}
|
|
660
635
|
catch (error) {
|
|
661
636
|
this.logger.error('[RoolChannel] Failed to alter collection:', error);
|
|
@@ -675,7 +650,8 @@ export class RoolChannel extends EventEmitter {
|
|
|
675
650
|
const previous = this._schema[name];
|
|
676
651
|
delete this._schema[name];
|
|
677
652
|
try {
|
|
678
|
-
await this.
|
|
653
|
+
await this.webdav.delete(collectionPath(name), { collection: true, headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
654
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
679
655
|
}
|
|
680
656
|
catch (error) {
|
|
681
657
|
this.logger.error('[RoolChannel] Failed to drop collection:', error);
|
|
@@ -811,12 +787,14 @@ export class RoolChannel extends EventEmitter {
|
|
|
811
787
|
}
|
|
812
788
|
/** @internal */
|
|
813
789
|
async _promptImpl(prompt, options, conversationId) {
|
|
814
|
-
const { attachments, parentInteractionId: explicitParent, signal,
|
|
790
|
+
const { attachments, parentInteractionId: explicitParent, signal, ...rest } = options ?? {};
|
|
815
791
|
const interactionId = generateEntityId();
|
|
816
792
|
let attachmentRefs;
|
|
817
793
|
if (attachments?.length) {
|
|
818
|
-
|
|
819
|
-
|
|
794
|
+
attachmentRefs = await Promise.all(attachments.map(async (attachment) => {
|
|
795
|
+
const path = typeof attachment === 'string' ? machinePath(attachment) : await this.uploadAttachment(attachment, conversationId);
|
|
796
|
+
return machineUri(path);
|
|
797
|
+
}));
|
|
820
798
|
}
|
|
821
799
|
// Auto-continue from active leaf if no explicit parent provided
|
|
822
800
|
const parentInteractionId = explicitParent !== undefined
|
|
@@ -840,7 +818,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
840
818
|
try {
|
|
841
819
|
result = await this.graphqlClient.prompt(this._id, prompt, this._channelId, conversationId, {
|
|
842
820
|
...rest,
|
|
843
|
-
locations: locations?.map(normalizeLocation),
|
|
844
821
|
attachmentRefs,
|
|
845
822
|
interactionId,
|
|
846
823
|
parentInteractionId,
|
|
@@ -850,25 +827,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
850
827
|
if (onAbort)
|
|
851
828
|
signal.removeEventListener('abort', onAbort);
|
|
852
829
|
}
|
|
853
|
-
// Collect modified objects — they arrive via SSE events during/after the mutation.
|
|
854
830
|
const objects = [];
|
|
855
|
-
const
|
|
856
|
-
for (const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
this._objectBuffer.delete(location);
|
|
860
|
-
objects.push(buffered);
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
missing.push(location);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (missing.length > 0) {
|
|
867
|
-
const fetched = await Promise.all(missing.map(location => this.graphqlClient.getObject(this._id, location)));
|
|
868
|
-
for (const obj of fetched) {
|
|
869
|
-
if (obj)
|
|
870
|
-
objects.push(obj);
|
|
871
|
-
}
|
|
831
|
+
const fetched = await Promise.all(result.modifiedObjectPaths.map((path) => this.getObject(path)));
|
|
832
|
+
for (const object of fetched) {
|
|
833
|
+
if (object)
|
|
834
|
+
objects.push(object);
|
|
872
835
|
}
|
|
873
836
|
return {
|
|
874
837
|
message: result.message,
|
|
@@ -900,70 +863,20 @@ export class RoolChannel extends EventEmitter {
|
|
|
900
863
|
return this.restClient.proxyFetch(this._id, url, init);
|
|
901
864
|
}
|
|
902
865
|
async uploadAttachment(file, conversationId) {
|
|
903
|
-
await this.ensureCollection('attachments');
|
|
904
|
-
const directory =
|
|
866
|
+
await this.ensureCollection('/rool-drive/attachments');
|
|
867
|
+
const directory = `/rool-drive/attachments/${conversationId}`;
|
|
905
868
|
await this.ensureCollection(directory);
|
|
906
869
|
const attachment = attachmentBody(file);
|
|
907
870
|
const path = `${directory}/${attachment.filename}`;
|
|
908
871
|
await this.webdav.put(path, attachment.body, { contentType: attachment.contentType });
|
|
909
|
-
|
|
910
|
-
if (!resource)
|
|
911
|
-
throw new Error('Failed to resolve uploaded attachment');
|
|
912
|
-
return resource;
|
|
872
|
+
return path;
|
|
913
873
|
}
|
|
914
874
|
async ensureCollection(path) {
|
|
915
|
-
// Note: not an object collection, a folder, which is "collection" in webdav land
|
|
916
875
|
const response = await this.webdav.request('MKCOL', path, { collection: true });
|
|
917
876
|
if (response.status === 201 || response.status === 405)
|
|
918
877
|
return;
|
|
919
878
|
throw new Error(`Failed to create collection ${path}: ${response.status} ${await response.text()}`);
|
|
920
879
|
}
|
|
921
|
-
/**
|
|
922
|
-
* Register a collector that resolves when the object arrives via SSE.
|
|
923
|
-
* @internal
|
|
924
|
-
*/
|
|
925
|
-
_collectObject(location) {
|
|
926
|
-
return new Promise((resolve, reject) => {
|
|
927
|
-
const buffered = this._objectBuffer.get(location);
|
|
928
|
-
if (buffered) {
|
|
929
|
-
this._objectBuffer.delete(location);
|
|
930
|
-
resolve(buffered);
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
const timer = setTimeout(() => {
|
|
934
|
-
this._objectResolvers.delete(location);
|
|
935
|
-
// Fallback: try to fetch from server
|
|
936
|
-
this.graphqlClient.getObject(this._id, location).then(obj => {
|
|
937
|
-
if (obj) {
|
|
938
|
-
resolve(obj);
|
|
939
|
-
}
|
|
940
|
-
else {
|
|
941
|
-
reject(new Error(`Timeout waiting for object ${location} from SSE`));
|
|
942
|
-
}
|
|
943
|
-
}).catch(reject);
|
|
944
|
-
}, OBJECT_COLLECT_TIMEOUT);
|
|
945
|
-
this._objectResolvers.set(location, (obj) => {
|
|
946
|
-
clearTimeout(timer);
|
|
947
|
-
resolve(obj);
|
|
948
|
-
});
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
/** @internal */
|
|
952
|
-
_cancelCollector(location) {
|
|
953
|
-
this._objectResolvers.delete(location);
|
|
954
|
-
this._objectBuffer.delete(location);
|
|
955
|
-
}
|
|
956
|
-
/** @internal */
|
|
957
|
-
_deliverObject(location, object) {
|
|
958
|
-
const resolver = this._objectResolvers.get(location);
|
|
959
|
-
if (resolver) {
|
|
960
|
-
resolver(object);
|
|
961
|
-
this._objectResolvers.delete(location);
|
|
962
|
-
}
|
|
963
|
-
else {
|
|
964
|
-
this._objectBuffer.set(location, object);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
880
|
/**
|
|
968
881
|
* Handle a channel event from the subscription.
|
|
969
882
|
* @internal
|
|
@@ -976,34 +889,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
976
889
|
case 'connected':
|
|
977
890
|
// Resync is handled by the client via _applyResyncData.
|
|
978
891
|
break;
|
|
979
|
-
case 'object_created':
|
|
980
|
-
if (event.location && event.object) {
|
|
981
|
-
if (event.objectStat)
|
|
982
|
-
this._objectStats.set(event.location, event.objectStat);
|
|
983
|
-
this._handleObjectCreated(event.location, event.object, changeSource);
|
|
984
|
-
}
|
|
985
|
-
break;
|
|
986
|
-
case 'object_updated':
|
|
987
|
-
if (event.location && event.object) {
|
|
988
|
-
if (event.objectStat)
|
|
989
|
-
this._objectStats.set(event.location, event.objectStat);
|
|
990
|
-
this._handleObjectUpdated(event.location, event.object, changeSource);
|
|
991
|
-
}
|
|
992
|
-
break;
|
|
993
|
-
case 'object_deleted':
|
|
994
|
-
if (event.location) {
|
|
995
|
-
this._objectStats.delete(event.location);
|
|
996
|
-
this._handleObjectDeleted(event.location, changeSource);
|
|
997
|
-
}
|
|
998
|
-
break;
|
|
999
|
-
case 'object_moved':
|
|
1000
|
-
if (event.from && event.to && event.object) {
|
|
1001
|
-
this._objectStats.delete(event.from);
|
|
1002
|
-
if (event.objectStat)
|
|
1003
|
-
this._objectStats.set(event.to, event.objectStat);
|
|
1004
|
-
this._handleObjectMoved(event.from, event.to, event.object, changeSource);
|
|
1005
|
-
}
|
|
1006
|
-
break;
|
|
1007
892
|
case 'schema_updated':
|
|
1008
893
|
if (event.schema) {
|
|
1009
894
|
this._schema = event.schema;
|
|
@@ -1025,6 +910,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
1025
910
|
}
|
|
1026
911
|
}
|
|
1027
912
|
break;
|
|
913
|
+
case 'channel_deleted':
|
|
914
|
+
if (event.channelId === this._channelId) {
|
|
915
|
+
this._channel = undefined;
|
|
916
|
+
this._activeLeaves.clear();
|
|
917
|
+
this.emit('reset', { source: changeSource });
|
|
918
|
+
}
|
|
919
|
+
break;
|
|
1028
920
|
case 'conversation_updated':
|
|
1029
921
|
if (event.channelId === this._channelId && event.conversationId) {
|
|
1030
922
|
if (!this._channel) {
|
|
@@ -1069,72 +961,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1069
961
|
break;
|
|
1070
962
|
}
|
|
1071
963
|
}
|
|
1072
|
-
/** @internal */
|
|
1073
|
-
_handleObjectCreated(location, object, source) {
|
|
1074
|
-
this._deliverObject(location, object);
|
|
1075
|
-
// Maintain local location list — prepend (most recently modified first)
|
|
1076
|
-
this._objectLocations = [location, ...this._objectLocations.filter(l => l !== location)];
|
|
1077
|
-
const pending = this._pendingMutations.get(location);
|
|
1078
|
-
if (pending !== undefined) {
|
|
1079
|
-
this._pendingMutations.delete(location);
|
|
1080
|
-
if (pending !== null) {
|
|
1081
|
-
// Already emitted objectCreated optimistically.
|
|
1082
|
-
// Emit objectUpdated only if AI resolved placeholders (data changed).
|
|
1083
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1084
|
-
this.emit('objectUpdated', { location, object, source });
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
else {
|
|
1089
|
-
this.emit('objectCreated', { location, object, source });
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
/** @internal */
|
|
1093
|
-
_handleObjectUpdated(location, object, source) {
|
|
1094
|
-
this._deliverObject(location, object);
|
|
1095
|
-
this._objectLocations = [location, ...this._objectLocations.filter(l => l !== location)];
|
|
1096
|
-
const pending = this._pendingMutations.get(location);
|
|
1097
|
-
if (pending !== undefined) {
|
|
1098
|
-
this._pendingMutations.delete(location);
|
|
1099
|
-
if (pending !== null) {
|
|
1100
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1101
|
-
this.emit('objectUpdated', { location, object, source });
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
else {
|
|
1106
|
-
this.emit('objectUpdated', { location, object, source });
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
/** @internal */
|
|
1110
|
-
_handleObjectDeleted(location, source) {
|
|
1111
|
-
this._objectLocations = this._objectLocations.filter(l => l !== location);
|
|
1112
|
-
const pending = this._pendingMutations.get(location);
|
|
1113
|
-
if (pending !== undefined) {
|
|
1114
|
-
this._pendingMutations.delete(location);
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
this.emit('objectDeleted', { location, source });
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
/** @internal */
|
|
1121
|
-
_handleObjectMoved(from, to, object, source) {
|
|
1122
|
-
this._deliverObject(to, object);
|
|
1123
|
-
// Drop old location, insert new one at the front.
|
|
1124
|
-
this._objectLocations = [to, ...this._objectLocations.filter(l => l !== from && l !== to)];
|
|
1125
|
-
const pending = this._pendingMutations.get(to);
|
|
1126
|
-
if (pending !== undefined) {
|
|
1127
|
-
this._pendingMutations.delete(to);
|
|
1128
|
-
if (pending !== null) {
|
|
1129
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1130
|
-
this.emit('objectUpdated', { location: to, object, source });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
else {
|
|
1135
|
-
this.emit('objectMoved', { from, to, object, source });
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
964
|
}
|
|
1139
965
|
/**
|
|
1140
966
|
* A lightweight handle for a specific conversation within a channel.
|
|
@@ -1178,37 +1004,37 @@ export class ConversationHandle {
|
|
|
1178
1004
|
async rename(name) {
|
|
1179
1005
|
return this._channel._renameConversationImpl(name, this._conversationId);
|
|
1180
1006
|
}
|
|
1181
|
-
/**
|
|
1182
|
-
async
|
|
1183
|
-
return this._channel.
|
|
1184
|
-
}
|
|
1185
|
-
/** Create a new object. */
|
|
1186
|
-
async createObject(collection, body, options) {
|
|
1187
|
-
return this._channel._createObjectImpl(collection, body, options, this._conversationId);
|
|
1007
|
+
/** Create or replace an object JSON file. */
|
|
1008
|
+
async putObject(path, body) {
|
|
1009
|
+
return this._channel._putObjectImpl(path, body, this._conversationId);
|
|
1188
1010
|
}
|
|
1189
|
-
/**
|
|
1190
|
-
async
|
|
1191
|
-
return this._channel.
|
|
1011
|
+
/** Patch an existing object JSON file. */
|
|
1012
|
+
async patchObject(path, options) {
|
|
1013
|
+
return this._channel._patchObjectImpl(path, options, this._conversationId);
|
|
1192
1014
|
}
|
|
1193
1015
|
/** Move (rename/relocate) an object. */
|
|
1194
1016
|
async moveObject(from, to, options) {
|
|
1195
1017
|
return this._channel._moveObjectImpl(from, to, options, this._conversationId);
|
|
1196
1018
|
}
|
|
1197
|
-
/** Delete
|
|
1198
|
-
async deleteObjects(
|
|
1199
|
-
return this._channel._deleteObjectsImpl(
|
|
1019
|
+
/** Delete object JSON files by path. */
|
|
1020
|
+
async deleteObjects(paths) {
|
|
1021
|
+
return this._channel._deleteObjectsImpl(paths, this._conversationId);
|
|
1022
|
+
}
|
|
1023
|
+
/** @deprecated Use deleteObjects instead. */
|
|
1024
|
+
async deletePaths(paths) {
|
|
1025
|
+
return this.deleteObjects(paths);
|
|
1200
1026
|
}
|
|
1201
1027
|
/** Send a prompt to the AI agent, scoped to this conversation's history. */
|
|
1202
1028
|
async prompt(text, options) {
|
|
1203
1029
|
return this._channel._promptImpl(text, options, this._conversationId);
|
|
1204
1030
|
}
|
|
1205
1031
|
/** Create a new collection schema. */
|
|
1206
|
-
async createCollection(name, fields) {
|
|
1207
|
-
return this._channel._createCollectionImpl(name, fields, this._conversationId);
|
|
1032
|
+
async createCollection(name, fields, options) {
|
|
1033
|
+
return this._channel._createCollectionImpl(name, fields, options, this._conversationId);
|
|
1208
1034
|
}
|
|
1209
1035
|
/** Alter an existing collection schema. */
|
|
1210
|
-
async alterCollection(name, fields) {
|
|
1211
|
-
return this._channel._alterCollectionImpl(name, fields, this._conversationId);
|
|
1036
|
+
async alterCollection(name, fields, options) {
|
|
1037
|
+
return this._channel._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
1212
1038
|
}
|
|
1213
1039
|
/** Drop a collection schema. */
|
|
1214
1040
|
async dropCollection(name) {
|