Skip to content

Commit 6dff62c

Browse files
committed
OpenService: Store delegated mode on the realm-global inventory
Two import paths of the registry in one realm now share the same flag, so an attached caller cannot register services that still dispatch locally.
1 parent f64cb2e commit 6dff62c

3 files changed

Lines changed: 44 additions & 17 deletions

File tree

code/core/src/shared/open-service/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -710,8 +710,10 @@ Every registered runtime plays **both** roles at once, decided per command:
710710
validates input, mutates state, and broadcasts the post-mutation snapshot through the normal command
711711
wrappers so every peer converges — then emits `services:command-result` or `services:command-error`.
712712

713-
A runtime never requests a command it implements (it runs that locally), so a responder never answers
714-
its own invoke echo: `onInvoke` only acts on commands in its `implementedCommandNames` set.
713+
Outside [delegated mode](#delegated-mode), a runtime never requests a command it implements (it runs
714+
that locally), so a responder never answers its own invoke echo: `onInvoke` only acts on commands in
715+
its `implementedCommandNames` set. A delegated runtime does request commands it implements — that is
716+
how dispatch reaches the Storybook it attached to.
715717

716718
### Events
717719

@@ -805,10 +807,12 @@ rejects outstanding calls with `OpenServiceRemoteCommandDisconnectedError`.
805807
### Delegated mode
806808

807809
A runtime that attaches to an already-running Storybook (rather than starting its own) must not
808-
execute anything itself: the Storybook it attached to owns the story index, the module graph, and the
809-
providers, so it is the implementer for every command. `setDelegatedMode(true)` in
810-
[service-registry.ts](./service-registry.ts) puts the whole runtime in that role; `isDelegatedMode()`
811-
reads it and the default is `false`.
810+
dispatch commands locally: the Storybook it attached to owns the story index, the module graph, and
811+
the providers, so it is the implementer for every command. Loads, toolset methods, and query handlers
812+
still run in this process. `setDelegatedMode(true)` in [service-registry.ts](./service-registry.ts)
813+
puts command dispatch in that role; `isDelegatedMode()` reads it and the default is `false`. The flag
814+
lives on the same realm-global inventory as the service map, so every import path in one realm sees
815+
the same value.
812816

813817
What changes is **dispatch**, not registration:
814818

code/core/src/shared/open-service/service-delegated-mode.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ describe('delegated mode flag', () => {
9191
clearRegistry();
9292
expect(isDelegatedMode()).toBe(false);
9393
});
94+
95+
it('shares the flag with a separately loaded copy of this module', async () => {
96+
setDelegatedMode(true);
97+
98+
vi.resetModules();
99+
const other = await import('./service-registry.ts');
100+
101+
expect(isDelegatedMode()).toBe(true);
102+
expect(other.isDelegatedMode()).toBe(true);
103+
104+
other.setDelegatedMode(false);
105+
expect(isDelegatedMode()).toBe(false);
106+
});
94107
});
95108

96109
describe('delegated command dispatch', () => {

code/core/src/shared/open-service/service-registry.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,34 @@ type RegistryEntry = {
5454

5555
const REGISTRY_SYMBOL = Symbol.for('storybook.open-service.registry');
5656

57+
type RegistryInventory = {
58+
entries: Map<string, RegistryEntry>;
59+
delegatedMode: boolean;
60+
};
61+
5762
/**
58-
* Returns the realm-global registry backing service registration.
63+
* Returns the realm-global inventory backing service registration and delegated mode.
5964
*
6065
* Lazily created so importing the module does not eagerly mutate global state. Anchoring it on a
61-
* `globalThis` symbol keeps runtime lookups, static builds, and tests pointed at one service inventory
62-
* even when the module is reached through different import paths.
66+
* `globalThis` symbol keeps the service map and the delegated-mode flag shared even when this file is
67+
* reached through different import paths.
6368
*/
64-
function getRegistry(): Map<string, RegistryEntry> {
69+
function getInventory(): RegistryInventory {
6570
const registryGlobal = globalThis as {
66-
[key: symbol]: Map<string, RegistryEntry> | undefined;
71+
[key: symbol]: RegistryInventory | undefined;
6772
};
6873

69-
registryGlobal[REGISTRY_SYMBOL] ??= new Map<string, RegistryEntry>();
74+
registryGlobal[REGISTRY_SYMBOL] ??= {
75+
entries: new Map<string, RegistryEntry>(),
76+
delegatedMode: false,
77+
};
7078

7179
return registryGlobal[REGISTRY_SYMBOL];
7280
}
7381

74-
let delegatedMode = false;
82+
function getRegistry(): Map<string, RegistryEntry> {
83+
return getInventory().entries;
84+
}
7585

7686
/**
7787
* Marks this runtime as delegated, so every registered service dispatches its commands over the
@@ -83,12 +93,12 @@ let delegatedMode = false;
8393
* attached to is the implementer.
8494
*/
8595
export function setDelegatedMode(enabled: boolean): void {
86-
delegatedMode = enabled;
96+
getInventory().delegatedMode = enabled;
8797
}
8898

8999
/** Whether this runtime delegates command dispatch to the Storybook it is attached to. */
90100
export function isDelegatedMode(): boolean {
91-
return delegatedMode;
101+
return getInventory().delegatedMode;
92102
}
93103

94104
function assertUniqueOperationNames(definition: AnyServiceDefinition): void {
@@ -316,7 +326,7 @@ export function registerService<
316326
commands: runtime.commands as Record<string, (input: unknown) => Promise<unknown>>,
317327
implementedCommandNames,
318328
commandNames: Object.keys(resolvedDefinition.commands),
319-
delegated: delegatedMode,
329+
delegated: isDelegatedMode(),
320330
runtime,
321331
});
322332

@@ -420,5 +430,5 @@ export function clearRegistry(): void {
420430
}
421431

422432
registry.clear();
423-
delegatedMode = false;
433+
getInventory().delegatedMode = false;
424434
}

0 commit comments

Comments
 (0)