@getuserfeedback/protocol 0.6.1 → 0.7.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 (48) hide show
  1. package/dist/app-event.d.ts +659 -17
  2. package/dist/app-event.d.ts.map +1 -1
  3. package/dist/app-event.js +71 -8
  4. package/dist/flow-assignments.d.ts +4 -4
  5. package/dist/host/constants.d.ts +2 -2
  6. package/dist/host/constants.d.ts.map +1 -1
  7. package/dist/host/constants.js +3 -1
  8. package/dist/host/sdk-types.d.ts +73 -28
  9. package/dist/host/sdk-types.d.ts.map +1 -1
  10. package/dist/host/sdk.d.ts.map +1 -1
  11. package/dist/identity-type.d.ts +4 -2
  12. package/dist/identity-type.d.ts.map +1 -1
  13. package/dist/identity-type.js +4 -2
  14. package/dist/index.d.ts +2 -86
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -6
  17. package/dist/widget-commands.d.ts +102 -3
  18. package/dist/widget-commands.d.ts.map +1 -1
  19. package/dist/widget-commands.js +5 -0
  20. package/dist/widget-config.d.ts +5 -5
  21. package/package.json +22 -2
  22. package/src/app-event.ts +211 -0
  23. package/src/client-meta.ts +25 -0
  24. package/src/defaults.ts +4 -0
  25. package/src/errors.ts +58 -0
  26. package/src/flow-assignments.test.ts +63 -0
  27. package/src/flow-assignments.ts +125 -0
  28. package/src/host/command-dispatch.ts +59 -0
  29. package/src/host/command-envelope.ts +41 -0
  30. package/src/host/command-settlement.ts +121 -0
  31. package/src/host/constants.ts +103 -0
  32. package/src/host/host-event-contract.ts +277 -0
  33. package/src/host/index.ts +13 -0
  34. package/src/host/lazy-handle-client.ts +207 -0
  35. package/src/host/request-id.ts +3 -0
  36. package/src/host/sdk-types.ts +506 -0
  37. package/src/host/sdk.ts +43 -0
  38. package/src/host/unique-id.ts +17 -0
  39. package/src/identity-type.ts +102 -0
  40. package/src/index.ts +136 -0
  41. package/src/protocol-root.test.ts +476 -0
  42. package/src/public-grant-scope.ts +25 -0
  43. package/src/runtime-endpoints.ts +69 -0
  44. package/src/scopes.ts +79 -0
  45. package/src/trpc-envelope.ts +9 -0
  46. package/src/version-resolution.ts +18 -0
  47. package/src/widget-commands.ts +152 -0
  48. package/src/widget-config.ts +157 -0
@@ -0,0 +1,211 @@
1
+ import * as z from "zod/mini";
2
+ import { appEventIdentityTypeSchema } from "./identity-type.js";
3
+
4
+ export { appEventIdentityTypeSchema } from "./identity-type.js";
5
+
6
+ export const appEventIdentitySchema = z.object({
7
+ type: appEventIdentityTypeSchema,
8
+ value: z.string().check(z.trim(), z.minLength(1)),
9
+ });
10
+
11
+ type AppEventFlagValue =
12
+ | string
13
+ | number
14
+ | boolean
15
+ | null
16
+ | AppEventFlagValue[]
17
+ | { [key: string]: AppEventFlagValue };
18
+
19
+ export type AppEventJsonValue =
20
+ | string
21
+ | number
22
+ | boolean
23
+ | null
24
+ | AppEventJsonValue[]
25
+ | { [key: string]: AppEventJsonValue };
26
+
27
+ const appEventFlagValueSchema: z.ZodMiniType<AppEventFlagValue> = z.lazy(() =>
28
+ z.union([
29
+ z.string(),
30
+ z.number(),
31
+ z.boolean(),
32
+ z.null(),
33
+ z.array(appEventFlagValueSchema),
34
+ z.record(z.string(), appEventFlagValueSchema),
35
+ ]),
36
+ );
37
+
38
+ const appEventJsonNumberSchema = z.number().check(
39
+ z.refine(Number.isFinite, {
40
+ message: "Expected a finite JSON number.",
41
+ }),
42
+ );
43
+
44
+ const appEventJsonValueSchema: z.ZodMiniType<AppEventJsonValue> = z.lazy(() =>
45
+ z.union([
46
+ z.string(),
47
+ appEventJsonNumberSchema,
48
+ z.boolean(),
49
+ z.null(),
50
+ z.array(appEventJsonValueSchema),
51
+ z.record(z.string(), appEventJsonValueSchema),
52
+ ]),
53
+ );
54
+
55
+ const appEventJsonObjectSchema = z.record(z.string(), appEventJsonValueSchema);
56
+
57
+ export const appEventFlagProviderSchema = z.object({
58
+ name: z.string().check(z.trim(), z.minLength(1)),
59
+ project: z.optional(z.string().check(z.trim(), z.minLength(1))),
60
+ environment: z.optional(z.string().check(z.trim(), z.minLength(1))),
61
+ });
62
+
63
+ export const appEventFlagSchema = z.object({
64
+ key: z.string().check(z.trim(), z.minLength(1)),
65
+ target: z.optional(z.string().check(z.trim(), z.minLength(1))),
66
+ value: appEventFlagValueSchema,
67
+ variant: z.optional(z.string().check(z.trim(), z.minLength(1))),
68
+ origin: z.optional(
69
+ z.enum(["runtime_config", "trigger_context", "provider_sync", "api"]),
70
+ ),
71
+ provider: z.optional(appEventFlagProviderSchema),
72
+ status: z.optional(z.string().check(z.trim(), z.minLength(1))),
73
+ reason: z.optional(z.string().check(z.trim(), z.minLength(1))),
74
+ evaluatedAt: z.optional(z.string().check(z.trim(), z.minLength(1))),
75
+ });
76
+
77
+ export const appEventCapabilitySchema = z.object({
78
+ key: z.string().check(z.trim(), z.minLength(1)),
79
+ source: z.optional(z.string().check(z.trim(), z.minLength(1))),
80
+ provider: z.optional(appEventFlagProviderSchema),
81
+ });
82
+
83
+ const appEventPageSchema = z.object({
84
+ path: z.optional(z.string()),
85
+ referrer: z.optional(z.string()),
86
+ search: z.optional(z.string()),
87
+ });
88
+
89
+ export const appEventContextSchema = z.object({
90
+ page: z.optional(appEventPageSchema),
91
+ locale: z.optional(z.string()),
92
+ userAgent: z.optional(z.string()),
93
+ flags: z.optional(z.array(appEventFlagSchema)),
94
+ capabilities: z.optional(z.array(appEventCapabilitySchema)),
95
+ });
96
+
97
+ export const appEventBaseSchema = z.object({
98
+ traits: z.optional(z.record(z.string(), z.unknown())),
99
+ identities: z.optional(z.array(appEventIdentitySchema)),
100
+ context: z.optional(appEventContextSchema),
101
+ messageId: z.optional(z.string()),
102
+ timestamp: z.optional(z.number()),
103
+ });
104
+
105
+ const appEventSurveyReferenceSchema = z.object({
106
+ surveyId: z.string(),
107
+ });
108
+
109
+ export const reservedSystemAppEventNames = [
110
+ "Response Submitted",
111
+ "Flow Viewed",
112
+ "Flow Dismissed",
113
+ "Identified",
114
+ ] as const;
115
+ const reservedSystemAppEventNameLowercaseSet = new Set<string>(
116
+ reservedSystemAppEventNames.map((event) => event.toLowerCase()),
117
+ );
118
+
119
+ const appEventSystemTrackNameSchema = z.enum(["Flow Viewed", "Flow Dismissed"]);
120
+
121
+ const appEventCustomerNameSchema = z.string().check(
122
+ z.trim(),
123
+ z.minLength(1),
124
+ z.maxLength(255),
125
+ z.check<string>((ctx) => {
126
+ if (!reservedSystemAppEventNameLowercaseSet.has(ctx.value.toLowerCase())) {
127
+ return;
128
+ }
129
+ ctx.issues.push({
130
+ code: "custom",
131
+ input: ctx.value,
132
+ message:
133
+ "Customer app event names cannot use reserved system event names.",
134
+ continue: false,
135
+ });
136
+ }),
137
+ );
138
+
139
+ export const appEventSystemBaseSchema = z.extend(appEventBaseSchema, {
140
+ origin: z.literal("system"),
141
+ });
142
+
143
+ const normalizeSystemAppEventOrigin = <T extends { origin?: "system" }>(
144
+ input: T,
145
+ ): Omit<T, "origin"> & { origin: "system" } => {
146
+ const { origin: _origin, ...rest } = input;
147
+ return {
148
+ ...rest,
149
+ origin: "system" as const,
150
+ };
151
+ };
152
+
153
+ export const appEventSurveyViewedSchema = z.pipe(
154
+ z.extend(appEventBaseSchema, {
155
+ origin: z.optional(z.literal("system")),
156
+ event: z.literal("Flow Viewed"),
157
+ references: appEventSurveyReferenceSchema,
158
+ }),
159
+ z.transform(normalizeSystemAppEventOrigin),
160
+ );
161
+
162
+ export const appEventFlowDismissedSchema = z.pipe(
163
+ z.extend(appEventBaseSchema, {
164
+ origin: z.optional(z.literal("system")),
165
+ event: z.literal("Flow Dismissed"),
166
+ references: appEventSurveyReferenceSchema,
167
+ }),
168
+ z.transform(normalizeSystemAppEventOrigin),
169
+ );
170
+
171
+ export const appEventSystemTrackSchema = z.extend(appEventSystemBaseSchema, {
172
+ event: appEventSystemTrackNameSchema,
173
+ references: appEventSurveyReferenceSchema,
174
+ });
175
+
176
+ const appEventLegacySystemTrackSchema = z.pipe(
177
+ z.extend(appEventBaseSchema, {
178
+ origin: z.optional(z.literal("system")),
179
+ event: appEventSystemTrackNameSchema,
180
+ references: appEventSurveyReferenceSchema,
181
+ }),
182
+ z.transform(normalizeSystemAppEventOrigin),
183
+ );
184
+
185
+ export const appEventCustomerTrackSchema = z.extend(appEventBaseSchema, {
186
+ origin: z.literal("customer"),
187
+ event: appEventCustomerNameSchema,
188
+ // TODO(migration): Normalize Date values in event properties to ISO strings.
189
+ // [from=manual-date-property-serialization] [to=protocol-date-property-normalization] [scope=slice] [priority=medium] [impact=medium] [risk=low]
190
+ properties: z.optional(appEventJsonObjectSchema),
191
+ });
192
+
193
+ export const appEventTrackSchema = z.union([
194
+ appEventLegacySystemTrackSchema,
195
+ appEventCustomerTrackSchema,
196
+ ]);
197
+
198
+ const appEventDiscriminatedPayloadSchema = z.discriminatedUnion("origin", [
199
+ appEventSystemTrackSchema,
200
+ appEventCustomerTrackSchema,
201
+ ]);
202
+
203
+ export const appEventPayloadSchema = z.union([
204
+ appEventLegacySystemTrackSchema,
205
+ appEventDiscriminatedPayloadSchema,
206
+ ]);
207
+
208
+ export type AppEventTrackInput = z.output<typeof appEventTrackSchema>;
209
+ export type AppEventPayload = z.output<typeof appEventPayloadSchema>;
210
+ export type AppEventFlag = z.output<typeof appEventFlagSchema>;
211
+ export type AppEventCapability = z.output<typeof appEventCapabilitySchema>;
@@ -0,0 +1,25 @@
1
+ import * as z from "zod/mini";
2
+
3
+ export const clientMetaSchema = z.object({
4
+ loader: z.enum(["sdk", "gtm", "segment", "rudderstack", "tealium", "custom"]),
5
+ clientName: z.optional(z.string().check(z.trim(), z.minLength(1))),
6
+ clientVersion: z.optional(z.string().check(z.trim(), z.minLength(1))),
7
+ integrator: z.optional(
8
+ z.object({
9
+ name: z.optional(z.string().check(z.trim(), z.minLength(1))),
10
+ version: z.optional(z.string().check(z.trim(), z.minLength(1))),
11
+ }),
12
+ ),
13
+ transport: z.optional(
14
+ z.enum(["script-tag", "esm", "loader", "tag-manager", "snippet"]),
15
+ ),
16
+ runtime: z.optional(
17
+ z.object({
18
+ framework: z.optional(z.enum(["next", "vite", "webpack", "plain"])),
19
+ runtime: z.optional(z.enum(["browser", "edge"])),
20
+ }),
21
+ ),
22
+ notes: z.optional(z.string().check(z.trim(), z.minLength(1))),
23
+ });
24
+
25
+ export type ClientMeta = z.output<typeof clientMetaSchema>;
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS: readonly [
2
+ "class",
3
+ "data-theme",
4
+ ] = ["class", "data-theme"];
package/src/errors.ts ADDED
@@ -0,0 +1,58 @@
1
+ import * as z from "zod/mini";
2
+
3
+ import { clientMetaSchema } from "./client-meta.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Error-telemetry definitions shared across widget / host.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export const ErrorCategory = {
10
+ INTERNAL: "INTERNAL",
11
+ INVALID_USAGE: "INVALID_USAGE",
12
+ HOST: "HOST",
13
+ } as const;
14
+
15
+ export type ErrorCategory = (typeof ErrorCategory)[keyof typeof ErrorCategory];
16
+
17
+ export const ErrorCategorySchema = z.enum([
18
+ ErrorCategory.INTERNAL,
19
+ ErrorCategory.INVALID_USAGE,
20
+ ErrorCategory.HOST,
21
+ ]);
22
+
23
+ export const ErrorEventSchema = z.object({
24
+ id: z.string(),
25
+ name: z.string(),
26
+ message: z.string(),
27
+ stack: z.optional(z.string()),
28
+ category: ErrorCategorySchema,
29
+ timestamp: z.number(),
30
+ probability: z.optional(z.number()),
31
+ context: z.object({
32
+ widgetVersion: z.string(),
33
+ clientMeta: z.optional(clientMetaSchema),
34
+ /** When true, telemetry was captured with debug mode on (affects how to interpret context). */
35
+ debug: z.optional(z.boolean()),
36
+ }),
37
+ });
38
+
39
+ export type ErrorEvent = z.output<typeof ErrorEventSchema>;
40
+
41
+ export type ProtocolErrorCode = "PARSE" | "VERSION" | "UNKNOWN_TYPE";
42
+
43
+ const MAX_DETAIL_LENGTH = 80;
44
+
45
+ export class ProtocolError extends Error {
46
+ code: ProtocolErrorCode;
47
+ details: string;
48
+
49
+ constructor(code: ProtocolErrorCode, details: string) {
50
+ const short =
51
+ details.length > MAX_DETAIL_LENGTH
52
+ ? details.slice(0, MAX_DETAIL_LENGTH)
53
+ : details;
54
+ super(short);
55
+ this.code = code;
56
+ this.details = short;
57
+ }
58
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ claimPendingFlowAssignmentsRequestSchema,
4
+ flowAssignmentRecipientRequestSchema,
5
+ updateFlowAssignmentLifecycleRequestSchema,
6
+ } from "./flow-assignments";
7
+
8
+ describe("flow assignment request contracts", () => {
9
+ it("accepts runtime targeting context for listPending", () => {
10
+ const result = flowAssignmentRecipientRequestSchema.parse({
11
+ identities: [{ type: "userId", value: "user_1" }],
12
+ context: {
13
+ flags: [{ key: "new_checkout", value: true }],
14
+ capabilities: [{ key: "checkout.drawer", source: "host-app" }],
15
+ },
16
+ });
17
+
18
+ expect(result.context?.flags?.[0]).toEqual({
19
+ key: "new_checkout",
20
+ value: true,
21
+ });
22
+ expect(result.context?.capabilities?.[0]).toEqual({
23
+ key: "checkout.drawer",
24
+ source: "host-app",
25
+ });
26
+ });
27
+
28
+ it("accepts runtime targeting context for claimPending", () => {
29
+ const result = claimPendingFlowAssignmentsRequestSchema.parse({
30
+ holdOwnerKey: "inst_1",
31
+ context: {
32
+ flags: [{ key: "pricing_test", value: "variant_a" }],
33
+ capabilities: [{ key: "billing.portal" }],
34
+ },
35
+ });
36
+
37
+ expect(result.identities).toEqual([]);
38
+ expect(result.context?.flags?.[0]?.key).toBe("pricing_test");
39
+ expect(result.context?.capabilities?.[0]?.key).toBe("billing.portal");
40
+ });
41
+
42
+ it("accepts runtime targeting context for lifecycle updates", () => {
43
+ const result = updateFlowAssignmentLifecycleRequestSchema.parse({
44
+ identities: [{ type: "userId", value: "user_1" }],
45
+ flowAssignmentId: "flasn_1",
46
+ flowVersionId: "flv_1",
47
+ holdGroupToken: "fahold_1",
48
+ context: {
49
+ flags: [{ key: "new_checkout", value: true }],
50
+ capabilities: [{ key: "checkout.drawer", source: "host-app" }],
51
+ },
52
+ });
53
+
54
+ expect(result.context?.flags?.[0]).toEqual({
55
+ key: "new_checkout",
56
+ value: true,
57
+ });
58
+ expect(result.context?.capabilities?.[0]).toEqual({
59
+ key: "checkout.drawer",
60
+ source: "host-app",
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,125 @@
1
+ import * as z from "zod/mini";
2
+
3
+ import {
4
+ appEventCapabilitySchema,
5
+ appEventFlagSchema,
6
+ appEventIdentitySchema,
7
+ } from "./app-event.js";
8
+
9
+ export const flowAssignmentStatusSchema = z.enum([
10
+ "pending",
11
+ "held",
12
+ "presented",
13
+ "completed",
14
+ "expired",
15
+ "superseded",
16
+ "withdrawn_pending",
17
+ "withdrawn_presented",
18
+ "withdrawn_final",
19
+ ]);
20
+
21
+ export const flowAssignmentTargetingContextSchema = z.object({
22
+ flags: z.optional(z.array(appEventFlagSchema)),
23
+ capabilities: z.optional(z.array(appEventCapabilitySchema)),
24
+ });
25
+
26
+ export const flowAssignmentRecipientRequestSchema = z.object({
27
+ identities: z._default(z.array(appEventIdentitySchema), []),
28
+ context: z.optional(flowAssignmentTargetingContextSchema),
29
+ at: z.optional(z.number()),
30
+ });
31
+
32
+ export const flowAssignmentRecordSchema = z.object({
33
+ flowAssignmentId: z.string().check(z.trim(), z.minLength(1)),
34
+ flowId: z.string().check(z.trim(), z.minLength(1)),
35
+ holdGroupToken: z.optional(z.string().check(z.trim(), z.minLength(1))),
36
+ presentedFlowVersionId: z.optional(
37
+ z.string().check(z.trim(), z.minLength(1)),
38
+ ),
39
+ latestFlowVersionId: z.string().check(z.trim(), z.minLength(1)),
40
+ status: flowAssignmentStatusSchema,
41
+ assignedAt: z.string(),
42
+ expiresAt: z.optional(z.string()),
43
+ heldAt: z.optional(z.string()),
44
+ holdExpiresAt: z.optional(z.string()),
45
+ presentedAt: z.optional(z.string()),
46
+ completedAt: z.optional(z.string()),
47
+ expiredAt: z.optional(z.string()),
48
+ supersededAt: z.optional(z.string()),
49
+ withdrawnAt: z.optional(z.string()),
50
+ withdrawnFinalAt: z.optional(z.string()),
51
+ });
52
+
53
+ export const listPendingFlowAssignmentsResponseSchema = z.object({
54
+ fetchedAt: z.string(),
55
+ flowAssignments: z.array(flowAssignmentRecordSchema),
56
+ });
57
+
58
+ export const claimPendingFlowAssignmentsRequestSchema = z.object({
59
+ identities: z._default(z.array(appEventIdentitySchema), []),
60
+ context: z.optional(flowAssignmentTargetingContextSchema),
61
+ holdOwnerKey: z.string().check(z.trim(), z.minLength(1)),
62
+ at: z.optional(z.number()),
63
+ limit: z.optional(z.number()),
64
+ });
65
+
66
+ export const claimPendingFlowAssignmentsResponseSchema = z.object({
67
+ fetchedAt: z.string(),
68
+ flowAssignments: z.array(flowAssignmentRecordSchema),
69
+ });
70
+
71
+ export const updateFlowAssignmentLifecycleRequestSchema = z.object({
72
+ identities: z._default(z.array(appEventIdentitySchema), []),
73
+ context: z.optional(flowAssignmentTargetingContextSchema),
74
+ flowAssignmentId: z.string().check(z.trim(), z.minLength(1)),
75
+ flowVersionId: z.optional(z.string().check(z.trim(), z.minLength(1))),
76
+ holdGroupToken: z.optional(z.string().check(z.trim(), z.minLength(1))),
77
+ at: z.optional(z.number()),
78
+ });
79
+
80
+ export const releaseFlowAssignmentClaimRequestSchema = z.object({
81
+ identities: z._default(z.array(appEventIdentitySchema), []),
82
+ holdGroupToken: z.string().check(z.trim(), z.minLength(1)),
83
+ holdOwnerKey: z.optional(z.string().check(z.trim(), z.minLength(1))),
84
+ at: z.optional(z.number()),
85
+ });
86
+
87
+ export const releaseFlowAssignmentClaimResponseSchema = z.object({
88
+ releasedCount: z.number(),
89
+ });
90
+
91
+ export const updateFlowAssignmentLifecycleResponseSchema =
92
+ flowAssignmentRecordSchema;
93
+ export const nullableUpdateFlowAssignmentLifecycleResponseSchema = z.nullable(
94
+ updateFlowAssignmentLifecycleResponseSchema,
95
+ );
96
+
97
+ export type FlowAssignmentStatus = z.output<typeof flowAssignmentStatusSchema>;
98
+ export type FlowAssignmentRecipientRequest = z.output<
99
+ typeof flowAssignmentRecipientRequestSchema
100
+ >;
101
+ export type FlowAssignmentTargetingContext = z.output<
102
+ typeof flowAssignmentTargetingContextSchema
103
+ >;
104
+ export type FlowAssignmentRecord = z.output<typeof flowAssignmentRecordSchema>;
105
+ export type ClaimPendingFlowAssignmentsRequest = z.output<
106
+ typeof claimPendingFlowAssignmentsRequestSchema
107
+ >;
108
+ export type ClaimPendingFlowAssignmentsResponse = z.output<
109
+ typeof claimPendingFlowAssignmentsResponseSchema
110
+ >;
111
+ export type ListPendingFlowAssignmentsResponse = z.output<
112
+ typeof listPendingFlowAssignmentsResponseSchema
113
+ >;
114
+ export type ReleaseFlowAssignmentClaimRequest = z.output<
115
+ typeof releaseFlowAssignmentClaimRequestSchema
116
+ >;
117
+ export type ReleaseFlowAssignmentClaimResponse = z.output<
118
+ typeof releaseFlowAssignmentClaimResponseSchema
119
+ >;
120
+ export type UpdateFlowAssignmentLifecycleRequest = z.output<
121
+ typeof updateFlowAssignmentLifecycleRequestSchema
122
+ >;
123
+ export type UpdateFlowAssignmentLifecycleResponse = z.output<
124
+ typeof updateFlowAssignmentLifecycleResponseSchema
125
+ >;
@@ -0,0 +1,59 @@
1
+ import { createCommandEnvelope } from "./command-envelope.js";
2
+ import { waitForCommandSettlement } from "./command-settlement.js";
3
+ import type { CommandEnvelope, PublicCommandPayload } from "./sdk-types.js";
4
+
5
+ /**
6
+ * A command input: the pure payload with `kind` and command-specific fields.
7
+ * Distributes over the PublicCommandPayload union so discriminant
8
+ * narrowing is preserved (e.g. `{ kind: "open", flowId }` type-checks).
9
+ */
10
+ export type CommandInput = PublicCommandPayload extends infer C
11
+ ? C extends PublicCommandPayload
12
+ ? C
13
+ : never
14
+ : never;
15
+
16
+ type DispatchOptions = {
17
+ idempotencyKey?: string;
18
+ };
19
+
20
+ /**
21
+ * Creates a dispatch function that wraps a command payload in an envelope,
22
+ * pushes it into the queue, and returns a promise that settles when the
23
+ * runtime acknowledges it.
24
+ *
25
+ * Request IDs are generated automatically.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const dispatch = createCommandDispatch((env) => queue.push(env));
30
+ * await dispatch({ kind: "open", flowId: "f1", flowHandleId: "h1" });
31
+ * ```
32
+ */
33
+ export const createCommandDispatch = (
34
+ push: (envelope: CommandEnvelope) => void,
35
+ ): ((input: CommandInput, options?: DispatchOptions) => Promise<void>) => {
36
+ return async (
37
+ input: CommandInput,
38
+ options?: DispatchOptions,
39
+ ): Promise<void> => {
40
+ const envelope: CommandEnvelope = createCommandEnvelope({
41
+ command: input,
42
+ idempotencyKey:
43
+ resolveIdempotencyKey(options?.idempotencyKey) ?? undefined,
44
+ });
45
+ push(envelope);
46
+ await waitForCommandSettlement({ requestId: envelope.requestId });
47
+ };
48
+ };
49
+
50
+ const resolveIdempotencyKey = (value?: string): string | null => {
51
+ if (value === undefined) {
52
+ return null;
53
+ }
54
+ const idempotencyKey = value.trim();
55
+ if (!idempotencyKey) {
56
+ throw new Error("idempotencyKey must be a non-empty string");
57
+ }
58
+ return idempotencyKey;
59
+ };
@@ -0,0 +1,41 @@
1
+ import { createCommandRequestId } from "./request-id.js";
2
+ import type {
3
+ ClientMeta,
4
+ CommandEnvelope,
5
+ PublicCommandPayload,
6
+ } from "./sdk-types.js";
7
+
8
+ export type CreateCommandEnvelopeInput = {
9
+ command: PublicCommandPayload;
10
+ requestId?: string | undefined;
11
+ idempotencyKey?: string | undefined;
12
+ instanceId?: string | undefined;
13
+ clientMeta?: ClientMeta | undefined;
14
+ };
15
+
16
+ const normalizeOptionalString = (value?: string): string | undefined => {
17
+ if (value === undefined) {
18
+ return undefined;
19
+ }
20
+ const normalized = value.trim();
21
+ return normalized.length > 0 ? normalized : undefined;
22
+ };
23
+
24
+ export const createCommandEnvelope = (
25
+ input: CreateCommandEnvelopeInput,
26
+ ): CommandEnvelope => {
27
+ const requestId =
28
+ normalizeOptionalString(input.requestId) ?? createCommandRequestId();
29
+ const idempotencyKey =
30
+ normalizeOptionalString(input.idempotencyKey) ?? requestId;
31
+ const instanceId = normalizeOptionalString(input.instanceId);
32
+
33
+ return {
34
+ version: "1",
35
+ command: input.command,
36
+ requestId,
37
+ idempotencyKey,
38
+ ...(instanceId !== undefined ? { instanceId } : {}),
39
+ ...(input.clientMeta !== undefined ? { clientMeta: input.clientMeta } : {}),
40
+ };
41
+ };