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
|
@@ -49,7 +49,7 @@ export const appearanceShard = {
|
|
|
49
49
|
version: VERSION,
|
|
50
50
|
views: [],
|
|
51
51
|
},
|
|
52
|
-
|
|
52
|
+
register(ctx) {
|
|
53
53
|
const zone = ctx.state({
|
|
54
54
|
user: { overrides: {} },
|
|
55
55
|
});
|
|
@@ -64,10 +64,6 @@ export const appearanceShard = {
|
|
|
64
64
|
};
|
|
65
65
|
ctx.actions.register(customize);
|
|
66
66
|
},
|
|
67
|
-
autostart() {
|
|
68
|
-
// Self-start so the `app.customize` action is registered before the
|
|
69
|
-
// user right-clicks a home card. No imperative work required.
|
|
70
|
-
},
|
|
71
67
|
deactivate() {
|
|
72
68
|
__unbindZone();
|
|
73
69
|
},
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createStateZones } from '../state/zones.svelte';
|
|
15
15
|
import { createGestureRegistry } from '../gestures';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { registeredShards, } from '../shards/lifecycle.svelte';
|
|
17
|
+
import { shardEntries, runAppActivate, runAppDeactivate, registerAllShards, erroredShards } from '../shards/lifecycle.svelte';
|
|
18
18
|
import { attachApp, acquireAppSlotHolds, detachApp, switchToApp, switchToHome, getActiveAppTree, } from '../layout/store.svelte';
|
|
19
19
|
import { activeApp, breadcrumbApp, getRegisteredApp, registeredApps } from './registry.svelte';
|
|
20
20
|
import { createZoneManager } from '../state/manage';
|
|
@@ -24,9 +24,8 @@ import { clearSelectionUnconditional } from '../actions/selection.svelte';
|
|
|
24
24
|
import { loadUserBindings } from '../actions/bindings-store';
|
|
25
25
|
import { toastManager } from '../overlays/toast';
|
|
26
26
|
import { clearAppNavEntries } from '../navigation/back-stack';
|
|
27
|
-
import { getActiveScopeId
|
|
27
|
+
import { getActiveScopeId } from '../documents/config';
|
|
28
28
|
import { sessionState } from '../projects/session-state.svelte';
|
|
29
|
-
import { createDocumentHandle } from '../documents/handle';
|
|
30
29
|
import { collectRestoredSlots } from '../layout/tree-walk';
|
|
31
30
|
// ---------- last-active-app user zone ------------------------------------
|
|
32
31
|
/**
|
|
@@ -98,7 +97,7 @@ function getOrCreateAppContext(appId, scopeId, args) {
|
|
|
98
97
|
* @throws If the app is not registered or a required shard is not registered.
|
|
99
98
|
*/
|
|
100
99
|
export async function launchApp(id, opts = {}) {
|
|
101
|
-
var _a, _b, _c, _d, _e, _f, _g
|
|
100
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
102
101
|
const app = getRegisteredApp(id);
|
|
103
102
|
if (!app) {
|
|
104
103
|
throw new Error(`Cannot launch app "${id}": not registered`);
|
|
@@ -113,18 +112,17 @@ export async function launchApp(id, opts = {}) {
|
|
|
113
112
|
else if (activeApp.id === id) {
|
|
114
113
|
// Re-entering the same app from Home — fire resume hooks.
|
|
115
114
|
for (const shardId of app.manifest.requiredShards) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
void ((_a = shard.resume) === null || _a === void 0 ? void 0 : _a.call(shard, shardCtx));
|
|
115
|
+
const entry = shardEntries.get(shardId);
|
|
116
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.shard.resume) && entry.ctx)
|
|
117
|
+
void entry.shard.resume(entry.ctx);
|
|
120
118
|
}
|
|
121
|
-
void ((
|
|
119
|
+
void ((_a = app.resume) === null || _a === void 0 ? void 0 : _a.call(app, getOrCreateAppContext(id)));
|
|
122
120
|
switchToApp();
|
|
123
|
-
void ((
|
|
121
|
+
void ((_b = app.onAppReady) === null || _b === void 0 ? void 0 : _b.call(app, getOrCreateAppContext(id)));
|
|
124
122
|
if (!opts.skipLastApp)
|
|
125
123
|
writeLastApp(id);
|
|
126
124
|
breadcrumbApp.id = id;
|
|
127
|
-
setActiveApp(id, new Set((
|
|
125
|
+
setActiveApp(id, new Set((_c = app.manifest.requiredShards) !== null && _c !== void 0 ? _c : []));
|
|
128
126
|
void loadUserBindings(id).then(setUserBindings);
|
|
129
127
|
return;
|
|
130
128
|
}
|
|
@@ -144,19 +142,34 @@ export async function launchApp(id, opts = {}) {
|
|
|
144
142
|
// detach to keep the preset manager state consistent.
|
|
145
143
|
attachApp(app);
|
|
146
144
|
try {
|
|
145
|
+
// v3: ensure every registered shard has been run through register().
|
|
146
|
+
// In production, host.bootstrap calls registerAllShards once at boot.
|
|
147
|
+
// In tests that skip bootstrap, this catch-up makes launchApp self-sufficient.
|
|
148
|
+
await registerAllShards();
|
|
149
|
+
// Surface register-time failures for required shards as a launch error
|
|
150
|
+
// (mirrors v2 activate failure semantics for missing factories / thrown register).
|
|
147
151
|
for (const shardId of app.manifest.requiredShards) {
|
|
148
|
-
|
|
152
|
+
const err = erroredShards.get(shardId);
|
|
153
|
+
if (err)
|
|
154
|
+
throw err.error;
|
|
149
155
|
}
|
|
156
|
+
// runAppActivate rotates the doc namespace binding AND fires
|
|
157
|
+
// onAppActivate. No per-shard activate() pass, no separate binding.
|
|
150
158
|
for (const shardId of app.manifest.requiredShards) {
|
|
151
|
-
|
|
152
|
-
if (!(shard === null || shard === void 0 ? void 0 : shard.autostart))
|
|
153
|
-
bindShardToApp(shardId, id);
|
|
159
|
+
await runAppActivate(shardId, id);
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
catch (err) {
|
|
163
|
+
// Roll back any partial onAppActivate calls.
|
|
164
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
165
|
+
try {
|
|
166
|
+
await runAppDeactivate(shardId, id);
|
|
167
|
+
}
|
|
168
|
+
catch ( /* swallow */_h) { /* swallow */ }
|
|
169
|
+
}
|
|
157
170
|
detachApp();
|
|
158
171
|
try {
|
|
159
|
-
toastManager.notify(`Couldn't launch "${(
|
|
172
|
+
toastManager.notify(`Couldn't launch "${(_d = app.manifest.label) !== null && _d !== void 0 ? _d : id}": ${err instanceof Error ? err.message : String(err)}`, { level: 'error', duration: 6000 });
|
|
160
173
|
}
|
|
161
174
|
catch (_j) {
|
|
162
175
|
// Toast layer not mounted (e.g. early boot, tests without Sh3).
|
|
@@ -164,47 +177,33 @@ export async function launchApp(id, opts = {}) {
|
|
|
164
177
|
}
|
|
165
178
|
throw err;
|
|
166
179
|
}
|
|
167
|
-
// Notify shards that an app is activating so they can set up app-scoped resources.
|
|
168
|
-
const appActivateCtx = {
|
|
169
|
-
appId: id,
|
|
170
|
-
documents(options) {
|
|
171
|
-
return createDocumentHandle(getActiveScopeId(), id, getDocumentBackend(), Object.assign(Object.assign({ format: 'text' }, options), { appId: id }));
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
for (const shardId of app.manifest.requiredShards) {
|
|
175
|
-
const shard = registeredShards.get(shardId);
|
|
176
|
-
const shardCtx = getShardContext(shardId);
|
|
177
|
-
if ((shard === null || shard === void 0 ? void 0 : shard.onAppActivate) && shardCtx) {
|
|
178
|
-
await shard.onAppActivate(id, appActivateCtx);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
180
|
// Collect the layout's restored slots for the restore hooks.
|
|
182
181
|
const tree = getActiveAppTree();
|
|
183
182
|
const restoredSlots = tree ? collectRestoredSlots(tree) : [];
|
|
184
183
|
// Await onLayoutWillRestore so async slot-contribution registrations
|
|
185
184
|
// complete before acquireAppSlotHolds triggers view factory mount.
|
|
186
185
|
for (const shardId of app.manifest.requiredShards) {
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
await shard.onLayoutWillRestore(restoredSlots);
|
|
186
|
+
const entry = shardEntries.get(shardId);
|
|
187
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.shard.onLayoutWillRestore) && entry.ctx) {
|
|
188
|
+
await entry.shard.onLayoutWillRestore(entry.ctx, restoredSlots);
|
|
190
189
|
}
|
|
191
190
|
}
|
|
192
191
|
// Shards have registered their view factories — safe to take the
|
|
193
192
|
// refcount holds on the app's slots now (pool's factory lookup
|
|
194
193
|
// happens in a microtask from this call).
|
|
195
194
|
acquireAppSlotHolds();
|
|
196
|
-
void ((
|
|
195
|
+
void ((_e = app.activate) === null || _e === void 0 ? void 0 : _e.call(app, getOrCreateAppContext(id, undefined, opts.args)));
|
|
197
196
|
activeApp.id = id;
|
|
198
|
-
setActiveApp(id, new Set((
|
|
197
|
+
setActiveApp(id, new Set((_f = app.manifest.requiredShards) !== null && _f !== void 0 ? _f : []));
|
|
199
198
|
void loadUserBindings(id).then(setUserBindings);
|
|
200
199
|
switchToApp();
|
|
201
200
|
for (const shardId of app.manifest.requiredShards) {
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
204
|
-
shard.onLayoutRestored(restoredSlots);
|
|
201
|
+
const entry = shardEntries.get(shardId);
|
|
202
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.shard.onLayoutRestored) && entry.ctx) {
|
|
203
|
+
entry.shard.onLayoutRestored(entry.ctx, restoredSlots);
|
|
205
204
|
}
|
|
206
205
|
}
|
|
207
|
-
void ((
|
|
206
|
+
void ((_g = app.onAppReady) === null || _g === void 0 ? void 0 : _g.call(app, getOrCreateAppContext(id)));
|
|
208
207
|
if (!opts.skipLastApp)
|
|
209
208
|
writeLastApp(id);
|
|
210
209
|
breadcrumbApp.id = id;
|
|
@@ -228,12 +227,11 @@ export function unloadApp(id, skipSwitchToHome = false) {
|
|
|
228
227
|
if (!app)
|
|
229
228
|
return;
|
|
230
229
|
void ((_a = app.deactivate) === null || _a === void 0 ? void 0 : _a.call(app));
|
|
231
|
-
//
|
|
230
|
+
// v3: call runAppDeactivate for every required shard. The shard stays
|
|
231
|
+
// active (its register() output is intact); only its per-app bindings
|
|
232
|
+
// and per-app contribution registrations are torn down.
|
|
232
233
|
for (const shardId of app.manifest.requiredShards) {
|
|
233
|
-
|
|
234
|
-
if (shard === null || shard === void 0 ? void 0 : shard.onAppDeactivate) {
|
|
235
|
-
void shard.onAppDeactivate(id);
|
|
236
|
-
}
|
|
234
|
+
void runAppDeactivate(shardId, id);
|
|
237
235
|
}
|
|
238
236
|
// Detach layout (releases the refcount holds; pool cleanup runs on
|
|
239
237
|
// the next microtask for any slots that no longer have a renderer).
|
|
@@ -242,22 +240,6 @@ export function unloadApp(id, skipSwitchToHome = false) {
|
|
|
242
240
|
if (!skipSwitchToHome)
|
|
243
241
|
switchToHome();
|
|
244
242
|
detachApp();
|
|
245
|
-
// Deactivate this app's required shards IF no other consumer needs
|
|
246
|
-
// them. Phase 8 has at most one app active at a time, so "no other
|
|
247
|
-
// consumer" reduces to "not self-starting AND not required by any
|
|
248
|
-
// other registered app that happens to already be active" — but we
|
|
249
|
-
// don't run multiple apps, so the only survivors are self-starters.
|
|
250
|
-
// The simple rule: deactivate a required shard unless it was
|
|
251
|
-
// self-starting (has an `autostart` field defined).
|
|
252
|
-
for (const shardId of app.manifest.requiredShards) {
|
|
253
|
-
const shard = registeredShards.get(shardId);
|
|
254
|
-
if (!shard)
|
|
255
|
-
continue;
|
|
256
|
-
clearShardBinding(shardId);
|
|
257
|
-
if (shard.autostart)
|
|
258
|
-
continue; // self-starter stays running
|
|
259
|
-
deactivateShard(shardId);
|
|
260
|
-
}
|
|
261
243
|
activeApp.id = null;
|
|
262
244
|
setActiveApp(null, new Set());
|
|
263
245
|
clearSelectionUnconditional();
|
|
@@ -304,9 +286,12 @@ export async function returnToHome() {
|
|
|
304
286
|
const app = activeApp.id ? getRegisteredApp(activeApp.id) : null;
|
|
305
287
|
if (app) {
|
|
306
288
|
for (const shardId of app.manifest.requiredShards) {
|
|
307
|
-
const
|
|
308
|
-
if ((
|
|
309
|
-
|
|
289
|
+
const entry = shardEntries.get(shardId);
|
|
290
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.shard.suspend) && entry.ctx) {
|
|
291
|
+
const result = await entry.shard.suspend(entry.ctx);
|
|
292
|
+
if (result === false)
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
310
295
|
}
|
|
311
296
|
if (app.suspend && (await app.suspend()) === false)
|
|
312
297
|
return false;
|
|
@@ -3,7 +3,7 @@ import { resetFramework } from '../__test__/reset';
|
|
|
3
3
|
import { makeApp, makeShard, makeAppManifest, makeShardManifest, makeTabsNode, makeTabEntry, makeSlotNode, makeTree, } from '../__test__/fixtures';
|
|
4
4
|
import { launchApp, returnToHome, unregisterApp } from './lifecycle';
|
|
5
5
|
import { registerApp } from './registry.svelte';
|
|
6
|
-
import { registerShard } from '../shards/
|
|
6
|
+
import { registerShard } from '../shards/lifecycle.svelte';
|
|
7
7
|
import { presetManager } from '../overlays/presets';
|
|
8
8
|
import { layoutStore, resetActivePresetToDefault } from '../layout/store.svelte';
|
|
9
9
|
import LayoutRenderer from '../layout/LayoutRenderer.svelte';
|
|
@@ -19,7 +19,7 @@ describe('launchApp — scenario A.1 step order', () => {
|
|
|
19
19
|
const order = [];
|
|
20
20
|
const shard = makeShard({
|
|
21
21
|
manifest: makeShardManifest({ id: 'shard-A' }),
|
|
22
|
-
|
|
22
|
+
register: () => {
|
|
23
23
|
order.push('shard.activate');
|
|
24
24
|
},
|
|
25
25
|
});
|
|
@@ -54,7 +54,7 @@ describe('launchApp — scenario A.2 shard failure', () => {
|
|
|
54
54
|
it('detaches the app and re-throws when a required shard throws during activate', async () => {
|
|
55
55
|
const badShard = makeShard({
|
|
56
56
|
manifest: makeShardManifest({ id: 'bad' }),
|
|
57
|
-
|
|
57
|
+
register: () => {
|
|
58
58
|
throw new Error('boom');
|
|
59
59
|
},
|
|
60
60
|
});
|
|
@@ -77,7 +77,7 @@ describe('launchApp — scenario A.3 re-entry from home', () => {
|
|
|
77
77
|
const shardActivate = vi.fn();
|
|
78
78
|
const shard = makeShard({
|
|
79
79
|
manifest: makeShardManifest({ id: 'shard-R' }),
|
|
80
|
-
|
|
80
|
+
register: shardActivate,
|
|
81
81
|
});
|
|
82
82
|
registerShard(shard);
|
|
83
83
|
const appResume = vi.fn();
|
|
@@ -107,7 +107,7 @@ describe('launchApp — scenario A.4 fast path', () => {
|
|
|
107
107
|
const shardDeactivate = vi.fn();
|
|
108
108
|
registerShard(makeShard({
|
|
109
109
|
manifest: makeShardManifest({ id: 'shard-F' }),
|
|
110
|
-
|
|
110
|
+
register: shardActivate,
|
|
111
111
|
deactivate: shardDeactivate,
|
|
112
112
|
}));
|
|
113
113
|
registerApp(makeApp({
|
|
@@ -163,7 +163,7 @@ describe('presets — scenario B.2 switch from shard.activate', () => {
|
|
|
163
163
|
it('does not throw "no app attached" when a shard calls presets.switch from activate', async () => {
|
|
164
164
|
registerShard(makeShard({
|
|
165
165
|
manifest: makeShardManifest({ id: 'switcher' }),
|
|
166
|
-
|
|
166
|
+
register: () => {
|
|
167
167
|
presetManager.switch('alt');
|
|
168
168
|
},
|
|
169
169
|
}));
|
|
@@ -297,7 +297,7 @@ describe('installPackage evict-before-register (simulated via registerLoadedBund
|
|
|
297
297
|
it('replaces an existing shard entry when a new version is registered', async () => {
|
|
298
298
|
var _a, _b;
|
|
299
299
|
const { registerLoadedBundle } = await import('../registry/register');
|
|
300
|
-
const { deactivateShard, registeredShards } = await import('../shards/
|
|
300
|
+
const { deactivateShard, registeredShards } = await import('../shards/lifecycle.svelte');
|
|
301
301
|
const s1 = makeShard({ manifest: makeShardManifest({ id: 'S', version: '' }) });
|
|
302
302
|
registerLoadedBundle({ shards: [s1], apps: [] }, { version: '1.0.0', sourceRegistry: '', contractVersion: '1' });
|
|
303
303
|
expect((_a = registeredShards.get('S')) === null || _a === void 0 ? void 0 : _a.manifest.version).toBe('1.0.0');
|
|
@@ -434,7 +434,7 @@ describe('sh3coreShard — sh3.app.reset-layout registration', () => {
|
|
|
434
434
|
// would — tests don't run bootstrap, so the shard's actions wouldn't
|
|
435
435
|
// otherwise be present.
|
|
436
436
|
const { sh3coreShard } = await import('../sh3core-shard/sh3coreShard.svelte');
|
|
437
|
-
const { activateShard } = await import('../shards/
|
|
437
|
+
const { activateShard } = await import('../shards/lifecycle.svelte');
|
|
438
438
|
const { addAutostartShard } = await import('../actions/state.svelte');
|
|
439
439
|
registerShard(sh3coreShard);
|
|
440
440
|
// Mirror what bootstrap() does: mark the framework shard as autostart so
|
|
@@ -457,7 +457,7 @@ describe('sh3coreShard — sh3.app.reset-layout registration', () => {
|
|
|
457
457
|
});
|
|
458
458
|
it('hides "app"-scope actions after returnToHome', async () => {
|
|
459
459
|
const { sh3coreShard } = await import('../sh3core-shard/sh3coreShard.svelte');
|
|
460
|
-
const { activateShard } = await import('../shards/
|
|
460
|
+
const { activateShard } = await import('../shards/lifecycle.svelte');
|
|
461
461
|
const { addAutostartShard } = await import('../actions/state.svelte');
|
|
462
462
|
registerShard(sh3coreShard);
|
|
463
463
|
addAutostartShard(sh3coreShard.manifest.id);
|
|
@@ -529,7 +529,7 @@ describe('launchApp — error toast on shard failure', () => {
|
|
|
529
529
|
.mockImplementation(() => ({ close: () => { } }));
|
|
530
530
|
const badShard = makeShard({
|
|
531
531
|
manifest: makeShardManifest({ id: 'bad-toast' }),
|
|
532
|
-
|
|
532
|
+
register: () => {
|
|
533
533
|
throw new Error('shard "other" not registered');
|
|
534
534
|
},
|
|
535
535
|
});
|
|
@@ -669,8 +669,8 @@ describe('launchApp — onAppActivate hook', () => {
|
|
|
669
669
|
const calls = [];
|
|
670
670
|
const shard = makeShard({
|
|
671
671
|
manifest: makeShardManifest({ id: 'activate-hook-shard' }),
|
|
672
|
-
|
|
673
|
-
onAppActivate(appId) { calls.push({ shardId: 'activate-hook-shard', appId }); },
|
|
672
|
+
register() { },
|
|
673
|
+
onAppActivate(_ctx, appId) { calls.push({ shardId: 'activate-hook-shard', appId }); },
|
|
674
674
|
});
|
|
675
675
|
registerShard(shard);
|
|
676
676
|
const app = makeApp({
|
|
@@ -680,32 +680,9 @@ describe('launchApp — onAppActivate hook', () => {
|
|
|
680
680
|
await launchApp('activate-hook-app');
|
|
681
681
|
expect(calls).toEqual([{ shardId: 'activate-hook-shard', appId: 'activate-hook-app' }]);
|
|
682
682
|
});
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const backend = new MemoryDocumentBackend();
|
|
687
|
-
__setDocumentBackend(backend);
|
|
688
|
-
// Capture appCtx so we can test synchronously after launch
|
|
689
|
-
let capturedAppCtx = null;
|
|
690
|
-
const shard = makeShard({
|
|
691
|
-
manifest: makeShardManifest({ id: 'ns-shard2' }),
|
|
692
|
-
activate() { },
|
|
693
|
-
onAppActivate(_appId, appCtx) { capturedAppCtx = appCtx; },
|
|
694
|
-
});
|
|
695
|
-
registerShard(shard);
|
|
696
|
-
const app = makeApp({
|
|
697
|
-
manifest: makeAppManifest({ id: 'ns-app2', requiredShards: ['ns-shard2'] }),
|
|
698
|
-
});
|
|
699
|
-
registerApp(app);
|
|
700
|
-
await launchApp('ns-app2');
|
|
701
|
-
const handle = capturedAppCtx.documents({ format: 'text' });
|
|
702
|
-
await handle.write('probe.txt', 'hello');
|
|
703
|
-
// Written under appId namespace, not shardId (scope is 'local' by default)
|
|
704
|
-
const inAppNs = await backend.list('local', 'ns-app2');
|
|
705
|
-
const inShardNs = await backend.list('local', 'ns-shard2');
|
|
706
|
-
expect(inAppNs.some((k) => k.path === 'probe.txt')).toBe(true);
|
|
707
|
-
expect(inShardNs).toHaveLength(0);
|
|
708
|
-
});
|
|
683
|
+
// v3: AppActivateContext is gone. ctx.documents (the shard's pre-minted
|
|
684
|
+
// handle) auto-resolves to the active app's namespace because
|
|
685
|
+
// runAppActivate rotated the binding. See lifecycle.test.ts for v3 coverage.
|
|
709
686
|
});
|
|
710
687
|
describe('unloadApp — onAppDeactivate hook', () => {
|
|
711
688
|
beforeEach(resetFramework);
|
|
@@ -713,8 +690,8 @@ describe('unloadApp — onAppDeactivate hook', () => {
|
|
|
713
690
|
const deactivated = [];
|
|
714
691
|
const shard = makeShard({
|
|
715
692
|
manifest: makeShardManifest({ id: 'deact-shard' }),
|
|
716
|
-
|
|
717
|
-
onAppDeactivate(appId) { deactivated.push(appId); },
|
|
693
|
+
register() { },
|
|
694
|
+
onAppDeactivate(_ctx, appId) { deactivated.push(appId); },
|
|
718
695
|
});
|
|
719
696
|
registerShard(shard);
|
|
720
697
|
const app = makeApp({
|
|
@@ -733,12 +710,12 @@ describe('launchApp — onLayoutWillRestore / onLayoutRestored hooks', () => {
|
|
|
733
710
|
const capturedSlots = [];
|
|
734
711
|
const shard = makeShard({
|
|
735
712
|
manifest: makeShardManifest({ id: 'restore-shard' }),
|
|
736
|
-
|
|
737
|
-
onLayoutWillRestore(slots) {
|
|
713
|
+
register() { },
|
|
714
|
+
onLayoutWillRestore(_ctx, slots) {
|
|
738
715
|
order.push('onLayoutWillRestore');
|
|
739
716
|
capturedSlots.push([...slots]);
|
|
740
717
|
},
|
|
741
|
-
onLayoutRestored(slots) {
|
|
718
|
+
onLayoutRestored(_ctx, slots) {
|
|
742
719
|
order.push('onLayoutRestored');
|
|
743
720
|
capturedSlots.push([...slots]);
|
|
744
721
|
},
|
|
@@ -775,7 +752,7 @@ describe('launchApp — onLayoutWillRestore / onLayoutRestored hooks', () => {
|
|
|
775
752
|
});
|
|
776
753
|
const shard = makeShard({
|
|
777
754
|
manifest: makeShardManifest({ id: 'slow-restore-shard' }),
|
|
778
|
-
|
|
755
|
+
register() { },
|
|
779
756
|
async onLayoutWillRestore() {
|
|
780
757
|
order.push('onLayoutWillRestore:start');
|
|
781
758
|
await hookGate;
|
|
@@ -824,11 +801,11 @@ describe('launchApp — document auto-scope', () => {
|
|
|
824
801
|
let handleB = null;
|
|
825
802
|
registerShard(makeShard({
|
|
826
803
|
manifest: makeShardManifest({ id: 'shard-share-A' }),
|
|
827
|
-
|
|
804
|
+
register(ctx) { handleA = ctx.documents; },
|
|
828
805
|
}));
|
|
829
806
|
registerShard(makeShard({
|
|
830
807
|
manifest: makeShardManifest({ id: 'shard-share-B' }),
|
|
831
|
-
|
|
808
|
+
register(ctx) { handleB = ctx.documents; },
|
|
832
809
|
}));
|
|
833
810
|
registerApp(makeApp({
|
|
834
811
|
manifest: makeAppManifest({
|
|
@@ -837,39 +814,16 @@ describe('launchApp — document auto-scope', () => {
|
|
|
837
814
|
}),
|
|
838
815
|
}));
|
|
839
816
|
await launchApp('app-share');
|
|
840
|
-
await handleA.
|
|
841
|
-
expect(await handleB.
|
|
817
|
+
await handleA.writeText('shared.json', '{"from":"A"}');
|
|
818
|
+
expect(await handleB.readText('shared.json')).toBe('{"from":"A"}');
|
|
842
819
|
expect(await backend.list('local', 'app-share')).toHaveLength(1);
|
|
843
820
|
expect(await backend.list('local', 'shard-share-A')).toHaveLength(0);
|
|
844
821
|
expect(await backend.list('local', 'shard-share-B')).toHaveLength(0);
|
|
845
822
|
});
|
|
846
823
|
});
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const { MemoryDocumentBackend } = await import('../documents/backends');
|
|
851
|
-
const { __setDocumentBackend } = await import('../documents/config');
|
|
852
|
-
const backend = new MemoryDocumentBackend();
|
|
853
|
-
__setDocumentBackend(backend);
|
|
854
|
-
let handle = null;
|
|
855
|
-
registerShard(makeShard({
|
|
856
|
-
manifest: makeShardManifest({ id: 'autostart-shard' }),
|
|
857
|
-
activate(ctx) { handle = ctx.documents({ format: 'text' }); },
|
|
858
|
-
autostart() { },
|
|
859
|
-
}));
|
|
860
|
-
registerApp(makeApp({
|
|
861
|
-
manifest: makeAppManifest({
|
|
862
|
-
id: 'app-with-autostart',
|
|
863
|
-
requiredShards: ['autostart-shard'],
|
|
864
|
-
}),
|
|
865
|
-
}));
|
|
866
|
-
await launchApp('app-with-autostart');
|
|
867
|
-
await handle.write('persistent.txt', 'lives in autostart-shard');
|
|
868
|
-
expect((await backend.list('local', 'autostart-shard')).map(d => d.path))
|
|
869
|
-
.toContain('persistent.txt');
|
|
870
|
-
expect(await backend.list('local', 'app-with-autostart')).toHaveLength(0);
|
|
871
|
-
});
|
|
872
|
-
});
|
|
824
|
+
// v3: autostart is gone. Every shard's document handle rotates to the
|
|
825
|
+
// active app's namespace via runAppActivate, no exceptions. See
|
|
826
|
+
// lifecycle.test.ts for the v3 binding-rotation coverage.
|
|
873
827
|
describe('unloadApp — clears document binding', () => {
|
|
874
828
|
beforeEach(resetFramework);
|
|
875
829
|
it('reverts a non-autostart shard back to shard-scope when the app is unloaded', async () => {
|
|
@@ -877,10 +831,10 @@ describe('unloadApp — clears document binding', () => {
|
|
|
877
831
|
const { __setDocumentBackend } = await import('../documents/config');
|
|
878
832
|
const backend = new MemoryDocumentBackend();
|
|
879
833
|
__setDocumentBackend(backend);
|
|
880
|
-
const { getShardBinding } = await import('../shards/
|
|
834
|
+
const { getShardBinding } = await import('../shards/lifecycle.svelte');
|
|
881
835
|
registerShard(makeShard({
|
|
882
836
|
manifest: makeShardManifest({ id: 'binding-shard' }),
|
|
883
|
-
|
|
837
|
+
register() { },
|
|
884
838
|
}));
|
|
885
839
|
registerApp(makeApp({
|
|
886
840
|
manifest: makeAppManifest({
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
import { ConflictPermissionError, } from './api';
|
|
20
20
|
import { resolvePrimitive } from './resolve-primitive';
|
|
21
21
|
function ownShardHandle(ctx) {
|
|
22
|
-
|
|
23
|
-
const h = ctx.documents({ format: 'text' });
|
|
22
|
+
const h = ctx.documents;
|
|
24
23
|
return {
|
|
25
24
|
status: (p) => h.status(p),
|
|
26
25
|
readBranch: (p, o) => h.readBranch(p, o),
|
package/dist/createShell.js
CHANGED
|
@@ -14,7 +14,7 @@ import { apiFetch } from './transport/apiFetch';
|
|
|
14
14
|
import { hydrateTokenOverrides } from './theme';
|
|
15
15
|
import { __setEnvServerUrl, getEnvServerUrl } from './env/index';
|
|
16
16
|
import { __setActiveScope, __setScopeResolver } from './documents/config';
|
|
17
|
-
import { __setScopeResolver as __setShardScopeResolver } from './shards/
|
|
17
|
+
import { __setScopeResolver as __setShardScopeResolver } from './shards/lifecycle.svelte';
|
|
18
18
|
import { initFromBoot } from './auth/index';
|
|
19
19
|
import SignInWall from './auth/SignInWall.svelte';
|
|
20
20
|
import { loadBundleModule } from './registry/loader';
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { DocumentBackend, DocumentHandle
|
|
1
|
+
import type { DocumentBackend, DocumentHandle } from './types';
|
|
2
2
|
/**
|
|
3
|
-
* Create a document handle scoped to a tenant
|
|
4
|
-
*
|
|
3
|
+
* Create a document handle scoped to a tenant and namespace. The framework
|
|
4
|
+
* pre-mints one handle per shard at boot; the namespace resolves lazily on
|
|
5
|
+
* every operation via `getShardBinding(shardId) ?? shardId` so it follows
|
|
6
|
+
* the shard's currently-bound app without re-minting.
|
|
7
|
+
*
|
|
8
|
+
* Format moves from the handle to per-call (readText/writeText/readJson/
|
|
9
|
+
* writeJson/readBinary/writeBinary) — see ADR-027.
|
|
5
10
|
*/
|
|
6
|
-
export declare function createDocumentHandle(tenantId: string | (() => string), shardOrNamespace: string | (() => string), backend: DocumentBackend
|
|
11
|
+
export declare function createDocumentHandle(tenantId: string | (() => string), shardOrNamespace: string | (() => string), backend: DocumentBackend): DocumentHandle;
|
package/dist/documents/handle.js
CHANGED
|
@@ -20,24 +20,19 @@ 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, shardOrNamespace, backend
|
|
31
|
+
export function createDocumentHandle(tenantId, shardOrNamespace, backend) {
|
|
27
32
|
const controllers = new Set();
|
|
28
33
|
const unsubscribers = new Set();
|
|
29
34
|
const resolveBoundTenant = typeof tenantId === 'function' ? tenantId : () => tenantId;
|
|
30
|
-
const
|
|
31
|
-
// `options.appId` is the explicit override — when set it wins over the lazy
|
|
32
|
-
// resolver (used by appCtx.documents and as a power-user escape hatch).
|
|
33
|
-
const resolveNamespace = options.appId
|
|
34
|
-
? () => options.appId
|
|
35
|
-
: baseNamespace;
|
|
36
|
-
function matchesExtensions(path) {
|
|
37
|
-
if (!options.extensions || options.extensions.length === 0)
|
|
38
|
-
return true;
|
|
39
|
-
return options.extensions.some((ext) => path.endsWith(ext));
|
|
40
|
-
}
|
|
35
|
+
const resolveNamespace = typeof shardOrNamespace === 'function' ? shardOrNamespace : () => shardOrNamespace;
|
|
41
36
|
function resolveTenant(opts) {
|
|
42
37
|
var _a;
|
|
43
38
|
return (_a = opts === null || opts === void 0 ? void 0 : opts.scope) !== null && _a !== void 0 ? _a : resolveBoundTenant();
|
|
@@ -48,26 +43,47 @@ export function createDocumentHandle(tenantId, shardOrNamespace, backend, option
|
|
|
48
43
|
const handle = {
|
|
49
44
|
async list(opts) {
|
|
50
45
|
const tid = resolveTenant(opts);
|
|
51
|
-
|
|
52
|
-
if (!options.extensions || options.extensions.length === 0)
|
|
53
|
-
return all;
|
|
54
|
-
return all.filter((meta) => matchesExtensions(meta.path));
|
|
46
|
+
return backend.list(tid, resolveNamespace());
|
|
55
47
|
},
|
|
56
|
-
async
|
|
48
|
+
async readText(path, opts) {
|
|
57
49
|
const content = await backend.read(resolveTenant(opts), resolveNamespace(), path);
|
|
58
50
|
if (content === null)
|
|
59
51
|
return null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
52
|
+
if (typeof content === 'string')
|
|
53
|
+
return content;
|
|
54
|
+
return new TextDecoder().decode(content);
|
|
63
55
|
},
|
|
64
|
-
async
|
|
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);
|
|
69
|
+
},
|
|
70
|
+
async writeText(path, content, opts) {
|
|
65
71
|
const tid = resolveTenant(opts);
|
|
66
72
|
const ns = resolveNamespace();
|
|
67
73
|
const existed = await backend.exists(tid, ns, path);
|
|
68
74
|
await backend.write(tid, ns, path, content);
|
|
69
75
|
emitChange(existed ? 'update' : 'create', path, tid);
|
|
70
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
|
+
},
|
|
71
87
|
async delete(path, opts) {
|
|
72
88
|
const tid = resolveTenant(opts);
|
|
73
89
|
const ns = resolveNamespace();
|
|
@@ -77,9 +93,6 @@ export function createDocumentHandle(tenantId, shardOrNamespace, backend, option
|
|
|
77
93
|
emitChange('delete', path, tid);
|
|
78
94
|
},
|
|
79
95
|
async rename(oldPath, newPath, opts) {
|
|
80
|
-
if (!matchesExtensions(newPath)) {
|
|
81
|
-
throw new Error(`Cannot rename to ${newPath}: violates handle extensions filter`);
|
|
82
|
-
}
|
|
83
96
|
for (const ctrl of controllers) {
|
|
84
97
|
if (ctrl.path === oldPath) {
|
|
85
98
|
throw new Error(`Cannot rename: active autosave on ${oldPath}; flush and dispose first`);
|
|
@@ -167,8 +180,6 @@ export function createDocumentHandle(tenantId, shardOrNamespace, backend, option
|
|
|
167
180
|
return;
|
|
168
181
|
if (change.shardId !== resolveNamespace())
|
|
169
182
|
return;
|
|
170
|
-
if (!matchesExtensions(change.path))
|
|
171
|
-
return;
|
|
172
183
|
callback(change);
|
|
173
184
|
});
|
|
174
185
|
unsubscribers.add(unsub);
|
|
@@ -234,7 +245,7 @@ class AutosaveControllerImpl {
|
|
|
234
245
|
const content = __classPrivateFieldGet(this, _AutosaveControllerImpl_pending, "f");
|
|
235
246
|
__classPrivateFieldSet(this, _AutosaveControllerImpl_pending, null, "f");
|
|
236
247
|
__classPrivateFieldSet(this, _AutosaveControllerImpl_dirty, false, "f");
|
|
237
|
-
await __classPrivateFieldGet(this, _AutosaveControllerImpl_handle, "f").
|
|
248
|
+
await __classPrivateFieldGet(this, _AutosaveControllerImpl_handle, "f").writeText(__classPrivateFieldGet(this, _AutosaveControllerImpl_path, "f"), content);
|
|
238
249
|
}
|
|
239
250
|
}
|
|
240
251
|
async dispose() {
|