sh3-core 0.20.1 → 0.20.2

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.
Files changed (70) hide show
  1. package/dist/documents/backends.d.ts +12 -0
  2. package/dist/documents/backends.js +230 -3
  3. package/dist/documents/backends.test.js +147 -1
  4. package/dist/documents/config.d.ts +2 -0
  5. package/dist/documents/config.js +4 -0
  6. package/dist/documents/handle.js +40 -0
  7. package/dist/documents/handle.test.js +88 -1
  8. package/dist/documents/http-backend.d.ts +6 -0
  9. package/dist/documents/http-backend.js +61 -0
  10. package/dist/documents/http-backend.test.js +51 -1
  11. package/dist/documents/picker-api.test.js +2 -2
  12. package/dist/documents/types.d.ts +76 -14
  13. package/dist/documents/types.js +4 -0
  14. package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
  15. package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
  16. package/dist/primitives/widgets/DocumentFilePicker.svelte +23 -1
  17. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
  18. package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
  19. package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
  20. package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
  21. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
  22. package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
  23. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
  24. package/dist/primitives/widgets/_DocumentBrowser.svelte +414 -27
  25. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  26. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
  27. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
  28. package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
  29. package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
  30. package/dist/sh3Api/headless.js +10 -0
  31. package/dist/shards/activate.svelte.js +2 -2
  32. package/dist/shell-shard/Terminal.svelte +4 -1
  33. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  34. package/dist/shell-shard/dispatch.d.ts +2 -0
  35. package/dist/shell-shard/dispatch.js +2 -0
  36. package/dist/shell-shard/manifest.js +7 -1
  37. package/dist/shell-shard/shellShard.svelte.js +1 -1
  38. package/dist/shell-shard/verbs/cat.d.ts +2 -0
  39. package/dist/shell-shard/verbs/cat.js +35 -0
  40. package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
  41. package/dist/shell-shard/verbs/cat.test.js +49 -0
  42. package/dist/shell-shard/verbs/index.js +12 -0
  43. package/dist/shell-shard/verbs/ls.d.ts +2 -0
  44. package/dist/shell-shard/verbs/ls.js +48 -0
  45. package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
  46. package/dist/shell-shard/verbs/ls.test.js +64 -0
  47. package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
  48. package/dist/shell-shard/verbs/mkdir.js +30 -0
  49. package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
  50. package/dist/shell-shard/verbs/mkdir.test.js +48 -0
  51. package/dist/shell-shard/verbs/mv.d.ts +2 -0
  52. package/dist/shell-shard/verbs/mv.js +33 -0
  53. package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
  54. package/dist/shell-shard/verbs/mv.test.js +55 -0
  55. package/dist/shell-shard/verbs/rm.d.ts +2 -0
  56. package/dist/shell-shard/verbs/rm.js +28 -0
  57. package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
  58. package/dist/shell-shard/verbs/rm.test.js +47 -0
  59. package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
  60. package/dist/shell-shard/verbs/scope-parse.js +33 -0
  61. package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
  62. package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
  63. package/dist/shell-shard/verbs/xfer.d.ts +2 -0
  64. package/dist/shell-shard/verbs/xfer.js +101 -0
  65. package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
  66. package/dist/shell-shard/verbs/xfer.test.js +96 -0
  67. package/dist/verbs/types.d.ts +18 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +1 -1
@@ -13,6 +13,12 @@ export declare class MemoryDocumentBackend implements DocumentBackend {
13
13
  shardId: string;
14
14
  }>>;
15
15
  readMeta(tenantId: string, shardId: string, path: string): Promise<DocStatus | null>;
16
+ mkdir(tenantId: string, shardId: string, path: string): Promise<void>;
17
+ rmdir(tenantId: string, shardId: string, path: string, opts: {
18
+ recursive: boolean;
19
+ }): Promise<void>;
20
+ renameFolder(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
21
+ listFolders(tenantId: string, shardId: string, prefix: string): Promise<string[]>;
16
22
  }
17
23
  export declare class IndexedDBDocumentBackend implements DocumentBackend {
18
24
  #private;
@@ -26,4 +32,10 @@ export declare class IndexedDBDocumentBackend implements DocumentBackend {
26
32
  listAllDocuments(tenantId: string): Promise<Array<DocumentMeta & {
27
33
  shardId: string;
28
34
  }>>;
35
+ mkdir(tenantId: string, shardId: string, path: string): Promise<void>;
36
+ rmdir(tenantId: string, shardId: string, path: string, opts: {
37
+ recursive: boolean;
38
+ }): Promise<void>;
39
+ renameFolder(tenantId: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
40
+ listFolders(tenantId: string, shardId: string, prefix: string): Promise<string[]>;
29
41
  }
@@ -16,7 +16,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
16
16
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
17
17
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
18
18
  };
19
- var _MemoryDocumentBackend_store, _IndexedDBDocumentBackend_instances, _IndexedDBDocumentBackend_dbPromise, _IndexedDBDocumentBackend_db, _IndexedDBDocumentBackend_tx;
19
+ var _MemoryDocumentBackend_store, _MemoryDocumentBackend_folders, _IndexedDBDocumentBackend_instances, _IndexedDBDocumentBackend_dbPromise, _IndexedDBDocumentBackend_db, _IndexedDBDocumentBackend_tx, _IndexedDBDocumentBackend_txOn;
20
20
  // ---------------------------------------------------------------------------
21
21
  // Helpers
22
22
  // ---------------------------------------------------------------------------
@@ -29,6 +29,7 @@ function keyPrefix(tenantId, shardId) {
29
29
  export class MemoryDocumentBackend {
30
30
  constructor() {
31
31
  _MemoryDocumentBackend_store.set(this, new Map());
32
+ _MemoryDocumentBackend_folders.set(this, new Set()); // composite keys: `${tenant}/${shard}/${path}`
32
33
  }
33
34
  async read(tenantId, shardId, path) {
34
35
  const entry = __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").get(compositeKey(tenantId, shardId, path));
@@ -111,14 +112,111 @@ export class MemoryDocumentBackend {
111
112
  return null;
112
113
  return { exists: true, version: 1, syncMode: 'sync', syncState: 'synced' };
113
114
  }
115
+ async mkdir(tenantId, shardId, path) {
116
+ if (__classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").has(compositeKey(tenantId, shardId, path))) {
117
+ throw new Error(`Cannot mkdir ${path}: a document occupies this path`);
118
+ }
119
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f").add(compositeKey(tenantId, shardId, path));
120
+ }
121
+ async rmdir(tenantId, shardId, path, opts) {
122
+ const folderKey = compositeKey(tenantId, shardId, path);
123
+ const docPrefix = folderKey + '/';
124
+ const docDescendants = [];
125
+ const folderDescendants = [];
126
+ for (const key of __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").keys()) {
127
+ if (key.startsWith(docPrefix))
128
+ docDescendants.push(key);
129
+ }
130
+ for (const key of __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f")) {
131
+ if (key === folderKey)
132
+ continue;
133
+ if (key.startsWith(docPrefix))
134
+ folderDescendants.push(key);
135
+ }
136
+ if (!opts.recursive && (docDescendants.length > 0 || folderDescendants.length > 0)) {
137
+ throw new Error(`Cannot rmdir ${path}: folder is not empty`);
138
+ }
139
+ for (const k of docDescendants)
140
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").delete(k);
141
+ for (const k of folderDescendants)
142
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f").delete(k);
143
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f").delete(folderKey);
144
+ }
145
+ async renameFolder(tenantId, shardId, oldPath, newPath) {
146
+ const oldFolderKey = compositeKey(tenantId, shardId, oldPath);
147
+ const newFolderKey = compositeKey(tenantId, shardId, newPath);
148
+ const oldDocPrefix = oldFolderKey + '/';
149
+ const oldHasExplicit = __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f").has(oldFolderKey);
150
+ const oldHasImplicit = [...__classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").keys()].some((k) => k.startsWith(oldDocPrefix)) ||
151
+ [...__classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f")].some((k) => k.startsWith(oldDocPrefix));
152
+ if (!oldHasExplicit && !oldHasImplicit) {
153
+ throw new Error(`Cannot rename folder ${oldPath}: does not exist`);
154
+ }
155
+ const newDocPrefix = newFolderKey + '/';
156
+ const newHasExplicit = __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f").has(newFolderKey);
157
+ const newHasImplicit = [...__classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").keys()].some((k) => k.startsWith(newDocPrefix)) ||
158
+ [...__classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f")].some((k) => k.startsWith(newDocPrefix));
159
+ if (newHasExplicit || newHasImplicit) {
160
+ throw new Error(`Cannot rename folder to ${newPath}: already exists`);
161
+ }
162
+ // Rewrite docs
163
+ const docMoves = [];
164
+ for (const [key, entry] of __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f")) {
165
+ if (key.startsWith(oldDocPrefix)) {
166
+ const rewritten = newFolderKey + '/' + key.slice(oldDocPrefix.length);
167
+ docMoves.push([key, rewritten, entry]);
168
+ }
169
+ }
170
+ for (const [oldKey, newKey, entry] of docMoves) {
171
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").delete(oldKey);
172
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").set(newKey, Object.assign(Object.assign({}, entry), { lastModified: Date.now() }));
173
+ }
174
+ // Rewrite folders
175
+ const folderMoves = [];
176
+ for (const key of __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f")) {
177
+ if (key === oldFolderKey) {
178
+ folderMoves.push([key, newFolderKey]);
179
+ }
180
+ else if (key.startsWith(oldDocPrefix)) {
181
+ folderMoves.push([key, newFolderKey + '/' + key.slice(oldDocPrefix.length)]);
182
+ }
183
+ }
184
+ for (const [oldKey, newKey] of folderMoves) {
185
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f").delete(oldKey);
186
+ __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f").add(newKey);
187
+ }
188
+ }
189
+ async listFolders(tenantId, shardId, prefix) {
190
+ const basePrefix = prefix
191
+ ? compositeKey(tenantId, shardId, prefix) + '/'
192
+ : keyPrefix(tenantId, shardId);
193
+ const out = new Set();
194
+ for (const key of __classPrivateFieldGet(this, _MemoryDocumentBackend_folders, "f")) {
195
+ if (!key.startsWith(basePrefix))
196
+ continue;
197
+ const rest = key.slice(basePrefix.length);
198
+ const slash = rest.indexOf('/');
199
+ out.add(slash >= 0 ? rest.slice(0, slash) : rest);
200
+ }
201
+ for (const key of __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").keys()) {
202
+ if (!key.startsWith(basePrefix))
203
+ continue;
204
+ const rest = key.slice(basePrefix.length);
205
+ const slash = rest.indexOf('/');
206
+ if (slash >= 0)
207
+ out.add(rest.slice(0, slash));
208
+ }
209
+ return [...out].sort();
210
+ }
114
211
  }
115
- _MemoryDocumentBackend_store = new WeakMap();
212
+ _MemoryDocumentBackend_store = new WeakMap(), _MemoryDocumentBackend_folders = new WeakMap();
116
213
  // ---------------------------------------------------------------------------
117
214
  // IndexedDBDocumentBackend
118
215
  // ---------------------------------------------------------------------------
119
216
  const IDB_NAME = 'sh3-documents';
120
217
  const IDB_STORE = 'docs';
121
- const IDB_VERSION = 2;
218
+ const IDB_FOLDERS = 'folders';
219
+ const IDB_VERSION = 3;
122
220
  export class IndexedDBDocumentBackend {
123
221
  constructor() {
124
222
  _IndexedDBDocumentBackend_instances.add(this);
@@ -274,6 +372,123 @@ export class IndexedDBDocumentBackend {
274
372
  req.onerror = () => reject(req.error);
275
373
  });
276
374
  }
375
+ async mkdir(tenantId, shardId, path) {
376
+ const docKey = compositeKey(tenantId, shardId, path);
377
+ const exists = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_tx).call(this, 'readonly', (s) => s.getKey(docKey));
378
+ if (exists !== undefined) {
379
+ throw new Error(`Cannot mkdir ${path}: a document occupies this path`);
380
+ }
381
+ await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_txOn).call(this, IDB_FOLDERS, 'readwrite', (s) => s.put(1, docKey));
382
+ }
383
+ async rmdir(tenantId, shardId, path, opts) {
384
+ const folderKey = compositeKey(tenantId, shardId, path);
385
+ const docPrefix = folderKey + '/';
386
+ const upper = docPrefix + '￿';
387
+ const docKeys = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_tx).call(this, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(docPrefix, upper, false, false)));
388
+ const folderKeys = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_txOn).call(this, IDB_FOLDERS, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(docPrefix, upper, false, false)));
389
+ if (!opts.recursive && (docKeys.length > 0 || folderKeys.length > 0)) {
390
+ throw new Error(`Cannot rmdir ${path}: folder is not empty`);
391
+ }
392
+ const db = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_db).call(this);
393
+ await new Promise((resolve, reject) => {
394
+ const tx = db.transaction([IDB_STORE, IDB_FOLDERS], 'readwrite');
395
+ const docStore = tx.objectStore(IDB_STORE);
396
+ const folderStore = tx.objectStore(IDB_FOLDERS);
397
+ for (const k of docKeys)
398
+ docStore.delete(k);
399
+ for (const k of folderKeys)
400
+ folderStore.delete(k);
401
+ folderStore.delete(folderKey);
402
+ tx.oncomplete = () => resolve();
403
+ tx.onerror = () => reject(tx.error);
404
+ });
405
+ }
406
+ async renameFolder(tenantId, shardId, oldPath, newPath) {
407
+ const oldFolderKey = compositeKey(tenantId, shardId, oldPath);
408
+ const newFolderKey = compositeKey(tenantId, shardId, newPath);
409
+ const oldPrefix = oldFolderKey + '/';
410
+ const newPrefix = newFolderKey + '/';
411
+ const upperOld = oldPrefix + '￿';
412
+ const upperNew = newPrefix + '￿';
413
+ const [oldDocs, oldFolders, oldExplicit, newDocs, newFolders, newExplicit] = await Promise.all([
414
+ __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_tx).call(this, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(oldPrefix, upperOld, false, false))),
415
+ __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_txOn).call(this, IDB_FOLDERS, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(oldPrefix, upperOld, false, false))),
416
+ __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_txOn).call(this, IDB_FOLDERS, 'readonly', (s) => s.getKey(oldFolderKey)),
417
+ __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_tx).call(this, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(newPrefix, upperNew, false, false))),
418
+ __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_txOn).call(this, IDB_FOLDERS, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(newPrefix, upperNew, false, false))),
419
+ __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_txOn).call(this, IDB_FOLDERS, 'readonly', (s) => s.getKey(newFolderKey)),
420
+ ]);
421
+ if (oldDocs.length === 0 && oldFolders.length === 0 && oldExplicit === undefined) {
422
+ throw new Error(`Cannot rename folder ${oldPath}: does not exist`);
423
+ }
424
+ if (newDocs.length > 0 || newFolders.length > 0 || newExplicit !== undefined) {
425
+ throw new Error(`Cannot rename folder to ${newPath}: already exists`);
426
+ }
427
+ // Read doc entries with values via cursor
428
+ const docEntries = await new Promise((resolve, reject) => {
429
+ __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_db).call(this).then((db) => {
430
+ const tx = db.transaction(IDB_STORE, 'readonly');
431
+ const store = tx.objectStore(IDB_STORE);
432
+ const req = store.openCursor(IDBKeyRange.bound(oldPrefix, upperOld, false, false));
433
+ const acc = [];
434
+ req.onsuccess = () => {
435
+ const cursor = req.result;
436
+ if (cursor) {
437
+ acc.push({ key: cursor.key, value: cursor.value });
438
+ cursor.continue();
439
+ }
440
+ else {
441
+ resolve(acc);
442
+ }
443
+ };
444
+ req.onerror = () => reject(req.error);
445
+ });
446
+ });
447
+ const db = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_db).call(this);
448
+ await new Promise((resolve, reject) => {
449
+ const tx = db.transaction([IDB_STORE, IDB_FOLDERS], 'readwrite');
450
+ const docStore = tx.objectStore(IDB_STORE);
451
+ const folderStore = tx.objectStore(IDB_FOLDERS);
452
+ for (const { key, value } of docEntries) {
453
+ const rewritten = newFolderKey + '/' + key.slice(oldPrefix.length);
454
+ docStore.delete(key);
455
+ docStore.put(Object.assign(Object.assign({}, value), { lastModified: Date.now() }), rewritten);
456
+ }
457
+ if (oldExplicit !== undefined) {
458
+ folderStore.delete(oldFolderKey);
459
+ }
460
+ folderStore.put(1, newFolderKey);
461
+ for (const k of oldFolders) {
462
+ const oldKeyStr = k;
463
+ const rewritten = newFolderKey + '/' + oldKeyStr.slice(oldPrefix.length);
464
+ folderStore.delete(k);
465
+ folderStore.put(1, rewritten);
466
+ }
467
+ tx.oncomplete = () => resolve();
468
+ tx.onerror = () => reject(tx.error);
469
+ });
470
+ }
471
+ async listFolders(tenantId, shardId, prefix) {
472
+ const basePrefix = prefix
473
+ ? compositeKey(tenantId, shardId, prefix) + '/'
474
+ : keyPrefix(tenantId, shardId);
475
+ const upper = basePrefix + '￿';
476
+ const out = new Set();
477
+ const folderKeys = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_txOn).call(this, IDB_FOLDERS, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(basePrefix, upper, false, false)));
478
+ for (const k of folderKeys) {
479
+ const rest = k.slice(basePrefix.length);
480
+ const slash = rest.indexOf('/');
481
+ out.add(slash >= 0 ? rest.slice(0, slash) : rest);
482
+ }
483
+ const docKeys = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_tx).call(this, 'readonly', (s) => s.getAllKeys(IDBKeyRange.bound(basePrefix, upper, false, false)));
484
+ for (const k of docKeys) {
485
+ const rest = k.slice(basePrefix.length);
486
+ const slash = rest.indexOf('/');
487
+ if (slash >= 0)
488
+ out.add(rest.slice(0, slash));
489
+ }
490
+ return [...out].sort();
491
+ }
277
492
  }
278
493
  _IndexedDBDocumentBackend_dbPromise = new WeakMap(), _IndexedDBDocumentBackend_instances = new WeakSet(), _IndexedDBDocumentBackend_db = function _IndexedDBDocumentBackend_db() {
279
494
  if (!__classPrivateFieldGet(this, _IndexedDBDocumentBackend_dbPromise, "f")) {
@@ -284,6 +499,9 @@ _IndexedDBDocumentBackend_dbPromise = new WeakMap(), _IndexedDBDocumentBackend_i
284
499
  if (!db.objectStoreNames.contains(IDB_STORE)) {
285
500
  db.createObjectStore(IDB_STORE);
286
501
  }
502
+ if (!db.objectStoreNames.contains(IDB_FOLDERS)) {
503
+ db.createObjectStore(IDB_FOLDERS);
504
+ }
287
505
  };
288
506
  req.onsuccess = () => resolve(req.result);
289
507
  req.onerror = () => reject(req.error);
@@ -301,4 +519,13 @@ async function _IndexedDBDocumentBackend_tx(mode, fn) {
301
519
  req.onsuccess = () => resolve(req.result);
302
520
  req.onerror = () => reject(req.error);
303
521
  });
522
+ }, _IndexedDBDocumentBackend_txOn = async function _IndexedDBDocumentBackend_txOn(storeName, mode, fn) {
523
+ const db = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_db).call(this);
524
+ return new Promise((resolve, reject) => {
525
+ const tx = db.transaction(storeName, mode);
526
+ const store = tx.objectStore(storeName);
527
+ const req = fn(store);
528
+ req.onsuccess = () => resolve(req.result);
529
+ req.onerror = () => reject(req.error);
530
+ });
304
531
  };
@@ -1,5 +1,5 @@
1
1
  import 'fake-indexeddb/auto';
2
- import { describe, it, expect } from 'vitest';
2
+ import { describe, it, expect, beforeEach } from 'vitest';
3
3
  import { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
4
4
  describe('DocumentBackend tenant-wide primitives', () => {
5
5
  it('listAllShards returns every shard that has content for a tenant', async () => {
@@ -99,3 +99,149 @@ describe('IndexedDBDocumentBackend.rename', () => {
99
99
  .rejects.toThrow(/not found/);
100
100
  });
101
101
  });
102
+ describe('MemoryDocumentBackend folder ops', () => {
103
+ let backend;
104
+ beforeEach(() => { backend = new MemoryDocumentBackend(); });
105
+ it('mkdir creates an empty folder visible via listFolders', async () => {
106
+ await backend.mkdir('t', 's', 'a');
107
+ expect(await backend.listFolders('t', 's', '')).toEqual(['a']);
108
+ });
109
+ it('mkdir is a no-op if the folder already exists', async () => {
110
+ await backend.mkdir('t', 's', 'a');
111
+ await backend.mkdir('t', 's', 'a');
112
+ expect(await backend.listFolders('t', 's', '')).toEqual(['a']);
113
+ });
114
+ it('mkdir throws if a document occupies the path', async () => {
115
+ await backend.write('t', 's', 'a', 'content');
116
+ await expect(backend.mkdir('t', 's', 'a')).rejects.toThrow();
117
+ });
118
+ it('listFolders surfaces folders implied by document paths', async () => {
119
+ await backend.write('t', 's', 'sub/a.md', 'x');
120
+ expect(await backend.listFolders('t', 's', '')).toEqual(['sub']);
121
+ });
122
+ it('listFolders merges explicit and implicit folders without duplicates', async () => {
123
+ await backend.mkdir('t', 's', 'sub');
124
+ await backend.write('t', 's', 'sub/a.md', 'x');
125
+ expect(await backend.listFolders('t', 's', '')).toEqual(['sub']);
126
+ });
127
+ it('listFolders with prefix returns immediate children', async () => {
128
+ await backend.mkdir('t', 's', 'a/b');
129
+ await backend.mkdir('t', 's', 'a/c');
130
+ await backend.write('t', 's', 'a/d/x.md', 'x');
131
+ const children = await backend.listFolders('t', 's', 'a');
132
+ expect(children.sort()).toEqual(['b', 'c', 'd']);
133
+ });
134
+ it('rmdir on non-empty folder without recursive throws', async () => {
135
+ await backend.write('t', 's', 'a/x.md', 'x');
136
+ await expect(backend.rmdir('t', 's', 'a', { recursive: false })).rejects.toThrow();
137
+ });
138
+ it('rmdir on empty folder without recursive succeeds', async () => {
139
+ await backend.mkdir('t', 's', 'a');
140
+ await backend.rmdir('t', 's', 'a', { recursive: false });
141
+ expect(await backend.listFolders('t', 's', '')).toEqual([]);
142
+ });
143
+ it('rmdir recursive removes folder and all descendants', async () => {
144
+ await backend.write('t', 's', 'a/x.md', 'x');
145
+ await backend.write('t', 's', 'a/b/y.md', 'y');
146
+ await backend.mkdir('t', 's', 'a/empty');
147
+ await backend.rmdir('t', 's', 'a', { recursive: true });
148
+ expect(await backend.listFolders('t', 's', '')).toEqual([]);
149
+ expect(await backend.list('t', 's')).toEqual([]);
150
+ });
151
+ it('renameFolder rewrites all descendant doc paths', async () => {
152
+ await backend.write('t', 's', 'old/x.md', 'x');
153
+ await backend.write('t', 's', 'old/sub/y.md', 'y');
154
+ await backend.renameFolder('t', 's', 'old', 'new');
155
+ const docs = (await backend.list('t', 's')).map((d) => d.path).sort();
156
+ expect(docs).toEqual(['new/sub/y.md', 'new/x.md']);
157
+ });
158
+ it('renameFolder rewrites empty subfolders too', async () => {
159
+ await backend.mkdir('t', 's', 'old/empty');
160
+ await backend.renameFolder('t', 's', 'old', 'new');
161
+ expect((await backend.listFolders('t', 's', 'new')).sort()).toEqual(['empty']);
162
+ });
163
+ it('renameFolder throws if newPath already exists', async () => {
164
+ await backend.mkdir('t', 's', 'a');
165
+ await backend.mkdir('t', 's', 'b');
166
+ await expect(backend.renameFolder('t', 's', 'a', 'b')).rejects.toThrow();
167
+ });
168
+ it('renameFolder throws if oldPath does not exist', async () => {
169
+ await expect(backend.renameFolder('t', 's', 'missing', 'b')).rejects.toThrow();
170
+ });
171
+ });
172
+ describe('IndexedDBDocumentBackend folder ops', () => {
173
+ let backend;
174
+ let t;
175
+ let s;
176
+ beforeEach(() => {
177
+ backend = new IndexedDBDocumentBackend();
178
+ t = 'tenant_' + Math.random().toString(36).slice(2, 10);
179
+ s = 'shard_' + Math.random().toString(36).slice(2, 10);
180
+ });
181
+ it('mkdir creates an empty folder visible via listFolders', async () => {
182
+ await backend.mkdir(t, s, 'a');
183
+ expect(await backend.listFolders(t, s, '')).toEqual(['a']);
184
+ });
185
+ it('mkdir is a no-op if the folder already exists', async () => {
186
+ await backend.mkdir(t, s, 'a');
187
+ await backend.mkdir(t, s, 'a');
188
+ expect(await backend.listFolders(t, s, '')).toEqual(['a']);
189
+ });
190
+ it('mkdir throws if a document occupies the path', async () => {
191
+ await backend.write(t, s, 'a', 'content');
192
+ await expect(backend.mkdir(t, s, 'a')).rejects.toThrow();
193
+ });
194
+ it('listFolders surfaces folders implied by document paths', async () => {
195
+ await backend.write(t, s, 'sub/a.md', 'x');
196
+ expect(await backend.listFolders(t, s, '')).toEqual(['sub']);
197
+ });
198
+ it('listFolders merges explicit and implicit folders without duplicates', async () => {
199
+ await backend.mkdir(t, s, 'sub');
200
+ await backend.write(t, s, 'sub/a.md', 'x');
201
+ expect(await backend.listFolders(t, s, '')).toEqual(['sub']);
202
+ });
203
+ it('listFolders with prefix returns immediate children', async () => {
204
+ await backend.mkdir(t, s, 'a/b');
205
+ await backend.mkdir(t, s, 'a/c');
206
+ await backend.write(t, s, 'a/d/x.md', 'x');
207
+ const children = await backend.listFolders(t, s, 'a');
208
+ expect(children.sort()).toEqual(['b', 'c', 'd']);
209
+ });
210
+ it('rmdir on non-empty folder without recursive throws', async () => {
211
+ await backend.write(t, s, 'a/x.md', 'x');
212
+ await expect(backend.rmdir(t, s, 'a', { recursive: false })).rejects.toThrow();
213
+ });
214
+ it('rmdir on empty folder without recursive succeeds', async () => {
215
+ await backend.mkdir(t, s, 'a');
216
+ await backend.rmdir(t, s, 'a', { recursive: false });
217
+ expect(await backend.listFolders(t, s, '')).toEqual([]);
218
+ });
219
+ it('rmdir recursive removes folder and all descendants', async () => {
220
+ await backend.write(t, s, 'a/x.md', 'x');
221
+ await backend.write(t, s, 'a/b/y.md', 'y');
222
+ await backend.mkdir(t, s, 'a/empty');
223
+ await backend.rmdir(t, s, 'a', { recursive: true });
224
+ expect(await backend.listFolders(t, s, '')).toEqual([]);
225
+ expect(await backend.list(t, s)).toEqual([]);
226
+ });
227
+ it('renameFolder rewrites all descendant doc paths', async () => {
228
+ await backend.write(t, s, 'old/x.md', 'x');
229
+ await backend.write(t, s, 'old/sub/y.md', 'y');
230
+ await backend.renameFolder(t, s, 'old', 'new');
231
+ const docs = (await backend.list(t, s)).map((d) => d.path).sort();
232
+ expect(docs).toEqual(['new/sub/y.md', 'new/x.md']);
233
+ });
234
+ it('renameFolder rewrites empty subfolders too', async () => {
235
+ await backend.mkdir(t, s, 'old/empty');
236
+ await backend.renameFolder(t, s, 'old', 'new');
237
+ expect((await backend.listFolders(t, s, 'new')).sort()).toEqual(['empty']);
238
+ });
239
+ it('renameFolder throws if newPath already exists', async () => {
240
+ await backend.mkdir(t, s, 'a');
241
+ await backend.mkdir(t, s, 'b');
242
+ await expect(backend.renameFolder(t, s, 'a', 'b')).rejects.toThrow();
243
+ });
244
+ it('renameFolder throws if oldPath does not exist', async () => {
245
+ await expect(backend.renameFolder(t, s, 'missing', 'b')).rejects.toThrow();
246
+ });
247
+ });
@@ -4,6 +4,8 @@ import type { DocumentBackend } from './types';
4
4
  * scopeId for all document operations. Wired by createShell after bootstrap. */
5
5
  export declare function __setScopeResolver(resolver: (() => string | null) | null): void;
6
6
  export declare function getActiveScopeId(): string;
7
+ /** The user's base (personal) tenant id — never overridden by the project resolver. */
8
+ export declare function getPersonalScopeId(): string;
7
9
  /** @deprecated use getActiveScopeId — kept until callers migrate. */
8
10
  export declare function getTenantId(): string;
9
11
  export declare function getDocumentBackend(): DocumentBackend;
@@ -27,6 +27,10 @@ export function getActiveScopeId() {
27
27
  var _a;
28
28
  return (_a = scopeResolver === null || scopeResolver === void 0 ? void 0 : scopeResolver()) !== null && _a !== void 0 ? _a : scopeId;
29
29
  }
30
+ /** The user's base (personal) tenant id — never overridden by the project resolver. */
31
+ export function getPersonalScopeId() {
32
+ return scopeId;
33
+ }
30
34
  /** @deprecated use getActiveScopeId — kept until callers migrate. */
31
35
  export function getTenantId() {
32
36
  return getActiveScopeId();
@@ -86,6 +86,46 @@ export function createDocumentHandle(tenantId, shardId, backend, options) {
86
86
  shardId,
87
87
  });
88
88
  },
89
+ async mkdir(path, opts) {
90
+ const tid = resolveTenant(opts);
91
+ await backend.mkdir(tid, shardId, path);
92
+ documentChanges.emit({ type: 'folder-create', path, tenantId: tid, shardId });
93
+ },
94
+ async rmdir(path, opts) {
95
+ var _a;
96
+ const recursive = (_a = opts === null || opts === void 0 ? void 0 : opts.recursive) !== null && _a !== void 0 ? _a : false;
97
+ if (recursive) {
98
+ const folderPrefix = path + '/';
99
+ for (const ctrl of controllers) {
100
+ if (ctrl.path === path || ctrl.path.startsWith(folderPrefix)) {
101
+ throw new Error(`Cannot rmdir ${path}: active autosave on ${ctrl.path}; flush and dispose first`);
102
+ }
103
+ }
104
+ }
105
+ const tid = resolveTenant(opts);
106
+ await backend.rmdir(tid, shardId, path, { recursive });
107
+ documentChanges.emit({ type: 'folder-delete', path, tenantId: tid, shardId });
108
+ },
109
+ async renameFolder(oldPath, newPath, opts) {
110
+ const folderPrefix = oldPath + '/';
111
+ for (const ctrl of controllers) {
112
+ if (ctrl.path === oldPath || ctrl.path.startsWith(folderPrefix)) {
113
+ throw new Error(`Cannot rename folder ${oldPath}: active autosave on ${ctrl.path}; flush and dispose first`);
114
+ }
115
+ }
116
+ const tid = resolveTenant(opts);
117
+ await backend.renameFolder(tid, shardId, oldPath, newPath);
118
+ documentChanges.emit({
119
+ type: 'folder-rename',
120
+ path: newPath,
121
+ oldPath,
122
+ tenantId: tid,
123
+ shardId,
124
+ });
125
+ },
126
+ async listFolders(prefix, opts) {
127
+ return backend.listFolders(resolveTenant(opts), shardId, prefix !== null && prefix !== void 0 ? prefix : '');
128
+ },
89
129
  async exists(path) {
90
130
  return backend.exists(tenantId, shardId, path);
91
131
  },
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from './backends';
3
3
  import { createDocumentHandle } from './handle';
4
4
  import { documentChanges } from './notifications';
@@ -28,6 +28,10 @@ describe('DocumentHandle.status()', () => {
28
28
  async exists() { return false; },
29
29
  async listAllShards() { return []; },
30
30
  async listAllDocuments() { return []; },
31
+ async mkdir() { },
32
+ async rmdir() { },
33
+ async renameFolder() { },
34
+ async listFolders() { return []; },
31
35
  };
32
36
  const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
33
37
  await expect(handle.status('a.txt')).rejects.toThrow(/status/);
@@ -46,6 +50,10 @@ describe('DocumentHandle.resolveConflict()', () => {
46
50
  async listAllShards() { return []; },
47
51
  async listAllDocuments() { return []; },
48
52
  async resolve(t, s, p, c) { resolved.push({ t, s, p, c }); },
53
+ async mkdir() { },
54
+ async rmdir() { },
55
+ async renameFolder() { },
56
+ async listFolders() { return []; },
49
57
  };
50
58
  const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
51
59
  await handle.resolveConflict('a.txt', 'local');
@@ -69,6 +77,10 @@ describe('DocumentHandle.readBranch()', () => {
69
77
  async listAllShards() { return []; },
70
78
  async listAllDocuments() { return []; },
71
79
  async readBranch(...args) { calls.push(args); return 'remote-content'; },
80
+ async mkdir() { },
81
+ async rmdir() { },
82
+ async renameFolder() { },
83
+ async listFolders() { return []; },
72
84
  };
73
85
  const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
74
86
  const out = await handle.readBranch('a.txt', 'peer-1');
@@ -86,6 +98,10 @@ describe('DocumentHandle.readBranch()', () => {
86
98
  async listAllShards() { return []; },
87
99
  async listAllDocuments() { return []; },
88
100
  async readBranch() { return null; },
101
+ async mkdir() { },
102
+ async rmdir() { },
103
+ async renameFolder() { },
104
+ async listFolders() { return []; },
89
105
  };
90
106
  const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
91
107
  expect(await handle.readBranch('a.txt', 'peer-1')).toBeNull();
@@ -164,3 +180,74 @@ describe('DocumentHandle.delete()', () => {
164
180
  unsub();
165
181
  });
166
182
  });
183
+ describe('DocumentHandle folder ops', () => {
184
+ let backend;
185
+ let handle;
186
+ beforeEach(() => {
187
+ backend = new MemoryDocumentBackend();
188
+ handle = createDocumentHandle('t', 's', backend, { format: 'text' });
189
+ });
190
+ it('mkdir forwards to backend with bound tenant/shard', async () => {
191
+ await handle.mkdir('a');
192
+ expect(await backend.listFolders('t', 's', '')).toEqual(['a']);
193
+ });
194
+ it('rmdir defaults recursive to false', async () => {
195
+ await handle.write('a/x.md', 'x');
196
+ await expect(handle.rmdir('a')).rejects.toThrow();
197
+ });
198
+ it('rmdir({recursive:true}) cascades', async () => {
199
+ await handle.write('a/x.md', 'x');
200
+ await handle.rmdir('a', { recursive: true });
201
+ expect(await handle.list()).toEqual([]);
202
+ });
203
+ it('renameFolder rewrites descendant paths', async () => {
204
+ await handle.write('old/x.md', 'x');
205
+ await handle.renameFolder('old', 'new');
206
+ const docs = (await handle.list()).map((d) => d.path).sort();
207
+ expect(docs).toEqual(['new/x.md']);
208
+ });
209
+ it('renameFolder refused when an autosave controller is inside the folder', async () => {
210
+ const ctrl = handle.autosave('a/x.md');
211
+ ctrl.update('content');
212
+ await expect(handle.renameFolder('a', 'b')).rejects.toThrow(/autosave/i);
213
+ await ctrl.dispose();
214
+ });
215
+ it('rmdir(recursive) refused when an autosave controller is inside the folder', async () => {
216
+ const ctrl = handle.autosave('a/x.md');
217
+ ctrl.update('content');
218
+ await expect(handle.rmdir('a', { recursive: true })).rejects.toThrow(/autosave/i);
219
+ await ctrl.dispose();
220
+ });
221
+ it('listFolders forwards to backend', async () => {
222
+ await handle.mkdir('a');
223
+ expect(await handle.listFolders()).toEqual(['a']);
224
+ expect(await handle.listFolders('a')).toEqual([]);
225
+ });
226
+ it('mkdir emits folder-create change event', async () => {
227
+ const events = [];
228
+ handle.watch((c) => events.push(c));
229
+ await handle.mkdir('a');
230
+ expect(events).toEqual([
231
+ { type: 'folder-create', path: 'a', tenantId: 't', shardId: 's' },
232
+ ]);
233
+ });
234
+ it('rmdir(recursive) emits a single folder-delete event', async () => {
235
+ await handle.write('a/x.md', 'x');
236
+ await handle.write('a/y.md', 'y');
237
+ const events = [];
238
+ handle.watch((c) => events.push(c));
239
+ await handle.rmdir('a', { recursive: true });
240
+ expect(events).toEqual([
241
+ { type: 'folder-delete', path: 'a', tenantId: 't', shardId: 's' },
242
+ ]);
243
+ });
244
+ it('renameFolder emits a single folder-rename event', async () => {
245
+ await handle.write('old/x.md', 'x');
246
+ const events = [];
247
+ handle.watch((c) => events.push(c));
248
+ await handle.renameFolder('old', 'new');
249
+ expect(events).toEqual([
250
+ { type: 'folder-rename', path: 'new', oldPath: 'old', tenantId: 't', shardId: 's' },
251
+ ]);
252
+ });
253
+ });