@getuserfeedback/protocol 3.1.3 → 3.1.4

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.
@@ -0,0 +1,16 @@
1
+ export type FlowCommandHandleController<THandleId extends string, TSettlement> = {
2
+ runCommand<TPayload>(buildPayload: (handleId: THandleId | undefined) => TPayload, dispatchAndWait: (payload: TPayload) => Promise<TSettlement>): Promise<TSettlement>;
3
+ getHandleId(): THandleId | undefined;
4
+ hasHandleId(handleId: string): boolean;
5
+ hasPendingCommands(): boolean;
6
+ subscribeHandle(subscriber: (handleId: THandleId) => void): () => void;
7
+ clearResolvedHandle(): THandleId | undefined;
8
+ reset(): void;
9
+ };
10
+ export type CreateFlowCommandHandleControllerOptions<THandleId extends string, TSettlement> = {
11
+ getHandleIdFromSettlement: (settlement: TSettlement) => THandleId | undefined;
12
+ isStaleHandleError: (error: unknown) => boolean;
13
+ onClearResolvedHandle?: (handleId: THandleId) => void;
14
+ };
15
+ export declare function createFlowCommandHandleController<THandleId extends string, TSettlement>({ getHandleIdFromSettlement, isStaleHandleError, onClearResolvedHandle, }: CreateFlowCommandHandleControllerOptions<THandleId, TSettlement>): FlowCommandHandleController<THandleId, TSettlement>;
16
+ //# sourceMappingURL=flow-command-handle-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flow-command-handle-controller.d.ts","sourceRoot":"","sources":["../../src/host/flow-command-handle-controller.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,2BAA2B,CACtC,SAAS,SAAS,MAAM,EACxB,WAAW,IACR;IACH,UAAU,CAAC,QAAQ,EAClB,YAAY,EAAE,CAAC,QAAQ,EAAE,SAAS,GAAG,SAAS,KAAK,QAAQ,EAC3D,eAAe,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,WAAW,CAAC,GAC1D,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB,WAAW,IAAI,SAAS,GAAG,SAAS,CAAC;IACrC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,OAAO,CAAC;IAC9B,eAAe,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACvE,mBAAmB,IAAI,SAAS,GAAG,SAAS,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,wCAAwC,CACnD,SAAS,SAAS,MAAM,EACxB,WAAW,IACR;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,SAAS,GAAG,SAAS,CAAC;IAC9E,kBAAkB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAChD,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;CACtD,CAAC;AAEF,wBAAgB,iCAAiC,CAChD,SAAS,SAAS,MAAM,EACxB,WAAW,EACV,EACD,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,GACrB,EAAE,wCAAwC,CAC1C,SAAS,EACT,WAAW,CACX,GAAG,2BAA2B,CAAC,SAAS,EAAE,WAAW,CAAC,CA6CtD"}
@@ -0,0 +1,39 @@
1
+ import { createLazyHandleClient, } from "./lazy-handle-client.js";
2
+ export function createFlowCommandHandleController({ getHandleIdFromSettlement, isStaleHandleError, onClearResolvedHandle, }) {
3
+ const handleClient = createLazyHandleClient({
4
+ getHandleIdFromSettlement,
5
+ });
6
+ const clearResolvedHandle = () => {
7
+ const previousHandleId = handleClient.getHandleId();
8
+ if (!previousHandleId) {
9
+ return undefined;
10
+ }
11
+ handleClient.reset();
12
+ onClearResolvedHandle?.(previousHandleId);
13
+ return previousHandleId;
14
+ };
15
+ const reset = () => {
16
+ const previousHandleId = handleClient.getHandleId();
17
+ handleClient.reset();
18
+ if (previousHandleId) {
19
+ onClearResolvedHandle?.(previousHandleId);
20
+ }
21
+ };
22
+ const runCommand = async (buildPayload, dispatchAndWait) => {
23
+ return handleClient.runCommandWithRetry(buildPayload, dispatchAndWait, {
24
+ shouldRetry: isStaleHandleError,
25
+ onBeforeRetry: () => {
26
+ clearResolvedHandle();
27
+ },
28
+ });
29
+ };
30
+ return {
31
+ runCommand,
32
+ getHandleId: () => handleClient.getHandleId(),
33
+ hasHandleId: (handleId) => handleClient.getHandleId() === handleId,
34
+ hasPendingCommands: () => handleClient.hasPendingCommands(),
35
+ subscribeHandle: (subscriber) => handleClient.subscribeHandle(subscriber),
36
+ clearResolvedHandle,
37
+ reset,
38
+ };
39
+ }
@@ -6,6 +6,7 @@ export * from "./command-envelope.js";
6
6
  export * from "./command-settlement.js";
7
7
  export * from "./constants.js";
8
8
  export * from "./external-id-argument-guards.js";
9
+ export * from "./flow-command-handle-controller.js";
9
10
  export * from "./host-event-contract.js";
10
11
  export * from "./lazy-handle-client.js";
11
12
  export { createCommandRequestId } from "./request-id.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/host/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oCAAoC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/host/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kCAAkC,CAAC;AACjD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oCAAoC,CAAC"}
@@ -6,6 +6,7 @@ export * from "./command-envelope.js";
6
6
  export * from "./command-settlement.js";
7
7
  export * from "./constants.js";
8
8
  export * from "./external-id-argument-guards.js";
9
+ export * from "./flow-command-handle-controller.js";
9
10
  export * from "./host-event-contract.js";
10
11
  export * from "./lazy-handle-client.js";
11
12
  export { createCommandRequestId } from "./request-id.js";
@@ -8,7 +8,12 @@ type StageMap<TStage extends string> = {
8
8
  };
9
9
  export type LazyHandleClient<THandleId extends string, TSettlement, TStage extends string = LifecycleStage> = {
10
10
  runCommand<TPayload>(buildPayload: (handleId: THandleId | undefined) => TPayload, dispatchAndWait: (payload: TPayload) => Promise<TSettlement>): Promise<TSettlement>;
11
+ runCommandWithRetry<TPayload>(buildPayload: (handleId: THandleId | undefined) => TPayload, dispatchAndWait: (payload: TPayload) => Promise<TSettlement>, options: {
12
+ shouldRetry: (error: unknown) => boolean;
13
+ onBeforeRetry?: (error: unknown) => void;
14
+ }): Promise<TSettlement>;
11
15
  getHandleId(): THandleId | undefined;
16
+ hasPendingCommands(): boolean;
12
17
  subscribeHandle(subscriber: Subscriber<THandleId>): () => void;
13
18
  getStage(): TStage;
14
19
  setStage(stage: TStage): void;
@@ -1 +1 @@
1
- {"version":3,"file":"lazy-handle-client.d.ts","sourceRoot":"","sources":["../../src/host/lazy-handle-client.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,CAAC,SAAS,SAAS,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;AAC1E,KAAK,eAAe,CAAC,MAAM,SAAS,MAAM,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AAEtE,KAAK,cAAc,GAAG,eAAe,GAAG,OAAO,GAAG,aAAa,CAAC;AAChE,KAAK,QAAQ,CAAC,MAAM,SAAS,MAAM,IAAI;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACpB,CAAC;AAQF,MAAM,MAAM,gBAAgB,CAC3B,SAAS,SAAS,MAAM,EACxB,WAAW,EACX,MAAM,SAAS,MAAM,GAAG,cAAc,IACnC;IACH,UAAU,CAAC,QAAQ,EAClB,YAAY,EAAE,CAAC,QAAQ,EAAE,SAAS,GAAG,SAAS,KAAK,QAAQ,EAC3D,eAAe,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,WAAW,CAAC,GAC1D,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB,WAAW,IAAI,SAAS,GAAG,SAAS,CAAC;IACrC,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;IAC/D,QAAQ,IAAI,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,CAAC,UAAU,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC;IAChE,KAAK,IAAI,IAAI,CAAC;IACd,UAAU,IAAI,IAAI,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,6BAA6B,CACxC,SAAS,SAAS,MAAM,EACxB,WAAW,EACX,MAAM,SAAS,MAAM,GAAG,cAAc,IACnC;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,SAAS,GAAG,SAAS,CAAC;IAC9E,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,oCAAoC,CACxC,SAAS,SAAS,MAAM,EACxB,WAAW,IACR;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,SAAS,GAAG,SAAS,CAAC;IAC9E,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,KAAK,mCAAmC,CACvC,SAAS,SAAS,MAAM,EACxB,WAAW,EACX,MAAM,SAAS,MAAM,IAClB;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,SAAS,GAAG,SAAS,CAAC;IAC9E,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAuHF,wBAAgB,sBAAsB,CAAC,SAAS,SAAS,MAAM,EAAE,WAAW,EAC3E,OAAO,EAAE,oCAAoC,CAAC,SAAS,EAAE,WAAW,CAAC,GACnE,gBAAgB,CAAC,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;AAC5D,wBAAgB,sBAAsB,CACrC,SAAS,SAAS,MAAM,EACxB,WAAW,EACX,MAAM,SAAS,MAAM,EAErB,OAAO,EAAE,mCAAmC,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,GAC1E,gBAAgB,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"lazy-handle-client.d.ts","sourceRoot":"","sources":["../../src/host/lazy-handle-client.ts"],"names":[],"mappings":"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"}
@@ -62,9 +62,49 @@ const createLazyHandleClientWithStages = (input) => {
62
62
  });
63
63
  return run;
64
64
  };
65
+ const runCommandWithRetry = (buildPayload, dispatchAndWait, options) => {
66
+ const execute = async () => {
67
+ let didRetry = false;
68
+ for (;;) {
69
+ if (stage === stages.invalidated) {
70
+ throw new Error("Handle client is invalidated");
71
+ }
72
+ const commandEpoch = epoch;
73
+ const payload = buildPayload(handleId);
74
+ try {
75
+ const settlement = await dispatchAndWait(payload);
76
+ const settledHandleId = input.getHandleIdFromSettlement(settlement);
77
+ if (settledHandleId && commandEpoch === epoch) {
78
+ publishHandle(settledHandleId);
79
+ }
80
+ return settlement;
81
+ }
82
+ catch (error) {
83
+ if (commandEpoch === epoch &&
84
+ !didRetry &&
85
+ options.shouldRetry(error)) {
86
+ didRetry = true;
87
+ options.onBeforeRetry?.(error);
88
+ continue;
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+ };
94
+ const run = queuedCommandCount === 0 ? execute() : tail.then(execute, execute);
95
+ queuedCommandCount += 1;
96
+ tail = run.then(() => {
97
+ queuedCommandCount -= 1;
98
+ }, () => {
99
+ queuedCommandCount -= 1;
100
+ });
101
+ return run;
102
+ };
65
103
  return {
66
104
  runCommand,
105
+ runCommandWithRetry,
67
106
  getHandleId: () => handleId,
107
+ hasPendingCommands: () => queuedCommandCount > 0,
68
108
  subscribeHandle: (subscriber) => {
69
109
  subscribers.add(subscriber);
70
110
  if (handleId) {
@@ -4,6 +4,8 @@
4
4
  */
5
5
  export { createCommandDispatch } from "./command-dispatch.js";
6
6
  export { COMMAND_SETTLEMENT_TIMEOUT_CODE, CommandSettlementError, CommandSettlementTimeoutError, toCommandSettlementError, waitForCommandSettlement, } from "./command-settlement.js";
7
+ export type { FlowCommandHandleController } from "./flow-command-handle-controller.js";
8
+ export { createFlowCommandHandleController } from "./flow-command-handle-controller.js";
7
9
  export type { FlowHandleFromSettlement, FlowState, } from "./host-event-contract.js";
8
10
  export { getFlowHandleFromSettlementDetail, isFlowStateChangedDetail, isHandleInvalidatedDetail, isInstanceFlowStateChangedDetail, } from "./host-event-contract.js";
9
11
  export type { AppEventCapability, AppEventFlag, AppEventFlagProvider, AppEventFlagValue, AppEventPayload, CapabilitiesInput, ClientMeta, ClientOptions, Command, CommandEnvelope, ConfigureOptions, ContainerPolicy, FlagsInput, InitOptions, PublicCommandPayload, ResponseMetadataInput, TelemetryEvent, } from "./sdk-types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/host/sdk.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EACN,+BAA+B,EAC/B,sBAAsB,EACtB,6BAA6B,EAC7B,wBAAwB,EACxB,wBAAwB,GACxB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,wBAAwB,EACxB,SAAS,GACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,iCAAiC,EACjC,wBAAwB,EACxB,yBAAyB,EACzB,gCAAgC,GAChC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACX,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,OAAO,EACP,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,qBAAqB,EAErB,cAAc,GACd,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/host/sdk.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EACN,+BAA+B,EAC/B,sBAAsB,EACtB,6BAA6B,EAC7B,wBAAwB,EACxB,wBAAwB,GACxB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AACvF,OAAO,EAAE,iCAAiC,EAAE,MAAM,qCAAqC,CAAC;AACxF,YAAY,EACX,wBAAwB,EACxB,SAAS,GACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,iCAAiC,EACjC,wBAAwB,EACxB,yBAAyB,EACzB,gCAAgC,GAChC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACX,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,OAAO,EACP,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,qBAAqB,EAErB,cAAc,GACd,MAAM,gBAAgB,CAAC"}
package/dist/host/sdk.js CHANGED
@@ -4,4 +4,5 @@
4
4
  */
5
5
  export { createCommandDispatch } from "./command-dispatch.js";
6
6
  export { COMMAND_SETTLEMENT_TIMEOUT_CODE, CommandSettlementError, CommandSettlementTimeoutError, toCommandSettlementError, waitForCommandSettlement, } from "./command-settlement.js";
7
+ export { createFlowCommandHandleController } from "./flow-command-handle-controller.js";
7
8
  export { getFlowHandleFromSettlementDetail, isFlowStateChangedDetail, isHandleInvalidatedDetail, isInstanceFlowStateChangedDetail, } from "./host-event-contract.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/protocol",
3
- "version": "3.1.3",
3
+ "version": "3.1.4",
4
4
  "description": "getuserfeedback widget protocol — host surface and (later) wire protocol",
5
5
  "keywords": [
6
6
  "getuserfeedback",
@@ -0,0 +1,239 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createFlowCommandHandleController } from "./flow-command-handle-controller";
3
+
4
+ describe("flow command handle controller", () => {
5
+ it("serializes commands and publishes settled handles to later payloads", async () => {
6
+ const dispatched: Array<{ flowHandleId?: string; kind: string }> = [];
7
+ const controller = createFlowCommandHandleController<
8
+ string,
9
+ { result?: { flowHandleId?: string } }
10
+ >({
11
+ getHandleIdFromSettlement: (settlement) =>
12
+ settlement.result?.flowHandleId,
13
+ isStaleHandleError: () => false,
14
+ });
15
+
16
+ await controller.runCommand(
17
+ (flowHandleId) => ({ kind: "prefetch", flowHandleId }),
18
+ async (payload) => {
19
+ dispatched.push(payload);
20
+ return { result: { flowHandleId: "handle_123" } };
21
+ },
22
+ );
23
+ await controller.runCommand(
24
+ (flowHandleId) => ({ kind: "open", flowHandleId }),
25
+ async (payload) => {
26
+ dispatched.push(payload);
27
+ return { result: { flowHandleId: "handle_123" } };
28
+ },
29
+ );
30
+
31
+ expect(dispatched).toEqual([
32
+ { kind: "prefetch", flowHandleId: undefined },
33
+ { kind: "open", flowHandleId: "handle_123" },
34
+ ]);
35
+ expect(controller.getHandleId()).toBe("handle_123");
36
+ expect(controller.hasHandleId("handle_123")).toBe(true);
37
+ });
38
+
39
+ it("clears stale handles and retries once without the handle", async () => {
40
+ const dispatched: Array<{ flowHandleId?: string; kind: string }> = [];
41
+ const clearedHandles: string[] = [];
42
+ const controller = createFlowCommandHandleController<
43
+ string,
44
+ { result?: { flowHandleId?: string } }
45
+ >({
46
+ getHandleIdFromSettlement: (settlement) =>
47
+ settlement.result?.flowHandleId,
48
+ isStaleHandleError: (error) =>
49
+ error instanceof Error && error.message === "stale",
50
+ onClearResolvedHandle: (flowHandleId) => {
51
+ clearedHandles.push(flowHandleId);
52
+ },
53
+ });
54
+
55
+ await controller.runCommand(
56
+ (flowHandleId) => ({ kind: "prefetch", flowHandleId }),
57
+ async (payload) => {
58
+ dispatched.push(payload);
59
+ return { result: { flowHandleId: "handle_stale" } };
60
+ },
61
+ );
62
+ let didFailOnce = false;
63
+ await controller.runCommand(
64
+ (flowHandleId) => ({ kind: "open", flowHandleId }),
65
+ async (payload) => {
66
+ dispatched.push(payload);
67
+ if (!didFailOnce) {
68
+ didFailOnce = true;
69
+ throw new Error("stale");
70
+ }
71
+ return { result: { flowHandleId: "handle_recovered" } };
72
+ },
73
+ );
74
+
75
+ expect(dispatched).toEqual([
76
+ { kind: "prefetch", flowHandleId: undefined },
77
+ { kind: "open", flowHandleId: "handle_stale" },
78
+ { kind: "open", flowHandleId: undefined },
79
+ ]);
80
+ expect(clearedHandles).toEqual(["handle_stale"]);
81
+ expect(controller.getHandleId()).toBe("handle_recovered");
82
+ });
83
+
84
+ it("keeps stale-handle retry in the same serialized command slot", async () => {
85
+ const dispatched: Array<{ flowHandleId?: string; kind: string }> = [];
86
+ const controller = createFlowCommandHandleController<
87
+ string,
88
+ { result?: { flowHandleId?: string } }
89
+ >({
90
+ getHandleIdFromSettlement: (settlement) =>
91
+ settlement.result?.flowHandleId,
92
+ isStaleHandleError: (error) =>
93
+ error instanceof Error && error.message === "stale",
94
+ });
95
+
96
+ await controller.runCommand(
97
+ (flowHandleId) => ({ kind: "prefetch", flowHandleId }),
98
+ async (payload) => {
99
+ dispatched.push(payload);
100
+ return { result: { flowHandleId: "handle_stale" } };
101
+ },
102
+ );
103
+ let didFailOnce = false;
104
+ const staleOpen = controller.runCommand(
105
+ (flowHandleId) => ({ kind: "open", flowHandleId }),
106
+ async (payload) => {
107
+ dispatched.push(payload);
108
+ if (!didFailOnce) {
109
+ didFailOnce = true;
110
+ throw new Error("stale");
111
+ }
112
+ return { result: { flowHandleId: "handle_recovered" } };
113
+ },
114
+ );
115
+ const queuedPrerender = controller.runCommand(
116
+ (flowHandleId) => ({ kind: "prerender", flowHandleId }),
117
+ async (payload) => {
118
+ dispatched.push(payload);
119
+ return { result: { flowHandleId: "handle_second" } };
120
+ },
121
+ );
122
+
123
+ await staleOpen;
124
+ await queuedPrerender;
125
+
126
+ expect(dispatched).toEqual([
127
+ { kind: "prefetch", flowHandleId: undefined },
128
+ { kind: "open", flowHandleId: "handle_stale" },
129
+ { kind: "open", flowHandleId: undefined },
130
+ { kind: "prerender", flowHandleId: "handle_recovered" },
131
+ ]);
132
+ expect(controller.getHandleId()).toBe("handle_second");
133
+ });
134
+
135
+ it("does not retry non-stale failures", async () => {
136
+ const dispatched: Array<{ flowHandleId?: string; kind: string }> = [];
137
+ const controller = createFlowCommandHandleController<
138
+ string,
139
+ { result?: { flowHandleId?: string } }
140
+ >({
141
+ getHandleIdFromSettlement: (settlement) =>
142
+ settlement.result?.flowHandleId,
143
+ isStaleHandleError: () => false,
144
+ });
145
+
146
+ await expect(
147
+ controller.runCommand(
148
+ (flowHandleId) => ({ kind: "open", flowHandleId }),
149
+ async (payload) => {
150
+ dispatched.push(payload);
151
+ throw new Error("boom");
152
+ },
153
+ ),
154
+ ).rejects.toThrow("boom");
155
+
156
+ expect(dispatched).toEqual([{ kind: "open", flowHandleId: undefined }]);
157
+ });
158
+
159
+ it("ignores handles from commands that settle after reset", async () => {
160
+ const dispatched: Array<{ flowHandleId?: string; kind: string }> = [];
161
+ let resolveSettlement:
162
+ | ((settlement: { result?: { flowHandleId?: string } }) => void)
163
+ | undefined;
164
+ const controller = createFlowCommandHandleController<
165
+ string,
166
+ { result?: { flowHandleId?: string } }
167
+ >({
168
+ getHandleIdFromSettlement: (settlement) =>
169
+ settlement.result?.flowHandleId,
170
+ isStaleHandleError: () => false,
171
+ });
172
+
173
+ const pendingCommand = controller.runCommand(
174
+ (flowHandleId) => ({ kind: "prefetch", flowHandleId }),
175
+ (payload) => {
176
+ dispatched.push(payload);
177
+ return new Promise((resolve) => {
178
+ resolveSettlement = resolve;
179
+ });
180
+ },
181
+ );
182
+ controller.reset();
183
+ resolveSettlement?.({ result: { flowHandleId: "handle_from_old_epoch" } });
184
+ await pendingCommand;
185
+ await controller.runCommand(
186
+ (flowHandleId) => ({ kind: "open", flowHandleId }),
187
+ async (payload) => {
188
+ dispatched.push(payload);
189
+ return { result: { flowHandleId: "handle_current" } };
190
+ },
191
+ );
192
+
193
+ expect(dispatched).toEqual([
194
+ { kind: "prefetch", flowHandleId: undefined },
195
+ { kind: "open", flowHandleId: undefined },
196
+ ]);
197
+ expect(controller.getHandleId()).toBe("handle_current");
198
+ });
199
+
200
+ it("does not retry stale failures from commands that fail after reset", async () => {
201
+ const dispatched: Array<{ flowHandleId?: string; kind: string }> = [];
202
+ let rejectSettlement: ((error: Error) => void) | undefined;
203
+ const controller = createFlowCommandHandleController<
204
+ string,
205
+ { result?: { flowHandleId?: string } }
206
+ >({
207
+ getHandleIdFromSettlement: (settlement) =>
208
+ settlement.result?.flowHandleId,
209
+ isStaleHandleError: (error) =>
210
+ error instanceof Error && error.message === "stale",
211
+ });
212
+
213
+ await controller.runCommand(
214
+ (flowHandleId) => ({ kind: "prefetch", flowHandleId }),
215
+ async (payload) => {
216
+ dispatched.push(payload);
217
+ return { result: { flowHandleId: "handle_before_reset" } };
218
+ },
219
+ );
220
+ const pendingCommand = controller.runCommand(
221
+ (flowHandleId) => ({ kind: "open", flowHandleId }),
222
+ (payload) => {
223
+ dispatched.push(payload);
224
+ return new Promise((_resolve, reject) => {
225
+ rejectSettlement = reject;
226
+ });
227
+ },
228
+ );
229
+ controller.reset();
230
+ rejectSettlement?.(new Error("stale"));
231
+
232
+ await expect(pendingCommand).rejects.toThrow("stale");
233
+ expect(dispatched).toEqual([
234
+ { kind: "prefetch", flowHandleId: undefined },
235
+ { kind: "open", flowHandleId: "handle_before_reset" },
236
+ ]);
237
+ expect(controller.getHandleId()).toBeUndefined();
238
+ });
239
+ });
@@ -0,0 +1,86 @@
1
+ import {
2
+ createLazyHandleClient,
3
+ type LazyHandleClient,
4
+ } from "./lazy-handle-client.js";
5
+
6
+ export type FlowCommandHandleController<
7
+ THandleId extends string,
8
+ TSettlement,
9
+ > = {
10
+ runCommand<TPayload>(
11
+ buildPayload: (handleId: THandleId | undefined) => TPayload,
12
+ dispatchAndWait: (payload: TPayload) => Promise<TSettlement>,
13
+ ): Promise<TSettlement>;
14
+ getHandleId(): THandleId | undefined;
15
+ hasHandleId(handleId: string): boolean;
16
+ hasPendingCommands(): boolean;
17
+ subscribeHandle(subscriber: (handleId: THandleId) => void): () => void;
18
+ clearResolvedHandle(): THandleId | undefined;
19
+ reset(): void;
20
+ };
21
+
22
+ export type CreateFlowCommandHandleControllerOptions<
23
+ THandleId extends string,
24
+ TSettlement,
25
+ > = {
26
+ getHandleIdFromSettlement: (settlement: TSettlement) => THandleId | undefined;
27
+ isStaleHandleError: (error: unknown) => boolean;
28
+ onClearResolvedHandle?: (handleId: THandleId) => void;
29
+ };
30
+
31
+ export function createFlowCommandHandleController<
32
+ THandleId extends string,
33
+ TSettlement,
34
+ >({
35
+ getHandleIdFromSettlement,
36
+ isStaleHandleError,
37
+ onClearResolvedHandle,
38
+ }: CreateFlowCommandHandleControllerOptions<
39
+ THandleId,
40
+ TSettlement
41
+ >): FlowCommandHandleController<THandleId, TSettlement> {
42
+ const handleClient: LazyHandleClient<THandleId, TSettlement> =
43
+ createLazyHandleClient<THandleId, TSettlement>({
44
+ getHandleIdFromSettlement,
45
+ });
46
+
47
+ const clearResolvedHandle = (): THandleId | undefined => {
48
+ const previousHandleId = handleClient.getHandleId();
49
+ if (!previousHandleId) {
50
+ return undefined;
51
+ }
52
+ handleClient.reset();
53
+ onClearResolvedHandle?.(previousHandleId);
54
+ return previousHandleId;
55
+ };
56
+
57
+ const reset = (): void => {
58
+ const previousHandleId = handleClient.getHandleId();
59
+ handleClient.reset();
60
+ if (previousHandleId) {
61
+ onClearResolvedHandle?.(previousHandleId);
62
+ }
63
+ };
64
+
65
+ const runCommand: FlowCommandHandleController<
66
+ THandleId,
67
+ TSettlement
68
+ >["runCommand"] = async (buildPayload, dispatchAndWait) => {
69
+ return handleClient.runCommandWithRetry(buildPayload, dispatchAndWait, {
70
+ shouldRetry: isStaleHandleError,
71
+ onBeforeRetry: () => {
72
+ clearResolvedHandle();
73
+ },
74
+ });
75
+ };
76
+
77
+ return {
78
+ runCommand,
79
+ getHandleId: () => handleClient.getHandleId(),
80
+ hasHandleId: (handleId) => handleClient.getHandleId() === handleId,
81
+ hasPendingCommands: () => handleClient.hasPendingCommands(),
82
+ subscribeHandle: (subscriber) => handleClient.subscribeHandle(subscriber),
83
+ clearResolvedHandle,
84
+ reset,
85
+ };
86
+ }
package/src/host/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./command-envelope.js";
7
7
  export * from "./command-settlement.js";
8
8
  export * from "./constants.js";
9
9
  export * from "./external-id-argument-guards.js";
10
+ export * from "./flow-command-handle-controller.js";
10
11
  export * from "./host-event-contract.js";
11
12
  export * from "./lazy-handle-client.js";
12
13
  export { createCommandRequestId } from "./request-id.js";
@@ -23,7 +23,16 @@ export type LazyHandleClient<
23
23
  buildPayload: (handleId: THandleId | undefined) => TPayload,
24
24
  dispatchAndWait: (payload: TPayload) => Promise<TSettlement>,
25
25
  ): Promise<TSettlement>;
26
+ runCommandWithRetry<TPayload>(
27
+ buildPayload: (handleId: THandleId | undefined) => TPayload,
28
+ dispatchAndWait: (payload: TPayload) => Promise<TSettlement>,
29
+ options: {
30
+ shouldRetry: (error: unknown) => boolean;
31
+ onBeforeRetry?: (error: unknown) => void;
32
+ },
33
+ ): Promise<TSettlement>;
26
34
  getHandleId(): THandleId | undefined;
35
+ hasPendingCommands(): boolean;
27
36
  subscribeHandle(subscriber: Subscriber<THandleId>): () => void;
28
37
  getStage(): TStage;
29
38
  setStage(stage: TStage): void;
@@ -141,9 +150,60 @@ const createLazyHandleClientWithStages = <
141
150
  return run;
142
151
  };
143
152
 
153
+ const runCommandWithRetry: LazyHandleClient<
154
+ THandleId,
155
+ TSettlement,
156
+ TStage
157
+ >["runCommandWithRetry"] = (buildPayload, dispatchAndWait, options) => {
158
+ const execute = async (): Promise<TSettlement> => {
159
+ let didRetry = false;
160
+ for (;;) {
161
+ if (stage === stages.invalidated) {
162
+ throw new Error("Handle client is invalidated");
163
+ }
164
+ const commandEpoch = epoch;
165
+ const payload = buildPayload(handleId);
166
+ try {
167
+ const settlement = await dispatchAndWait(payload);
168
+ const settledHandleId = input.getHandleIdFromSettlement(settlement);
169
+ if (settledHandleId && commandEpoch === epoch) {
170
+ publishHandle(settledHandleId);
171
+ }
172
+ return settlement;
173
+ } catch (error) {
174
+ if (
175
+ commandEpoch === epoch &&
176
+ !didRetry &&
177
+ options.shouldRetry(error)
178
+ ) {
179
+ didRetry = true;
180
+ options.onBeforeRetry?.(error);
181
+ continue;
182
+ }
183
+ throw error;
184
+ }
185
+ }
186
+ };
187
+
188
+ const run =
189
+ queuedCommandCount === 0 ? execute() : tail.then(execute, execute);
190
+ queuedCommandCount += 1;
191
+ tail = run.then(
192
+ () => {
193
+ queuedCommandCount -= 1;
194
+ },
195
+ () => {
196
+ queuedCommandCount -= 1;
197
+ },
198
+ );
199
+ return run;
200
+ };
201
+
144
202
  return {
145
203
  runCommand,
204
+ runCommandWithRetry,
146
205
  getHandleId: () => handleId,
206
+ hasPendingCommands: () => queuedCommandCount > 0,
147
207
  subscribeHandle: (subscriber) => {
148
208
  subscribers.add(subscriber);
149
209
  if (handleId) {
package/src/host/sdk.ts CHANGED
@@ -11,6 +11,8 @@ export {
11
11
  toCommandSettlementError,
12
12
  waitForCommandSettlement,
13
13
  } from "./command-settlement.js";
14
+ export type { FlowCommandHandleController } from "./flow-command-handle-controller.js";
15
+ export { createFlowCommandHandleController } from "./flow-command-handle-controller.js";
14
16
  export type {
15
17
  FlowHandleFromSettlement,
16
18
  FlowState,