sh3-core 0.21.2 → 0.22.1

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 (96) hide show
  1. package/dist/__test__/fixtures.js +1 -1
  2. package/dist/__test__/reset.js +1 -1
  3. package/dist/__test__/smoke.test.js +2 -2
  4. package/dist/actions/contextMenuModel.test.js +6 -3
  5. package/dist/actions/ctx-actions.svelte.test.js +9 -9
  6. package/dist/actions/dispatcher-v3.test.js +8 -0
  7. package/dist/actions/dispatcher.svelte.d.ts +1 -2
  8. package/dist/actions/dispatcher.svelte.js +6 -7
  9. package/dist/actions/dispatcher.test.js +9 -12
  10. package/dist/actions/listActionsFromEntries.test.js +1 -2
  11. package/dist/actions/listActive.test.js +2 -3
  12. package/dist/actions/menuBarModel.test.js +1 -7
  13. package/dist/actions/paletteModel.test.js +1 -3
  14. package/dist/actions/scope-helpers.test.js +4 -4
  15. package/dist/actions/shardContext.test.js +2 -2
  16. package/dist/actions/state.svelte.d.ts +12 -2
  17. package/dist/actions/state.svelte.js +15 -12
  18. package/dist/actions/state.test.js +4 -4
  19. package/dist/api.d.ts +3 -3
  20. package/dist/api.js +1 -1
  21. package/dist/app/admin/adminShard.svelte.js +1 -1
  22. package/dist/app/store/storeShard.svelte.js +10 -5
  23. package/dist/app-appearance/appearanceShard.svelte.js +1 -5
  24. package/dist/apps/lifecycle.js +65 -33
  25. package/dist/apps/lifecycle.test.js +198 -10
  26. package/dist/artifact.d.ts +2 -0
  27. package/dist/build.js +1 -1
  28. package/dist/conflicts/adapter-documents.js +1 -2
  29. package/dist/createShell.js +1 -1
  30. package/dist/documents/handle.d.ts +9 -4
  31. package/dist/documents/handle.js +69 -45
  32. package/dist/documents/handle.test.js +99 -27
  33. package/dist/documents/index.d.ts +1 -1
  34. package/dist/documents/types.d.ts +16 -20
  35. package/dist/host.d.ts +1 -1
  36. package/dist/host.js +9 -56
  37. package/dist/host.svelte.test.js +31 -63
  38. package/dist/layout/LayoutRenderer.svelte +1 -1
  39. package/dist/layout/SlotContainer.svelte +1 -0
  40. package/dist/layout/inspection.js +19 -14
  41. package/dist/layout/inspection.svelte.test.js +136 -1
  42. package/dist/layout/slotHostPool.svelte.d.ts +2 -1
  43. package/dist/layout/slotHostPool.svelte.js +6 -3
  44. package/dist/layout/slotHostPool.test.js +17 -0
  45. package/dist/layout/store.projectScope.test.js +76 -0
  46. package/dist/layout/store.svelte.d.ts +6 -0
  47. package/dist/layout/store.svelte.js +43 -13
  48. package/dist/layout/tree-walk.d.ts +8 -1
  49. package/dist/layout/tree-walk.js +11 -1
  50. package/dist/layout/tree-walk.test.js +53 -1
  51. package/dist/layout/types.d.ts +27 -0
  52. package/dist/layout/types.test.js +28 -0
  53. package/dist/layouts-shard/LayoutsSection.svelte +1 -1
  54. package/dist/layouts-shard/layoutsShard.svelte.js +2 -5
  55. package/dist/layouts-shard/layoutsShard.svelte.test.js +2 -2
  56. package/dist/overlays/FloatFrame.svelte +4 -1
  57. package/dist/overlays/float.d.ts +7 -1
  58. package/dist/overlays/float.js +4 -0
  59. package/dist/projects-shard/ProjectsSection.svelte +1 -5
  60. package/dist/projects-shard/projectsShard.svelte.js +1 -5
  61. package/dist/registry/installer.js +1 -1
  62. package/dist/registry/loader.d.ts +1 -1
  63. package/dist/registry/loader.js +3 -3
  64. package/dist/registry/permission-descriptions.test.js +2 -2
  65. package/dist/registry/register.js +1 -1
  66. package/dist/registry/register.test.js +1 -1
  67. package/dist/runtime/runVerb-shell.test.js +1 -1
  68. package/dist/runtime/runVerb.js +2 -2
  69. package/dist/runtime/runVerb.test.js +9 -9
  70. package/dist/sh3Api/headless.js +1 -1
  71. package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -6
  72. package/dist/shards/ctx-fetch.test.js +9 -9
  73. package/dist/shards/lifecycle.svelte.d.ts +108 -0
  74. package/dist/shards/lifecycle.svelte.js +551 -0
  75. package/dist/shards/lifecycle.test.js +139 -0
  76. package/dist/shards/types.d.ts +56 -22
  77. package/dist/shell-shard/shellShard.svelte.js +11 -5
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. package/package.json +1 -1
  81. package/dist/shards/activate-browse.test.js +0 -120
  82. package/dist/shards/activate-contributions.test.js +0 -141
  83. package/dist/shards/activate-error-isolation.test.js +0 -98
  84. package/dist/shards/activate-fields.svelte.test.d.ts +0 -1
  85. package/dist/shards/activate-fields.svelte.test.js +0 -121
  86. package/dist/shards/activate-on-key-revoked.test.d.ts +0 -1
  87. package/dist/shards/activate-on-key-revoked.test.js +0 -60
  88. package/dist/shards/activate-runtime.test.d.ts +0 -1
  89. package/dist/shards/activate-runtime.test.js +0 -299
  90. package/dist/shards/activate-scopeid.test.d.ts +0 -1
  91. package/dist/shards/activate-scopeid.test.js +0 -21
  92. package/dist/shards/activate.svelte.d.ts +0 -102
  93. package/dist/shards/activate.svelte.js +0 -403
  94. /package/dist/{shards/activate-browse.test.d.ts → actions/dispatcher-v3.test.d.ts} +0 -0
  95. /package/dist/{shards/activate-contributions.test.d.ts → layout/store.projectScope.test.d.ts} +0 -0
  96. /package/dist/shards/{activate-error-isolation.test.d.ts → lifecycle.test.d.ts} +0 -0
@@ -20,76 +20,100 @@ var _AutosaveControllerImpl_instances, _AutosaveControllerImpl_handle, _Autosave
20
20
  import { documentChanges } from './notifications';
21
21
  const DEFAULT_DEBOUNCE_MS = 1000;
22
22
  /**
23
- * Create a document handle scoped to a tenant, shard, and file filter.
24
- * The framework calls this from `ShardContext.documents()`.
23
+ * Create a document handle scoped to a tenant and namespace. The framework
24
+ * pre-mints one handle per shard at boot; the namespace resolves lazily on
25
+ * every operation via `getShardBinding(shardId) ?? shardId` so it follows
26
+ * the shard's currently-bound app without re-minting.
27
+ *
28
+ * Format moves from the handle to per-call (readText/writeText/readJson/
29
+ * writeJson/readBinary/writeBinary) — see ADR-027.
25
30
  */
26
- export function createDocumentHandle(tenantId, shardId, backend, options) {
31
+ export function createDocumentHandle(tenantId, shardOrNamespace, backend) {
27
32
  const controllers = new Set();
28
33
  const unsubscribers = new Set();
29
- function matchesExtensions(path) {
30
- if (!options.extensions || options.extensions.length === 0)
31
- return true;
32
- return options.extensions.some((ext) => path.endsWith(ext));
33
- }
34
+ const resolveBoundTenant = typeof tenantId === 'function' ? tenantId : () => tenantId;
35
+ const resolveNamespace = typeof shardOrNamespace === 'function' ? shardOrNamespace : () => shardOrNamespace;
34
36
  function resolveTenant(opts) {
35
37
  var _a;
36
- return (_a = opts === null || opts === void 0 ? void 0 : opts.scope) !== null && _a !== void 0 ? _a : tenantId;
38
+ return (_a = opts === null || opts === void 0 ? void 0 : opts.scope) !== null && _a !== void 0 ? _a : resolveBoundTenant();
37
39
  }
38
40
  function emitChange(type, path, tid) {
39
- documentChanges.emit({ type, path, tenantId: tid, shardId });
41
+ documentChanges.emit({ type, path, tenantId: tid, shardId: resolveNamespace() });
40
42
  }
41
43
  const handle = {
42
44
  async list(opts) {
43
45
  const tid = resolveTenant(opts);
44
- const all = await backend.list(tid, shardId);
45
- if (!options.extensions || options.extensions.length === 0)
46
- return all;
47
- return all.filter((meta) => matchesExtensions(meta.path));
46
+ return backend.list(tid, resolveNamespace());
48
47
  },
49
- async read(path, opts) {
50
- const content = await backend.read(resolveTenant(opts), shardId, path);
48
+ async readText(path, opts) {
49
+ const content = await backend.read(resolveTenant(opts), resolveNamespace(), path);
51
50
  if (content === null)
52
51
  return null;
53
- // Phase 1: text format only. Binary returns as-is from the backend
54
- // but the handle types it as string for text-format handles.
55
- return typeof content === 'string' ? content : new TextDecoder().decode(content);
52
+ if (typeof content === 'string')
53
+ return content;
54
+ return new TextDecoder().decode(content);
55
+ },
56
+ async readBinary(path, opts) {
57
+ const content = await backend.read(resolveTenant(opts), resolveNamespace(), path);
58
+ if (content === null)
59
+ return null;
60
+ if (content instanceof ArrayBuffer)
61
+ return content;
62
+ return new TextEncoder().encode(content).buffer;
63
+ },
64
+ async readJson(path, opts) {
65
+ const text = await this.readText(path, opts);
66
+ if (text === null)
67
+ return null;
68
+ return JSON.parse(text);
56
69
  },
57
- async write(path, content, opts) {
70
+ async writeText(path, content, opts) {
58
71
  const tid = resolveTenant(opts);
59
- const existed = await backend.exists(tid, shardId, path);
60
- await backend.write(tid, shardId, path, content);
72
+ const ns = resolveNamespace();
73
+ const existed = await backend.exists(tid, ns, path);
74
+ await backend.write(tid, ns, path, content);
61
75
  emitChange(existed ? 'update' : 'create', path, tid);
62
76
  },
77
+ async writeBinary(path, content, opts) {
78
+ const tid = resolveTenant(opts);
79
+ const ns = resolveNamespace();
80
+ const existed = await backend.exists(tid, ns, path);
81
+ await backend.write(tid, ns, path, content);
82
+ emitChange(existed ? 'update' : 'create', path, tid);
83
+ },
84
+ async writeJson(path, data, opts) {
85
+ await this.writeText(path, JSON.stringify(data), opts);
86
+ },
63
87
  async delete(path, opts) {
64
88
  const tid = resolveTenant(opts);
65
- const existed = await backend.exists(tid, shardId, path);
66
- await backend.delete(tid, shardId, path);
89
+ const ns = resolveNamespace();
90
+ const existed = await backend.exists(tid, ns, path);
91
+ await backend.delete(tid, ns, path);
67
92
  if (existed)
68
93
  emitChange('delete', path, tid);
69
94
  },
70
95
  async rename(oldPath, newPath, opts) {
71
- if (!matchesExtensions(newPath)) {
72
- throw new Error(`Cannot rename to ${newPath}: violates handle extensions filter`);
73
- }
74
96
  for (const ctrl of controllers) {
75
97
  if (ctrl.path === oldPath) {
76
98
  throw new Error(`Cannot rename: active autosave on ${oldPath}; flush and dispose first`);
77
99
  }
78
100
  }
79
101
  const tid = resolveTenant(opts);
80
- await backend.rename(tid, shardId, oldPath, newPath);
102
+ const ns = resolveNamespace();
103
+ await backend.rename(tid, ns, oldPath, newPath);
81
104
  documentChanges.emit({
82
105
  type: 'rename',
83
106
  path: newPath,
84
107
  oldPath,
85
108
  tenantId: tid,
86
- shardId,
109
+ shardId: ns,
87
110
  });
88
111
  },
89
112
  async mkdir(path, opts) {
90
113
  const tid = resolveTenant(opts);
91
- await backend.mkdir(tid, shardId, path);
92
- documentChanges.emit({ type: 'folder-create', path, tenantId: tid, shardId });
114
+ const ns = resolveNamespace();
115
+ await backend.mkdir(tid, ns, path);
116
+ documentChanges.emit({ type: 'folder-create', path, tenantId: tid, shardId: ns });
93
117
  },
94
118
  async rmdir(path, opts) {
95
119
  var _a;
@@ -103,8 +127,9 @@ export function createDocumentHandle(tenantId, shardId, backend, options) {
103
127
  }
104
128
  }
105
129
  const tid = resolveTenant(opts);
106
- await backend.rmdir(tid, shardId, path, { recursive });
107
- documentChanges.emit({ type: 'folder-delete', path, tenantId: tid, shardId });
130
+ const ns = resolveNamespace();
131
+ await backend.rmdir(tid, ns, path, { recursive });
132
+ documentChanges.emit({ type: 'folder-delete', path, tenantId: tid, shardId: ns });
108
133
  },
109
134
  async renameFolder(oldPath, newPath, opts) {
110
135
  const folderPrefix = oldPath + '/';
@@ -114,25 +139,26 @@ export function createDocumentHandle(tenantId, shardId, backend, options) {
114
139
  }
115
140
  }
116
141
  const tid = resolveTenant(opts);
117
- await backend.renameFolder(tid, shardId, oldPath, newPath);
142
+ const ns = resolveNamespace();
143
+ await backend.renameFolder(tid, ns, oldPath, newPath);
118
144
  documentChanges.emit({
119
145
  type: 'folder-rename',
120
146
  path: newPath,
121
147
  oldPath,
122
148
  tenantId: tid,
123
- shardId,
149
+ shardId: ns,
124
150
  });
125
151
  },
126
152
  async listFolders(prefix, opts) {
127
- return backend.listFolders(resolveTenant(opts), shardId, prefix !== null && prefix !== void 0 ? prefix : '');
153
+ return backend.listFolders(resolveTenant(opts), resolveNamespace(), prefix !== null && prefix !== void 0 ? prefix : '');
128
154
  },
129
155
  async exists(path) {
130
- return backend.exists(tenantId, shardId, path);
156
+ return backend.exists(resolveBoundTenant(), resolveNamespace(), path);
131
157
  },
132
158
  async status(path) {
133
159
  if (!backend.readMeta)
134
160
  throw new Error('Backend does not support status()');
135
- return backend.readMeta(tenantId, shardId, path);
161
+ return backend.readMeta(resolveBoundTenant(), resolveNamespace(), path);
136
162
  },
137
163
  async resolveConflict(path, choice) {
138
164
  if (!backend.resolve)
@@ -140,21 +166,19 @@ export function createDocumentHandle(tenantId, shardId, backend, options) {
140
166
  if (typeof choice !== 'string' && !(typeof choice === 'object' && 'origin' in choice)) {
141
167
  throw new Error('choice must be a string or { origin } object');
142
168
  }
143
- return backend.resolve(tenantId, shardId, path, choice);
169
+ return backend.resolve(resolveBoundTenant(), resolveNamespace(), path, choice);
144
170
  },
145
171
  async readBranch(path, origin) {
146
172
  if (!backend.readBranch)
147
173
  throw new Error('Backend does not support readBranch()');
148
- return backend.readBranch(tenantId, shardId, path, origin);
174
+ return backend.readBranch(resolveBoundTenant(), resolveNamespace(), path, origin);
149
175
  },
150
176
  watch(callback) {
151
177
  // Subscribe to global emitter, filtered to this handle's scope.
152
178
  const unsub = documentChanges.subscribe((change) => {
153
- if (change.tenantId !== tenantId)
154
- return;
155
- if (change.shardId !== shardId)
179
+ if (change.tenantId !== resolveBoundTenant())
156
180
  return;
157
- if (!matchesExtensions(change.path))
181
+ if (change.shardId !== resolveNamespace())
158
182
  return;
159
183
  callback(change);
160
184
  });
@@ -221,7 +245,7 @@ class AutosaveControllerImpl {
221
245
  const content = __classPrivateFieldGet(this, _AutosaveControllerImpl_pending, "f");
222
246
  __classPrivateFieldSet(this, _AutosaveControllerImpl_pending, null, "f");
223
247
  __classPrivateFieldSet(this, _AutosaveControllerImpl_dirty, false, "f");
224
- await __classPrivateFieldGet(this, _AutosaveControllerImpl_handle, "f").write(__classPrivateFieldGet(this, _AutosaveControllerImpl_path, "f"), content);
248
+ await __classPrivateFieldGet(this, _AutosaveControllerImpl_handle, "f").writeText(__classPrivateFieldGet(this, _AutosaveControllerImpl_path, "f"), content);
225
249
  }
226
250
  }
227
251
  async dispose() {
@@ -4,7 +4,7 @@ import { createDocumentHandle } from './handle';
4
4
  import { documentChanges } from './notifications';
5
5
  function harness() {
6
6
  const backend = new MemoryDocumentBackend();
7
- const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
7
+ const handle = createDocumentHandle('tenant1', 'shard1', backend);
8
8
  return { backend, handle };
9
9
  }
10
10
  describe('DocumentHandle.status()', () => {
@@ -14,7 +14,7 @@ describe('DocumentHandle.status()', () => {
14
14
  });
15
15
  it('returns DocStatus after a write', async () => {
16
16
  const { handle } = harness();
17
- await handle.write('a.txt', 'hi');
17
+ await handle.writeText('a.txt', 'hi');
18
18
  const s = await handle.status('a.txt');
19
19
  expect(s).toMatchObject({ exists: true, version: 1, syncState: 'synced' });
20
20
  });
@@ -33,7 +33,7 @@ describe('DocumentHandle.status()', () => {
33
33
  async renameFolder() { },
34
34
  async listFolders() { return []; },
35
35
  };
36
- const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
36
+ const handle = createDocumentHandle('t', 's', backend);
37
37
  await expect(handle.status('a.txt')).rejects.toThrow(/status/);
38
38
  });
39
39
  });
@@ -55,7 +55,7 @@ describe('DocumentHandle.resolveConflict()', () => {
55
55
  async renameFolder() { },
56
56
  async listFolders() { return []; },
57
57
  };
58
- const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
58
+ const handle = createDocumentHandle('tenant1', 'shard1', backend);
59
59
  await handle.resolveConflict('a.txt', 'local');
60
60
  expect(resolved).toEqual([{ t: 'tenant1', s: 'shard1', p: 'a.txt', c: 'local' }]);
61
61
  });
@@ -82,7 +82,7 @@ describe('DocumentHandle.readBranch()', () => {
82
82
  async renameFolder() { },
83
83
  async listFolders() { return []; },
84
84
  };
85
- const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
85
+ const handle = createDocumentHandle('tenant1', 'shard1', backend);
86
86
  const out = await handle.readBranch('a.txt', 'peer-1');
87
87
  expect(out).toBe('remote-content');
88
88
  expect(calls[0]).toEqual(['tenant1', 'shard1', 'a.txt', 'peer-1']);
@@ -103,7 +103,7 @@ describe('DocumentHandle.readBranch()', () => {
103
103
  async renameFolder() { },
104
104
  async listFolders() { return []; },
105
105
  };
106
- const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
106
+ const handle = createDocumentHandle('t', 's', backend);
107
107
  expect(await handle.readBranch('a.txt', 'peer-1')).toBeNull();
108
108
  });
109
109
  it('throws if the backend does not implement readBranch', async () => {
@@ -114,7 +114,7 @@ describe('DocumentHandle.readBranch()', () => {
114
114
  describe('DocumentHandle.rename', () => {
115
115
  it('moves the doc and emits one rename event', async () => {
116
116
  const { backend, handle } = harness();
117
- await handle.write('old.txt', 'hello');
117
+ await handle.writeText('old.txt', 'hello');
118
118
  const events = [];
119
119
  const unsub = (await import('./notifications')).documentChanges.subscribe((c) => events.push(c));
120
120
  await handle.rename('old.txt', 'new.txt');
@@ -131,7 +131,7 @@ describe('DocumentHandle.rename', () => {
131
131
  });
132
132
  it('throws when an autosave controller is active on oldPath', async () => {
133
133
  const { handle } = harness();
134
- await handle.write('old.txt', 'v1');
134
+ await handle.writeText('old.txt', 'v1');
135
135
  const ctrl = handle.autosave('old.txt', { debounceMs: 10000 });
136
136
  ctrl.update('v2');
137
137
  await expect(handle.rename('old.txt', 'new.txt'))
@@ -140,23 +140,15 @@ describe('DocumentHandle.rename', () => {
140
140
  });
141
141
  it('does not throw when an autosave controller exists for an unrelated path', async () => {
142
142
  const { handle } = harness();
143
- await handle.write('old.txt', 'src');
144
- await handle.write('other.txt', 'unrelated');
143
+ await handle.writeText('old.txt', 'src');
144
+ await handle.writeText('other.txt', 'unrelated');
145
145
  const ctrl = handle.autosave('other.txt', { debounceMs: 10000 });
146
146
  ctrl.update('still-unrelated');
147
147
  await expect(handle.rename('old.txt', 'new.txt')).resolves.toBeUndefined();
148
148
  await ctrl.dispose();
149
149
  });
150
- it('throws when newPath violates the handle extensions filter', async () => {
151
- const backend = new MemoryDocumentBackend();
152
- const handle = createDocumentHandle('t1', 's1', backend, {
153
- format: 'text',
154
- extensions: ['.txt'],
155
- });
156
- await handle.write('a.txt', 'hi');
157
- await expect(handle.rename('a.txt', 'a.md'))
158
- .rejects.toThrow(/extensions/);
159
- });
150
+ // v3: handle extensions filter is gone. Rename allows any path; per-format
151
+ // restrictions are now caller-side via the appropriate readX/writeX method.
160
152
  });
161
153
  describe('DocumentHandle.delete()', () => {
162
154
  it('emits a delete event when the path existed', async () => {
@@ -185,23 +177,23 @@ describe('DocumentHandle folder ops', () => {
185
177
  let handle;
186
178
  beforeEach(() => {
187
179
  backend = new MemoryDocumentBackend();
188
- handle = createDocumentHandle('t', 's', backend, { format: 'text' });
180
+ handle = createDocumentHandle('t', 's', backend);
189
181
  });
190
182
  it('mkdir forwards to backend with bound tenant/shard', async () => {
191
183
  await handle.mkdir('a');
192
184
  expect(await backend.listFolders('t', 's', '')).toEqual(['a']);
193
185
  });
194
186
  it('rmdir defaults recursive to false', async () => {
195
- await handle.write('a/x.md', 'x');
187
+ await handle.writeText('a/x.md', 'x');
196
188
  await expect(handle.rmdir('a')).rejects.toThrow();
197
189
  });
198
190
  it('rmdir({recursive:true}) cascades', async () => {
199
- await handle.write('a/x.md', 'x');
191
+ await handle.writeText('a/x.md', 'x');
200
192
  await handle.rmdir('a', { recursive: true });
201
193
  expect(await handle.list()).toEqual([]);
202
194
  });
203
195
  it('renameFolder rewrites descendant paths', async () => {
204
- await handle.write('old/x.md', 'x');
196
+ await handle.writeText('old/x.md', 'x');
205
197
  await handle.renameFolder('old', 'new');
206
198
  const docs = (await handle.list()).map((d) => d.path).sort();
207
199
  expect(docs).toEqual(['new/x.md']);
@@ -232,8 +224,8 @@ describe('DocumentHandle folder ops', () => {
232
224
  ]);
233
225
  });
234
226
  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');
227
+ await handle.writeText('a/x.md', 'x');
228
+ await handle.writeText('a/y.md', 'y');
237
229
  const events = [];
238
230
  handle.watch((c) => events.push(c));
239
231
  await handle.rmdir('a', { recursive: true });
@@ -242,7 +234,7 @@ describe('DocumentHandle folder ops', () => {
242
234
  ]);
243
235
  });
244
236
  it('renameFolder emits a single folder-rename event', async () => {
245
- await handle.write('old/x.md', 'x');
237
+ await handle.writeText('old/x.md', 'x');
246
238
  const events = [];
247
239
  handle.watch((c) => events.push(c));
248
240
  await handle.renameFolder('old', 'new');
@@ -251,3 +243,83 @@ describe('DocumentHandle folder ops', () => {
251
243
  ]);
252
244
  });
253
245
  });
246
+ describe('createDocumentHandle — namespace resolution', () => {
247
+ it('uses the shard/namespace string as the namespace root', async () => {
248
+ const backend = new MemoryDocumentBackend();
249
+ const handle = createDocumentHandle('tenant-1', 'sh3-editor', backend);
250
+ await handle.writeText('test.guml', 'content');
251
+ const keys = await backend.list('tenant-1', 'sh3-editor');
252
+ expect(keys.some((k) => k.path === 'test.guml')).toBe(true);
253
+ });
254
+ });
255
+ describe('createDocumentHandle — lazy resolvers', () => {
256
+ it('resolves namespace per operation from a function', async () => {
257
+ const { MemoryDocumentBackend } = await import('./backends');
258
+ const backend = new MemoryDocumentBackend();
259
+ let ns = 'shard-A';
260
+ const handle = createDocumentHandle('local', () => ns, backend);
261
+ await handle.writeText('foo.txt', 'first');
262
+ expect((await backend.list('local', 'shard-A')).map(d => d.path)).toContain('foo.txt');
263
+ ns = 'app-X';
264
+ await handle.writeText('bar.txt', 'second');
265
+ expect((await backend.list('local', 'app-X')).map(d => d.path)).toContain('bar.txt');
266
+ expect((await backend.list('local', 'shard-A')).map(d => d.path)).not.toContain('bar.txt');
267
+ });
268
+ it('resolves tenant per operation from a function', async () => {
269
+ const { MemoryDocumentBackend } = await import('./backends');
270
+ const backend = new MemoryDocumentBackend();
271
+ let tenant = 'alice';
272
+ const handle = createDocumentHandle(() => tenant, () => 'shard-A', backend);
273
+ await handle.writeText('p.txt', 'a');
274
+ expect((await backend.list('alice', 'shard-A')).map(d => d.path)).toContain('p.txt');
275
+ tenant = 'bob';
276
+ await handle.writeText('q.txt', 'b');
277
+ expect((await backend.list('bob', 'shard-A')).map(d => d.path)).toContain('q.txt');
278
+ });
279
+ it('watchers resolve namespace at emit time, not subscribe time', async () => {
280
+ const { MemoryDocumentBackend } = await import('./backends');
281
+ const backend = new MemoryDocumentBackend();
282
+ let ns = 'shard-A';
283
+ const handle = createDocumentHandle('local', () => ns, backend);
284
+ const seen = [];
285
+ handle.watch((c) => seen.push(`${c.type}:${c.path}`));
286
+ await handle.writeText('a.txt', 'x');
287
+ ns = 'app-X';
288
+ await handle.writeText('b.txt', 'y');
289
+ expect(seen).toContain('create:a.txt');
290
+ expect(seen).toContain('create:b.txt');
291
+ });
292
+ it('still accepts string tenant + namespace for back-compat', async () => {
293
+ const { MemoryDocumentBackend } = await import('./backends');
294
+ const backend = new MemoryDocumentBackend();
295
+ const handle = createDocumentHandle('local', 'shard-A', backend);
296
+ await handle.writeText('foo.txt', 'hi');
297
+ expect((await backend.list('local', 'shard-A')).map(d => d.path)).toContain('foo.txt');
298
+ });
299
+ });
300
+ describe('DocumentHandle — per-call format (v3)', () => {
301
+ let backend;
302
+ let handle;
303
+ beforeEach(() => {
304
+ backend = new MemoryDocumentBackend();
305
+ handle = createDocumentHandle(() => 'tenant-a', () => 'shard-a', backend);
306
+ });
307
+ it('readJson returns parsed object', async () => {
308
+ await handle.writeJson('config.json', { a: 1, b: 'two' });
309
+ expect(await handle.readJson('config.json')).toEqual({ a: 1, b: 'two' });
310
+ });
311
+ it('readJson returns null for missing doc', async () => {
312
+ expect(await handle.readJson('missing.json')).toBeNull();
313
+ });
314
+ it('readText round-trips', async () => {
315
+ await handle.writeText('note.md', 'hello');
316
+ expect(await handle.readText('note.md')).toBe('hello');
317
+ });
318
+ it('readBinary round-trips', async () => {
319
+ const data = new Uint8Array([1, 2, 3, 4]).buffer;
320
+ await handle.writeBinary('blob.bin', data);
321
+ const out = await handle.readBinary('blob.bin');
322
+ expect(out).not.toBeNull();
323
+ expect(new Uint8Array(out)).toEqual(new Uint8Array(data));
324
+ });
325
+ });
@@ -1,4 +1,4 @@
1
- export type { DocumentFormat, DocumentHandleOptions, DocumentMeta, DocumentChange, DocumentBackend, DocumentHandle, AutosaveController, ScopeOption, } from './types';
1
+ export type { DocumentMeta, DocumentChange, DocumentBackend, DocumentHandle, AutosaveController, ScopeOption, } from './types';
2
2
  export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
3
3
  export { HttpDocumentBackend } from './http-backend';
4
4
  export { createDocumentHandle } from './handle';
@@ -36,21 +36,6 @@ export declare const PERMISSION_DOCUMENTS_READ = "documents:read";
36
36
  export declare const PERMISSION_DOCUMENTS_WRITE = "documents:write";
37
37
  /** Permission to manage document mount points (admin-only). */
38
38
  export declare const PERMISSION_DOCUMENTS_MOUNT = "documents:mount";
39
- /**
40
- * Format hint for document content. Determines whether reads return a string
41
- * (`text`) or an `ArrayBuffer` (`binary`).
42
- */
43
- export type DocumentFormat = 'text' | 'binary';
44
- /**
45
- * Options passed to `ctx.documents()` to scope the handle. The handle
46
- * only operates on files matching the declared extensions (if provided).
47
- * Omitting `extensions` means "all files."
48
- */
49
- export interface DocumentHandleOptions {
50
- format: DocumentFormat;
51
- /** File extensions this handle operates on, e.g. ['.guml', '.md']. */
52
- extensions?: string[];
53
- }
54
39
  /** Metadata about a stored document. */
55
40
  export interface DocumentMeta {
56
41
  path: string;
@@ -219,12 +204,23 @@ export interface ScopeOption {
219
204
  scope?: string;
220
205
  }
221
206
  export interface DocumentHandle {
222
- /** List documents matching the handle's extensions filter. */
207
+ /** List documents stored under this handle's namespace. */
223
208
  list(opts?: ScopeOption): Promise<DocumentMeta[]>;
224
- /** Read a document by path. Returns null if not found. */
225
- read(path: string, opts?: ScopeOption): Promise<string | null>;
226
- /** Write (create or overwrite) a document. Explicit save. */
227
- write(path: string, content: string, opts?: ScopeOption): Promise<void>;
209
+ /** Read a document as text. Returns null if not found. */
210
+ readText(path: string, opts?: ScopeOption): Promise<string | null>;
211
+ /** Read a document as a binary ArrayBuffer. Returns null if not found. */
212
+ readBinary(path: string, opts?: ScopeOption): Promise<ArrayBuffer | null>;
213
+ /**
214
+ * Read a document and JSON-parse it. Returns null if not found.
215
+ * Throws if the stored content is not valid JSON.
216
+ */
217
+ readJson<T = unknown>(path: string, opts?: ScopeOption): Promise<T | null>;
218
+ /** Write a string. */
219
+ writeText(path: string, content: string, opts?: ScopeOption): Promise<void>;
220
+ /** Write a binary ArrayBuffer. */
221
+ writeBinary(path: string, content: ArrayBuffer, opts?: ScopeOption): Promise<void>;
222
+ /** JSON-stringify and write. */
223
+ writeJson(path: string, data: unknown, opts?: ScopeOption): Promise<void>;
228
224
  /** Delete a document. */
229
225
  delete(path: string, opts?: ScopeOption): Promise<void>;
230
226
  /**
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { registerShard as registerShardInternal } from './shards/activate.svelte';
1
+ import { registerShard as registerShardInternal } from './shards/lifecycle.svelte';
2
2
  import { registerApp } from './apps/registry.svelte';
3
3
  import { __setBackend } from './state/zones.svelte';
4
4
  import { setLocalOwner } from './auth/index';
package/dist/host.js CHANGED
@@ -15,8 +15,7 @@
15
15
  * import-hygiene rule is: shards and apps import from `api.ts`, the host
16
16
  * imports from `host.ts`.
17
17
  */
18
- import { registerShard as registerShardInternal, activateShard, registeredShards, } from './shards/activate.svelte';
19
- import { addAutostartShard } from './actions/state.svelte';
18
+ import { registerShard as registerShardInternal, registerAllShards, } from './shards/lifecycle.svelte';
20
19
  import { registerApp, registeredApps } from './apps/registry.svelte';
21
20
  import { launchApp, readLastApp, clearLastApp } from './apps/lifecycle';
22
21
  import { sh3coreShard } from './sh3core-shard/sh3coreShard.svelte';
@@ -84,21 +83,9 @@ export async function bootstrap(config) {
84
83
  }
85
84
  // 3. Load any packages installed in a previous session from IndexedDB
86
85
  await loadInstalledPackages();
87
- // 4. Activate every self-starting shard. Track them in the dispatcher's
88
- // autostartShards set so the `'app'` action scope treats their actions as
89
- // ambient (active even inside apps that don't list them as required).
90
- for (const [id, shard] of registeredShards) {
91
- if (shard.autostart) {
92
- addAutostartShard(id);
93
- try {
94
- await activateShard(id, { phase: 'autostart' });
95
- }
96
- catch (_a) {
97
- // Already logged + recorded in erroredShards by activateShard.
98
- // One bad self-starting shard must not prevent the sh3 from booting.
99
- }
100
- }
101
- }
86
+ // 4. v3: run register(ctx) on every registered shard. Lifecycle module
87
+ // handles error isolation; one failing shard does not block boot.
88
+ await registerAllShards();
102
89
  // 5. Read the last-active app from the user zone. If auto-launch fails,
103
90
  // clear the slot so the next reload lands on home instead of looping
104
91
  // into the same failure. No toast — the user did not initiate this.
@@ -143,44 +130,10 @@ export async function bootstrapSatellite(config) {
143
130
  }
144
131
  // 3. Load any packages installed in a previous session from IndexedDB
145
132
  await loadInstalledPackages();
146
- // 4. Autostart sweep mirror host bootstrap. Every shard with an
147
- // `autostart` hook runs in satellites too: services (LLM providers),
148
- // ambient action hosts (`__sh3core__` provides the command palette
149
- // via `sh3.palette.open`), and any user-installed self-starter.
150
- // `addAutostartShard` keeps action scoping consistent with the host
151
- // so palette/global actions remain ambient inside `'app'` scope.
152
- const autostartActivated = new Set();
153
- for (const [id, shard] of registeredShards) {
154
- if (shard.autostart) {
155
- addAutostartShard(id);
156
- try {
157
- await activateShard(id, { phase: 'autostart' });
158
- autostartActivated.add(id);
159
- }
160
- catch (_a) {
161
- // Already logged + recorded in erroredShards by activateShard.
162
- // One bad self-starting shard must not prevent the satellite from booting.
163
- }
164
- }
165
- }
166
- // 5. Activate explicit satellite shards (those carried by activateShards
167
- // in the payload — typically view-providing shards walked from the
168
- // layout). Skip ids already activated by the autostart sweep so the
169
- // diagnostic phase stays correct and we do no redundant work.
170
- for (const id of config.activateShardIds) {
171
- if (autostartActivated.has(id))
172
- continue;
173
- if (registeredShards.has(id)) {
174
- try {
175
- await activateShard(id, { phase: 'satellite' });
176
- }
177
- catch (err) {
178
- console.error(`[sh3] satellite activation of "${id}" failed:`, err);
179
- }
180
- }
181
- else {
182
- console.warn(`[sh3] satellite requested shard "${id}" but it is not registered`);
183
- }
184
- }
133
+ // 4. v3: one-pass register sweep replaces the old autostart + satellite
134
+ // passes. `config.activateShardIds` is now a no-op (the field is
135
+ // retained on the interface for back-compat; Phase 6 deletes it).
136
+ void config;
137
+ await registerAllShards();
185
138
  }
186
139
  export { installPackage, listInstalledPackages } from './registry/installer';