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/apps/lifecycle.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
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
|
+
import { attachApp, acquireAppSlotHolds, detachApp, switchToApp, switchToHome, getActiveAppTree, } from '../layout/store.svelte';
|
|
18
19
|
import { activeApp, breadcrumbApp, getRegisteredApp, registeredApps } from './registry.svelte';
|
|
19
20
|
import { createZoneManager } from '../state/manage';
|
|
20
21
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
@@ -25,6 +26,7 @@ import { toastManager } from '../overlays/toast';
|
|
|
25
26
|
import { clearAppNavEntries } from '../navigation/back-stack';
|
|
26
27
|
import { getActiveScopeId } from '../documents/config';
|
|
27
28
|
import { sessionState } from '../projects/session-state.svelte';
|
|
29
|
+
import { collectRestoredSlots } from '../layout/tree-walk';
|
|
28
30
|
// ---------- last-active-app user zone ------------------------------------
|
|
29
31
|
/**
|
|
30
32
|
* Framework-reserved user-zone slot storing which app to boot into on
|
|
@@ -95,7 +97,7 @@ function getOrCreateAppContext(appId, scopeId, args) {
|
|
|
95
97
|
* @throws If the app is not registered or a required shard is not registered.
|
|
96
98
|
*/
|
|
97
99
|
export async function launchApp(id, opts = {}) {
|
|
98
|
-
var _a, _b, _c, _d, _e, _f, _g
|
|
100
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
99
101
|
const app = getRegisteredApp(id);
|
|
100
102
|
if (!app) {
|
|
101
103
|
throw new Error(`Cannot launch app "${id}": not registered`);
|
|
@@ -110,18 +112,17 @@ export async function launchApp(id, opts = {}) {
|
|
|
110
112
|
else if (activeApp.id === id) {
|
|
111
113
|
// Re-entering the same app from Home — fire resume hooks.
|
|
112
114
|
for (const shardId of app.manifest.requiredShards) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
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);
|
|
117
118
|
}
|
|
118
|
-
void ((
|
|
119
|
+
void ((_a = app.resume) === null || _a === void 0 ? void 0 : _a.call(app, getOrCreateAppContext(id)));
|
|
119
120
|
switchToApp();
|
|
120
|
-
void ((
|
|
121
|
+
void ((_b = app.onAppReady) === null || _b === void 0 ? void 0 : _b.call(app, getOrCreateAppContext(id)));
|
|
121
122
|
if (!opts.skipLastApp)
|
|
122
123
|
writeLastApp(id);
|
|
123
124
|
breadcrumbApp.id = id;
|
|
124
|
-
setActiveApp(id, new Set((
|
|
125
|
+
setActiveApp(id, new Set((_c = app.manifest.requiredShards) !== null && _c !== void 0 ? _c : []));
|
|
125
126
|
void loadUserBindings(id).then(setUserBindings);
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
@@ -141,14 +142,34 @@ export async function launchApp(id, opts = {}) {
|
|
|
141
142
|
// detach to keep the preset manager state consistent.
|
|
142
143
|
attachApp(app);
|
|
143
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).
|
|
144
151
|
for (const shardId of app.manifest.requiredShards) {
|
|
145
|
-
|
|
152
|
+
const err = erroredShards.get(shardId);
|
|
153
|
+
if (err)
|
|
154
|
+
throw err.error;
|
|
155
|
+
}
|
|
156
|
+
// runAppActivate rotates the doc namespace binding AND fires
|
|
157
|
+
// onAppActivate. No per-shard activate() pass, no separate binding.
|
|
158
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
159
|
+
await runAppActivate(shardId, id);
|
|
146
160
|
}
|
|
147
161
|
}
|
|
148
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
|
+
}
|
|
149
170
|
detachApp();
|
|
150
171
|
try {
|
|
151
|
-
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 });
|
|
152
173
|
}
|
|
153
174
|
catch (_j) {
|
|
154
175
|
// Toast layer not mounted (e.g. early boot, tests without Sh3).
|
|
@@ -156,16 +177,33 @@ export async function launchApp(id, opts = {}) {
|
|
|
156
177
|
}
|
|
157
178
|
throw err;
|
|
158
179
|
}
|
|
180
|
+
// Collect the layout's restored slots for the restore hooks.
|
|
181
|
+
const tree = getActiveAppTree();
|
|
182
|
+
const restoredSlots = tree ? collectRestoredSlots(tree) : [];
|
|
183
|
+
// Await onLayoutWillRestore so async slot-contribution registrations
|
|
184
|
+
// complete before acquireAppSlotHolds triggers view factory mount.
|
|
185
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
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);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
159
191
|
// Shards have registered their view factories — safe to take the
|
|
160
192
|
// refcount holds on the app's slots now (pool's factory lookup
|
|
161
193
|
// happens in a microtask from this call).
|
|
162
194
|
acquireAppSlotHolds();
|
|
163
|
-
void ((
|
|
195
|
+
void ((_e = app.activate) === null || _e === void 0 ? void 0 : _e.call(app, getOrCreateAppContext(id, undefined, opts.args)));
|
|
164
196
|
activeApp.id = id;
|
|
165
|
-
setActiveApp(id, new Set((
|
|
197
|
+
setActiveApp(id, new Set((_f = app.manifest.requiredShards) !== null && _f !== void 0 ? _f : []));
|
|
166
198
|
void loadUserBindings(id).then(setUserBindings);
|
|
167
199
|
switchToApp();
|
|
168
|
-
|
|
200
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
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);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
void ((_g = app.onAppReady) === null || _g === void 0 ? void 0 : _g.call(app, getOrCreateAppContext(id)));
|
|
169
207
|
if (!opts.skipLastApp)
|
|
170
208
|
writeLastApp(id);
|
|
171
209
|
breadcrumbApp.id = id;
|
|
@@ -189,6 +227,12 @@ export function unloadApp(id, skipSwitchToHome = false) {
|
|
|
189
227
|
if (!app)
|
|
190
228
|
return;
|
|
191
229
|
void ((_a = app.deactivate) === null || _a === void 0 ? void 0 : _a.call(app));
|
|
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.
|
|
233
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
234
|
+
void runAppDeactivate(shardId, id);
|
|
235
|
+
}
|
|
192
236
|
// Detach layout (releases the refcount holds; pool cleanup runs on
|
|
193
237
|
// the next microtask for any slots that no longer have a renderer).
|
|
194
238
|
// Switch to home first so LayoutRenderer stops reading the app's
|
|
@@ -196,21 +240,6 @@ export function unloadApp(id, skipSwitchToHome = false) {
|
|
|
196
240
|
if (!skipSwitchToHome)
|
|
197
241
|
switchToHome();
|
|
198
242
|
detachApp();
|
|
199
|
-
// Deactivate this app's required shards IF no other consumer needs
|
|
200
|
-
// them. Phase 8 has at most one app active at a time, so "no other
|
|
201
|
-
// consumer" reduces to "not self-starting AND not required by any
|
|
202
|
-
// other registered app that happens to already be active" — but we
|
|
203
|
-
// don't run multiple apps, so the only survivors are self-starters.
|
|
204
|
-
// The simple rule: deactivate a required shard unless it was
|
|
205
|
-
// self-starting (has an `autostart` field defined).
|
|
206
|
-
for (const shardId of app.manifest.requiredShards) {
|
|
207
|
-
const shard = registeredShards.get(shardId);
|
|
208
|
-
if (!shard)
|
|
209
|
-
continue;
|
|
210
|
-
if (shard.autostart)
|
|
211
|
-
continue; // self-starter stays running
|
|
212
|
-
deactivateShard(shardId);
|
|
213
|
-
}
|
|
214
243
|
activeApp.id = null;
|
|
215
244
|
setActiveApp(null, new Set());
|
|
216
245
|
clearSelectionUnconditional();
|
|
@@ -257,9 +286,12 @@ export async function returnToHome() {
|
|
|
257
286
|
const app = activeApp.id ? getRegisteredApp(activeApp.id) : null;
|
|
258
287
|
if (app) {
|
|
259
288
|
for (const shardId of app.manifest.requiredShards) {
|
|
260
|
-
const
|
|
261
|
-
if ((
|
|
262
|
-
|
|
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
|
+
}
|
|
263
295
|
}
|
|
264
296
|
if (app.suspend && (await app.suspend()) === false)
|
|
265
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
|
});
|
|
@@ -660,3 +660,191 @@ describe('launchApp — ctx.args', () => {
|
|
|
660
660
|
expect(receivedArgs).toEqual({});
|
|
661
661
|
});
|
|
662
662
|
});
|
|
663
|
+
// ---------------------------------------------------------------------------
|
|
664
|
+
// Scenario — onAppActivate / onAppDeactivate / layout hooks
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
describe('launchApp — onAppActivate hook', () => {
|
|
667
|
+
beforeEach(resetFramework);
|
|
668
|
+
it('calls onAppActivate on required shards after activation with correct appId', async () => {
|
|
669
|
+
const calls = [];
|
|
670
|
+
const shard = makeShard({
|
|
671
|
+
manifest: makeShardManifest({ id: 'activate-hook-shard' }),
|
|
672
|
+
register() { },
|
|
673
|
+
onAppActivate(_ctx, appId) { calls.push({ shardId: 'activate-hook-shard', appId }); },
|
|
674
|
+
});
|
|
675
|
+
registerShard(shard);
|
|
676
|
+
const app = makeApp({
|
|
677
|
+
manifest: makeAppManifest({ id: 'activate-hook-app', requiredShards: ['activate-hook-shard'] }),
|
|
678
|
+
});
|
|
679
|
+
registerApp(app);
|
|
680
|
+
await launchApp('activate-hook-app');
|
|
681
|
+
expect(calls).toEqual([{ shardId: 'activate-hook-shard', appId: 'activate-hook-app' }]);
|
|
682
|
+
});
|
|
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.
|
|
686
|
+
});
|
|
687
|
+
describe('unloadApp — onAppDeactivate hook', () => {
|
|
688
|
+
beforeEach(resetFramework);
|
|
689
|
+
it('calls onAppDeactivate on required shards when the app is unloaded', async () => {
|
|
690
|
+
const deactivated = [];
|
|
691
|
+
const shard = makeShard({
|
|
692
|
+
manifest: makeShardManifest({ id: 'deact-shard' }),
|
|
693
|
+
register() { },
|
|
694
|
+
onAppDeactivate(_ctx, appId) { deactivated.push(appId); },
|
|
695
|
+
});
|
|
696
|
+
registerShard(shard);
|
|
697
|
+
const app = makeApp({
|
|
698
|
+
manifest: makeAppManifest({ id: 'deact-app', requiredShards: ['deact-shard'] }),
|
|
699
|
+
});
|
|
700
|
+
registerApp(app);
|
|
701
|
+
await launchApp('deact-app');
|
|
702
|
+
unregisterApp('deact-app');
|
|
703
|
+
expect(deactivated).toEqual(['deact-app']);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
describe('launchApp — onLayoutWillRestore / onLayoutRestored hooks', () => {
|
|
707
|
+
beforeEach(resetFramework);
|
|
708
|
+
it('calls onLayoutWillRestore before slot acquisition and onLayoutRestored after switchToApp', async () => {
|
|
709
|
+
const order = [];
|
|
710
|
+
const capturedSlots = [];
|
|
711
|
+
const shard = makeShard({
|
|
712
|
+
manifest: makeShardManifest({ id: 'restore-shard' }),
|
|
713
|
+
register() { },
|
|
714
|
+
onLayoutWillRestore(_ctx, slots) {
|
|
715
|
+
order.push('onLayoutWillRestore');
|
|
716
|
+
capturedSlots.push([...slots]);
|
|
717
|
+
},
|
|
718
|
+
onLayoutRestored(_ctx, slots) {
|
|
719
|
+
order.push('onLayoutRestored');
|
|
720
|
+
capturedSlots.push([...slots]);
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
registerShard(shard);
|
|
724
|
+
const app = makeApp({
|
|
725
|
+
manifest: makeAppManifest({ id: 'restore-app', requiredShards: ['restore-shard'] }),
|
|
726
|
+
initialLayout: [
|
|
727
|
+
{ name: 'default', tree: makeTree(makeSlotNode('r-slot', 'restore:view')) },
|
|
728
|
+
],
|
|
729
|
+
});
|
|
730
|
+
registerApp(app);
|
|
731
|
+
await launchApp('restore-app');
|
|
732
|
+
await Promise.resolve();
|
|
733
|
+
expect(order).toEqual(['onLayoutWillRestore', 'onLayoutRestored']);
|
|
734
|
+
// Both calls receive the same slot list
|
|
735
|
+
expect(capturedSlots[0]).toEqual(capturedSlots[1]);
|
|
736
|
+
expect(capturedSlots[0]).toHaveLength(1);
|
|
737
|
+
expect(capturedSlots[0][0]).toMatchObject({ slotId: 'r-slot', viewId: 'restore:view' });
|
|
738
|
+
});
|
|
739
|
+
it('awaits onLayoutWillRestore before slot factories mount', async () => {
|
|
740
|
+
// This is the load-bearing assertion: a slow async hook must complete
|
|
741
|
+
// before any acquireAppSlotHosts triggers a view factory mount() call.
|
|
742
|
+
// If launchApp uses `void shard.onLayoutWillRestore(...)`, the Promise
|
|
743
|
+
// resolves AFTER the microtask that mounts views — failing this test.
|
|
744
|
+
const order = [];
|
|
745
|
+
let resolveHook = null;
|
|
746
|
+
const hookGate = new Promise((r) => { resolveHook = r; });
|
|
747
|
+
registerView('slow-restore:view', {
|
|
748
|
+
mount() {
|
|
749
|
+
order.push('mount');
|
|
750
|
+
return { unmount() { } };
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
const shard = makeShard({
|
|
754
|
+
manifest: makeShardManifest({ id: 'slow-restore-shard' }),
|
|
755
|
+
register() { },
|
|
756
|
+
async onLayoutWillRestore() {
|
|
757
|
+
order.push('onLayoutWillRestore:start');
|
|
758
|
+
await hookGate;
|
|
759
|
+
order.push('onLayoutWillRestore:end');
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
registerShard(shard);
|
|
763
|
+
const app = makeApp({
|
|
764
|
+
manifest: makeAppManifest({ id: 'slow-restore-app', requiredShards: ['slow-restore-shard'] }),
|
|
765
|
+
initialLayout: [
|
|
766
|
+
{ name: 'default', tree: makeTree(makeSlotNode('sr-slot', 'slow-restore:view')) },
|
|
767
|
+
],
|
|
768
|
+
});
|
|
769
|
+
registerApp(app);
|
|
770
|
+
const launching = launchApp('slow-restore-app');
|
|
771
|
+
// Let microtasks run so the hook has started (launchApp awaits shard
|
|
772
|
+
// activation and onAppActivate before reaching onLayoutWillRestore, so
|
|
773
|
+
// we need several drains to get past those).
|
|
774
|
+
for (let i = 0; i < 10; i++)
|
|
775
|
+
await Promise.resolve();
|
|
776
|
+
// At this point the hook is suspended on the gate; mount must NOT have run yet.
|
|
777
|
+
expect(order).toEqual(['onLayoutWillRestore:start']);
|
|
778
|
+
resolveHook();
|
|
779
|
+
await launching;
|
|
780
|
+
for (let i = 0; i < 5; i++)
|
|
781
|
+
await Promise.resolve();
|
|
782
|
+
// The hook completed before any view factory ran.
|
|
783
|
+
const hookEndIdx = order.indexOf('onLayoutWillRestore:end');
|
|
784
|
+
const mountIdx = order.indexOf('mount');
|
|
785
|
+
expect(hookEndIdx).toBeGreaterThanOrEqual(0);
|
|
786
|
+
expect(mountIdx).toBeGreaterThanOrEqual(0);
|
|
787
|
+
expect(hookEndIdx).toBeLessThan(mountIdx);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
// ---------------------------------------------------------------------------
|
|
791
|
+
// Scenario E — document auto-scope: two shards in same app share namespace
|
|
792
|
+
// ---------------------------------------------------------------------------
|
|
793
|
+
describe('launchApp — document auto-scope', () => {
|
|
794
|
+
beforeEach(resetFramework);
|
|
795
|
+
it('routes ctx.documents() from two non-autostart shards in the same app to the appId namespace', async () => {
|
|
796
|
+
const { MemoryDocumentBackend } = await import('../documents/backends');
|
|
797
|
+
const { __setDocumentBackend } = await import('../documents/config');
|
|
798
|
+
const backend = new MemoryDocumentBackend();
|
|
799
|
+
__setDocumentBackend(backend);
|
|
800
|
+
let handleA = null;
|
|
801
|
+
let handleB = null;
|
|
802
|
+
registerShard(makeShard({
|
|
803
|
+
manifest: makeShardManifest({ id: 'shard-share-A' }),
|
|
804
|
+
register(ctx) { handleA = ctx.documents; },
|
|
805
|
+
}));
|
|
806
|
+
registerShard(makeShard({
|
|
807
|
+
manifest: makeShardManifest({ id: 'shard-share-B' }),
|
|
808
|
+
register(ctx) { handleB = ctx.documents; },
|
|
809
|
+
}));
|
|
810
|
+
registerApp(makeApp({
|
|
811
|
+
manifest: makeAppManifest({
|
|
812
|
+
id: 'app-share',
|
|
813
|
+
requiredShards: ['shard-share-A', 'shard-share-B'],
|
|
814
|
+
}),
|
|
815
|
+
}));
|
|
816
|
+
await launchApp('app-share');
|
|
817
|
+
await handleA.writeText('shared.json', '{"from":"A"}');
|
|
818
|
+
expect(await handleB.readText('shared.json')).toBe('{"from":"A"}');
|
|
819
|
+
expect(await backend.list('local', 'app-share')).toHaveLength(1);
|
|
820
|
+
expect(await backend.list('local', 'shard-share-A')).toHaveLength(0);
|
|
821
|
+
expect(await backend.list('local', 'shard-share-B')).toHaveLength(0);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
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.
|
|
827
|
+
describe('unloadApp — clears document binding', () => {
|
|
828
|
+
beforeEach(resetFramework);
|
|
829
|
+
it('reverts a non-autostart shard back to shard-scope when the app is unloaded', async () => {
|
|
830
|
+
const { MemoryDocumentBackend } = await import('../documents/backends');
|
|
831
|
+
const { __setDocumentBackend } = await import('../documents/config');
|
|
832
|
+
const backend = new MemoryDocumentBackend();
|
|
833
|
+
__setDocumentBackend(backend);
|
|
834
|
+
const { getShardBinding } = await import('../shards/lifecycle.svelte');
|
|
835
|
+
registerShard(makeShard({
|
|
836
|
+
manifest: makeShardManifest({ id: 'binding-shard' }),
|
|
837
|
+
register() { },
|
|
838
|
+
}));
|
|
839
|
+
registerApp(makeApp({
|
|
840
|
+
manifest: makeAppManifest({
|
|
841
|
+
id: 'binding-app',
|
|
842
|
+
requiredShards: ['binding-shard'],
|
|
843
|
+
}),
|
|
844
|
+
}));
|
|
845
|
+
await launchApp('binding-app');
|
|
846
|
+
expect(getShardBinding('binding-shard')).toBe('binding-app');
|
|
847
|
+
unregisterApp('binding-app');
|
|
848
|
+
expect(getShardBinding('binding-shard')).toBeNull();
|
|
849
|
+
});
|
|
850
|
+
});
|
package/dist/artifact.d.ts
CHANGED
|
@@ -31,4 +31,6 @@ export interface ArtifactManifest {
|
|
|
31
31
|
}>;
|
|
32
32
|
/** Shard ids this app requires to be installed on the server. Written by sh3Artifact for app/combo bundles. */
|
|
33
33
|
requiredShards?: string[];
|
|
34
|
+
/** Shard ids that are bundled inside this combo artifact. Written by sh3Artifact for combo bundles. */
|
|
35
|
+
bundledShards?: string[];
|
|
34
36
|
}
|
package/dist/build.js
CHANGED
|
@@ -362,7 +362,7 @@ export function sh3Artifact(options = {}) {
|
|
|
362
362
|
if (!finalAuthor) {
|
|
363
363
|
throw new Error('[sh3-artifact] Missing "author". Add it to package.json or pass it via sh3Artifact({ manifest: { author } }).');
|
|
364
364
|
}
|
|
365
|
-
const manifest = Object.assign(Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: artifactVersion, contractVersion: 1 }, (hasServer ? { server: 'server.js' } : {})), { description: finalDescription, author: finalAuthor }), ((type === 'app' || type === 'combo') ? { requiredShards } : {})), overrides);
|
|
365
|
+
const manifest = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: artifactVersion, contractVersion: 1 }, (hasServer ? { server: 'server.js' } : {})), { description: finalDescription, author: finalAuthor }), ((type === 'app' || type === 'combo') ? { requiredShards } : {})), (type === 'combo' && bundledShardIds.size > 0 ? { bundledShards: [...bundledShardIds] } : {})), overrides);
|
|
366
366
|
// Read the emitted JS files as bytes for the archive
|
|
367
367
|
const clientBytes = readFileSync(join(outDir, 'client.js'));
|
|
368
368
|
const serverBytes = hasServer ? readFileSync(join(outDir, 'server.js')) : undefined;
|
|
@@ -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,
|
|
11
|
+
export declare function createDocumentHandle(tenantId: string | (() => string), shardOrNamespace: string | (() => string), backend: DocumentBackend): DocumentHandle;
|