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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Shard, ShardContext } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
4
|
+
* Populated by `registerShard`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const registeredShards: Map<string, Shard>;
|
|
7
|
+
/**
|
|
8
|
+
* Reactive map of shard ids that failed during the lifecycle. Populated
|
|
9
|
+
* by registerAllShards and related operations.
|
|
10
|
+
*/
|
|
11
|
+
export interface ShardErrorEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
error: unknown;
|
|
14
|
+
phase: 'register' | 'launch' | 'satellite';
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const erroredShards: Map<string, ShardErrorEntry>;
|
|
18
|
+
/** Read the app id currently bound to this shard, or null. */
|
|
19
|
+
export declare function getShardBinding(shardId: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Update which app's namespace this shard's `ctx.documents` resolves to.
|
|
22
|
+
* Pass `appId` to bind, `null` to unbind. Internal — only the lifecycle
|
|
23
|
+
* module and `apps/lifecycle.ts` call this.
|
|
24
|
+
*/
|
|
25
|
+
export declare function rotateShardDocumentNamespace(shardId: string, appId: string | null): void;
|
|
26
|
+
export interface ShardEntry {
|
|
27
|
+
shard: Shard;
|
|
28
|
+
ctx: ShardContext;
|
|
29
|
+
viewIds: Set<string>;
|
|
30
|
+
verbNames: Set<string>;
|
|
31
|
+
/** Cleanup fns from things registered inside `register()`. Disposed only on shard re-register or framework teardown. */
|
|
32
|
+
bootCleanupFns: (() => Promise<void> | void)[];
|
|
33
|
+
/** Per-(shardId, appId) cleanup bags for things registered inside `onAppActivate`. Keyed by appId. */
|
|
34
|
+
appCleanupBags: Map<string, (() => Promise<void> | void)[]>;
|
|
35
|
+
/** Active appId during `onAppActivate` invocation, so register sites can route disposers to the right bag. */
|
|
36
|
+
activeAppId: string | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function __setScopeResolver(resolver: (() => 'tenant' | 'project') | null): void;
|
|
39
|
+
/**
|
|
40
|
+
* Build a ShardContext for the given shard. The ctx is permanent for the
|
|
41
|
+
* shard's lifetime; the same instance is passed to `register`, every
|
|
42
|
+
* `onAppActivate`/`onAppDeactivate`, and every auxiliary hook.
|
|
43
|
+
*
|
|
44
|
+
* `entry` tracks cleanup bags. When the shard is inside an `onAppActivate`
|
|
45
|
+
* call, register sites route disposers to the per-app bag (keyed by
|
|
46
|
+
* `entry.activeAppId`); otherwise they go to the boot bag.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildShardContext(shard: Shard, entry: ShardEntry): ShardContext;
|
|
49
|
+
export declare const shardEntries: Map<string, ShardEntry>;
|
|
50
|
+
export declare const activeShards: Map<string, Shard>;
|
|
51
|
+
/**
|
|
52
|
+
* Run `register(ctx)` on every registered shard. Idempotent — calling on
|
|
53
|
+
* an already-entered shard is a no-op. Errors are recorded in
|
|
54
|
+
* `erroredShards` with phase 'register'; one failure does not block others.
|
|
55
|
+
*/
|
|
56
|
+
export declare function registerAllShards(): Promise<void>;
|
|
57
|
+
export declare function runAppActivate(shardId: string, appId: string): Promise<void>;
|
|
58
|
+
export declare function runAppDeactivate(shardId: string, appId: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Tear down the active entry for a shard and rebuild it from the current
|
|
61
|
+
* `registeredShards.get(id)` value. Used by `registerShard` when replacing
|
|
62
|
+
* an existing shard with a fresh module (package update, dev hot-reload).
|
|
63
|
+
*
|
|
64
|
+
* Fires `onAppDeactivate` for every bound app, calls `deactivate?.()`,
|
|
65
|
+
* flushes all cleanup bags, builds a fresh ctx, then re-runs `register()`.
|
|
66
|
+
* Caller is responsible for re-invoking `runAppActivate` for any apps
|
|
67
|
+
* currently active that require this shard.
|
|
68
|
+
*/
|
|
69
|
+
export declare function rebuildShardEntry(shardId: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Register (or re-register) a shard. Records the shard in `registeredShards`.
|
|
72
|
+
* If the shard is already active (in `shardEntries`), triggers a hot-swap
|
|
73
|
+
* via `rebuildShardEntry` so the new module replaces the old one cleanly.
|
|
74
|
+
*/
|
|
75
|
+
export declare function registerShard(shard: Shard): void;
|
|
76
|
+
/** True if the shard has been registered AND its register() has run. */
|
|
77
|
+
export declare function isActive(id: string): boolean;
|
|
78
|
+
/** Return the ShardContext for an active shard, or undefined. */
|
|
79
|
+
export declare function getShardContext(id: string): ShardContext | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Enumerate every view declared as `standalone` across the currently
|
|
82
|
+
* registered/active shards.
|
|
83
|
+
*/
|
|
84
|
+
export declare function listStandaloneViews(): Array<{
|
|
85
|
+
shardId: string;
|
|
86
|
+
viewId: string;
|
|
87
|
+
label: string;
|
|
88
|
+
}>;
|
|
89
|
+
/** Test-only reset. */
|
|
90
|
+
export declare function __resetLifecycleForTest(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Test-only reset for the full shard registry. Wipes all live entries,
|
|
93
|
+
* registered shards, and error records.
|
|
94
|
+
*/
|
|
95
|
+
export declare function __resetShardRegistryForTest(): void;
|
|
96
|
+
/**
|
|
97
|
+
* @deprecated v2 compat shim — use `registerAllShards()` to run register
|
|
98
|
+
* for every registered shard. This shim activates a single shard by
|
|
99
|
+
* invoking its register hook directly. Retained for test fixtures during
|
|
100
|
+
* the migration; Phase 7 sweeps callers.
|
|
101
|
+
*/
|
|
102
|
+
export declare function activateShard(id: string): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated v2 compat shim — in v3, shards stay alive for the whole
|
|
105
|
+
* session. This shim performs a full teardown of the entry (for tests
|
|
106
|
+
* that explicitly want to verify cleanup paths).
|
|
107
|
+
*/
|
|
108
|
+
export declare function deactivateShard(id: string): void;
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shard lifecycle (v3) — the only entry the framework uses to bring
|
|
3
|
+
* shards online and bind them to apps. Replaces shards/activate.svelte.ts
|
|
4
|
+
* and shards/app-binding.svelte.ts.
|
|
5
|
+
*
|
|
6
|
+
* See docs/superpowers/specs/2026-05-18-shard-contract-v3-design.md
|
|
7
|
+
* and ADR-027 for the design rationale.
|
|
8
|
+
*/
|
|
9
|
+
import { sh3 } from '../sh3Runtime.svelte';
|
|
10
|
+
import { registerView, registerVerb as fwRegisterVerb } from './registry';
|
|
11
|
+
import { createDocumentHandle, getDocumentBackend, getActiveScopeId } from '../documents';
|
|
12
|
+
import { getEnvServerUrl } from '../env/index';
|
|
13
|
+
import { apiFetch } from '../transport/apiFetch';
|
|
14
|
+
import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
15
|
+
import { createZoneManager } from '../state/manage';
|
|
16
|
+
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
17
|
+
import { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from '../documents/types';
|
|
18
|
+
import { createBrowseCapability } from '../documents/browse';
|
|
19
|
+
import { createDocumentPicker } from '../documents/picker-primitive';
|
|
20
|
+
import { createShardKeysApi } from '../keys/client';
|
|
21
|
+
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
22
|
+
import { makeSh3Api } from '../sh3Api/headless';
|
|
23
|
+
import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, onAnyChange as contributionsOnAnyChange, } from '../contributions';
|
|
24
|
+
import { registerAction } from '../actions/registry';
|
|
25
|
+
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
26
|
+
import { openContextMenu as sh3OpenContextMenu, openPalette as sh3OpenPalette, } from '../actions/listeners';
|
|
27
|
+
import { unregisterView, unregisterVerb as fwUnregisterVerb } from './registry';
|
|
28
|
+
import { clearSelectionForShard } from '../actions/selection.svelte';
|
|
29
|
+
import { fetchEnvState } from '../env/client';
|
|
30
|
+
import { subscribe as subscribeKeyRevocation } from '../keys/revocation-bus.svelte';
|
|
31
|
+
const shardAppBindings = $state(new Map());
|
|
32
|
+
/**
|
|
33
|
+
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
34
|
+
* Populated by `registerShard`.
|
|
35
|
+
*/
|
|
36
|
+
export const registeredShards = $state(new Map());
|
|
37
|
+
export const erroredShards = $state(new Map());
|
|
38
|
+
/** Read the app id currently bound to this shard, or null. */
|
|
39
|
+
export function getShardBinding(shardId) {
|
|
40
|
+
var _a;
|
|
41
|
+
return (_a = shardAppBindings.get(shardId)) !== null && _a !== void 0 ? _a : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Update which app's namespace this shard's `ctx.documents` resolves to.
|
|
45
|
+
* Pass `appId` to bind, `null` to unbind. Internal — only the lifecycle
|
|
46
|
+
* module and `apps/lifecycle.ts` call this.
|
|
47
|
+
*/
|
|
48
|
+
export function rotateShardDocumentNamespace(shardId, appId) {
|
|
49
|
+
if (appId === null)
|
|
50
|
+
shardAppBindings.delete(shardId);
|
|
51
|
+
else
|
|
52
|
+
shardAppBindings.set(shardId, appId);
|
|
53
|
+
}
|
|
54
|
+
let scopeResolver = null;
|
|
55
|
+
export function __setScopeResolver(resolver) {
|
|
56
|
+
scopeResolver = resolver;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a ShardContext for the given shard. The ctx is permanent for the
|
|
60
|
+
* shard's lifetime; the same instance is passed to `register`, every
|
|
61
|
+
* `onAppActivate`/`onAppDeactivate`, and every auxiliary hook.
|
|
62
|
+
*
|
|
63
|
+
* `entry` tracks cleanup bags. When the shard is inside an `onAppActivate`
|
|
64
|
+
* call, register sites route disposers to the per-app bag (keyed by
|
|
65
|
+
* `entry.activeAppId`); otherwise they go to the boot bag.
|
|
66
|
+
*/
|
|
67
|
+
export function buildShardContext(shard, entry) {
|
|
68
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
69
|
+
const id = shard.manifest.id;
|
|
70
|
+
function trackDisposer(fn) {
|
|
71
|
+
var _a;
|
|
72
|
+
if (entry.activeAppId !== null) {
|
|
73
|
+
const bag = (_a = entry.appCleanupBags.get(entry.activeAppId)) !== null && _a !== void 0 ? _a : [];
|
|
74
|
+
bag.push(fn);
|
|
75
|
+
entry.appCleanupBags.set(entry.activeAppId, bag);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
entry.bootCleanupFns.push(fn);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// envState must be declared at top level for Svelte 5 runes.
|
|
82
|
+
const envState = $state({
|
|
83
|
+
proxy: null,
|
|
84
|
+
defaults: null,
|
|
85
|
+
});
|
|
86
|
+
const contributions = {
|
|
87
|
+
register(pointId, descriptor, contribOpts) {
|
|
88
|
+
var _a;
|
|
89
|
+
let stored = descriptor;
|
|
90
|
+
if (pointId === 'sh3.controllable-field') {
|
|
91
|
+
const owner = { shardId: id };
|
|
92
|
+
if (((_a = contribOpts === null || contribOpts === void 0 ? void 0 : contribOpts.scope) === null || _a === void 0 ? void 0 : _a.slotId) !== undefined) {
|
|
93
|
+
owner.slotId = contribOpts.scope.slotId;
|
|
94
|
+
}
|
|
95
|
+
stored = { owner, descriptor };
|
|
96
|
+
}
|
|
97
|
+
const dispose = contributionsRegister(pointId, stored, contribOpts);
|
|
98
|
+
trackDisposer(() => dispose());
|
|
99
|
+
return dispose;
|
|
100
|
+
},
|
|
101
|
+
list(pointId) {
|
|
102
|
+
return contributionsList(pointId);
|
|
103
|
+
},
|
|
104
|
+
listPoints() {
|
|
105
|
+
return contributionsListPoints();
|
|
106
|
+
},
|
|
107
|
+
onChange(pointId, cb) {
|
|
108
|
+
const off = contributionsOnChange(pointId, cb);
|
|
109
|
+
trackDisposer(() => off());
|
|
110
|
+
return off;
|
|
111
|
+
},
|
|
112
|
+
onAnyChange(cb) {
|
|
113
|
+
const off = contributionsOnAnyChange(cb);
|
|
114
|
+
trackDisposer(() => off());
|
|
115
|
+
return off;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
const hasBrowse = (_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_DOCUMENTS_BROWSE);
|
|
119
|
+
const browseCap = hasBrowse
|
|
120
|
+
? createBrowseCapability(getActiveScopeId, getDocumentBackend(), {
|
|
121
|
+
canRead: (_c = (_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_READ)) !== null && _c !== void 0 ? _c : false,
|
|
122
|
+
canWrite: (_e = (_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_WRITE)) !== null && _e !== void 0 ? _e : false,
|
|
123
|
+
})
|
|
124
|
+
: undefined;
|
|
125
|
+
// Pre-mint exactly one document handle per shard. The handle's
|
|
126
|
+
// namespace resolves lazily on every operation via `getShardBinding`.
|
|
127
|
+
const documents = createDocumentHandle(getActiveScopeId, () => { var _a; return (_a = getShardBinding(id)) !== null && _a !== void 0 ? _a : id; }, getDocumentBackend());
|
|
128
|
+
entry.bootCleanupFns.push(() => documents.dispose());
|
|
129
|
+
// Internal expose so registerAllShards can hydrate env state after register().
|
|
130
|
+
const exposedEnv = envState;
|
|
131
|
+
const ctx = {
|
|
132
|
+
state: (schema) => sh3.state(id, schema),
|
|
133
|
+
registerView: (viewId, factory) => {
|
|
134
|
+
registerView(viewId, factory, shard.manifest.id);
|
|
135
|
+
entry.viewIds.add(viewId);
|
|
136
|
+
},
|
|
137
|
+
registerVerb: (verb) => {
|
|
138
|
+
var _a;
|
|
139
|
+
let prefixed;
|
|
140
|
+
if (id === 'shell') {
|
|
141
|
+
prefixed = verb.name;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const ns = (_a = shard.manifest.verbNamespace) !== null && _a !== void 0 ? _a : id;
|
|
145
|
+
prefixed = `${ns}:${verb.name}`;
|
|
146
|
+
}
|
|
147
|
+
if (fwRegisterVerb(prefixed, Object.assign(Object.assign({}, verb), { name: prefixed }), id)) {
|
|
148
|
+
entry.verbNames.add(prefixed);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
documents,
|
|
152
|
+
fetch(path, init) {
|
|
153
|
+
return apiFetch(this.resolveUrl(path), init);
|
|
154
|
+
},
|
|
155
|
+
get serverUrl() {
|
|
156
|
+
return getEnvServerUrl();
|
|
157
|
+
},
|
|
158
|
+
resolveUrl(path) {
|
|
159
|
+
const isAbsolute = path.startsWith('http://') || path.startsWith('https://');
|
|
160
|
+
if (isAbsolute)
|
|
161
|
+
return path;
|
|
162
|
+
const base = getEnvServerUrl();
|
|
163
|
+
const sep = path.startsWith('/') ? '' : '/';
|
|
164
|
+
return `${base}${sep}${path}`;
|
|
165
|
+
},
|
|
166
|
+
env(defaults) {
|
|
167
|
+
if (envState.proxy) {
|
|
168
|
+
console.warn(`[sh3] Shard "${id}" called ctx.env() more than once; extra calls are ignored.`);
|
|
169
|
+
return envState.proxy;
|
|
170
|
+
}
|
|
171
|
+
envState.defaults = defaults;
|
|
172
|
+
envState.proxy = Object.assign({}, defaults);
|
|
173
|
+
return envState.proxy;
|
|
174
|
+
},
|
|
175
|
+
async envUpdate(patch) {
|
|
176
|
+
if (!envState.proxy || !envState.defaults) {
|
|
177
|
+
throw new Error(`Shard "${id}" called envUpdate() without declaring env state`);
|
|
178
|
+
}
|
|
179
|
+
const { putEnvState } = await import('../env/client');
|
|
180
|
+
const previous = $state.snapshot(envState.proxy);
|
|
181
|
+
Object.assign(envState.proxy, patch);
|
|
182
|
+
try {
|
|
183
|
+
const snapshot = $state.snapshot(envState.proxy);
|
|
184
|
+
await putEnvState(id, snapshot);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
Object.assign(envState.proxy, previous);
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
get isAdmin() {
|
|
192
|
+
return checkIsAdmin();
|
|
193
|
+
},
|
|
194
|
+
get tenantId() {
|
|
195
|
+
return getActiveScopeId();
|
|
196
|
+
},
|
|
197
|
+
getScope: () => { var _a; return (_a = scopeResolver === null || scopeResolver === void 0 ? void 0 : scopeResolver()) !== null && _a !== void 0 ? _a : 'tenant'; },
|
|
198
|
+
zones: ((_f = shard.manifest.permissions) === null || _f === void 0 ? void 0 : _f.includes(PERMISSION_STATE_MANAGE))
|
|
199
|
+
? createZoneManager()
|
|
200
|
+
: undefined,
|
|
201
|
+
browse: browseCap,
|
|
202
|
+
documentPicker: browseCap
|
|
203
|
+
? createDocumentPicker(() => browseCap.listDocuments())
|
|
204
|
+
: createDocumentPicker(async () => {
|
|
205
|
+
const docs = await getDocumentBackend().list(getActiveScopeId(), id);
|
|
206
|
+
return docs.map(d => (Object.assign(Object.assign({}, d), { shardId: id })));
|
|
207
|
+
}),
|
|
208
|
+
keys: ((_g = shard.manifest.permissions) === null || _g === void 0 ? void 0 : _g.includes(PERMISSION_KEYS_MINT))
|
|
209
|
+
? createShardKeysApi({
|
|
210
|
+
shardId: id,
|
|
211
|
+
shardPermissions: (_h = shard.manifest.permissions) !== null && _h !== void 0 ? _h : [],
|
|
212
|
+
})
|
|
213
|
+
: undefined,
|
|
214
|
+
contributions,
|
|
215
|
+
actions: {
|
|
216
|
+
register(action) {
|
|
217
|
+
const dispose = registerAction(action, id);
|
|
218
|
+
trackDisposer(() => dispose());
|
|
219
|
+
return dispose;
|
|
220
|
+
},
|
|
221
|
+
selection: makeSelectionApi(id),
|
|
222
|
+
openContextMenu(opts) { sh3OpenContextMenu(opts); },
|
|
223
|
+
openPalette(opts) { sh3OpenPalette(opts); },
|
|
224
|
+
},
|
|
225
|
+
sh3: makeSh3Api({
|
|
226
|
+
callerKind: 'shard',
|
|
227
|
+
callerShardId: id,
|
|
228
|
+
zones: ((_j = shard.manifest.permissions) === null || _j === void 0 ? void 0 : _j.includes(PERMISSION_STATE_MANAGE))
|
|
229
|
+
? createZoneManager()
|
|
230
|
+
: undefined,
|
|
231
|
+
}),
|
|
232
|
+
};
|
|
233
|
+
// Stash env state on the ctx for registerAllShards' hydration step.
|
|
234
|
+
ctx.__envState = exposedEnv;
|
|
235
|
+
return ctx;
|
|
236
|
+
}
|
|
237
|
+
export const shardEntries = $state(new Map());
|
|
238
|
+
export const activeShards = $state(new Map());
|
|
239
|
+
/**
|
|
240
|
+
* Run `register(ctx)` on every registered shard. Idempotent — calling on
|
|
241
|
+
* an already-entered shard is a no-op. Errors are recorded in
|
|
242
|
+
* `erroredShards` with phase 'register'; one failure does not block others.
|
|
243
|
+
*/
|
|
244
|
+
export async function registerAllShards() {
|
|
245
|
+
for (const [id, shard] of registeredShards) {
|
|
246
|
+
if (shardEntries.has(id))
|
|
247
|
+
continue;
|
|
248
|
+
const entry = {
|
|
249
|
+
shard,
|
|
250
|
+
ctx: undefined,
|
|
251
|
+
viewIds: new Set(),
|
|
252
|
+
verbNames: new Set(),
|
|
253
|
+
bootCleanupFns: [],
|
|
254
|
+
appCleanupBags: new Map(),
|
|
255
|
+
activeAppId: null,
|
|
256
|
+
};
|
|
257
|
+
const ctx = buildShardContext(shard, entry);
|
|
258
|
+
entry.ctx = ctx;
|
|
259
|
+
shardEntries.set(id, entry);
|
|
260
|
+
activeShards.set(id, shard);
|
|
261
|
+
try {
|
|
262
|
+
await shard.register(ctx);
|
|
263
|
+
for (const view of shard.manifest.views) {
|
|
264
|
+
if (!entry.viewIds.has(view.id)) {
|
|
265
|
+
throw new Error(`Shard "${id}" declared view "${view.id}" in its manifest but registered no factory for it.`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Wire onKeyRevoked subscription per shard at register time. Only
|
|
269
|
+
// shards that declare the hook incur the subscription overhead.
|
|
270
|
+
if (shard.onKeyRevoked) {
|
|
271
|
+
const off = subscribeKeyRevocation(id, async (keyId) => {
|
|
272
|
+
try {
|
|
273
|
+
await shard.onKeyRevoked(ctx, keyId);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
console.error(`[sh3] onKeyRevoked failed in "${id}":`, err);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
entry.bootCleanupFns.push(() => off());
|
|
280
|
+
}
|
|
281
|
+
// Env hydrate parity with v2.
|
|
282
|
+
const envState = ctx.__envState;
|
|
283
|
+
if ((envState === null || envState === void 0 ? void 0 : envState.proxy) && envState.defaults) {
|
|
284
|
+
try {
|
|
285
|
+
const stored = await fetchEnvState(id);
|
|
286
|
+
const merged = Object.assign({}, envState.defaults, stored);
|
|
287
|
+
Object.assign(envState.proxy, merged);
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
console.warn(`[sh3] Failed to hydrate env state for shard "${id}":`, err instanceof Error ? err.message : err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
erroredShards.delete(id);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
for (const fn of entry.bootCleanupFns) {
|
|
297
|
+
try {
|
|
298
|
+
void fn();
|
|
299
|
+
}
|
|
300
|
+
catch ( /* swallow */_a) { /* swallow */ }
|
|
301
|
+
}
|
|
302
|
+
for (const name of entry.verbNames)
|
|
303
|
+
fwUnregisterVerb(name);
|
|
304
|
+
for (const viewId of entry.viewIds)
|
|
305
|
+
unregisterView(viewId);
|
|
306
|
+
clearSelectionForShard(id);
|
|
307
|
+
shardEntries.delete(id);
|
|
308
|
+
activeShards.delete(id);
|
|
309
|
+
erroredShards.set(id, { id, error: err, phase: 'register', timestamp: Date.now() });
|
|
310
|
+
console.error(`[sh3] Shard "${id}" failed to register:`, err);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
export async function runAppActivate(shardId, appId) {
|
|
315
|
+
const entry = shardEntries.get(shardId);
|
|
316
|
+
if (!entry)
|
|
317
|
+
return;
|
|
318
|
+
rotateShardDocumentNamespace(shardId, appId);
|
|
319
|
+
if (entry.shard.onAppActivate) {
|
|
320
|
+
entry.activeAppId = appId;
|
|
321
|
+
try {
|
|
322
|
+
await entry.shard.onAppActivate(entry.ctx, appId);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
entry.activeAppId = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export async function runAppDeactivate(shardId, appId) {
|
|
330
|
+
const entry = shardEntries.get(shardId);
|
|
331
|
+
if (!entry)
|
|
332
|
+
return;
|
|
333
|
+
if (entry.shard.onAppDeactivate) {
|
|
334
|
+
try {
|
|
335
|
+
await entry.shard.onAppDeactivate(entry.ctx, appId);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
console.error(`[sh3] onAppDeactivate for "${shardId}" / "${appId}" threw:`, err);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const bag = entry.appCleanupBags.get(appId);
|
|
342
|
+
if (bag) {
|
|
343
|
+
for (const fn of bag) {
|
|
344
|
+
try {
|
|
345
|
+
await fn();
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
console.error(`[sh3] app-cleanup disposer threw:`, err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
entry.appCleanupBags.delete(appId);
|
|
352
|
+
}
|
|
353
|
+
rotateShardDocumentNamespace(shardId, null);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Tear down the active entry for a shard and rebuild it from the current
|
|
357
|
+
* `registeredShards.get(id)` value. Used by `registerShard` when replacing
|
|
358
|
+
* an existing shard with a fresh module (package update, dev hot-reload).
|
|
359
|
+
*
|
|
360
|
+
* Fires `onAppDeactivate` for every bound app, calls `deactivate?.()`,
|
|
361
|
+
* flushes all cleanup bags, builds a fresh ctx, then re-runs `register()`.
|
|
362
|
+
* Caller is responsible for re-invoking `runAppActivate` for any apps
|
|
363
|
+
* currently active that require this shard.
|
|
364
|
+
*/
|
|
365
|
+
export async function rebuildShardEntry(shardId) {
|
|
366
|
+
var _a, _b;
|
|
367
|
+
const old = shardEntries.get(shardId);
|
|
368
|
+
if (old) {
|
|
369
|
+
for (const appId of old.appCleanupBags.keys()) {
|
|
370
|
+
await runAppDeactivate(shardId, appId);
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await ((_b = (_a = old.shard).deactivate) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
console.error(err);
|
|
377
|
+
}
|
|
378
|
+
for (const fn of old.bootCleanupFns) {
|
|
379
|
+
try {
|
|
380
|
+
await fn();
|
|
381
|
+
}
|
|
382
|
+
catch ( /* swallow */_c) { /* swallow */ }
|
|
383
|
+
}
|
|
384
|
+
for (const name of old.verbNames)
|
|
385
|
+
fwUnregisterVerb(name);
|
|
386
|
+
for (const viewId of old.viewIds)
|
|
387
|
+
unregisterView(viewId);
|
|
388
|
+
clearSelectionForShard(shardId);
|
|
389
|
+
shardEntries.delete(shardId);
|
|
390
|
+
activeShards.delete(shardId);
|
|
391
|
+
}
|
|
392
|
+
const fresh = registeredShards.get(shardId);
|
|
393
|
+
if (!fresh)
|
|
394
|
+
return;
|
|
395
|
+
const entry = {
|
|
396
|
+
shard: fresh,
|
|
397
|
+
ctx: undefined,
|
|
398
|
+
viewIds: new Set(),
|
|
399
|
+
verbNames: new Set(),
|
|
400
|
+
bootCleanupFns: [],
|
|
401
|
+
appCleanupBags: new Map(),
|
|
402
|
+
activeAppId: null,
|
|
403
|
+
};
|
|
404
|
+
entry.ctx = buildShardContext(fresh, entry);
|
|
405
|
+
shardEntries.set(shardId, entry);
|
|
406
|
+
activeShards.set(shardId, fresh);
|
|
407
|
+
await fresh.register(entry.ctx);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Register (or re-register) a shard. Records the shard in `registeredShards`.
|
|
411
|
+
* If the shard is already active (in `shardEntries`), triggers a hot-swap
|
|
412
|
+
* via `rebuildShardEntry` so the new module replaces the old one cleanly.
|
|
413
|
+
*/
|
|
414
|
+
export function registerShard(shard) {
|
|
415
|
+
const id = shard.manifest.id;
|
|
416
|
+
const wasActive = shardEntries.has(id);
|
|
417
|
+
registeredShards.set(id, shard);
|
|
418
|
+
erroredShards.delete(id);
|
|
419
|
+
if (wasActive) {
|
|
420
|
+
void rebuildShardEntry(id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** True if the shard has been registered AND its register() has run. */
|
|
424
|
+
export function isActive(id) {
|
|
425
|
+
return shardEntries.has(id);
|
|
426
|
+
}
|
|
427
|
+
/** Return the ShardContext for an active shard, or undefined. */
|
|
428
|
+
export function getShardContext(id) {
|
|
429
|
+
var _a;
|
|
430
|
+
return (_a = shardEntries.get(id)) === null || _a === void 0 ? void 0 : _a.ctx;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Enumerate every view declared as `standalone` across the currently
|
|
434
|
+
* registered/active shards.
|
|
435
|
+
*/
|
|
436
|
+
export function listStandaloneViews() {
|
|
437
|
+
const out = [];
|
|
438
|
+
for (const shard of activeShards.values()) {
|
|
439
|
+
for (const view of shard.manifest.views) {
|
|
440
|
+
if (view.standalone) {
|
|
441
|
+
out.push({ shardId: shard.manifest.id, viewId: view.id, label: view.label });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
/** Test-only reset. */
|
|
448
|
+
export function __resetLifecycleForTest() {
|
|
449
|
+
shardAppBindings.clear();
|
|
450
|
+
shardEntries.clear();
|
|
451
|
+
activeShards.clear();
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Test-only reset for the full shard registry. Wipes all live entries,
|
|
455
|
+
* registered shards, and error records.
|
|
456
|
+
*/
|
|
457
|
+
export function __resetShardRegistryForTest() {
|
|
458
|
+
shardAppBindings.clear();
|
|
459
|
+
shardEntries.clear();
|
|
460
|
+
activeShards.clear();
|
|
461
|
+
registeredShards.clear();
|
|
462
|
+
erroredShards.clear();
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* @deprecated v2 compat shim — use `registerAllShards()` to run register
|
|
466
|
+
* for every registered shard. This shim activates a single shard by
|
|
467
|
+
* invoking its register hook directly. Retained for test fixtures during
|
|
468
|
+
* the migration; Phase 7 sweeps callers.
|
|
469
|
+
*/
|
|
470
|
+
export async function activateShard(id) {
|
|
471
|
+
if (shardEntries.has(id))
|
|
472
|
+
return;
|
|
473
|
+
const shard = registeredShards.get(id);
|
|
474
|
+
if (!shard)
|
|
475
|
+
throw new Error(`Cannot activate shard "${id}": not registered`);
|
|
476
|
+
const entry = {
|
|
477
|
+
shard,
|
|
478
|
+
ctx: undefined,
|
|
479
|
+
viewIds: new Set(),
|
|
480
|
+
verbNames: new Set(),
|
|
481
|
+
bootCleanupFns: [],
|
|
482
|
+
appCleanupBags: new Map(),
|
|
483
|
+
activeAppId: null,
|
|
484
|
+
};
|
|
485
|
+
const ctx = buildShardContext(shard, entry);
|
|
486
|
+
entry.ctx = ctx;
|
|
487
|
+
shardEntries.set(id, entry);
|
|
488
|
+
activeShards.set(id, shard);
|
|
489
|
+
try {
|
|
490
|
+
await shard.register(ctx);
|
|
491
|
+
for (const view of shard.manifest.views) {
|
|
492
|
+
if (!entry.viewIds.has(view.id)) {
|
|
493
|
+
throw new Error(`Shard "${id}" declared view "${view.id}" in its manifest but registered no factory for it.`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (shard.onKeyRevoked) {
|
|
497
|
+
const off = subscribeKeyRevocation(id, async (keyId) => {
|
|
498
|
+
try {
|
|
499
|
+
await shard.onKeyRevoked(ctx, keyId);
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
console.error(`[sh3] onKeyRevoked failed in "${id}":`, err);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
entry.bootCleanupFns.push(() => off());
|
|
506
|
+
}
|
|
507
|
+
erroredShards.delete(id);
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
for (const fn of entry.bootCleanupFns) {
|
|
511
|
+
try {
|
|
512
|
+
void fn();
|
|
513
|
+
}
|
|
514
|
+
catch ( /* swallow */_a) { /* swallow */ }
|
|
515
|
+
}
|
|
516
|
+
for (const name of entry.verbNames)
|
|
517
|
+
fwUnregisterVerb(name);
|
|
518
|
+
for (const viewId of entry.viewIds)
|
|
519
|
+
unregisterView(viewId);
|
|
520
|
+
clearSelectionForShard(id);
|
|
521
|
+
shardEntries.delete(id);
|
|
522
|
+
activeShards.delete(id);
|
|
523
|
+
erroredShards.set(id, { id, error: err, phase: 'register', timestamp: Date.now() });
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* @deprecated v2 compat shim — in v3, shards stay alive for the whole
|
|
529
|
+
* session. This shim performs a full teardown of the entry (for tests
|
|
530
|
+
* that explicitly want to verify cleanup paths).
|
|
531
|
+
*/
|
|
532
|
+
export function deactivateShard(id) {
|
|
533
|
+
var _a, _b;
|
|
534
|
+
const entry = shardEntries.get(id);
|
|
535
|
+
if (!entry)
|
|
536
|
+
return;
|
|
537
|
+
void ((_b = (_a = entry.shard).deactivate) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
538
|
+
for (const fn of entry.bootCleanupFns) {
|
|
539
|
+
try {
|
|
540
|
+
void fn();
|
|
541
|
+
}
|
|
542
|
+
catch ( /* swallow */_c) { /* swallow */ }
|
|
543
|
+
}
|
|
544
|
+
for (const name of entry.verbNames)
|
|
545
|
+
fwUnregisterVerb(name);
|
|
546
|
+
for (const viewId of entry.viewIds)
|
|
547
|
+
unregisterView(viewId);
|
|
548
|
+
clearSelectionForShard(id);
|
|
549
|
+
shardEntries.delete(id);
|
|
550
|
+
activeShards.delete(id);
|
|
551
|
+
}
|