Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/explorer.core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ React lifecycle and StrictMode concerns.
`ConfirmCopyMoveCommand` (`file-explorer:rename-prompt`, `mkdir-prompt`,
`confirm-delete`, `confirm-copy-move`) are declared as prompts that resolve on
confirm and reject on cancel, but no package registers a handler for them.
They are `Command.silent`, so a call never settles.
They use the `async` dispatch policy, so a call fails at once with a
`CommandError` with `kind: "no-handlers"`.

### Constraints

Expand Down
10 changes: 5 additions & 5 deletions packages/explorer.core/src/public/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface RenamePromptPayload {
* basename on confirm, rejects on cancel. The handler lives in
* `file-explorer-react` so the panel renderer owns the dialog.
*/
export const RenamePromptCommand = Command.silent("file-explorer:rename-prompt")
export const RenamePromptCommand = Command.async("file-explorer:rename-prompt")
.input(passthrough<RenamePromptPayload>())
.output(passthrough<{ name: string }>())
.build();
Expand All @@ -27,7 +27,7 @@ export interface MkdirPromptPayload {
* Open the mkdir prompt UI under `parentPath`. Resolves with the
* new directory name on confirm, rejects on cancel.
*/
export const MkdirPromptCommand = Command.silent("file-explorer:mkdir-prompt")
export const MkdirPromptCommand = Command.async("file-explorer:mkdir-prompt")
.input(passthrough<MkdirPromptPayload>())
.output(passthrough<{ name: string }>())
.build();
Expand All @@ -40,7 +40,7 @@ export interface ConfirmDeletePayload {
* on cancel. The actual deletion is performed by the caller via
* `files:delete-file`.
*/
export const ConfirmDeleteCommand = Command.silent("file-explorer:confirm-delete")
export const ConfirmDeleteCommand = Command.async("file-explorer:confirm-delete")
.input(passthrough<ConfirmDeletePayload>())
.output(passthrough<void>())
.build();
Expand All @@ -55,7 +55,7 @@ export interface ConfirmCopyMovePayload {
* rejects on cancel. The caller iterates and dispatches
* `files:move-file` (move) or single-file copy operations.
*/
export const ConfirmCopyMoveCommand = Command.silent("file-explorer:confirm-copy-move")
export const ConfirmCopyMoveCommand = Command.async("file-explorer:confirm-copy-move")
.input(passthrough<ConfirmCopyMovePayload>())
.output(passthrough<void>())
.build();
Expand All @@ -77,7 +77,7 @@ export interface NewFileExplorerPanelPayload {
* (the suffix after `file-explorer:`) so callers can compose with
* `dock:focus-panel` etc.
*/
export const NewFileExplorerPanelCommand = Command.silent("file-explorer:new-panel")
export const NewFileExplorerPanelCommand = Command.async("file-explorer:new-panel")
.input(passthrough<NewFileExplorerPanelPayload>())
.output(passthrough<{ panelId: string }>())
.build();
6 changes: 3 additions & 3 deletions packages/mime.core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ re-implementing the policy, so all callers agree on the winner.
`No mime-renderer registered for "<mime>"`.
- Unknown extension: the MIME type is `application/octet-stream`, which usually
has no renderer, so the error above names `application/octet-stream`.
- The commands are `Command.silent`: if no handler is registered (the fragment
was not booted, or for `files:open`, `@statewalker/explorer.core` was not),
the returned promise never settles. There is no error.
- If no handler is registered (the fragment was not booted, or for
`files:open`, `@statewalker/explorer.core` was not), the returned promise
rejects at once with a `CommandError` with `kind: "no-handlers"`.

### Constraints

Expand Down
4 changes: 2 additions & 2 deletions packages/mime.core/src/public/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface VisualizeFilePayload {
*/
referencePanelId?: string;
}
export const VisualizeFileCommand = Command.silent("files:visualize")
export const VisualizeFileCommand = Command.async("files:visualize")
.input(passthrough<VisualizeFilePayload>())
.output(passthrough<void>())
.build();
Expand All @@ -43,7 +43,7 @@ export interface OpenPayload {
* — directories navigate the panel identified by `target`, files
* delegate to `files:visualize`.
*/
export const OpenCommand = Command.silent("files:open")
export const OpenCommand = Command.async("files:open")
.input(passthrough<OpenPayload>())
.output(passthrough<void>())
.build();
10 changes: 4 additions & 6 deletions packages/platform.browser/tests/activate.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,16 @@ describe("platform.browser initPlatformWeb(ctx)", () => {
getCommands(ctx).call(PreferenceSetCommand, { key: "t", value: 1 }).promise,
).resolves.toBeUndefined();

// After cleanup, every platform command stays unsettled (no listener,
// silent policy leaves it pending so the caller can supply external
// resolution if needed).
// After cleanup no platform command has a handler left: a call fails at
// once with "no-handlers" instead of waiting forever.
await cleanup();
for (const key of PLATFORM_COMMAND_KEYS) {
const decl = Command.silent(key)
const decl = Command.async(key)
.input(passthrough<Record<string, unknown>>())
.output(passthrough<unknown>())
.build();
const cmd = getCommands(ctx).call(decl, {});
await Promise.resolve();
expect(cmd.settled).toBe(false);
await expect(cmd.promise).rejects.toMatchObject({ kind: "no-handlers", commandKey: key });
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ describe("copy-to-clipboard browser handler", () => {
}
});

it("unregister leaves subsequent fires unsettled (noop default)", () => {
it("after unregister a call fails at once with no-handlers", async () => {
const ctx = {};
const unregister = registerCopyToClipboardBrowser(getCommands(ctx));
unregister();
const command = getCommands(ctx).call(CopyToClipboardCommand, { text: "x" });
expect(command.settled).toBe(false);
await expect(command.promise).rejects.toMatchObject({ kind: "no-handlers" });
});
});
4 changes: 2 additions & 2 deletions packages/platform.core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ command.reject(new UserCancelledError());

## Internals

### A call with no handler waits forever
### A call with no handler fails at once

Every declaration uses the `silent` dispatch policy of `@statewalker/shared-commands`: when no handler is registered at call time, the call neither resolves nor rejects, and a handler registered later does not pick it up. On a host that does not implement a command (for example the pickers or the clipboard under `@statewalker/platform.node`), or when the call is made before the host fragment has started, `await ...promise` hangs with no error. Start the platform fragment before the fragments that call it, and guard calls with a timeout where the host may lack the command.
Every declaration uses the `async` dispatch policy of `@statewalker/shared-commands`: when no handler is registered at call time, the call rejects immediately with `CommandError` with `kind: "no-handlers"` (`commandKey` names the command). A handler registered later does not pick up earlier calls. This happens on a host that does not implement a command (for example the pickers or the clipboard under `@statewalker/platform.node`), or when the call is made before the host fragment has started: start the platform fragment before the fragments that call it, and catch the error where the host may lack the command. A handler that takes the call without settling it (a listener that only observes) still leaves it pending.

### Cancellation is a type, not a string

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface CopyToClipboardPayload {
text: string;
}

export const CopyToClipboardCommand = Command.silent(COPY_TO_CLIPBOARD_COMMAND_KEY)
export const CopyToClipboardCommand = Command.async(COPY_TO_CLIPBOARD_COMMAND_KEY)
.input(passthrough<CopyToClipboardPayload>())
.output(passthrough<void>())
.build();
2 changes: 1 addition & 1 deletion packages/platform.core/src/commands/download-blob/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface DownloadBlobPayload {
filename: string;
}

export const DownloadBlobCommand = Command.silent(DOWNLOAD_BLOB_COMMAND_KEY)
export const DownloadBlobCommand = Command.async(DOWNLOAD_BLOB_COMMAND_KEY)
.input(passthrough<DownloadBlobPayload>())
.output(passthrough<void>())
.build();
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface DownloadToFilesResult {
bytes: number;
}

export const DownloadToFilesCommand = Command.silent(DOWNLOAD_TO_FILES_COMMAND_KEY)
export const DownloadToFilesCommand = Command.async(DOWNLOAD_TO_FILES_COMMAND_KEY)
.input(passthrough<DownloadToFilesPayload>())
.output(passthrough<DownloadToFilesResult>())
.build();
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface PickDirectoryResult {
label: string;
}

export const PickDirectoryCommand = Command.silent(PICK_DIRECTORY_COMMAND_KEY)
export const PickDirectoryCommand = Command.async(PICK_DIRECTORY_COMMAND_KEY)
.input(passthrough<PickDirectoryPayload>())
.output(passthrough<PickDirectoryResult>())
.build();
2 changes: 1 addition & 1 deletion packages/platform.core/src/commands/pick-file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface PickFileResult {
names: string[];
}

export const PickFileCommand = Command.silent(PICK_FILE_COMMAND_KEY)
export const PickFileCommand = Command.async(PICK_FILE_COMMAND_KEY)
.input(passthrough<PickFilePayload>())
.output(passthrough<PickFileResult>())
.build();
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface PreferenceGetResult {
value: unknown | undefined;
}

export const PreferenceGetCommand = Command.silent(PREFERENCE_GET_COMMAND_KEY)
export const PreferenceGetCommand = Command.async(PREFERENCE_GET_COMMAND_KEY)
.input(passthrough<PreferenceGetPayload>())
.output(passthrough<PreferenceGetResult>())
.build();
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface PreferenceSetPayload {
value: unknown;
}

export const PreferenceSetCommand = Command.silent(PREFERENCE_SET_COMMAND_KEY)
export const PreferenceSetCommand = Command.async(PREFERENCE_SET_COMMAND_KEY)
.input(passthrough<PreferenceSetPayload>())
.output(passthrough<void>())
.build();
2 changes: 1 addition & 1 deletion packages/platform.node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Every get reads `/preferences.json` and every set reads, changes one key and wri

### Commands this package does not answer

Pickers, clipboard, `download-blob`, `download-to-files` and URL state have no Node handler. Their declarations use the `silent` policy, so a call to one of them on Node never settles: no result, no error. Do not await them in code that may run headless without a timeout.
Pickers, clipboard, `download-blob`, `download-to-files` and URL state have no Node handler. A call to one of them on Node rejects at once with a `CommandError` with `kind: "no-handlers"`; code that may run headless should catch it.

### Dependencies

Expand Down
2 changes: 1 addition & 1 deletion packages/render.core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ when its last panel closes, unless `meta.persistent === true`.
starts empty. Read and write failures log
`[layout-store] failed to read dock-layout.json` /
`[layout-store] failed to persist dock-layout.json`; `connect()` never rejects.
- The commands are `Command.silent`: without `initSpecStore`, their promises never settle.
- Without `initSpecStore`, a call rejects at once with a `CommandError` with `kind: "no-handlers"`.

### Constraints

Expand Down
10 changes: 5 additions & 5 deletions packages/render.core/src/internal/spec-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ describe("spec:create / spec:patch commands", () => {
const store = ws.requireAdapter(SpecStore);
const sizeBefore = countSpecs(store);
await cleanup();
// After cleanup, commands.call(CreateSpecCommand, ...) falls through to
// CreateSpecCommand's declared default (() => {}), which silently
// resolves with no payload — critically, no SpecStore.create is
// called anymore.
commands.call(CreateSpecCommand, { catalogId: "c", spec: 1 });
// After cleanup no handler is left: the call fails at once with
// "no-handlers", and no SpecStore.create is called anymore.
await expect(
commands.call(CreateSpecCommand, { catalogId: "c", spec: 1 }).promise,
).rejects.toMatchObject({ kind: "no-handlers", commandKey: "spec:create" });
expect(countSpecs(store)).toBe(sizeBefore);
});
});
Expand Down
4 changes: 2 additions & 2 deletions packages/render.core/src/public/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface CreateSpecResult {
specId: string;
}

export const CreateSpecCommand = Command.silent("spec:create")
export const CreateSpecCommand = Command.async("spec:create")
.input(passthrough<CreateSpecPayload>())
.output(passthrough<CreateSpecResult>())
.build();
Expand All @@ -21,7 +21,7 @@ export interface PatchSpecPayload {
patch: SpecPatch;
}

export const PatchSpecCommand = Command.silent("spec:patch")
export const PatchSpecCommand = Command.async("spec:patch")
.input(passthrough<PatchSpecPayload>())
.output(passthrough<void>())
.build();
4 changes: 2 additions & 2 deletions packages/settings.core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ across workspace `onLoad` / `onUnload` until `cleanup` runs.

### What breaks

- Both commands are `Command.silent`. If `initSettings` was not booted, the
promise returned by `commands.call(...)` never settles; nothing is logged.
- If `initSettings` was not booted, the promise returned by
`commands.call(...)` rejects at once with a `CommandError` with `kind: "no-handlers"`.
- State is in memory only. A reload opens with the dialog closed and no active tab.

### Dependencies
Expand Down
4 changes: 2 additions & 2 deletions packages/settings.core/src/public/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export interface OpenSettingsPayload {
* `SettingsManager`; consumers fire the command without importing
* the adapter directly.
*/
export const OpenSettingsCommand = Command.silent("settings:open")
export const OpenSettingsCommand = Command.async("settings:open")
.input(passthrough<OpenSettingsPayload>())
.output(passthrough<void>())
.build();

/** Close the settings dialog. */
export const CloseSettingsCommand = Command.silent("settings:close")
export const CloseSettingsCommand = Command.async("settings:close")
.input(passthrough<void>())
.output(passthrough<void>())
.build();
4 changes: 2 additions & 2 deletions packages/shell.core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ or HMR remount does not lose panels added since the last save.
- A layout that DockView cannot apply logs
`[chat-mini:dock] failed to restore in-memory layout` and falls back to the
saved layout. Save errors log `[chat-mini:dock] failed to persist layout`.
- The commands are `Command.silent`: without `initDock`, their promises never
settle.
- Without `initDock`, a call rejects at once with a `CommandError` with
`kind: "no-handlers"`.
- `onActivePanelChange` is not called on subscribe; read `getActivePanelId()`
first if you need the current value. Before mount, `getPanelIds()` is empty.

Expand Down
8 changes: 4 additions & 4 deletions packages/shell.core/src/public/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,23 @@ export interface ShowDockPanelPayload {
* (vision audit C7 / proposal §5.5) — the dock fragment is the only
* place that knows the panel kind is `"json"`.
*/
export const ShowDockPanelCommand = Command.silent("dock:show-panel")
export const ShowDockPanelCommand = Command.async("dock:show-panel")
.input(passthrough<ShowDockPanelPayload>())
.output(passthrough<void>())
.build();

export interface ClosePanelPayload {
panelId: string;
}
export const ClosePanelCommand = Command.silent("dock:close-panel")
export const ClosePanelCommand = Command.async("dock:close-panel")
.input(passthrough<ClosePanelPayload>())
.output(passthrough<void>())
.build();

export interface FocusPanelPayload {
panelId: string;
}
export const FocusPanelCommand = Command.silent("dock:focus-panel")
export const FocusPanelCommand = Command.async("dock:focus-panel")
.input(passthrough<FocusPanelPayload>())
.output(passthrough<void>())
.build();
Expand All @@ -55,7 +55,7 @@ export interface SetPanelTitlePayload {
* file tabs, etc. — so the dock fragment owns the mechanism rather
* than each catalog reinventing it.
*/
export const SetPanelTitleCommand = Command.silent("dock:set-panel-title")
export const SetPanelTitleCommand = Command.async("dock:set-panel-title")
.input(passthrough<SetPanelTitlePayload>())
.output(passthrough<void>())
.build();
2 changes: 1 addition & 1 deletion packages/webapp.browser/src/open-web-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface OpenWebAppPayload {
}

/** Command: host `project` and open (or focus) its web-app dock tab. */
export const OpenWebAppCommand = Command.silent("webapp:open")
export const OpenWebAppCommand = Command.async("webapp:open")
.input(passthrough<OpenWebAppPayload>())
.output(passthrough<void>())
.build();
Expand Down
4 changes: 2 additions & 2 deletions packages/workspace.browser/src/public/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type WorkspaceVoidResult = Record<string, never>;
* `requestPermission()` call is allowed by the browser.
*/
export const WORKSPACE_RECONNECT_COMMAND_KEY = "workspace:reconnect";
export const WorkspaceReconnectCommand = Command.silent(WORKSPACE_RECONNECT_COMMAND_KEY)
export const WorkspaceReconnectCommand = Command.async(WORKSPACE_RECONNECT_COMMAND_KEY)
.input(passthrough<WorkspaceVoidPayload>())
.output(passthrough<WorkspaceVoidResult>())
.build();
Expand All @@ -45,7 +45,7 @@ export const WorkspaceReconnectCommand = Command.silent(WORKSPACE_RECONNECT_COMM
* runtime is fully torn down before the next pick lands.
*/
export const WORKSPACE_DISCONNECT_COMMAND_KEY = "workspace:disconnect";
export const WorkspaceDisconnectCommand = Command.silent(WORKSPACE_DISCONNECT_COMMAND_KEY)
export const WorkspaceDisconnectCommand = Command.async(WORKSPACE_DISCONNECT_COMMAND_KEY)
.input(passthrough<WorkspaceVoidPayload>())
.output(passthrough<WorkspaceVoidResult>())
.build();
2 changes: 1 addition & 1 deletion packages/workspace.core/src/public/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface ChangeWorkspaceResult {
workspace: Workspace;
}

export const ChangeWorkspaceCommand = Command.silent(CHANGE_WORKSPACE_COMMAND_KEY)
export const ChangeWorkspaceCommand = Command.async(CHANGE_WORKSPACE_COMMAND_KEY)
.input(passthrough<ChangeWorkspacePayload>())
.output(passthrough<ChangeWorkspaceResult>())
.build();
Loading
Loading