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.
- package/dist/__test__/fixtures.js +1 -1
- package/dist/__test__/reset.js +1 -1
- 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 +3 -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 +65 -33
- package/dist/apps/lifecycle.test.js +198 -10
- package/dist/artifact.d.ts +2 -0
- package/dist/build.js +1 -1
- 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 +69 -45
- package/dist/documents/handle.test.js +99 -27
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/types.d.ts +16 -20
- package/dist/host.d.ts +1 -1
- package/dist/host.js +9 -56
- package/dist/host.svelte.test.js +31 -63
- package/dist/layout/LayoutRenderer.svelte +1 -1
- package/dist/layout/SlotContainer.svelte +1 -0
- package/dist/layout/inspection.js +19 -14
- package/dist/layout/inspection.svelte.test.js +136 -1
- package/dist/layout/slotHostPool.svelte.d.ts +2 -1
- package/dist/layout/slotHostPool.svelte.js +6 -3
- package/dist/layout/slotHostPool.test.js +17 -0
- package/dist/layout/store.projectScope.test.js +76 -0
- package/dist/layout/store.svelte.d.ts +6 -0
- package/dist/layout/store.svelte.js +43 -13
- package/dist/layout/tree-walk.d.ts +8 -1
- package/dist/layout/tree-walk.js +11 -1
- package/dist/layout/tree-walk.test.js +53 -1
- package/dist/layout/types.d.ts +27 -0
- package/dist/layout/types.test.js +28 -0
- 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/overlays/FloatFrame.svelte +4 -1
- package/dist/overlays/float.d.ts +7 -1
- package/dist/overlays/float.js +4 -0
- package/dist/projects-shard/ProjectsSection.svelte +1 -5
- 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/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 +56 -22
- package/dist/shell-shard/shellShard.svelte.js +11 -5
- 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.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 -299
- 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 -403
- /package/dist/{shards/activate-browse.test.d.ts → actions/dispatcher-v3.test.d.ts} +0 -0
- /package/dist/{shards/activate-contributions.test.d.ts → layout/store.projectScope.test.d.ts} +0 -0
- /package/dist/shards/{activate-error-isolation.test.d.ts → lifecycle.test.d.ts} +0 -0
package/dist/documents/handle.js
CHANGED
|
@@ -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
|
|
24
|
-
*
|
|
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,
|
|
31
|
+
export function createDocumentHandle(tenantId, shardOrNamespace, backend) {
|
|
27
32
|
const controllers = new Set();
|
|
28
33
|
const unsubscribers = new Set();
|
|
29
|
-
function
|
|
30
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
|
50
|
-
const content = await backend.read(resolveTenant(opts),
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
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
|
|
70
|
+
async writeText(path, content, opts) {
|
|
58
71
|
const tid = resolveTenant(opts);
|
|
59
|
-
const
|
|
60
|
-
await backend.
|
|
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
|
|
66
|
-
await backend.
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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),
|
|
153
|
+
return backend.listFolders(resolveTenant(opts), resolveNamespace(), prefix !== null && prefix !== void 0 ? prefix : '');
|
|
128
154
|
},
|
|
129
155
|
async exists(path) {
|
|
130
|
-
return backend.exists(
|
|
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(
|
|
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(
|
|
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(
|
|
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 !==
|
|
154
|
-
return;
|
|
155
|
-
if (change.shardId !== shardId)
|
|
179
|
+
if (change.tenantId !== resolveBoundTenant())
|
|
156
180
|
return;
|
|
157
|
-
if (
|
|
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").
|
|
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
|
|
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,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 {
|
|
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
|
|
207
|
+
/** List documents stored under this handle's namespace. */
|
|
223
208
|
list(opts?: ScopeOption): Promise<DocumentMeta[]>;
|
|
224
|
-
/** Read a document
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
|
|
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/
|
|
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';
|