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.
Files changed (80) hide show
  1. package/dist/__test__/fixtures.js +1 -1
  2. package/dist/__test__/reset.js +1 -3
  3. package/dist/__test__/smoke.test.js +2 -2
  4. package/dist/actions/contextMenuModel.test.js +6 -3
  5. package/dist/actions/ctx-actions.svelte.test.js +9 -9
  6. package/dist/actions/dispatcher-v3.test.js +8 -0
  7. package/dist/actions/dispatcher.svelte.d.ts +1 -2
  8. package/dist/actions/dispatcher.svelte.js +6 -7
  9. package/dist/actions/dispatcher.test.js +9 -12
  10. package/dist/actions/listActionsFromEntries.test.js +1 -2
  11. package/dist/actions/listActive.test.js +2 -3
  12. package/dist/actions/menuBarModel.test.js +1 -7
  13. package/dist/actions/paletteModel.test.js +1 -3
  14. package/dist/actions/scope-helpers.test.js +4 -4
  15. package/dist/actions/shardContext.test.js +2 -2
  16. package/dist/actions/state.svelte.d.ts +12 -2
  17. package/dist/actions/state.svelte.js +15 -12
  18. package/dist/actions/state.test.js +4 -4
  19. package/dist/api.d.ts +4 -3
  20. package/dist/api.js +1 -1
  21. package/dist/app/admin/adminShard.svelte.js +1 -1
  22. package/dist/app/store/storeShard.svelte.js +10 -5
  23. package/dist/app-appearance/appearanceShard.svelte.js +1 -5
  24. package/dist/apps/lifecycle.js +49 -64
  25. package/dist/apps/lifecycle.test.js +30 -76
  26. package/dist/conflicts/adapter-documents.js +1 -2
  27. package/dist/createShell.js +1 -1
  28. package/dist/documents/handle.d.ts +9 -4
  29. package/dist/documents/handle.js +40 -29
  30. package/dist/documents/handle.test.js +60 -51
  31. package/dist/documents/index.d.ts +1 -1
  32. package/dist/documents/types.d.ts +16 -26
  33. package/dist/host.d.ts +1 -1
  34. package/dist/host.js +9 -56
  35. package/dist/host.svelte.test.js +31 -63
  36. package/dist/layouts-shard/LayoutsSection.svelte +1 -1
  37. package/dist/layouts-shard/layoutsShard.svelte.js +2 -5
  38. package/dist/layouts-shard/layoutsShard.svelte.test.js +2 -2
  39. package/dist/projects-shard/projectsShard.svelte.js +1 -5
  40. package/dist/registry/installer.js +1 -1
  41. package/dist/registry/loader.d.ts +1 -1
  42. package/dist/registry/loader.js +3 -3
  43. package/dist/registry/permission-descriptions.test.js +2 -2
  44. package/dist/registry/register.js +1 -1
  45. package/dist/registry/register.test.js +1 -1
  46. package/dist/runtime/runVerb-shell.test.js +1 -1
  47. package/dist/runtime/runVerb.js +2 -2
  48. package/dist/runtime/runVerb.test.js +9 -9
  49. package/dist/server-shard/types.d.ts +56 -0
  50. package/dist/sh3Api/headless.js +1 -1
  51. package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -6
  52. package/dist/shards/ctx-fetch.test.js +9 -9
  53. package/dist/shards/lifecycle.svelte.d.ts +108 -0
  54. package/dist/shards/lifecycle.svelte.js +551 -0
  55. package/dist/shards/lifecycle.test.js +139 -0
  56. package/dist/shards/types.d.ts +30 -63
  57. package/dist/shell-shard/shellShard.svelte.js +1 -4
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +1 -1
  61. package/dist/shards/activate-browse.test.js +0 -120
  62. package/dist/shards/activate-contributions.test.js +0 -141
  63. package/dist/shards/activate-error-isolation.test.d.ts +0 -1
  64. package/dist/shards/activate-error-isolation.test.js +0 -98
  65. package/dist/shards/activate-fields.svelte.test.d.ts +0 -1
  66. package/dist/shards/activate-fields.svelte.test.js +0 -121
  67. package/dist/shards/activate-on-key-revoked.test.d.ts +0 -1
  68. package/dist/shards/activate-on-key-revoked.test.js +0 -60
  69. package/dist/shards/activate-runtime.test.d.ts +0 -1
  70. package/dist/shards/activate-runtime.test.js +0 -344
  71. package/dist/shards/activate-scopeid.test.d.ts +0 -1
  72. package/dist/shards/activate-scopeid.test.js +0 -21
  73. package/dist/shards/activate.svelte.d.ts +0 -102
  74. package/dist/shards/activate.svelte.js +0 -407
  75. package/dist/shards/app-binding.svelte.d.ts +0 -8
  76. package/dist/shards/app-binding.svelte.js +0 -30
  77. package/dist/shards/app-binding.test.d.ts +0 -1
  78. package/dist/shards/app-binding.test.js +0 -25
  79. /package/dist/{shards/activate-browse.test.d.ts → actions/dispatcher-v3.test.d.ts} +0 -0
  80. /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
- activate(ctx) {
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
  },
@@ -13,8 +13,8 @@
13
13
  */
14
14
  import { createStateZones } from '../state/zones.svelte';
15
15
  import { createGestureRegistry } from '../gestures';
16
- import { activateShard, deactivateShard, getShardContext, registeredShards, } from '../shards/activate.svelte';
17
- import { bindShardToApp, clearShardBinding } from '../shards/app-binding.svelte';
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, getDocumentBackend } from '../documents/config';
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, _h;
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 shard = registeredShards.get(shardId);
117
- const shardCtx = getShardContext(shardId);
118
- if (shard && shardCtx)
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 ((_b = app.resume) === null || _b === void 0 ? void 0 : _b.call(app, getOrCreateAppContext(id)));
119
+ void ((_a = app.resume) === null || _a === void 0 ? void 0 : _a.call(app, getOrCreateAppContext(id)));
122
120
  switchToApp();
123
- void ((_c = app.onAppReady) === null || _c === void 0 ? void 0 : _c.call(app, getOrCreateAppContext(id)));
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((_d = app.manifest.requiredShards) !== null && _d !== void 0 ? _d : []));
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
- await activateShard(shardId, { phase: 'launch' });
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
- const shard = registeredShards.get(shardId);
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 "${(_e = app.manifest.label) !== null && _e !== void 0 ? _e : id}": ${err instanceof Error ? err.message : String(err)}`, { level: 'error', duration: 6000 });
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 shard = registeredShards.get(shardId);
188
- if (shard === null || shard === void 0 ? void 0 : shard.onLayoutWillRestore) {
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 ((_f = app.activate) === null || _f === void 0 ? void 0 : _f.call(app, getOrCreateAppContext(id, undefined, opts.args)));
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((_g = app.manifest.requiredShards) !== null && _g !== void 0 ? _g : []));
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 shard = registeredShards.get(shardId);
203
- if (shard === null || shard === void 0 ? void 0 : shard.onLayoutRestored) {
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 ((_h = app.onAppReady) === null || _h === void 0 ? void 0 : _h.call(app, getOrCreateAppContext(id)));
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
- // Notify shards that the app is deactivating so they can tear down app-scoped resources.
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
- const shard = registeredShards.get(shardId);
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 shard = registeredShards.get(shardId);
308
- if ((shard === null || shard === void 0 ? void 0 : shard.suspend) && (await shard.suspend()) === false)
309
- return false;
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/activate.svelte';
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
- activate: () => {
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
- activate: () => {
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
- activate: shardActivate,
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
- activate: shardActivate,
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
- activate: () => {
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/activate.svelte');
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/activate.svelte');
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/activate.svelte');
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
- activate: () => {
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
- activate() { },
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
- it('provides a documents() factory on AppActivateContext that uses appId namespace', async () => {
684
- const { MemoryDocumentBackend } = await import('../documents/backends');
685
- const { __setDocumentBackend } = await import('../documents/config');
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
- activate() { },
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
- activate() { },
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
- activate() { },
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
- activate(ctx) { handleA = ctx.documents({ format: 'text' }); },
804
+ register(ctx) { handleA = ctx.documents; },
828
805
  }));
829
806
  registerShard(makeShard({
830
807
  manifest: makeShardManifest({ id: 'shard-share-B' }),
831
- activate(ctx) { handleB = ctx.documents({ format: 'text' }); },
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.write('shared.json', '{"from":"A"}');
841
- expect(await handleB.read('shared.json')).toBe('{"from":"A"}');
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
- describe('launchApp document auto-scope autostart exception', () => {
848
- beforeEach(resetFramework);
849
- it('keeps autostart shards on shard-scoped namespace even when required by an active app', async () => {
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/app-binding.svelte');
834
+ const { getShardBinding } = await import('../shards/lifecycle.svelte');
881
835
  registerShard(makeShard({
882
836
  manifest: makeShardManifest({ id: 'binding-shard' }),
883
- activate() { },
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
- // No extension filter — the adapter takes caller-supplied paths, any type.
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),
@@ -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/activate.svelte';
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, DocumentHandleOptions } from './types';
1
+ import type { DocumentBackend, DocumentHandle } from './types';
2
2
  /**
3
- * Create a document handle scoped to a tenant, shard, and file filter.
4
- * The framework calls this from `ShardContext.documents()`.
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, options: DocumentHandleOptions): DocumentHandle;
11
+ export declare function createDocumentHandle(tenantId: string | (() => string), shardOrNamespace: string | (() => string), backend: DocumentBackend): DocumentHandle;
@@ -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, shard, and file filter.
24
- * The framework calls this from `ShardContext.documents()`.
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, options) {
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 baseNamespace = typeof shardOrNamespace === 'function' ? shardOrNamespace : () => shardOrNamespace;
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
- const all = await backend.list(tid, resolveNamespace());
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 read(path, opts) {
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
- // Phase 1: text format only. Binary returns as-is from the backend
61
- // but the handle types it as string for text-format handles.
62
- return typeof content === 'string' ? content : new TextDecoder().decode(content);
52
+ if (typeof content === 'string')
53
+ return content;
54
+ return new TextDecoder().decode(content);
63
55
  },
64
- async write(path, content, opts) {
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").write(__classPrivateFieldGet(this, _AutosaveControllerImpl_path, "f"), content);
248
+ await __classPrivateFieldGet(this, _AutosaveControllerImpl_handle, "f").writeText(__classPrivateFieldGet(this, _AutosaveControllerImpl_path, "f"), content);
238
249
  }
239
250
  }
240
251
  async dispose() {