sh3-core 0.22.0 → 0.22.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.
- package/dist/__test__/fixtures.js +1 -1
- package/dist/__test__/reset.js +1 -3
- package/dist/__test__/smoke.test.js +2 -2
- package/dist/actions/contextMenuModel.test.js +6 -3
- package/dist/actions/ctx-actions.svelte.test.js +9 -9
- package/dist/actions/dispatcher-v3.test.js +8 -0
- package/dist/actions/dispatcher.svelte.d.ts +1 -2
- package/dist/actions/dispatcher.svelte.js +6 -7
- package/dist/actions/dispatcher.test.js +9 -12
- package/dist/actions/listActionsFromEntries.test.js +1 -2
- package/dist/actions/listActive.test.js +2 -3
- package/dist/actions/menuBarModel.test.js +1 -7
- package/dist/actions/paletteModel.test.js +1 -3
- package/dist/actions/scope-helpers.test.js +4 -4
- package/dist/actions/shardContext.test.js +2 -2
- package/dist/actions/state.svelte.d.ts +12 -2
- package/dist/actions/state.svelte.js +15 -12
- package/dist/actions/state.test.js +4 -4
- package/dist/api.d.ts +4 -3
- package/dist/api.js +1 -1
- package/dist/app/admin/adminShard.svelte.js +1 -1
- package/dist/app/store/storeShard.svelte.js +10 -5
- package/dist/app-appearance/appearanceShard.svelte.js +1 -5
- package/dist/apps/lifecycle.js +49 -64
- package/dist/apps/lifecycle.test.js +30 -76
- package/dist/conflicts/adapter-documents.js +1 -2
- package/dist/createShell.js +1 -1
- package/dist/documents/handle.d.ts +9 -4
- package/dist/documents/handle.js +40 -29
- package/dist/documents/handle.test.js +60 -51
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/types.d.ts +16 -26
- package/dist/host.d.ts +1 -1
- package/dist/host.js +9 -56
- package/dist/host.svelte.test.js +31 -63
- package/dist/layouts-shard/LayoutsSection.svelte +1 -1
- package/dist/layouts-shard/layoutsShard.svelte.js +2 -5
- package/dist/layouts-shard/layoutsShard.svelte.test.js +2 -2
- package/dist/projects-shard/projectsShard.svelte.js +1 -5
- package/dist/registry/installer.js +1 -1
- package/dist/registry/loader.d.ts +1 -1
- package/dist/registry/loader.js +3 -3
- package/dist/registry/permission-descriptions.test.js +2 -2
- package/dist/registry/register.js +1 -1
- package/dist/registry/register.test.js +1 -1
- package/dist/runtime/runVerb-shell.test.js +1 -1
- package/dist/runtime/runVerb.js +2 -2
- package/dist/runtime/runVerb.test.js +9 -9
- package/dist/server-shard/types.d.ts +56 -0
- package/dist/sh3Api/headless.js +1 -1
- package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -6
- package/dist/shards/ctx-fetch.test.js +9 -9
- package/dist/shards/lifecycle.svelte.d.ts +108 -0
- package/dist/shards/lifecycle.svelte.js +551 -0
- package/dist/shards/lifecycle.test.js +139 -0
- package/dist/shards/types.d.ts +30 -63
- package/dist/shell-shard/shellShard.svelte.js +1 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/shards/activate-browse.test.js +0 -120
- package/dist/shards/activate-contributions.test.js +0 -141
- package/dist/shards/activate-error-isolation.test.d.ts +0 -1
- package/dist/shards/activate-error-isolation.test.js +0 -98
- package/dist/shards/activate-fields.svelte.test.d.ts +0 -1
- package/dist/shards/activate-fields.svelte.test.js +0 -121
- package/dist/shards/activate-on-key-revoked.test.d.ts +0 -1
- package/dist/shards/activate-on-key-revoked.test.js +0 -60
- package/dist/shards/activate-runtime.test.d.ts +0 -1
- package/dist/shards/activate-runtime.test.js +0 -344
- package/dist/shards/activate-scopeid.test.d.ts +0 -1
- package/dist/shards/activate-scopeid.test.js +0 -21
- package/dist/shards/activate.svelte.d.ts +0 -102
- package/dist/shards/activate.svelte.js +0 -407
- package/dist/shards/app-binding.svelte.d.ts +0 -8
- package/dist/shards/app-binding.svelte.js +0 -30
- package/dist/shards/app-binding.test.d.ts +0 -1
- package/dist/shards/app-binding.test.js +0 -25
- /package/dist/{shards/activate-browse.test.d.ts → actions/dispatcher-v3.test.d.ts} +0 -0
- /package/dist/shards/{activate-contributions.test.d.ts → lifecycle.test.d.ts} +0 -0
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
144
|
-
await handle.
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
236
|
-
await handle.
|
|
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.
|
|
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,20 +243,11 @@ describe('DocumentHandle folder ops', () => {
|
|
|
251
243
|
]);
|
|
252
244
|
});
|
|
253
245
|
});
|
|
254
|
-
describe('createDocumentHandle —
|
|
255
|
-
it('uses
|
|
256
|
-
const backend = new MemoryDocumentBackend();
|
|
257
|
-
const handle = createDocumentHandle('tenant-1', 'sh3-editor', backend, { format: 'text', appId: 'guml-ide' });
|
|
258
|
-
await handle.write('test.guml', 'content');
|
|
259
|
-
const keysAppId = await backend.list('tenant-1', 'guml-ide');
|
|
260
|
-
expect(keysAppId.some((k) => k.path === 'test.guml')).toBe(true);
|
|
261
|
-
const keysShardId = await backend.list('tenant-1', 'sh3-editor');
|
|
262
|
-
expect(keysShardId).toHaveLength(0);
|
|
263
|
-
});
|
|
264
|
-
it('falls back to shardId when appId is not provided', async () => {
|
|
246
|
+
describe('createDocumentHandle — namespace resolution', () => {
|
|
247
|
+
it('uses the shard/namespace string as the namespace root', async () => {
|
|
265
248
|
const backend = new MemoryDocumentBackend();
|
|
266
|
-
const handle = createDocumentHandle('tenant-1', 'sh3-editor', backend
|
|
267
|
-
await handle.
|
|
249
|
+
const handle = createDocumentHandle('tenant-1', 'sh3-editor', backend);
|
|
250
|
+
await handle.writeText('test.guml', 'content');
|
|
268
251
|
const keys = await backend.list('tenant-1', 'sh3-editor');
|
|
269
252
|
expect(keys.some((k) => k.path === 'test.guml')).toBe(true);
|
|
270
253
|
});
|
|
@@ -274,11 +257,11 @@ describe('createDocumentHandle — lazy resolvers', () => {
|
|
|
274
257
|
const { MemoryDocumentBackend } = await import('./backends');
|
|
275
258
|
const backend = new MemoryDocumentBackend();
|
|
276
259
|
let ns = 'shard-A';
|
|
277
|
-
const handle = createDocumentHandle('local', () => ns, backend
|
|
278
|
-
await handle.
|
|
260
|
+
const handle = createDocumentHandle('local', () => ns, backend);
|
|
261
|
+
await handle.writeText('foo.txt', 'first');
|
|
279
262
|
expect((await backend.list('local', 'shard-A')).map(d => d.path)).toContain('foo.txt');
|
|
280
263
|
ns = 'app-X';
|
|
281
|
-
await handle.
|
|
264
|
+
await handle.writeText('bar.txt', 'second');
|
|
282
265
|
expect((await backend.list('local', 'app-X')).map(d => d.path)).toContain('bar.txt');
|
|
283
266
|
expect((await backend.list('local', 'shard-A')).map(d => d.path)).not.toContain('bar.txt');
|
|
284
267
|
});
|
|
@@ -286,31 +269,57 @@ describe('createDocumentHandle — lazy resolvers', () => {
|
|
|
286
269
|
const { MemoryDocumentBackend } = await import('./backends');
|
|
287
270
|
const backend = new MemoryDocumentBackend();
|
|
288
271
|
let tenant = 'alice';
|
|
289
|
-
const handle = createDocumentHandle(() => tenant, () => 'shard-A', backend
|
|
290
|
-
await handle.
|
|
272
|
+
const handle = createDocumentHandle(() => tenant, () => 'shard-A', backend);
|
|
273
|
+
await handle.writeText('p.txt', 'a');
|
|
291
274
|
expect((await backend.list('alice', 'shard-A')).map(d => d.path)).toContain('p.txt');
|
|
292
275
|
tenant = 'bob';
|
|
293
|
-
await handle.
|
|
276
|
+
await handle.writeText('q.txt', 'b');
|
|
294
277
|
expect((await backend.list('bob', 'shard-A')).map(d => d.path)).toContain('q.txt');
|
|
295
278
|
});
|
|
296
279
|
it('watchers resolve namespace at emit time, not subscribe time', async () => {
|
|
297
280
|
const { MemoryDocumentBackend } = await import('./backends');
|
|
298
281
|
const backend = new MemoryDocumentBackend();
|
|
299
282
|
let ns = 'shard-A';
|
|
300
|
-
const handle = createDocumentHandle('local', () => ns, backend
|
|
283
|
+
const handle = createDocumentHandle('local', () => ns, backend);
|
|
301
284
|
const seen = [];
|
|
302
285
|
handle.watch((c) => seen.push(`${c.type}:${c.path}`));
|
|
303
|
-
await handle.
|
|
286
|
+
await handle.writeText('a.txt', 'x');
|
|
304
287
|
ns = 'app-X';
|
|
305
|
-
await handle.
|
|
288
|
+
await handle.writeText('b.txt', 'y');
|
|
306
289
|
expect(seen).toContain('create:a.txt');
|
|
307
290
|
expect(seen).toContain('create:b.txt');
|
|
308
291
|
});
|
|
309
292
|
it('still accepts string tenant + namespace for back-compat', async () => {
|
|
310
293
|
const { MemoryDocumentBackend } = await import('./backends');
|
|
311
294
|
const backend = new MemoryDocumentBackend();
|
|
312
|
-
const handle = createDocumentHandle('local', 'shard-A', backend
|
|
313
|
-
await handle.
|
|
295
|
+
const handle = createDocumentHandle('local', 'shard-A', backend);
|
|
296
|
+
await handle.writeText('foo.txt', 'hi');
|
|
314
297
|
expect((await backend.list('local', 'shard-A')).map(d => d.path)).toContain('foo.txt');
|
|
315
298
|
});
|
|
316
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 {
|
|
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,27 +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
|
-
* When set, this handle's namespace root becomes `{scope}/docs/{appId}/`
|
|
55
|
-
* instead of `{scope}/docs/{shardId}/`. Use in `onAppActivate` to get
|
|
56
|
-
* a handle scoped to the app's shared document namespace.
|
|
57
|
-
*/
|
|
58
|
-
appId?: string;
|
|
59
|
-
}
|
|
60
39
|
/** Metadata about a stored document. */
|
|
61
40
|
export interface DocumentMeta {
|
|
62
41
|
path: string;
|
|
@@ -225,12 +204,23 @@ export interface ScopeOption {
|
|
|
225
204
|
scope?: string;
|
|
226
205
|
}
|
|
227
206
|
export interface DocumentHandle {
|
|
228
|
-
/** List documents
|
|
207
|
+
/** List documents stored under this handle's namespace. */
|
|
229
208
|
list(opts?: ScopeOption): Promise<DocumentMeta[]>;
|
|
230
|
-
/** Read a document
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
|
|
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>;
|
|
234
224
|
/** Delete a document. */
|
|
235
225
|
delete(path: string, opts?: ScopeOption): Promise<void>;
|
|
236
226
|
/**
|
package/dist/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { registerShard as registerShardInternal } from './shards/
|
|
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,
|
|
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.
|
|
88
|
-
//
|
|
89
|
-
|
|
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.
|
|
147
|
-
// `
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
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';
|
package/dist/host.svelte.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
2
2
|
import { resetFramework } from './__test__/reset';
|
|
3
3
|
import { makeShard, makeShardManifest } from './__test__/fixtures';
|
|
4
4
|
import { bootstrapSatellite } from './host';
|
|
5
|
-
import { registerShard, activeShards, erroredShards, } from './shards/
|
|
5
|
+
import { registerShard, activeShards, erroredShards, } from './shards/lifecycle.svelte';
|
|
6
6
|
import { getLiveDispatcherState } from './actions/state.svelte';
|
|
7
7
|
import { listActions } from './actions/registry';
|
|
8
8
|
// loadInstalledPackages reads IndexedDB; neutralize it so the satellite
|
|
@@ -11,82 +11,50 @@ vi.mock('./registry/installer', async (orig) => {
|
|
|
11
11
|
const real = await orig();
|
|
12
12
|
return Object.assign(Object.assign({}, real), { loadInstalledPackages: vi.fn(async () => { }) });
|
|
13
13
|
});
|
|
14
|
-
describe('bootstrapSatellite —
|
|
14
|
+
describe('bootstrapSatellite — v3 register sweep', () => {
|
|
15
15
|
beforeEach(resetFramework);
|
|
16
|
-
it('
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
manifest: makeShardManifest({ id: 'svc-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
it('runs register() on every registered shard regardless of activateShardIds', async () => {
|
|
17
|
+
const registerA = vi.fn();
|
|
18
|
+
const registerB = vi.fn();
|
|
19
|
+
registerShard(makeShard({
|
|
20
|
+
manifest: makeShardManifest({ id: 'svc-a' }),
|
|
21
|
+
register: registerA,
|
|
22
|
+
}));
|
|
23
|
+
registerShard(makeShard({
|
|
24
|
+
manifest: makeShardManifest({ id: 'svc-b' }),
|
|
25
|
+
register: registerB,
|
|
26
|
+
}));
|
|
25
27
|
await bootstrapSatellite({ activateShardIds: [] });
|
|
26
|
-
expect(
|
|
27
|
-
expect(
|
|
28
|
-
expect(activeShards.has('svc-
|
|
28
|
+
expect(registerA).toHaveBeenCalledTimes(1);
|
|
29
|
+
expect(registerB).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(activeShards.has('svc-a')).toBe(true);
|
|
31
|
+
expect(activeShards.has('svc-b')).toBe(true);
|
|
29
32
|
});
|
|
30
|
-
it('
|
|
31
|
-
const svc = makeShard({
|
|
32
|
-
manifest: makeShardManifest({ id: 'svc-llm' }),
|
|
33
|
-
autostart: () => { },
|
|
34
|
-
});
|
|
35
|
-
registerShard(svc);
|
|
36
|
-
await bootstrapSatellite({ activateShardIds: [] });
|
|
37
|
-
expect(getLiveDispatcherState().autostartShards.has('svc-llm')).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
it('records a throwing autostart shard with phase "autostart" and continues activating the rest', async () => {
|
|
33
|
+
it('records a throwing register with phase "register" and continues activating the rest', async () => {
|
|
40
34
|
var _a;
|
|
41
|
-
const
|
|
42
|
-
|
|
35
|
+
const goodRegister = vi.fn();
|
|
36
|
+
registerShard(makeShard({
|
|
43
37
|
manifest: makeShardManifest({ id: 'svc-bad' }),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
autostart: () => { },
|
|
48
|
-
});
|
|
49
|
-
const good = makeShard({
|
|
38
|
+
register: () => { throw new Error('boom'); },
|
|
39
|
+
}));
|
|
40
|
+
registerShard(makeShard({
|
|
50
41
|
manifest: makeShardManifest({ id: 'svc-good' }),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
registerShard(bad);
|
|
55
|
-
registerShard(good);
|
|
42
|
+
register: goodRegister,
|
|
43
|
+
}));
|
|
56
44
|
await bootstrapSatellite({ activateShardIds: [] });
|
|
57
|
-
expect((_a = erroredShards.get('svc-bad')) === null || _a === void 0 ? void 0 : _a.phase).toBe('
|
|
45
|
+
expect((_a = erroredShards.get('svc-bad')) === null || _a === void 0 ? void 0 : _a.phase).toBe('register');
|
|
58
46
|
expect(activeShards.has('svc-bad')).toBe(false);
|
|
59
|
-
expect(
|
|
47
|
+
expect(goodRegister).toHaveBeenCalledTimes(1);
|
|
60
48
|
expect(activeShards.has('svc-good')).toBe(true);
|
|
61
49
|
});
|
|
62
|
-
it('
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
activate,
|
|
67
|
-
autostart: () => { },
|
|
68
|
-
});
|
|
69
|
-
registerShard(svc);
|
|
70
|
-
await bootstrapSatellite({ activateShardIds: ['svc-llm'] });
|
|
71
|
-
expect(activate).toHaveBeenCalledTimes(1);
|
|
50
|
+
it('exposes getLiveDispatcherState without autostartShards', async () => {
|
|
51
|
+
await bootstrapSatellite({ activateShardIds: [] });
|
|
52
|
+
const state = getLiveDispatcherState();
|
|
53
|
+
expect('autostartShards' in state).toBe(false);
|
|
72
54
|
});
|
|
73
55
|
it('registers sh3.palette.open after bootstrapSatellite completes (palette reachable)', async () => {
|
|
74
56
|
await bootstrapSatellite({ activateShardIds: [] });
|
|
75
57
|
const ids = listActions().map((entry) => entry.action.id);
|
|
76
58
|
expect(ids).toContain('sh3.palette.open');
|
|
77
59
|
});
|
|
78
|
-
it('still activates non-autostart shards passed in activateShardIds with phase "satellite"', async () => {
|
|
79
|
-
var _a;
|
|
80
|
-
const activate = vi.fn(() => {
|
|
81
|
-
throw new Error('explicit-fail');
|
|
82
|
-
});
|
|
83
|
-
const view = makeShard({
|
|
84
|
-
manifest: makeShardManifest({ id: 'view-only' }),
|
|
85
|
-
activate,
|
|
86
|
-
});
|
|
87
|
-
registerShard(view);
|
|
88
|
-
await bootstrapSatellite({ activateShardIds: ['view-only'] });
|
|
89
|
-
expect(activate).toHaveBeenCalledTimes(1);
|
|
90
|
-
expect((_a = erroredShards.get('view-only')) === null || _a === void 0 ? void 0 : _a.phase).toBe('satellite');
|
|
91
|
-
});
|
|
92
60
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { getLayouts } from './layoutsState.svelte';
|
|
9
9
|
import { restoreToFloat } from './layoutsApi';
|
|
10
|
-
import { listStandaloneViews } from '../shards/
|
|
10
|
+
import { listStandaloneViews } from '../shards/lifecycle.svelte';
|
|
11
11
|
import { toastManager } from '../overlays/toast';
|
|
12
12
|
import { sh3 } from '../sh3Runtime.svelte';
|
|
13
13
|
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* submenu and the home-page card grid.
|
|
9
9
|
*/
|
|
10
10
|
import { VERSION } from '../version';
|
|
11
|
-
import { listStandaloneViews } from '../shards/
|
|
11
|
+
import { listStandaloneViews } from '../shards/lifecycle.svelte';
|
|
12
12
|
import { getSelection } from '../actions/selection.svelte';
|
|
13
13
|
import { modalManager } from '../overlays/modal';
|
|
14
14
|
import { toastManager } from '../overlays/toast';
|
|
@@ -137,7 +137,7 @@ export const layoutsShard = {
|
|
|
137
137
|
version: VERSION,
|
|
138
138
|
views: [],
|
|
139
139
|
},
|
|
140
|
-
|
|
140
|
+
register(ctx) {
|
|
141
141
|
const zone = ctx.state({
|
|
142
142
|
user: { layouts: [] },
|
|
143
143
|
});
|
|
@@ -222,9 +222,6 @@ export const layoutsShard = {
|
|
|
222
222
|
});
|
|
223
223
|
});
|
|
224
224
|
},
|
|
225
|
-
autostart() {
|
|
226
|
-
/* self-start so the action is available before any app launches. */
|
|
227
|
-
},
|
|
228
225
|
deactivate() {
|
|
229
226
|
__unbindZone();
|
|
230
227
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { registerShard, activateShard } from '../shards/
|
|
2
|
+
import { registerShard, activateShard } from '../shards/lifecycle.svelte';
|
|
3
3
|
import { addAutostartShard } from '../actions/state.svelte';
|
|
4
4
|
import { floatManager } from '../overlays/float';
|
|
5
5
|
import { layoutsShard } from './layoutsShard.svelte';
|
|
@@ -15,7 +15,7 @@ const stubShard = {
|
|
|
15
15
|
version: '0.0.0',
|
|
16
16
|
views: [{ id: 'shell:terminal', label: 'Sh3', standalone: true }],
|
|
17
17
|
},
|
|
18
|
-
|
|
18
|
+
register(ctx) {
|
|
19
19
|
ctx.registerView('shell:terminal', {
|
|
20
20
|
mount: () => ({ unmount() { } }),
|
|
21
21
|
});
|
|
@@ -56,7 +56,7 @@ export const projectsShard = {
|
|
|
56
56
|
version: VERSION,
|
|
57
57
|
views: [{ id: PROJECTS_MANAGE_VIEW, label: 'Project Manager' }],
|
|
58
58
|
},
|
|
59
|
-
|
|
59
|
+
register(ctx) {
|
|
60
60
|
void refreshProjects();
|
|
61
61
|
if (typeof document !== 'undefined') {
|
|
62
62
|
document.addEventListener('visibilitychange', () => {
|
|
@@ -141,8 +141,4 @@ export const projectsShard = {
|
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
},
|
|
144
|
-
autostart() {
|
|
145
|
-
/* register on the self-starting path so the project list is available
|
|
146
|
-
on the home screen without an app launch. */
|
|
147
|
-
},
|
|
148
144
|
};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { loadBundleModule } from './loader';
|
|
18
18
|
import { savePackage, loadBundle, listInstalled, removePackage } from './storage';
|
|
19
|
-
import { deactivateShard } from '../shards/
|
|
19
|
+
import { deactivateShard } from '../shards/lifecycle.svelte';
|
|
20
20
|
import { unregisterApp } from '../apps/lifecycle';
|
|
21
21
|
import { registerLoadedBundle } from './register';
|
|
22
22
|
import { extractBundlePermissions } from './permission-descriptions';
|
|
@@ -37,7 +37,7 @@ export declare function loadBundleModule(bytes: ArrayBuffer): Promise<LoadedBund
|
|
|
37
37
|
/**
|
|
38
38
|
* Type guard: returns true if the loaded module is a shard.
|
|
39
39
|
*
|
|
40
|
-
* A shard has
|
|
40
|
+
* A shard has a `register` function and `manifest.views` array. An app
|
|
41
41
|
* has neither — it has `initialLayout` and `manifest.requiredShards`.
|
|
42
42
|
*/
|
|
43
43
|
export declare function isShard(mod: Shard | App): mod is Shard;
|
package/dist/registry/loader.js
CHANGED
|
@@ -125,12 +125,12 @@ export async function loadBundleModule(bytes) {
|
|
|
125
125
|
/**
|
|
126
126
|
* Type guard: returns true if the loaded module is a shard.
|
|
127
127
|
*
|
|
128
|
-
* A shard has
|
|
128
|
+
* A shard has a `register` function and `manifest.views` array. An app
|
|
129
129
|
* has neither — it has `initialLayout` and `manifest.requiredShards`.
|
|
130
130
|
*/
|
|
131
131
|
export function isShard(mod) {
|
|
132
|
-
return ('
|
|
133
|
-
typeof mod.
|
|
132
|
+
return ('register' in mod &&
|
|
133
|
+
typeof mod.register === 'function' &&
|
|
134
134
|
Array.isArray(mod.manifest.views));
|
|
135
135
|
}
|
|
136
136
|
/**
|