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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-daemon-protocol",
"comment": "Add typed resolved phased-request, enabled-state selection, engine-shape, and client-scoped operation result contracts.",
"type": "minor"
}
],
"packageName": "@rushstack/rush-daemon-protocol",
"email": "mojazayeri@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-daemon",
"comment": "Add an opt-in phased request router that validates a caller-resolved selection, reconciles warm invalidations, runs one real graph iteration, scopes ordered streams and events to the client, and safely aborts on cancellation or disconnect.",
"type": "minor"
}
],
"packageName": "@rushstack/rush-daemon",
"email": "mojazayeri@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-terminal-renderer",
"comment": "Use authoritative daemon operation-header counters when collating partial warm iterations.",
"type": "patch"
}
],
"packageName": "@rushstack/rush-terminal-renderer",
"email": "mojazayeri@users.noreply.github.com"
}
3 changes: 3 additions & 0 deletions common/config/subspaces/default/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions common/reviews/api/rush-daemon-protocol.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export type DaemonJsonValue = string | number | boolean | DaemonJsonNull | reado
readonly [key: string]: DaemonJsonValue;
};

// @beta
export type DaemonPhasedOperationEnabledState = true | 'ignore-dependency-changes';

// @beta
export class DaemonProtocolError extends Error {
constructor(code: DaemonProtocolErrorCode, message: string, options?: IDaemonProtocolErrorOptions);
Expand Down Expand Up @@ -278,6 +281,41 @@ export interface IDaemonOperationStreamClosedPayload {
readonly operationId: string;
}

// @beta
export interface IDaemonPhasedEngineShape {
readonly phaseNames: ReadonlyArray<string>;
readonly pluginNames: ReadonlyArray<string>;
}

// @beta
export interface IDaemonPhasedOperationResult {
readonly errorMessage?: string;
readonly operationId: string;
readonly status: string;
}

// @beta
export interface IDaemonPhasedOperationSelection {
readonly enabledState: DaemonPhasedOperationEnabledState;
readonly operationId: string;
}

// @beta
export interface IDaemonPhasedRequest {
readonly commandName: string;
readonly engineShape: IDaemonPhasedEngineShape;
readonly operationSelection: ReadonlyArray<IDaemonPhasedOperationSelection>;
readonly requestId: string;
}

// @beta
export interface IDaemonPhasedRequestResult {
readonly aborted: boolean;
readonly operationResults: ReadonlyArray<IDaemonPhasedOperationResult>;
readonly requestId: string;
readonly scheduled: boolean;
}

// @beta
export interface IDaemonPingMessage {
// (undocumented)
Expand Down
18 changes: 18 additions & 0 deletions common/reviews/api/rush-daemon.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
/// <reference types="node" />

import type { GetInputsSnapshotAsyncFn } from '@microsoft/rush-lib';
import type { IDaemonEventEnvelope } from '@rushstack/rush-daemon-protocol';
import type { IDaemonPaths } from '@rushstack/rush-daemon-transport';
import type { IDaemonPhasedRequest } from '@rushstack/rush-daemon-protocol';
import type { IDaemonPhasedRequestResult } from '@rushstack/rush-daemon-protocol';
import type { IInputsSnapshot } from '@microsoft/rush-lib';
import type { IOperationGraph } from '@microsoft/rush-lib';
import type { Operation } from '@microsoft/rush-lib';
Expand Down Expand Up @@ -58,6 +61,15 @@ export interface IMapWorkspaceInvalidationsOptions {
readonly operationGraph: IOperationGraph;
}

// @beta
export interface IPhasedRequestClient {
readonly abortSignal: AbortSignal;
getNextEventSequence(): number;
readonly sessionId: string;
writeEventAsync(event: IDaemonEventEnvelope): Promise<void>;
writeLogChunkAsync(operationId: string, stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
}

// @public
export interface IRequestLease {
// (undocumented)
Expand Down Expand Up @@ -217,6 +229,12 @@ export interface IWorkspaceSessionOptions {
// @beta
export type MapWorkspaceInvalidationsToOperationsAsync = (options: IMapWorkspaceInvalidationsOptions) => Promise<Iterable<Operation>>;

// @beta
export class PhasedRequestRouter {
constructor(workspaceSession: IWorkspaceSession);
executeAsync(request: IDaemonPhasedRequest, client: IPhasedRequestClient): Promise<IDaemonPhasedRequestResult>;
}

// @public
export enum RequestExclusivityClass {
// (undocumented)
Expand Down
2 changes: 2 additions & 0 deletions common/reviews/api/rush-terminal-renderer.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type { DaemonVerbosity } from '@rushstack/rush-daemon-protocol';
import type { IDaemonClientCaps } from '@rushstack/rush-daemon-protocol';
import type { IDaemonEventEnvelope } from '@rushstack/rush-daemon-protocol';
import type { IDaemonOperationHeaderPayload } from '@rushstack/rush-daemon-protocol';
import { ITerminalChunk } from '@rushstack/terminal';
import { TerminalWritable } from '@rushstack/terminal';

Expand Down Expand Up @@ -82,6 +83,7 @@ export class OperationStreamRegistry {
constructor(options: IOperationStreamRegistryOptions);
closeOperation(operationId: string): void;
registerOperation(): void;
setOperationHeader(header: IDaemonOperationHeaderPayload): void;
writeChunk(operationId: string, chunk: ITerminalChunk): void;
}

Expand Down
3 changes: 3 additions & 0 deletions libraries/rush-daemon-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ The engine-agnostic **wire layer** spoken by every client of the Rush daemon (`r
reference when the reporter package lands) plus namespaced `rushd.*` extension events.
- **Per-subscription verbosity** — a pure filter applied at event serialization so each
client receives its own verbosity subset without mutating shared engine state.
- **Resolved phased-request contracts** — engine-agnostic request, enabled-state selection,
and client-scoped result types for integrations that have already parsed a command and
resolved it against a real warm operation graph.

Part of the Rush 6 / rushd re-architecture:
[microsoft/rushstack#5894](https://github.com/microsoft/rushstack/issues/5894).
Expand Down
83 changes: 83 additions & 0 deletions libraries/rush-daemon-protocol/src/DaemonPhasedRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

/**
* The enabled state assigned to one selected operation by a phased request.
*
* @beta
*/
export type DaemonPhasedOperationEnabledState = true | 'ignore-dependency-changes';

/**
* One caller-resolved operation selection.
*
* @remarks
* Operation identifiers come from the integration-owned real operation graph. Command-line parsing and graph
* construction remain outside the wire contract.
*
* @beta
*/
export interface IDaemonPhasedOperationSelection {
/** The non-disabled state to apply with the real graph's enabled-state API. */
readonly enabledState: DaemonPhasedOperationEnabledState;
/** The integration-resolved operation identifier. */
readonly operationId: string;
}

/**
* The explicit phase and plugin shape of the warm graph used by a phased request.
*
* @beta
*/
export interface IDaemonPhasedEngineShape {
/** Every phase represented by the warm graph. */
readonly phaseNames: ReadonlyArray<string>;
/** Every plugin applied when the warm graph was constructed. */
readonly pluginNames: ReadonlyArray<string>;
}

/**
* A typed phased request after an integration has parsed the command and resolved its operation selection.
*
* @beta
*/
export interface IDaemonPhasedRequest {
/** The parsed phased command name. */
readonly commandName: string;
/** The exact warm engine shape against which the selection was resolved. */
readonly engineShape: IDaemonPhasedEngineShape;
/** The caller-resolved selected operations and their enabled states. */
readonly operationSelection: ReadonlyArray<IDaemonPhasedOperationSelection>;
/** A client-generated identifier unique within the connection. */
readonly requestId: string;
}

/**
* The client-scoped result for one selected operation.
*
* @beta
*/
export interface IDaemonPhasedOperationResult {
/** The operation identifier used by the request and its streamed output. */
readonly operationId: string;
/** The raw Rush operation status. */
readonly status: string;
/** The operation error message, when execution produced one. */
readonly errorMessage?: string;
}

/**
* The result of routing one phased request through a warm operation graph.
*
* @beta
*/
export interface IDaemonPhasedRequestResult {
/** Whether cancellation or disconnect aborted the iteration. */
readonly aborted: boolean;
/** Results only for operations enabled for this client. */
readonly operationResults: ReadonlyArray<IDaemonPhasedOperationResult>;
/** The identifier copied from the request. */
readonly requestId: string;
/** Whether the real graph scheduled work for this iteration. */
readonly scheduled: boolean;
}
8 changes: 8 additions & 0 deletions libraries/rush-daemon-protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,11 @@ export { decodeDaemonEventFrame, encodeDaemonEventFrame, serializeDaemonEventFor
export type { IDaemonActivityPayload, IDaemonOperationRegisteredPayload, IDaemonOperationStatusChangedPayload } from './DaemonOperationPayloads';
export { RUSHD_OPERATION_HEADER, RUSHD_OPERATION_STREAM_CLOSED } from './DaemonRushdExtensions';
export type { IDaemonExtensionEventPayload, IDaemonOperationHeaderPayload, IDaemonOperationStreamClosedPayload } from './DaemonRushdExtensions';
export type {
DaemonPhasedOperationEnabledState,
IDaemonPhasedEngineShape,
IDaemonPhasedOperationResult,
IDaemonPhasedOperationSelection,
IDaemonPhasedRequest,
IDaemonPhasedRequestResult
} from './DaemonPhasedRequest';
13 changes: 13 additions & 0 deletions libraries/rush-daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,16 @@ no paths to classify and therefore remains a full invalidation. The routing laye
workspace session rather than run a stale graph.
The default daemon executable does not construct or route this graph while the command-independent plugin shape and per-iteration runner
lifetime tracked by [rushstack#5895](https://github.com/microsoft/rushstack/issues/5895) remain incomplete.

`PhasedRequestRouter` is the opt-in execution boundary once an integration has supplied that real warm graph. The
integration parses the command and supplies an explicit phase/plugin shape plus operation enabled-state selection;
the router validates both, reconciles retained invalidations, applies the selection with `IOperationGraph.setEnabledStates`,
and runs at most one scheduled iteration. Requests are serialized until shared-build merging is implemented. A
requesting client receives only its enabled dependency closure's WS1 raw chunks and structured events through
backpressured, ordered callbacks, followed by client-scoped operation results. Cancellation or disconnect aborts the
current iteration without closing daemon-owned runners or the graph.

This layer deliberately does not add control-frame admission or reconstruct `PhasedScriptAction` command/plugin
initialization. The typed phased request contract begins after an integration has produced a validated selection for
the exact warm engine shape; full command parsing remains blocked by
[rushstack#5895](https://github.com/microsoft/rushstack/issues/5895).
3 changes: 2 additions & 1 deletion libraries/rush-daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"@microsoft/rush-lib": "workspace:*",
"@rushstack/node-core-library": "workspace:*",
"@rushstack/rush-daemon-protocol": "workspace:*",
"@rushstack/rush-daemon-transport": "workspace:*"
"@rushstack/rush-daemon-transport": "workspace:*",
"@rushstack/terminal": "workspace:*"
},
"devDependencies": {
"@rushstack/heft": "workspace:*",
Expand Down
33 changes: 33 additions & 0 deletions libraries/rush-daemon/src/PhasedRequestClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { IDaemonEventEnvelope } from '@rushstack/rush-daemon-protocol';

/**
* A client-scoped destination for one routed phased request.
*
* @remarks
* The client must abort `abortSignal` when its request is cancelled or its connection closes. Writes are invoked
* serially in engine order; each promise provides the destination's backpressure boundary.
*
* @beta
*/
export interface IPhasedRequestClient {
/** Aborted by the transport when the request is cancelled or disconnected. */
readonly abortSignal: AbortSignal;
/** The connection session identifier used in structured event envelopes. */
readonly sessionId: string;

/** Returns the next structured-event sequence number for this connection. */
getNextEventSequence(): number;

/** Writes one structured event through the client's backpressured destination. */
writeEventAsync(event: IDaemonEventEnvelope): Promise<void>;

/** Writes one operation-scoped output chunk through the client's backpressured destination. */
writeLogChunkAsync(
operationId: string,
stream: 'stdout' | 'stderr',
chunk: Uint8Array
): Promise<void>;
}
68 changes: 68 additions & 0 deletions libraries/rush-daemon/src/PhasedRequestEventMultiplexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type {
IOperationExecutionResult,
OperationStatus,
_IOperationActivityOptions,
_IOperationGraphEventSink
} from '@microsoft/rush-lib';
import type { ITerminalChunk } from '@rushstack/terminal';

export class PhasedRequestEventMultiplexer implements _IOperationGraphEventSink {
readonly #workspaceSink: _IOperationGraphEventSink | undefined;
#requestSink: _IOperationGraphEventSink | undefined;

public constructor(workspaceSink: _IOperationGraphEventSink | undefined) {
this.#workspaceSink = workspaceSink;
}

public subscribe(requestSink: _IOperationGraphEventSink): () => void {
if (this.#requestSink) {
throw new Error('A phased request event subscription is already active.');
}
this.#requestSink = requestSink;
let subscribed: boolean = true;
return () => {
if (subscribed) {
subscribed = false;
if (this.#requestSink === requestSink) {
this.#requestSink = undefined;
}
}
};
}

public onOperationRegistered(operationId: string, silent: boolean): void {
this.#workspaceSink?.onOperationRegistered?.(operationId, silent);
this.#requestSink?.onOperationRegistered?.(operationId, silent);
}

public onOperationStatusChanged(
result: IOperationExecutionResult,
previousStatus: OperationStatus
): void {
this.#workspaceSink?.onOperationStatusChanged?.(result, previousStatus);
this.#requestSink?.onOperationStatusChanged?.(result, previousStatus);
}

public onOperationHeader(operationId: string, completed: number, total: number): void {
this.#workspaceSink?.onOperationHeader?.(operationId, completed, total);
this.#requestSink?.onOperationHeader?.(operationId, completed, total);
}

public onOperationChunk(operationId: string, chunk: ITerminalChunk): void {
this.#workspaceSink?.onOperationChunk?.(operationId, chunk);
this.#requestSink?.onOperationChunk?.(operationId, chunk);
}

public onOperationStreamClosed(operationId: string): void {
this.#workspaceSink?.onOperationStreamClosed?.(operationId);
this.#requestSink?.onOperationStreamClosed?.(operationId);
}

public onActivity(text: string, options?: _IOperationActivityOptions): void {
this.#workspaceSink?.onActivity?.(text, options);
this.#requestSink?.onActivity?.(text, options);
}
}
Loading