inngest 4.2.2 → 4.2.3
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.
- package/CHANGELOG.md +8 -0
- package/api/schema.d.cts +2 -2
- package/api/schema.d.ts +2 -2
- package/components/InngestGroupTools.cjs +5 -4
- package/components/InngestGroupTools.cjs.map +1 -1
- package/components/InngestGroupTools.js +5 -4
- package/components/InngestGroupTools.js.map +1 -1
- package/components/StreamTools.cjs +3 -1
- package/components/StreamTools.cjs.map +1 -1
- package/components/StreamTools.d.cts.map +1 -1
- package/components/StreamTools.d.ts.map +1 -1
- package/components/StreamTools.js +3 -1
- package/components/StreamTools.js.map +1 -1
- package/components/execution/als.cjs.map +1 -1
- package/components/execution/als.d.cts +1 -0
- package/components/execution/als.d.cts.map +1 -1
- package/components/execution/als.d.ts +1 -0
- package/components/execution/als.d.ts.map +1 -1
- package/components/execution/als.js.map +1 -1
- package/components/realtime/types.d.cts +7 -7
- package/components/realtime/types.d.cts.map +1 -1
- package/components/realtime/types.d.ts +7 -7
- package/components/realtime/types.d.ts.map +1 -1
- package/node.cjs +4 -16
- package/node.cjs.map +1 -1
- package/node.d.cts.map +1 -1
- package/node.d.ts.map +1 -1
- package/node.js +3 -16
- package/node.js.map +1 -1
- package/package.json +1 -1
- package/react.d.cts.map +1 -1
- package/react.d.ts.map +1 -1
- package/types.d.cts +24 -24
- package/types.d.ts +24 -24
- package/version.cjs +1 -1
- package/version.cjs.map +1 -1
- package/version.d.cts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# inngest
|
|
2
2
|
|
|
3
|
+
## 4.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1460](https://github.com/inngest/inngest-js/pull/1460) [`ce5110de`](https://github.com/inngest/inngest-js/commit/ce5110dea3fd64ed26fbf5bed86c10bbf1d85ae6) Thanks [@amh4r](https://github.com/amh4r)! - Fix multi-byte UTF-8 chars corrupted when split over chunks
|
|
8
|
+
|
|
9
|
+
- [#1461](https://github.com/inngest/inngest-js/pull/1461) [`d75b59ce`](https://github.com/inngest/inngest-js/commit/d75b59ce5690d0dd7bbc7fb9a76138896bcbeca9) Thanks [@amh4r](https://github.com/amh4r)! - Fix CountQueuingStrategy erroring when stubbed in edge runtimes
|
|
10
|
+
|
|
3
11
|
## 4.2.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/api/schema.d.cts
CHANGED
|
@@ -27,8 +27,8 @@ declare const stepSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.Zo
|
|
|
27
27
|
error: z.ZodType<JsonError, z.ZodTypeDef, JsonError>;
|
|
28
28
|
}, "strict", z.ZodTypeAny, {
|
|
29
29
|
error: {
|
|
30
|
-
name?: string | undefined;
|
|
31
30
|
error?: string | undefined;
|
|
31
|
+
name?: string | undefined;
|
|
32
32
|
message?: string | undefined;
|
|
33
33
|
stack?: string | undefined;
|
|
34
34
|
} & {
|
|
@@ -39,8 +39,8 @@ declare const stepSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.Zo
|
|
|
39
39
|
type: "error";
|
|
40
40
|
}, {
|
|
41
41
|
error: {
|
|
42
|
-
name?: string | undefined;
|
|
43
42
|
error?: string | undefined;
|
|
43
|
+
name?: string | undefined;
|
|
44
44
|
message?: string | undefined;
|
|
45
45
|
stack?: string | undefined;
|
|
46
46
|
} & {
|
package/api/schema.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ declare const stepSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.Zo
|
|
|
27
27
|
error: z.ZodType<JsonError, z.ZodTypeDef, JsonError>;
|
|
28
28
|
}, "strict", z.ZodTypeAny, {
|
|
29
29
|
error: {
|
|
30
|
-
name?: string | undefined;
|
|
31
30
|
error?: string | undefined;
|
|
31
|
+
name?: string | undefined;
|
|
32
32
|
message?: string | undefined;
|
|
33
33
|
stack?: string | undefined;
|
|
34
34
|
} & {
|
|
@@ -39,8 +39,8 @@ declare const stepSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.Zo
|
|
|
39
39
|
type: "error";
|
|
40
40
|
}, {
|
|
41
41
|
error: {
|
|
42
|
-
name?: string | undefined;
|
|
43
42
|
error?: string | undefined;
|
|
43
|
+
name?: string | undefined;
|
|
44
44
|
message?: string | undefined;
|
|
45
45
|
stack?: string | undefined;
|
|
46
46
|
} & {
|
|
@@ -95,11 +95,12 @@ const createGroupTools = (deps) => {
|
|
|
95
95
|
...currentCtx,
|
|
96
96
|
execution: {
|
|
97
97
|
...currentCtx.execution,
|
|
98
|
-
|
|
99
|
-
experimentStepID: experimentStepHashedId,
|
|
98
|
+
experimentContext: {
|
|
99
|
+
experimentStepID: experimentStepHashedId ?? "",
|
|
100
100
|
experimentName: stepOpts.id,
|
|
101
|
-
variant: selectedVariant
|
|
102
|
-
|
|
101
|
+
variant: selectedVariant,
|
|
102
|
+
selectionStrategy: select.__experimentConfig.strategy
|
|
103
|
+
},
|
|
103
104
|
experimentStepTracker: stepTracker
|
|
104
105
|
}
|
|
105
106
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InngestGroupTools.cjs","names":["options: ParallelOptions","getAsyncCtxSync","getAsyncLocalStorage","isALSFallback","nestedCtx: AsyncContext","experiment: GroupExperiment","getStepOptions","experimentStepHashedId: string | undefined","selectedVariant: string","currentCtx","selectCtx: AsyncContext","result","NonRetriableError","result: unknown"],"sources":["../../src/components/InngestGroupTools.ts"],"sourcesContent":["import type { IsNever } from \"../helpers/types.ts\";\nimport type { StepOptionsOrId } from \"../types.ts\";\nimport {\n type AsyncContext,\n getAsyncCtxSync,\n getAsyncLocalStorage,\n isALSFallback,\n} from \"./execution/als.ts\";\nimport { getStepOptions } from \"./InngestStepTools.ts\";\nimport { NonRetriableError } from \"./NonRetriableError.ts\";\n\n/**\n * Options for the `group.parallel()` helper.\n */\nexport interface ParallelOptions {\n /**\n * The parallel mode to apply to all steps created within the callback.\n *\n * - `\"race\"`: Steps will be executed with race semantics, meaning the first\n * step to complete will \"win\" and remaining steps may be cancelled.\n */\n mode?: \"race\";\n}\n\n/**\n * A helper that sets the parallel mode for all steps created within the\n * callback. This allows you to use native `Promise.race()` with cleaner syntax.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\nconst parallel = async <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n): Promise<T> => {\n const options: ParallelOptions =\n typeof optionsOrCallback === \"function\" ? {} : optionsOrCallback;\n const callback =\n typeof optionsOrCallback === \"function\" ? optionsOrCallback : maybeCallback;\n\n if (!callback) {\n throw new Error(\"`group.parallel()` requires a callback function\");\n }\n\n const currentCtx = getAsyncCtxSync();\n\n if (!currentCtx?.execution) {\n throw new Error(\n \"`group.parallel()` must be called within an Inngest function execution\",\n );\n }\n\n const als = await getAsyncLocalStorage();\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.parallel()` requires AsyncLocalStorage support, which is not available in this runtime. \" +\n \"Workaround: Pass `parallelMode` directly to each step:\\n\" +\n ' step.run({ id: \"my-step\", parallelMode: \"race\" }, fn)',\n );\n }\n\n // Create a new context with the parallelMode set\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n parallelMode: options.mode ?? \"race\",\n },\n };\n\n // Run the callback inside the nested context\n return als.run(nestedCtx, callback);\n};\n\n/**\n * Configuration for how the experiment selects a variant.\n */\nexport interface ExperimentStrategyConfig {\n strategy: string;\n weights?: Record<string, number>;\n nullishBucket?: boolean;\n}\n\n/**\n * A callable selection function that also carries strategy metadata.\n */\nexport interface ExperimentSelectFn {\n (variantNames?: string[]): Promise<string> | string;\n __experimentConfig: ExperimentStrategyConfig;\n}\n\n/**\n * Options for `group.experiment()`.\n */\nexport interface ExperimentOptions<\n TVariants extends Record<string, () => unknown>,\n> {\n /**\n * A map of variant names to callbacks. The selected variant's callback will\n * be executed at the top level so that any `step.*` calls inside it go\n * through normal step discovery.\n */\n variants: TVariants;\n\n /**\n * A selection function that returns the name of the variant to execute.\n * The result is memoized via a step so the same variant is used on retries.\n */\n select: ExperimentSelectFn;\n}\n\n/**\n * Options for `group.experiment()` when `withVariant` is true, which causes\n * the return type to include both the result and the selected variant name.\n */\nexport interface ExperimentOptionsWithVariant<\n TVariants extends Record<string, () => unknown>,\n> extends ExperimentOptions<TVariants> {\n /**\n * When true, the return value includes the variant name alongside the result.\n */\n withVariant: true;\n}\n\n/**\n * Computes the return type of an experiment based on variant callbacks.\n *\n * When `TConstraint` is `never`, the return type is inferred as the union of\n * all variant callback return types. Otherwise `TConstraint` is used directly.\n */\nexport type VariantResult<\n TConstraint,\n TVariants extends Record<string, () => unknown>,\n> = IsNever<TConstraint> extends true\n ? Awaited<ReturnType<TVariants[keyof TVariants]>>\n : TConstraint;\n\n/**\n * Metadata values stored alongside the experiment step for UI rendering.\n */\nexport interface ExperimentMetadataValues {\n experiment_name: string;\n variant_selected: string;\n selection_strategy: string;\n available_variants: string[];\n variant_weights?: Record<string, number>;\n}\n\n/**\n * Overloaded interface for `group.experiment()`.\n */\nexport interface GroupExperiment {\n /**\n * Run an A/B experiment that selects and executes a variant. Returns both\n * the result and the selected variant name.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptionsWithVariant<TVariants>,\n ): Promise<{\n result: VariantResult<never, TVariants>;\n variant: string;\n }>;\n\n /**\n * Run an A/B experiment that selects and executes a variant. Returns only\n * the variant callback's result.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptions<TVariants>,\n ): Promise<VariantResult<never, TVariants>>;\n}\n\n/**\n * Tools for grouping and coordinating steps.\n *\n * @public\n */\nexport interface GroupTools {\n /**\n * Run a callback where all steps automatically receive a `parallelMode`\n * option, removing the need to tag each step individually. Defaults to\n * `\"race\"` mode.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\n parallel: <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n ) => Promise<T>;\n\n /**\n * Run an A/B experiment within a function. Selects a variant via a memoized\n * step, then executes the selected variant's callback at the top level so\n * its `step.*` calls go through normal step discovery.\n *\n * @example\n * ```ts\n * const result = await group.experiment(\"checkout-flow\", {\n * variants: {\n * control: () => step.run(\"control-checkout\", () => oldCheckout()),\n * new_flow: () => step.run(\"new-checkout\", () => newCheckout()),\n * },\n * select: Object.assign(() => \"control\", {\n * __experimentConfig: { strategy: \"weighted\", weights: { control: 80, new_flow: 20 } },\n * }),\n * });\n * ```\n */\n experiment: GroupExperiment;\n}\n\n/**\n * Dependencies injected into `createGroupTools` from the execution engine.\n */\nexport interface GroupToolsDeps {\n /**\n * A `step.run` variant with `opts.type = \"group.experiment\"`, extracted from\n * step tools via the experiment symbol. Undefined when not available.\n */\n // biome-ignore lint/suspicious/noExplicitAny: internal plumbing\n experimentStepRun?: (...args: any[]) => Promise<any>;\n}\n\n/**\n * Create the `group` tools object provided on the function execution context.\n *\n * @public\n */\nexport const createGroupTools = (deps?: GroupToolsDeps): GroupTools => {\n const experiment: GroupExperiment = (async (\n idOrOptions: StepOptionsOrId,\n // biome-ignore lint/suspicious/noExplicitAny: implementation signature for overloaded interface\n options: any,\n ) => {\n if (!deps?.experimentStepRun) {\n throw new Error(\n \"`group.experiment()` requires step tools to be available. \" +\n \"Ensure you are calling this within an Inngest function execution.\",\n );\n }\n\n const { variants, select, withVariant } = options;\n const variantNames = Object.keys(variants);\n\n if (variantNames.length === 0) {\n throw new Error(\n \"`group.experiment()` requires at least one variant to be defined.\",\n );\n }\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.experiment()` requires AsyncLocalStorage support, which is not available in this runtime.\",\n );\n }\n\n const stepOpts = getStepOptions(idOrOptions);\n\n // Use the experiment step run to memoize the variant selection.\n // This creates a StepPlanned opcode with opts.type = \"group.experiment\".\n let experimentStepHashedId: string | undefined;\n\n const selectedVariant: string = await deps.experimentStepRun(\n idOrOptions,\n async () => {\n // Capture the hashed step ID so we can propagate it to variant sub-steps.\n experimentStepHashedId =\n getAsyncCtxSync()?.execution?.executingStep?.id;\n\n const alsInstance = await getAsyncLocalStorage();\n const currentCtx = getAsyncCtxSync()!;\n const selectCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution!,\n insideExperimentSelect: true,\n },\n };\n const result = await alsInstance.run(selectCtx, () =>\n select(variantNames),\n );\n\n if (!variantNames.includes(result)) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): select() returned \"${result}\" ` +\n `which is not a known variant. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Attach experiment metadata to this step's OutgoingOp.\n const ctx = getAsyncCtxSync();\n const execInstance = ctx?.execution?.instance;\n\n if (execInstance && experimentStepHashedId) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.experiment\",\n \"step\",\n \"merge\",\n {\n experiment_name: stepOpts.id,\n variant_selected: result,\n selection_strategy: select.__experimentConfig.strategy,\n available_variants: variantNames,\n ...(select.__experimentConfig.weights && {\n variant_weights: select.__experimentConfig.weights,\n }),\n } satisfies ExperimentMetadataValues,\n );\n\n if (select.__experimentConfig.nullishBucket) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.warnings\",\n \"step\",\n \"merge\",\n {\n message:\n \"experiment.bucket() received a null/undefined value; \" +\n 'hashing empty string \"\" for variant selection',\n },\n );\n }\n }\n\n return result;\n },\n );\n\n // Look up and execute the selected variant's callback at the top level\n // so its step.* calls go through normal step discovery.\n const variantFn = variants[selectedVariant];\n\n if (!variantFn) {\n throw new Error(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n `was selected but is not defined. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Propagate experiment context via ALS so variant sub-steps include\n // experiment fields in their OutgoingOp.opts. Also track whether any\n // step tool is invoked to detect zero-step variants.\n //\n // TODO: On replay, experimentStepHashedId is undefined because it's\n // captured inside the selection step callback, which doesn't run when\n // memoized. This means sub-steps discovered during replay won't carry\n // experimentContext in their OutgoingOp.opts. Fixing this requires an\n // engine-level change to expose the hashed step ID outside the callback\n // (e.g. via ALS before the callback runs, or returned alongside the\n // memoized result). Tracked in EXE-1330.\n const currentCtx = getAsyncCtxSync();\n const stepTracker = { found: false };\n let result: unknown;\n\n if (currentCtx?.execution && !isALSFallback()) {\n const als = await getAsyncLocalStorage();\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n ...(experimentStepHashedId && {\n experimentContext: {\n experimentStepID: experimentStepHashedId,\n experimentName: stepOpts.id,\n variant: selectedVariant,\n },\n }),\n experimentStepTracker: stepTracker,\n },\n };\n result = await als.run(nestedCtx, () => variantFn());\n } else {\n result = await variantFn();\n }\n\n // If the variant returned without invoking any step tools, it will\n // silently re-execute on every replay. Throw a non-retriable error\n // to prevent this.\n if (!stepTracker.found && !isALSFallback()) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n \"did not invoke any step tools. Wrap your variant logic in \" +\n \"step.run() to ensure it is memoized and not re-executed on replay.\",\n );\n }\n\n if (withVariant) {\n return { result, variant: selectedVariant };\n }\n\n return result;\n }) as GroupExperiment;\n\n return { parallel, experiment };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InngestGroupTools.cjs","names":["options: ParallelOptions","getAsyncCtxSync","getAsyncLocalStorage","isALSFallback","nestedCtx: AsyncContext","experiment: GroupExperiment","getStepOptions","experimentStepHashedId: string | undefined","selectedVariant: string","currentCtx","selectCtx: AsyncContext","result","NonRetriableError","result: unknown"],"sources":["../../src/components/InngestGroupTools.ts"],"sourcesContent":["import type { IsNever } from \"../helpers/types.ts\";\nimport type { StepOptionsOrId } from \"../types.ts\";\nimport {\n type AsyncContext,\n getAsyncCtxSync,\n getAsyncLocalStorage,\n isALSFallback,\n} from \"./execution/als.ts\";\nimport { getStepOptions } from \"./InngestStepTools.ts\";\nimport { NonRetriableError } from \"./NonRetriableError.ts\";\n\n/**\n * Options for the `group.parallel()` helper.\n */\nexport interface ParallelOptions {\n /**\n * The parallel mode to apply to all steps created within the callback.\n *\n * - `\"race\"`: Steps will be executed with race semantics, meaning the first\n * step to complete will \"win\" and remaining steps may be cancelled.\n */\n mode?: \"race\";\n}\n\n/**\n * A helper that sets the parallel mode for all steps created within the\n * callback. This allows you to use native `Promise.race()` with cleaner syntax.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\nconst parallel = async <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n): Promise<T> => {\n const options: ParallelOptions =\n typeof optionsOrCallback === \"function\" ? {} : optionsOrCallback;\n const callback =\n typeof optionsOrCallback === \"function\" ? optionsOrCallback : maybeCallback;\n\n if (!callback) {\n throw new Error(\"`group.parallel()` requires a callback function\");\n }\n\n const currentCtx = getAsyncCtxSync();\n\n if (!currentCtx?.execution) {\n throw new Error(\n \"`group.parallel()` must be called within an Inngest function execution\",\n );\n }\n\n const als = await getAsyncLocalStorage();\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.parallel()` requires AsyncLocalStorage support, which is not available in this runtime. \" +\n \"Workaround: Pass `parallelMode` directly to each step:\\n\" +\n ' step.run({ id: \"my-step\", parallelMode: \"race\" }, fn)',\n );\n }\n\n // Create a new context with the parallelMode set\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n parallelMode: options.mode ?? \"race\",\n },\n };\n\n // Run the callback inside the nested context\n return als.run(nestedCtx, callback);\n};\n\n/**\n * Configuration for how the experiment selects a variant.\n */\nexport interface ExperimentStrategyConfig {\n strategy: string;\n weights?: Record<string, number>;\n nullishBucket?: boolean;\n}\n\n/**\n * A callable selection function that also carries strategy metadata.\n */\nexport interface ExperimentSelectFn {\n (variantNames?: string[]): Promise<string> | string;\n __experimentConfig: ExperimentStrategyConfig;\n}\n\n/**\n * Options for `group.experiment()`.\n */\nexport interface ExperimentOptions<\n TVariants extends Record<string, () => unknown>,\n> {\n /**\n * A map of variant names to callbacks. The selected variant's callback will\n * be executed at the top level so that any `step.*` calls inside it go\n * through normal step discovery.\n */\n variants: TVariants;\n\n /**\n * A selection function that returns the name of the variant to execute.\n * The result is memoized via a step so the same variant is used on retries.\n */\n select: ExperimentSelectFn;\n}\n\n/**\n * Options for `group.experiment()` when `withVariant` is true, which causes\n * the return type to include both the result and the selected variant name.\n */\nexport interface ExperimentOptionsWithVariant<\n TVariants extends Record<string, () => unknown>,\n> extends ExperimentOptions<TVariants> {\n /**\n * When true, the return value includes the variant name alongside the result.\n */\n withVariant: true;\n}\n\n/**\n * Computes the return type of an experiment based on variant callbacks.\n *\n * When `TConstraint` is `never`, the return type is inferred as the union of\n * all variant callback return types. Otherwise `TConstraint` is used directly.\n */\nexport type VariantResult<\n TConstraint,\n TVariants extends Record<string, () => unknown>,\n> = IsNever<TConstraint> extends true\n ? Awaited<ReturnType<TVariants[keyof TVariants]>>\n : TConstraint;\n\n/**\n * Metadata values stored alongside the experiment step for UI rendering.\n */\nexport interface ExperimentMetadataValues {\n experiment_name: string;\n variant_selected: string;\n selection_strategy: string;\n available_variants: string[];\n variant_weights?: Record<string, number>;\n}\n\n/**\n * Overloaded interface for `group.experiment()`.\n */\nexport interface GroupExperiment {\n /**\n * Run an A/B experiment that selects and executes a variant. Returns both\n * the result and the selected variant name.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptionsWithVariant<TVariants>,\n ): Promise<{\n result: VariantResult<never, TVariants>;\n variant: string;\n }>;\n\n /**\n * Run an A/B experiment that selects and executes a variant. Returns only\n * the variant callback's result.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptions<TVariants>,\n ): Promise<VariantResult<never, TVariants>>;\n}\n\n/**\n * Tools for grouping and coordinating steps.\n *\n * @public\n */\nexport interface GroupTools {\n /**\n * Run a callback where all steps automatically receive a `parallelMode`\n * option, removing the need to tag each step individually. Defaults to\n * `\"race\"` mode.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\n parallel: <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n ) => Promise<T>;\n\n /**\n * Run an A/B experiment within a function. Selects a variant via a memoized\n * step, then executes the selected variant's callback at the top level so\n * its `step.*` calls go through normal step discovery.\n *\n * @example\n * ```ts\n * const result = await group.experiment(\"checkout-flow\", {\n * variants: {\n * control: () => step.run(\"control-checkout\", () => oldCheckout()),\n * new_flow: () => step.run(\"new-checkout\", () => newCheckout()),\n * },\n * select: Object.assign(() => \"control\", {\n * __experimentConfig: { strategy: \"weighted\", weights: { control: 80, new_flow: 20 } },\n * }),\n * });\n * ```\n */\n experiment: GroupExperiment;\n}\n\n/**\n * Dependencies injected into `createGroupTools` from the execution engine.\n */\nexport interface GroupToolsDeps {\n /**\n * A `step.run` variant with `opts.type = \"group.experiment\"`, extracted from\n * step tools via the experiment symbol. Undefined when not available.\n */\n // biome-ignore lint/suspicious/noExplicitAny: internal plumbing\n experimentStepRun?: (...args: any[]) => Promise<any>;\n}\n\n/**\n * Create the `group` tools object provided on the function execution context.\n *\n * @public\n */\nexport const createGroupTools = (deps?: GroupToolsDeps): GroupTools => {\n const experiment: GroupExperiment = (async (\n idOrOptions: StepOptionsOrId,\n // biome-ignore lint/suspicious/noExplicitAny: implementation signature for overloaded interface\n options: any,\n ) => {\n if (!deps?.experimentStepRun) {\n throw new Error(\n \"`group.experiment()` requires step tools to be available. \" +\n \"Ensure you are calling this within an Inngest function execution.\",\n );\n }\n\n const { variants, select, withVariant } = options;\n const variantNames = Object.keys(variants);\n\n if (variantNames.length === 0) {\n throw new Error(\n \"`group.experiment()` requires at least one variant to be defined.\",\n );\n }\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.experiment()` requires AsyncLocalStorage support, which is not available in this runtime.\",\n );\n }\n\n const stepOpts = getStepOptions(idOrOptions);\n\n // Use the experiment step run to memoize the variant selection.\n // This creates a StepPlanned opcode with opts.type = \"group.experiment\".\n let experimentStepHashedId: string | undefined;\n\n const selectedVariant: string = await deps.experimentStepRun(\n idOrOptions,\n async () => {\n // Capture the hashed step ID so we can propagate it to variant sub-steps.\n experimentStepHashedId =\n getAsyncCtxSync()?.execution?.executingStep?.id;\n\n const alsInstance = await getAsyncLocalStorage();\n const currentCtx = getAsyncCtxSync()!;\n const selectCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution!,\n insideExperimentSelect: true,\n },\n };\n const result = await alsInstance.run(selectCtx, () =>\n select(variantNames),\n );\n\n if (!variantNames.includes(result)) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): select() returned \"${result}\" ` +\n `which is not a known variant. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Attach experiment metadata to this step's OutgoingOp.\n const ctx = getAsyncCtxSync();\n const execInstance = ctx?.execution?.instance;\n\n if (execInstance && experimentStepHashedId) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.experiment\",\n \"step\",\n \"merge\",\n {\n experiment_name: stepOpts.id,\n variant_selected: result,\n selection_strategy: select.__experimentConfig.strategy,\n available_variants: variantNames,\n ...(select.__experimentConfig.weights && {\n variant_weights: select.__experimentConfig.weights,\n }),\n } satisfies ExperimentMetadataValues,\n );\n\n if (select.__experimentConfig.nullishBucket) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.warnings\",\n \"step\",\n \"merge\",\n {\n message:\n \"experiment.bucket() received a null/undefined value; \" +\n 'hashing empty string \"\" for variant selection',\n },\n );\n }\n }\n\n return result;\n },\n );\n\n // Look up and execute the selected variant's callback at the top level\n // so its step.* calls go through normal step discovery.\n const variantFn = variants[selectedVariant];\n\n if (!variantFn) {\n throw new Error(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n `was selected but is not defined. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Propagate experiment context via ALS so variant sub-steps include\n // experiment fields in their OutgoingOp.opts. The executor reads these\n // fields from opts and emits the step-scoped `inngest.experiment`\n // metadata span itself — the SDK does not need to call addMetadata()\n // for variant steps. See the companion executor change in inngest/inngest\n // for the server-side emission path.\n //\n // Also track whether any step tool is invoked to detect zero-step\n // variants.\n //\n // NOTE: experimentStepHashedId may be undefined on replay because it\n // is captured inside the selection step callback, which doesn't run\n // when memoized. We still set experimentContext (with an empty string\n // for the hashed ID fallback) so that variant sub-steps discovered on\n // replay still carry experiment fields in their opts and the executor\n // can attach metadata to their ClickHouse rows.\n const currentCtx = getAsyncCtxSync();\n const stepTracker = { found: false };\n let result: unknown;\n\n if (currentCtx?.execution && !isALSFallback()) {\n const als = await getAsyncLocalStorage();\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n experimentContext: {\n experimentStepID: experimentStepHashedId ?? \"\",\n experimentName: stepOpts.id,\n variant: selectedVariant,\n selectionStrategy: select.__experimentConfig.strategy,\n },\n experimentStepTracker: stepTracker,\n },\n };\n result = await als.run(nestedCtx, () => variantFn());\n } else {\n result = await variantFn();\n }\n\n // If the variant returned without invoking any step tools, it will\n // silently re-execute on every replay. Throw a non-retriable error\n // to prevent this.\n if (!stepTracker.found && !isALSFallback()) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n \"did not invoke any step tools. Wrap your variant logic in \" +\n \"step.run() to ensure it is memoized and not re-executed on replay.\",\n );\n }\n\n if (withVariant) {\n return { result, variant: selectedVariant };\n }\n\n return result;\n }) as GroupExperiment;\n\n return { parallel, experiment };\n};\n"],"mappings":"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"}
|
|
@@ -95,11 +95,12 @@ const createGroupTools = (deps) => {
|
|
|
95
95
|
...currentCtx,
|
|
96
96
|
execution: {
|
|
97
97
|
...currentCtx.execution,
|
|
98
|
-
|
|
99
|
-
experimentStepID: experimentStepHashedId,
|
|
98
|
+
experimentContext: {
|
|
99
|
+
experimentStepID: experimentStepHashedId ?? "",
|
|
100
100
|
experimentName: stepOpts.id,
|
|
101
|
-
variant: selectedVariant
|
|
102
|
-
|
|
101
|
+
variant: selectedVariant,
|
|
102
|
+
selectionStrategy: select.__experimentConfig.strategy
|
|
103
|
+
},
|
|
103
104
|
experimentStepTracker: stepTracker
|
|
104
105
|
}
|
|
105
106
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InngestGroupTools.js","names":["options: ParallelOptions","nestedCtx: AsyncContext","experiment: GroupExperiment","experimentStepHashedId: string | undefined","selectedVariant: string","currentCtx","selectCtx: AsyncContext","result","result: unknown"],"sources":["../../src/components/InngestGroupTools.ts"],"sourcesContent":["import type { IsNever } from \"../helpers/types.ts\";\nimport type { StepOptionsOrId } from \"../types.ts\";\nimport {\n type AsyncContext,\n getAsyncCtxSync,\n getAsyncLocalStorage,\n isALSFallback,\n} from \"./execution/als.ts\";\nimport { getStepOptions } from \"./InngestStepTools.ts\";\nimport { NonRetriableError } from \"./NonRetriableError.ts\";\n\n/**\n * Options for the `group.parallel()` helper.\n */\nexport interface ParallelOptions {\n /**\n * The parallel mode to apply to all steps created within the callback.\n *\n * - `\"race\"`: Steps will be executed with race semantics, meaning the first\n * step to complete will \"win\" and remaining steps may be cancelled.\n */\n mode?: \"race\";\n}\n\n/**\n * A helper that sets the parallel mode for all steps created within the\n * callback. This allows you to use native `Promise.race()` with cleaner syntax.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\nconst parallel = async <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n): Promise<T> => {\n const options: ParallelOptions =\n typeof optionsOrCallback === \"function\" ? {} : optionsOrCallback;\n const callback =\n typeof optionsOrCallback === \"function\" ? optionsOrCallback : maybeCallback;\n\n if (!callback) {\n throw new Error(\"`group.parallel()` requires a callback function\");\n }\n\n const currentCtx = getAsyncCtxSync();\n\n if (!currentCtx?.execution) {\n throw new Error(\n \"`group.parallel()` must be called within an Inngest function execution\",\n );\n }\n\n const als = await getAsyncLocalStorage();\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.parallel()` requires AsyncLocalStorage support, which is not available in this runtime. \" +\n \"Workaround: Pass `parallelMode` directly to each step:\\n\" +\n ' step.run({ id: \"my-step\", parallelMode: \"race\" }, fn)',\n );\n }\n\n // Create a new context with the parallelMode set\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n parallelMode: options.mode ?? \"race\",\n },\n };\n\n // Run the callback inside the nested context\n return als.run(nestedCtx, callback);\n};\n\n/**\n * Configuration for how the experiment selects a variant.\n */\nexport interface ExperimentStrategyConfig {\n strategy: string;\n weights?: Record<string, number>;\n nullishBucket?: boolean;\n}\n\n/**\n * A callable selection function that also carries strategy metadata.\n */\nexport interface ExperimentSelectFn {\n (variantNames?: string[]): Promise<string> | string;\n __experimentConfig: ExperimentStrategyConfig;\n}\n\n/**\n * Options for `group.experiment()`.\n */\nexport interface ExperimentOptions<\n TVariants extends Record<string, () => unknown>,\n> {\n /**\n * A map of variant names to callbacks. The selected variant's callback will\n * be executed at the top level so that any `step.*` calls inside it go\n * through normal step discovery.\n */\n variants: TVariants;\n\n /**\n * A selection function that returns the name of the variant to execute.\n * The result is memoized via a step so the same variant is used on retries.\n */\n select: ExperimentSelectFn;\n}\n\n/**\n * Options for `group.experiment()` when `withVariant` is true, which causes\n * the return type to include both the result and the selected variant name.\n */\nexport interface ExperimentOptionsWithVariant<\n TVariants extends Record<string, () => unknown>,\n> extends ExperimentOptions<TVariants> {\n /**\n * When true, the return value includes the variant name alongside the result.\n */\n withVariant: true;\n}\n\n/**\n * Computes the return type of an experiment based on variant callbacks.\n *\n * When `TConstraint` is `never`, the return type is inferred as the union of\n * all variant callback return types. Otherwise `TConstraint` is used directly.\n */\nexport type VariantResult<\n TConstraint,\n TVariants extends Record<string, () => unknown>,\n> = IsNever<TConstraint> extends true\n ? Awaited<ReturnType<TVariants[keyof TVariants]>>\n : TConstraint;\n\n/**\n * Metadata values stored alongside the experiment step for UI rendering.\n */\nexport interface ExperimentMetadataValues {\n experiment_name: string;\n variant_selected: string;\n selection_strategy: string;\n available_variants: string[];\n variant_weights?: Record<string, number>;\n}\n\n/**\n * Overloaded interface for `group.experiment()`.\n */\nexport interface GroupExperiment {\n /**\n * Run an A/B experiment that selects and executes a variant. Returns both\n * the result and the selected variant name.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptionsWithVariant<TVariants>,\n ): Promise<{\n result: VariantResult<never, TVariants>;\n variant: string;\n }>;\n\n /**\n * Run an A/B experiment that selects and executes a variant. Returns only\n * the variant callback's result.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptions<TVariants>,\n ): Promise<VariantResult<never, TVariants>>;\n}\n\n/**\n * Tools for grouping and coordinating steps.\n *\n * @public\n */\nexport interface GroupTools {\n /**\n * Run a callback where all steps automatically receive a `parallelMode`\n * option, removing the need to tag each step individually. Defaults to\n * `\"race\"` mode.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\n parallel: <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n ) => Promise<T>;\n\n /**\n * Run an A/B experiment within a function. Selects a variant via a memoized\n * step, then executes the selected variant's callback at the top level so\n * its `step.*` calls go through normal step discovery.\n *\n * @example\n * ```ts\n * const result = await group.experiment(\"checkout-flow\", {\n * variants: {\n * control: () => step.run(\"control-checkout\", () => oldCheckout()),\n * new_flow: () => step.run(\"new-checkout\", () => newCheckout()),\n * },\n * select: Object.assign(() => \"control\", {\n * __experimentConfig: { strategy: \"weighted\", weights: { control: 80, new_flow: 20 } },\n * }),\n * });\n * ```\n */\n experiment: GroupExperiment;\n}\n\n/**\n * Dependencies injected into `createGroupTools` from the execution engine.\n */\nexport interface GroupToolsDeps {\n /**\n * A `step.run` variant with `opts.type = \"group.experiment\"`, extracted from\n * step tools via the experiment symbol. Undefined when not available.\n */\n // biome-ignore lint/suspicious/noExplicitAny: internal plumbing\n experimentStepRun?: (...args: any[]) => Promise<any>;\n}\n\n/**\n * Create the `group` tools object provided on the function execution context.\n *\n * @public\n */\nexport const createGroupTools = (deps?: GroupToolsDeps): GroupTools => {\n const experiment: GroupExperiment = (async (\n idOrOptions: StepOptionsOrId,\n // biome-ignore lint/suspicious/noExplicitAny: implementation signature for overloaded interface\n options: any,\n ) => {\n if (!deps?.experimentStepRun) {\n throw new Error(\n \"`group.experiment()` requires step tools to be available. \" +\n \"Ensure you are calling this within an Inngest function execution.\",\n );\n }\n\n const { variants, select, withVariant } = options;\n const variantNames = Object.keys(variants);\n\n if (variantNames.length === 0) {\n throw new Error(\n \"`group.experiment()` requires at least one variant to be defined.\",\n );\n }\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.experiment()` requires AsyncLocalStorage support, which is not available in this runtime.\",\n );\n }\n\n const stepOpts = getStepOptions(idOrOptions);\n\n // Use the experiment step run to memoize the variant selection.\n // This creates a StepPlanned opcode with opts.type = \"group.experiment\".\n let experimentStepHashedId: string | undefined;\n\n const selectedVariant: string = await deps.experimentStepRun(\n idOrOptions,\n async () => {\n // Capture the hashed step ID so we can propagate it to variant sub-steps.\n experimentStepHashedId =\n getAsyncCtxSync()?.execution?.executingStep?.id;\n\n const alsInstance = await getAsyncLocalStorage();\n const currentCtx = getAsyncCtxSync()!;\n const selectCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution!,\n insideExperimentSelect: true,\n },\n };\n const result = await alsInstance.run(selectCtx, () =>\n select(variantNames),\n );\n\n if (!variantNames.includes(result)) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): select() returned \"${result}\" ` +\n `which is not a known variant. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Attach experiment metadata to this step's OutgoingOp.\n const ctx = getAsyncCtxSync();\n const execInstance = ctx?.execution?.instance;\n\n if (execInstance && experimentStepHashedId) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.experiment\",\n \"step\",\n \"merge\",\n {\n experiment_name: stepOpts.id,\n variant_selected: result,\n selection_strategy: select.__experimentConfig.strategy,\n available_variants: variantNames,\n ...(select.__experimentConfig.weights && {\n variant_weights: select.__experimentConfig.weights,\n }),\n } satisfies ExperimentMetadataValues,\n );\n\n if (select.__experimentConfig.nullishBucket) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.warnings\",\n \"step\",\n \"merge\",\n {\n message:\n \"experiment.bucket() received a null/undefined value; \" +\n 'hashing empty string \"\" for variant selection',\n },\n );\n }\n }\n\n return result;\n },\n );\n\n // Look up and execute the selected variant's callback at the top level\n // so its step.* calls go through normal step discovery.\n const variantFn = variants[selectedVariant];\n\n if (!variantFn) {\n throw new Error(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n `was selected but is not defined. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Propagate experiment context via ALS so variant sub-steps include\n // experiment fields in their OutgoingOp.opts. Also track whether any\n // step tool is invoked to detect zero-step variants.\n //\n // TODO: On replay, experimentStepHashedId is undefined because it's\n // captured inside the selection step callback, which doesn't run when\n // memoized. This means sub-steps discovered during replay won't carry\n // experimentContext in their OutgoingOp.opts. Fixing this requires an\n // engine-level change to expose the hashed step ID outside the callback\n // (e.g. via ALS before the callback runs, or returned alongside the\n // memoized result). Tracked in EXE-1330.\n const currentCtx = getAsyncCtxSync();\n const stepTracker = { found: false };\n let result: unknown;\n\n if (currentCtx?.execution && !isALSFallback()) {\n const als = await getAsyncLocalStorage();\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n ...(experimentStepHashedId && {\n experimentContext: {\n experimentStepID: experimentStepHashedId,\n experimentName: stepOpts.id,\n variant: selectedVariant,\n },\n }),\n experimentStepTracker: stepTracker,\n },\n };\n result = await als.run(nestedCtx, () => variantFn());\n } else {\n result = await variantFn();\n }\n\n // If the variant returned without invoking any step tools, it will\n // silently re-execute on every replay. Throw a non-retriable error\n // to prevent this.\n if (!stepTracker.found && !isALSFallback()) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n \"did not invoke any step tools. Wrap your variant logic in \" +\n \"step.run() to ensure it is memoized and not re-executed on replay.\",\n );\n }\n\n if (withVariant) {\n return { result, variant: selectedVariant };\n }\n\n return result;\n }) as GroupExperiment;\n\n return { parallel, experiment };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InngestGroupTools.js","names":["options: ParallelOptions","nestedCtx: AsyncContext","experiment: GroupExperiment","experimentStepHashedId: string | undefined","selectedVariant: string","currentCtx","selectCtx: AsyncContext","result","result: unknown"],"sources":["../../src/components/InngestGroupTools.ts"],"sourcesContent":["import type { IsNever } from \"../helpers/types.ts\";\nimport type { StepOptionsOrId } from \"../types.ts\";\nimport {\n type AsyncContext,\n getAsyncCtxSync,\n getAsyncLocalStorage,\n isALSFallback,\n} from \"./execution/als.ts\";\nimport { getStepOptions } from \"./InngestStepTools.ts\";\nimport { NonRetriableError } from \"./NonRetriableError.ts\";\n\n/**\n * Options for the `group.parallel()` helper.\n */\nexport interface ParallelOptions {\n /**\n * The parallel mode to apply to all steps created within the callback.\n *\n * - `\"race\"`: Steps will be executed with race semantics, meaning the first\n * step to complete will \"win\" and remaining steps may be cancelled.\n */\n mode?: \"race\";\n}\n\n/**\n * A helper that sets the parallel mode for all steps created within the\n * callback. This allows you to use native `Promise.race()` with cleaner syntax.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\nconst parallel = async <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n): Promise<T> => {\n const options: ParallelOptions =\n typeof optionsOrCallback === \"function\" ? {} : optionsOrCallback;\n const callback =\n typeof optionsOrCallback === \"function\" ? optionsOrCallback : maybeCallback;\n\n if (!callback) {\n throw new Error(\"`group.parallel()` requires a callback function\");\n }\n\n const currentCtx = getAsyncCtxSync();\n\n if (!currentCtx?.execution) {\n throw new Error(\n \"`group.parallel()` must be called within an Inngest function execution\",\n );\n }\n\n const als = await getAsyncLocalStorage();\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.parallel()` requires AsyncLocalStorage support, which is not available in this runtime. \" +\n \"Workaround: Pass `parallelMode` directly to each step:\\n\" +\n ' step.run({ id: \"my-step\", parallelMode: \"race\" }, fn)',\n );\n }\n\n // Create a new context with the parallelMode set\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n parallelMode: options.mode ?? \"race\",\n },\n };\n\n // Run the callback inside the nested context\n return als.run(nestedCtx, callback);\n};\n\n/**\n * Configuration for how the experiment selects a variant.\n */\nexport interface ExperimentStrategyConfig {\n strategy: string;\n weights?: Record<string, number>;\n nullishBucket?: boolean;\n}\n\n/**\n * A callable selection function that also carries strategy metadata.\n */\nexport interface ExperimentSelectFn {\n (variantNames?: string[]): Promise<string> | string;\n __experimentConfig: ExperimentStrategyConfig;\n}\n\n/**\n * Options for `group.experiment()`.\n */\nexport interface ExperimentOptions<\n TVariants extends Record<string, () => unknown>,\n> {\n /**\n * A map of variant names to callbacks. The selected variant's callback will\n * be executed at the top level so that any `step.*` calls inside it go\n * through normal step discovery.\n */\n variants: TVariants;\n\n /**\n * A selection function that returns the name of the variant to execute.\n * The result is memoized via a step so the same variant is used on retries.\n */\n select: ExperimentSelectFn;\n}\n\n/**\n * Options for `group.experiment()` when `withVariant` is true, which causes\n * the return type to include both the result and the selected variant name.\n */\nexport interface ExperimentOptionsWithVariant<\n TVariants extends Record<string, () => unknown>,\n> extends ExperimentOptions<TVariants> {\n /**\n * When true, the return value includes the variant name alongside the result.\n */\n withVariant: true;\n}\n\n/**\n * Computes the return type of an experiment based on variant callbacks.\n *\n * When `TConstraint` is `never`, the return type is inferred as the union of\n * all variant callback return types. Otherwise `TConstraint` is used directly.\n */\nexport type VariantResult<\n TConstraint,\n TVariants extends Record<string, () => unknown>,\n> = IsNever<TConstraint> extends true\n ? Awaited<ReturnType<TVariants[keyof TVariants]>>\n : TConstraint;\n\n/**\n * Metadata values stored alongside the experiment step for UI rendering.\n */\nexport interface ExperimentMetadataValues {\n experiment_name: string;\n variant_selected: string;\n selection_strategy: string;\n available_variants: string[];\n variant_weights?: Record<string, number>;\n}\n\n/**\n * Overloaded interface for `group.experiment()`.\n */\nexport interface GroupExperiment {\n /**\n * Run an A/B experiment that selects and executes a variant. Returns both\n * the result and the selected variant name.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptionsWithVariant<TVariants>,\n ): Promise<{\n result: VariantResult<never, TVariants>;\n variant: string;\n }>;\n\n /**\n * Run an A/B experiment that selects and executes a variant. Returns only\n * the variant callback's result.\n */\n <TVariants extends Record<string, () => unknown>>(\n idOrOptions: StepOptionsOrId,\n options: ExperimentOptions<TVariants>,\n ): Promise<VariantResult<never, TVariants>>;\n}\n\n/**\n * Tools for grouping and coordinating steps.\n *\n * @public\n */\nexport interface GroupTools {\n /**\n * Run a callback where all steps automatically receive a `parallelMode`\n * option, removing the need to tag each step individually. Defaults to\n * `\"race\"` mode.\n *\n * @example\n * ```ts\n * // Defaults to \"race\" mode\n * const winner = await group.parallel(async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * step.run(\"c\", () => \"c\"),\n * ]);\n * });\n *\n * // Or explicitly specify the mode\n * const winner = await group.parallel({ mode: \"race\" }, async () => {\n * return Promise.race([\n * step.run(\"a\", () => \"a\"),\n * step.run(\"b\", () => \"b\"),\n * ]);\n * });\n * ```\n */\n parallel: <T>(\n optionsOrCallback: ParallelOptions | (() => Promise<T>),\n maybeCallback?: () => Promise<T>,\n ) => Promise<T>;\n\n /**\n * Run an A/B experiment within a function. Selects a variant via a memoized\n * step, then executes the selected variant's callback at the top level so\n * its `step.*` calls go through normal step discovery.\n *\n * @example\n * ```ts\n * const result = await group.experiment(\"checkout-flow\", {\n * variants: {\n * control: () => step.run(\"control-checkout\", () => oldCheckout()),\n * new_flow: () => step.run(\"new-checkout\", () => newCheckout()),\n * },\n * select: Object.assign(() => \"control\", {\n * __experimentConfig: { strategy: \"weighted\", weights: { control: 80, new_flow: 20 } },\n * }),\n * });\n * ```\n */\n experiment: GroupExperiment;\n}\n\n/**\n * Dependencies injected into `createGroupTools` from the execution engine.\n */\nexport interface GroupToolsDeps {\n /**\n * A `step.run` variant with `opts.type = \"group.experiment\"`, extracted from\n * step tools via the experiment symbol. Undefined when not available.\n */\n // biome-ignore lint/suspicious/noExplicitAny: internal plumbing\n experimentStepRun?: (...args: any[]) => Promise<any>;\n}\n\n/**\n * Create the `group` tools object provided on the function execution context.\n *\n * @public\n */\nexport const createGroupTools = (deps?: GroupToolsDeps): GroupTools => {\n const experiment: GroupExperiment = (async (\n idOrOptions: StepOptionsOrId,\n // biome-ignore lint/suspicious/noExplicitAny: implementation signature for overloaded interface\n options: any,\n ) => {\n if (!deps?.experimentStepRun) {\n throw new Error(\n \"`group.experiment()` requires step tools to be available. \" +\n \"Ensure you are calling this within an Inngest function execution.\",\n );\n }\n\n const { variants, select, withVariant } = options;\n const variantNames = Object.keys(variants);\n\n if (variantNames.length === 0) {\n throw new Error(\n \"`group.experiment()` requires at least one variant to be defined.\",\n );\n }\n\n if (isALSFallback()) {\n throw new Error(\n \"`group.experiment()` requires AsyncLocalStorage support, which is not available in this runtime.\",\n );\n }\n\n const stepOpts = getStepOptions(idOrOptions);\n\n // Use the experiment step run to memoize the variant selection.\n // This creates a StepPlanned opcode with opts.type = \"group.experiment\".\n let experimentStepHashedId: string | undefined;\n\n const selectedVariant: string = await deps.experimentStepRun(\n idOrOptions,\n async () => {\n // Capture the hashed step ID so we can propagate it to variant sub-steps.\n experimentStepHashedId =\n getAsyncCtxSync()?.execution?.executingStep?.id;\n\n const alsInstance = await getAsyncLocalStorage();\n const currentCtx = getAsyncCtxSync()!;\n const selectCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution!,\n insideExperimentSelect: true,\n },\n };\n const result = await alsInstance.run(selectCtx, () =>\n select(variantNames),\n );\n\n if (!variantNames.includes(result)) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): select() returned \"${result}\" ` +\n `which is not a known variant. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Attach experiment metadata to this step's OutgoingOp.\n const ctx = getAsyncCtxSync();\n const execInstance = ctx?.execution?.instance;\n\n if (execInstance && experimentStepHashedId) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.experiment\",\n \"step\",\n \"merge\",\n {\n experiment_name: stepOpts.id,\n variant_selected: result,\n selection_strategy: select.__experimentConfig.strategy,\n available_variants: variantNames,\n ...(select.__experimentConfig.weights && {\n variant_weights: select.__experimentConfig.weights,\n }),\n } satisfies ExperimentMetadataValues,\n );\n\n if (select.__experimentConfig.nullishBucket) {\n execInstance.addMetadata(\n experimentStepHashedId,\n \"inngest.warnings\",\n \"step\",\n \"merge\",\n {\n message:\n \"experiment.bucket() received a null/undefined value; \" +\n 'hashing empty string \"\" for variant selection',\n },\n );\n }\n }\n\n return result;\n },\n );\n\n // Look up and execute the selected variant's callback at the top level\n // so its step.* calls go through normal step discovery.\n const variantFn = variants[selectedVariant];\n\n if (!variantFn) {\n throw new Error(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n `was selected but is not defined. Available variants: ${variantNames.join(\", \")}`,\n );\n }\n\n // Propagate experiment context via ALS so variant sub-steps include\n // experiment fields in their OutgoingOp.opts. The executor reads these\n // fields from opts and emits the step-scoped `inngest.experiment`\n // metadata span itself — the SDK does not need to call addMetadata()\n // for variant steps. See the companion executor change in inngest/inngest\n // for the server-side emission path.\n //\n // Also track whether any step tool is invoked to detect zero-step\n // variants.\n //\n // NOTE: experimentStepHashedId may be undefined on replay because it\n // is captured inside the selection step callback, which doesn't run\n // when memoized. We still set experimentContext (with an empty string\n // for the hashed ID fallback) so that variant sub-steps discovered on\n // replay still carry experiment fields in their opts and the executor\n // can attach metadata to their ClickHouse rows.\n const currentCtx = getAsyncCtxSync();\n const stepTracker = { found: false };\n let result: unknown;\n\n if (currentCtx?.execution && !isALSFallback()) {\n const als = await getAsyncLocalStorage();\n const nestedCtx: AsyncContext = {\n ...currentCtx,\n execution: {\n ...currentCtx.execution,\n experimentContext: {\n experimentStepID: experimentStepHashedId ?? \"\",\n experimentName: stepOpts.id,\n variant: selectedVariant,\n selectionStrategy: select.__experimentConfig.strategy,\n },\n experimentStepTracker: stepTracker,\n },\n };\n result = await als.run(nestedCtx, () => variantFn());\n } else {\n result = await variantFn();\n }\n\n // If the variant returned without invoking any step tools, it will\n // silently re-execute on every replay. Throw a non-retriable error\n // to prevent this.\n if (!stepTracker.found && !isALSFallback()) {\n throw new NonRetriableError(\n `group.experiment(\"${stepOpts.id}\"): variant \"${selectedVariant}\" ` +\n \"did not invoke any step tools. Wrap your variant logic in \" +\n \"step.run() to ensure it is memoized and not re-executed on replay.\",\n );\n }\n\n if (withVariant) {\n return { result, variant: selectedVariant };\n }\n\n return result;\n }) as GroupExperiment;\n\n return { parallel, experiment };\n};\n"],"mappings":"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"}
|
|
@@ -31,7 +31,9 @@ var Stream = class {
|
|
|
31
31
|
this.onActivated = opts?.onActivated;
|
|
32
32
|
this.onWriteError = opts?.onWriteError;
|
|
33
33
|
let readableStrategy;
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
readableStrategy = new CountQueuingStrategy({ highWaterMark: 1024 });
|
|
36
|
+
} catch {}
|
|
35
37
|
this.transform = new TransformStream(void 0, void 0, readableStrategy);
|
|
36
38
|
this.writer = this.transform.writable.getWriter();
|
|
37
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamTools.cjs","names":["readableStrategy: QueuingStrategy<Uint8Array> | undefined","getAsyncCtxSync","buildSseCommitEvent","buildSseRollbackEvent","sseEvent: string","buildSseStreamEvent","iterable: AsyncIterable<string>","chunks: string[]","buildSseRedirectEvent","buildSseSucceededEvent","buildSseFailedEvent","getAsyncCtx","stream: StreamTools"],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":["import { getAsyncCtx, getAsyncCtxSync } from \"./execution/als.ts\";\nimport {\n buildSseCommitEvent,\n buildSseFailedEvent,\n buildSseRedirectEvent,\n buildSseRollbackEvent,\n buildSseStreamEvent,\n buildSseSucceededEvent,\n type SseResponse,\n} from \"./execution/streaming.ts\";\n\n/**\n * Accepted source types for `stream.pipe()`.\n *\n * - `ReadableStream` — piped directly\n * - `AsyncIterable<string>` — iterated; each yielded value becomes a chunk\n * - `() => AsyncIterable<string>` — factory invoked lazily, then iterated\n */\nexport type PipeSource =\n | ReadableStream\n | AsyncIterable<string>\n | (() => AsyncIterable<string>);\n\n/**\n * The public interface for stream tools available to user code.\n */\nexport interface StreamTools {\n /**\n * Push data to the client as an SSE stream event. Fire-and-forget from the\n * caller's perspective.\n *\n * Outside of an Inngest execution context this is a silent no-op (graceful\n * degradation).\n */\n push(data: unknown): void;\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Resolves with the concatenated content of all chunks when the source is\n * fully consumed.\n *\n * Accepts a `ReadableStream`, an `AsyncIterable<string>`, or a factory\n * function that returns an `AsyncIterable<string>` (e.g. an async\n * generator function).\n *\n * Outside of an Inngest execution context this resolves with an empty string.\n */\n pipe(source: PipeSource): Promise<string>;\n}\n\n/**\n * Wraps a `TransformStream<Uint8Array>` to provide push/pipe SSE streaming\n * capabilities within an Inngest execution.\n *\n * @internal\n */\nexport class Stream implements StreamTools {\n private transform: TransformStream<Uint8Array, Uint8Array>;\n private writer: WritableStreamDefaultWriter<Uint8Array>;\n private encoder = new TextEncoder();\n private _activated = false;\n private _errored = false;\n private writeChain: Promise<void> = Promise.resolve();\n\n /**\n * Optional callback invoked the first time `push` or `pipe` is called.\n * Used by the execution engine to fire a checkpoint that returns the SSE\n * Response to the client immediately.\n */\n private onActivated?: () => void;\n\n /**\n * Optional callback invoked when a write to the underlying stream fails\n * (e.g. the client disconnected or the transform stream errored). Used by\n * the execution engine to emit diagnostic logs.\n */\n private onWriteError?: (err: unknown) => void;\n\n constructor(opts?: {\n onActivated?: () => void;\n onWriteError?: (err: unknown) => void;\n }) {\n this.onActivated = opts?.onActivated;\n this.onWriteError = opts?.onWriteError;\n\n let readableStrategy: QueuingStrategy<Uint8Array> | undefined;\n\n // `CountQueuingStrategy` is not available in some runtimes (e.g. Next.js\n // Edge), so fall back to a plain `TransformStream` when missing.\n if (typeof CountQueuingStrategy !== \"undefined\") {\n readableStrategy = new CountQueuingStrategy({\n // Use a generous high water mark so that writes don't block due to\n // backpressure before the consumer reads.\n highWaterMark: 1024,\n });\n }\n\n this.transform = new TransformStream<Uint8Array, Uint8Array>(\n undefined,\n undefined,\n readableStrategy,\n );\n this.writer = this.transform.writable.getWriter();\n }\n\n /**\n * Whether `push` or `pipe` has been called at least once.\n */\n get activated(): boolean {\n return this._activated;\n }\n\n /**\n * The readable side of the underlying transform stream. Consumers (i.e. the\n * HTTP response) read SSE events from here.\n */\n get readable(): ReadableStream<Uint8Array> {\n return this.transform.readable;\n }\n\n /**\n * Resolve the current hashed step ID for stream events. Returns the\n * executing step's hashed ID (read from ALS), or undefined if outside a step.\n */\n private currentHashedStepId(): string | undefined {\n return getAsyncCtxSync()?.execution?.executingStep?.hashedId;\n }\n\n private activate(): void {\n if (!this._activated) {\n this._activated = true;\n this.onActivated?.();\n }\n }\n\n /**\n * Encode and write an SSE event string to the underlying writer.\n */\n private writeEncoded(sseEvent: string): Promise<void> {\n return this.writer.write(this.encoder.encode(sseEvent));\n }\n\n /**\n * Enqueue a pre-built SSE event string onto the write chain.\n */\n private enqueue(sseEvent: string): void {\n if (this._errored) return;\n\n this.writeChain = this.writeChain\n .then(() => this.writeEncoded(sseEvent))\n .catch((err) => {\n // Writer errored (e.g. stream closed) — swallow so the chain\n // doesn't break and subsequent writes fail gracefully.\n this._errored = true;\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Emit an `inngest.commit` SSE event indicating that uncommitted streamed data\n * should be committed (i.e. will not be rolled back). Internal use only.\n */\n commit(hashedStepId: string | null): void {\n this.enqueue(buildSseCommitEvent(hashedStepId));\n }\n\n /**\n * Emit an `inngest.rollback` SSE event indicating the uncommitted streamed\n * data should be discarded (e.g. step errored). Internal use only.\n */\n rollback(hashedStepId: string | null): void {\n this.enqueue(buildSseRollbackEvent(hashedStepId));\n }\n\n /**\n * Serialize `data` into an SSE stream event and enqueue it. Returns `false`\n * if serialization fails (e.g. circular reference) so callers can skip.\n */\n private enqueueStreamEvent(data: unknown, hashedStepId?: string): boolean {\n let sseEvent: string;\n try {\n sseEvent = buildSseStreamEvent(data, hashedStepId);\n } catch {\n return false;\n }\n\n this.enqueue(sseEvent);\n return true;\n }\n\n /**\n * Write a single SSE stream event containing `data`. The current step's\n * hashed ID is automatically included as stepId for rollback tracking.\n */\n push(data: unknown): void {\n this.activate();\n this.enqueueStreamEvent(data, this.currentHashedStepId());\n }\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Returns the concatenated content of all chunks.\n */\n async pipe(source: PipeSource): Promise<string> {\n this.activate();\n\n let iterable: AsyncIterable<string>;\n if (source instanceof ReadableStream) {\n iterable = this.readableToAsyncIterable(source);\n } else if (typeof source === \"function\") {\n iterable = source();\n } else {\n iterable = source;\n }\n\n return this.pipeIterable(iterable);\n }\n\n /**\n * Adapt a ReadableStream into an AsyncIterable<string>. TypeScript's\n * ReadableStream type doesn't declare Symbol.asyncIterator, so we use the\n * reader API for type safety.\n */\n private async *readableToAsyncIterable(\n readable: ReadableStream,\n ): AsyncIterable<string> {\n const reader = readable.getReader();\n const decoder = new TextDecoder();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield typeof value === \"string\"\n ? value\n : decoder.decode(value, { stream: true });\n }\n // flush any partially buffered multibyte characters from the decoder\n const final = decoder.decode();\n if (final) yield final;\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * Core pipe loop: iterate an async iterable, writing each chunk as an SSE\n * stream event and collecting the concatenated result.\n */\n private async pipeIterable(source: AsyncIterable<string>): Promise<string> {\n const hashedStepId = this.currentHashedStepId();\n const chunks: string[] = [];\n\n for await (const chunk of source) {\n if (this._errored) break;\n\n chunks.push(chunk);\n\n if (!this.enqueueStreamEvent(chunk, hashedStepId)) {\n continue;\n }\n\n await this.writeChain;\n }\n\n return chunks.join(\"\");\n }\n\n /**\n * Write a redirect info event. Tells the client where to reconnect if the\n * durable endpoint goes async. Does NOT close the writer — more stream\n * events may follow before the durable endpoint actually switches to async\n * mode. Internal use only.\n */\n sendRedirectInfo(data: { runId: string; url: string }): void {\n this.enqueue(buildSseRedirectEvent(data));\n }\n\n /**\n * Write a succeeded result event and close the writer. Internal use only.\n */\n closeSucceeded(response: SseResponse): void {\n let sseEvent: string;\n try {\n sseEvent = buildSseSucceededEvent(response);\n } catch {\n sseEvent = buildSseFailedEvent(\"Failed to serialize result\");\n }\n this.closeWriter(sseEvent);\n }\n\n /**\n * Write a failed result event and close the writer. Internal use only.\n */\n closeFailed(error: string): void {\n this.closeWriter(buildSseFailedEvent(error));\n }\n\n /**\n * Optionally write a final SSE event, then close the writer.\n */\n private closeWriter(finalEvent?: string): void {\n this.writeChain = this.writeChain\n .then(async () => {\n if (finalEvent) {\n await this.writeEncoded(finalEvent);\n }\n await this.writer.close();\n })\n .catch((err) => {\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Close the writer without writing a result event. Used when the durable endpoint goes\n * async and the real result will arrive on the redirected stream.\n */\n end(): void {\n this.closeWriter();\n }\n}\n\n/** Synchronous ALS lookup for the stream tools (fast path). */\nconst getStreamToolsSync = (): Stream | undefined => {\n const ctx = getAsyncCtxSync();\n return ctx?.execution?.stream;\n};\n\nconst getDeferredStreamTooling = async (): Promise<Stream | undefined> => {\n const ctx = await getAsyncCtx();\n return ctx?.execution?.stream;\n};\n\n/**\n * Stream tools that use ALS to resolve the current execution context.\n * Outside an Inngest execution, `push()` is a no-op and `pipe()` resolves immediately.\n */\nexport const stream: StreamTools = {\n push: (data) => {\n // Sync fast path: activate the stream before the next microtask tick.\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n syncStream.push(data);\n return;\n }\n\n // Fallback: ALS not yet initialized (first import still resolving).\n void getDeferredStreamTooling()\n .then((s) => {\n s?.push(data);\n })\n .catch(() => {\n // ALS initialization failure — already warned in als.ts.\n // push() is best-effort, so silently degrade.\n });\n },\n pipe: async (source) => {\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n return syncStream.pipe(source);\n }\n\n const s = await getDeferredStreamTooling();\n if (s) {\n return s.pipe(source);\n }\n return \"\";\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StreamTools.cjs","names":["readableStrategy: QueuingStrategy<Uint8Array> | undefined","getAsyncCtxSync","buildSseCommitEvent","buildSseRollbackEvent","sseEvent: string","buildSseStreamEvent","iterable: AsyncIterable<string>","chunks: string[]","buildSseRedirectEvent","buildSseSucceededEvent","buildSseFailedEvent","getAsyncCtx","stream: StreamTools"],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":["import { getAsyncCtx, getAsyncCtxSync } from \"./execution/als.ts\";\nimport {\n buildSseCommitEvent,\n buildSseFailedEvent,\n buildSseRedirectEvent,\n buildSseRollbackEvent,\n buildSseStreamEvent,\n buildSseSucceededEvent,\n type SseResponse,\n} from \"./execution/streaming.ts\";\n\n/**\n * Accepted source types for `stream.pipe()`.\n *\n * - `ReadableStream` — piped directly\n * - `AsyncIterable<string>` — iterated; each yielded value becomes a chunk\n * - `() => AsyncIterable<string>` — factory invoked lazily, then iterated\n */\nexport type PipeSource =\n | ReadableStream\n | AsyncIterable<string>\n | (() => AsyncIterable<string>);\n\n/**\n * The public interface for stream tools available to user code.\n */\nexport interface StreamTools {\n /**\n * Push data to the client as an SSE stream event. Fire-and-forget from the\n * caller's perspective.\n *\n * Outside of an Inngest execution context this is a silent no-op (graceful\n * degradation).\n */\n push(data: unknown): void;\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Resolves with the concatenated content of all chunks when the source is\n * fully consumed.\n *\n * Accepts a `ReadableStream`, an `AsyncIterable<string>`, or a factory\n * function that returns an `AsyncIterable<string>` (e.g. an async\n * generator function).\n *\n * Outside of an Inngest execution context this resolves with an empty string.\n */\n pipe(source: PipeSource): Promise<string>;\n}\n\n/**\n * Wraps a `TransformStream<Uint8Array>` to provide push/pipe SSE streaming\n * capabilities within an Inngest execution.\n *\n * @internal\n */\nexport class Stream implements StreamTools {\n private transform: TransformStream<Uint8Array, Uint8Array>;\n private writer: WritableStreamDefaultWriter<Uint8Array>;\n private encoder = new TextEncoder();\n private _activated = false;\n private _errored = false;\n private writeChain: Promise<void> = Promise.resolve();\n\n /**\n * Optional callback invoked the first time `push` or `pipe` is called.\n * Used by the execution engine to fire a checkpoint that returns the SSE\n * Response to the client immediately.\n */\n private onActivated?: () => void;\n\n /**\n * Optional callback invoked when a write to the underlying stream fails\n * (e.g. the client disconnected or the transform stream errored). Used by\n * the execution engine to emit diagnostic logs.\n */\n private onWriteError?: (err: unknown) => void;\n\n constructor(opts?: {\n onActivated?: () => void;\n onWriteError?: (err: unknown) => void;\n }) {\n this.onActivated = opts?.onActivated;\n this.onWriteError = opts?.onWriteError;\n\n let readableStrategy: QueuingStrategy<Uint8Array> | undefined;\n\n // `CountQueuingStrategy` is not available in some runtimes (e.g. Next.js\n // Edge), where it may exist as a stub that throws on instantiation. Fall\n // back to a plain `TransformStream` when it's missing or broken.\n try {\n readableStrategy = new CountQueuingStrategy({\n // Use a generous high water mark so that writes don't block due to\n // backpressure before the consumer reads.\n highWaterMark: 1024,\n });\n } catch {\n // Leave `readableStrategy` undefined\n }\n\n this.transform = new TransformStream<Uint8Array, Uint8Array>(\n undefined,\n undefined,\n readableStrategy,\n );\n this.writer = this.transform.writable.getWriter();\n }\n\n /**\n * Whether `push` or `pipe` has been called at least once.\n */\n get activated(): boolean {\n return this._activated;\n }\n\n /**\n * The readable side of the underlying transform stream. Consumers (i.e. the\n * HTTP response) read SSE events from here.\n */\n get readable(): ReadableStream<Uint8Array> {\n return this.transform.readable;\n }\n\n /**\n * Resolve the current hashed step ID for stream events. Returns the\n * executing step's hashed ID (read from ALS), or undefined if outside a step.\n */\n private currentHashedStepId(): string | undefined {\n return getAsyncCtxSync()?.execution?.executingStep?.hashedId;\n }\n\n private activate(): void {\n if (!this._activated) {\n this._activated = true;\n this.onActivated?.();\n }\n }\n\n /**\n * Encode and write an SSE event string to the underlying writer.\n */\n private writeEncoded(sseEvent: string): Promise<void> {\n return this.writer.write(this.encoder.encode(sseEvent));\n }\n\n /**\n * Enqueue a pre-built SSE event string onto the write chain.\n */\n private enqueue(sseEvent: string): void {\n if (this._errored) return;\n\n this.writeChain = this.writeChain\n .then(() => this.writeEncoded(sseEvent))\n .catch((err) => {\n // Writer errored (e.g. stream closed) — swallow so the chain\n // doesn't break and subsequent writes fail gracefully.\n this._errored = true;\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Emit an `inngest.commit` SSE event indicating that uncommitted streamed data\n * should be committed (i.e. will not be rolled back). Internal use only.\n */\n commit(hashedStepId: string | null): void {\n this.enqueue(buildSseCommitEvent(hashedStepId));\n }\n\n /**\n * Emit an `inngest.rollback` SSE event indicating the uncommitted streamed\n * data should be discarded (e.g. step errored). Internal use only.\n */\n rollback(hashedStepId: string | null): void {\n this.enqueue(buildSseRollbackEvent(hashedStepId));\n }\n\n /**\n * Serialize `data` into an SSE stream event and enqueue it. Returns `false`\n * if serialization fails (e.g. circular reference) so callers can skip.\n */\n private enqueueStreamEvent(data: unknown, hashedStepId?: string): boolean {\n let sseEvent: string;\n try {\n sseEvent = buildSseStreamEvent(data, hashedStepId);\n } catch {\n return false;\n }\n\n this.enqueue(sseEvent);\n return true;\n }\n\n /**\n * Write a single SSE stream event containing `data`. The current step's\n * hashed ID is automatically included as stepId for rollback tracking.\n */\n push(data: unknown): void {\n this.activate();\n this.enqueueStreamEvent(data, this.currentHashedStepId());\n }\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Returns the concatenated content of all chunks.\n */\n async pipe(source: PipeSource): Promise<string> {\n this.activate();\n\n let iterable: AsyncIterable<string>;\n if (source instanceof ReadableStream) {\n iterable = this.readableToAsyncIterable(source);\n } else if (typeof source === \"function\") {\n iterable = source();\n } else {\n iterable = source;\n }\n\n return this.pipeIterable(iterable);\n }\n\n /**\n * Adapt a ReadableStream into an AsyncIterable<string>. TypeScript's\n * ReadableStream type doesn't declare Symbol.asyncIterator, so we use the\n * reader API for type safety.\n */\n private async *readableToAsyncIterable(\n readable: ReadableStream,\n ): AsyncIterable<string> {\n const reader = readable.getReader();\n const decoder = new TextDecoder();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield typeof value === \"string\"\n ? value\n : decoder.decode(value, { stream: true });\n }\n // flush any partially buffered multibyte characters from the decoder\n const final = decoder.decode();\n if (final) yield final;\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * Core pipe loop: iterate an async iterable, writing each chunk as an SSE\n * stream event and collecting the concatenated result.\n */\n private async pipeIterable(source: AsyncIterable<string>): Promise<string> {\n const hashedStepId = this.currentHashedStepId();\n const chunks: string[] = [];\n\n for await (const chunk of source) {\n if (this._errored) break;\n\n chunks.push(chunk);\n\n if (!this.enqueueStreamEvent(chunk, hashedStepId)) {\n continue;\n }\n\n await this.writeChain;\n }\n\n return chunks.join(\"\");\n }\n\n /**\n * Write a redirect info event. Tells the client where to reconnect if the\n * durable endpoint goes async. Does NOT close the writer — more stream\n * events may follow before the durable endpoint actually switches to async\n * mode. Internal use only.\n */\n sendRedirectInfo(data: { runId: string; url: string }): void {\n this.enqueue(buildSseRedirectEvent(data));\n }\n\n /**\n * Write a succeeded result event and close the writer. Internal use only.\n */\n closeSucceeded(response: SseResponse): void {\n let sseEvent: string;\n try {\n sseEvent = buildSseSucceededEvent(response);\n } catch {\n sseEvent = buildSseFailedEvent(\"Failed to serialize result\");\n }\n this.closeWriter(sseEvent);\n }\n\n /**\n * Write a failed result event and close the writer. Internal use only.\n */\n closeFailed(error: string): void {\n this.closeWriter(buildSseFailedEvent(error));\n }\n\n /**\n * Optionally write a final SSE event, then close the writer.\n */\n private closeWriter(finalEvent?: string): void {\n this.writeChain = this.writeChain\n .then(async () => {\n if (finalEvent) {\n await this.writeEncoded(finalEvent);\n }\n await this.writer.close();\n })\n .catch((err) => {\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Close the writer without writing a result event. Used when the durable endpoint goes\n * async and the real result will arrive on the redirected stream.\n */\n end(): void {\n this.closeWriter();\n }\n}\n\n/** Synchronous ALS lookup for the stream tools (fast path). */\nconst getStreamToolsSync = (): Stream | undefined => {\n const ctx = getAsyncCtxSync();\n return ctx?.execution?.stream;\n};\n\nconst getDeferredStreamTooling = async (): Promise<Stream | undefined> => {\n const ctx = await getAsyncCtx();\n return ctx?.execution?.stream;\n};\n\n/**\n * Stream tools that use ALS to resolve the current execution context.\n * Outside an Inngest execution, `push()` is a no-op and `pipe()` resolves immediately.\n */\nexport const stream: StreamTools = {\n push: (data) => {\n // Sync fast path: activate the stream before the next microtask tick.\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n syncStream.push(data);\n return;\n }\n\n // Fallback: ALS not yet initialized (first import still resolving).\n void getDeferredStreamTooling()\n .then((s) => {\n s?.push(data);\n })\n .catch(() => {\n // ALS initialization failure — already warned in als.ts.\n // push() is best-effort, so silently degrade.\n });\n },\n pipe: async (source) => {\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n return syncStream.pipe(source);\n }\n\n const s = await getDeferredStreamTooling();\n if (s) {\n return s.pipe(source);\n }\n return \"\";\n },\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamTools.d.cts","names":[],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;;;;;AAGwB,KAHZ,UAAA,GACR,cAEoB,GADpB,aACoB,CAAA,MAAA,CAAA,GAAA,CAAA,GAAA,GAAb,aAAa,CAAA,MAAA,CAAA,CAAA;AAKxB;;;AAqB4B,UArBX,WAAA,CAqBW;EAAO;AASnC;;;;;;
|
|
1
|
+
{"version":3,"file":"StreamTools.d.cts","names":[],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;;;;;AAGwB,KAHZ,UAAA,GACR,cAEoB,GADpB,aACoB,CAAA,MAAA,CAAA,GAAA,CAAA,GAAA,GAAb,aAAa,CAAA,MAAA,CAAA,CAAA;AAKxB;;;AAqB4B,UArBX,WAAA,CAqBW;EAAO;AASnC;;;;;;MAmO2B,CAAA,IAAA,EAAA,OAAA,CAAA,EAAA,IAAA;;;AAyD3B;;;;;;;;;eArSe,aAAa;;;;;;;;cASf,MAAA,YAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA+Db,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAuFZ,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;2BA6EP;;;;;;;;;;;;;;;;;;;cAyDd,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamTools.d.ts","names":[],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;;;;;AAGwB,KAHZ,UAAA,GACR,cAEoB,GADpB,aACoB,CAAA,MAAA,CAAA,GAAA,CAAA,GAAA,GAAb,aAAa,CAAA,MAAA,CAAA,CAAA;AAKxB;;;AAqB4B,UArBX,WAAA,CAqBW;EAAO;AASnC;;;;;;
|
|
1
|
+
{"version":3,"file":"StreamTools.d.ts","names":[],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;;;;;AAGwB,KAHZ,UAAA,GACR,cAEoB,GADpB,aACoB,CAAA,MAAA,CAAA,GAAA,CAAA,GAAA,GAAb,aAAa,CAAA,MAAA,CAAA,CAAA;AAKxB;;;AAqB4B,UArBX,WAAA,CAqBW;EAAO;AASnC;;;;;;MAmO2B,CAAA,IAAA,EAAA,OAAA,CAAA,EAAA,IAAA;;;AAyD3B;;;;;;;;;eArSe,aAAa;;;;;;;;cASf,MAAA,YAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA+Db,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAuFZ,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;2BA6EP;;;;;;;;;;;;;;;;;;;cAyDd,QAAQ"}
|
|
@@ -31,7 +31,9 @@ var Stream = class {
|
|
|
31
31
|
this.onActivated = opts?.onActivated;
|
|
32
32
|
this.onWriteError = opts?.onWriteError;
|
|
33
33
|
let readableStrategy;
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
readableStrategy = new CountQueuingStrategy({ highWaterMark: 1024 });
|
|
36
|
+
} catch {}
|
|
35
37
|
this.transform = new TransformStream(void 0, void 0, readableStrategy);
|
|
36
38
|
this.writer = this.transform.writable.getWriter();
|
|
37
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamTools.js","names":["readableStrategy: QueuingStrategy<Uint8Array> | undefined","sseEvent: string","iterable: AsyncIterable<string>","chunks: string[]","stream: StreamTools"],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":["import { getAsyncCtx, getAsyncCtxSync } from \"./execution/als.ts\";\nimport {\n buildSseCommitEvent,\n buildSseFailedEvent,\n buildSseRedirectEvent,\n buildSseRollbackEvent,\n buildSseStreamEvent,\n buildSseSucceededEvent,\n type SseResponse,\n} from \"./execution/streaming.ts\";\n\n/**\n * Accepted source types for `stream.pipe()`.\n *\n * - `ReadableStream` — piped directly\n * - `AsyncIterable<string>` — iterated; each yielded value becomes a chunk\n * - `() => AsyncIterable<string>` — factory invoked lazily, then iterated\n */\nexport type PipeSource =\n | ReadableStream\n | AsyncIterable<string>\n | (() => AsyncIterable<string>);\n\n/**\n * The public interface for stream tools available to user code.\n */\nexport interface StreamTools {\n /**\n * Push data to the client as an SSE stream event. Fire-and-forget from the\n * caller's perspective.\n *\n * Outside of an Inngest execution context this is a silent no-op (graceful\n * degradation).\n */\n push(data: unknown): void;\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Resolves with the concatenated content of all chunks when the source is\n * fully consumed.\n *\n * Accepts a `ReadableStream`, an `AsyncIterable<string>`, or a factory\n * function that returns an `AsyncIterable<string>` (e.g. an async\n * generator function).\n *\n * Outside of an Inngest execution context this resolves with an empty string.\n */\n pipe(source: PipeSource): Promise<string>;\n}\n\n/**\n * Wraps a `TransformStream<Uint8Array>` to provide push/pipe SSE streaming\n * capabilities within an Inngest execution.\n *\n * @internal\n */\nexport class Stream implements StreamTools {\n private transform: TransformStream<Uint8Array, Uint8Array>;\n private writer: WritableStreamDefaultWriter<Uint8Array>;\n private encoder = new TextEncoder();\n private _activated = false;\n private _errored = false;\n private writeChain: Promise<void> = Promise.resolve();\n\n /**\n * Optional callback invoked the first time `push` or `pipe` is called.\n * Used by the execution engine to fire a checkpoint that returns the SSE\n * Response to the client immediately.\n */\n private onActivated?: () => void;\n\n /**\n * Optional callback invoked when a write to the underlying stream fails\n * (e.g. the client disconnected or the transform stream errored). Used by\n * the execution engine to emit diagnostic logs.\n */\n private onWriteError?: (err: unknown) => void;\n\n constructor(opts?: {\n onActivated?: () => void;\n onWriteError?: (err: unknown) => void;\n }) {\n this.onActivated = opts?.onActivated;\n this.onWriteError = opts?.onWriteError;\n\n let readableStrategy: QueuingStrategy<Uint8Array> | undefined;\n\n // `CountQueuingStrategy` is not available in some runtimes (e.g. Next.js\n // Edge), so fall back to a plain `TransformStream` when missing.\n if (typeof CountQueuingStrategy !== \"undefined\") {\n readableStrategy = new CountQueuingStrategy({\n // Use a generous high water mark so that writes don't block due to\n // backpressure before the consumer reads.\n highWaterMark: 1024,\n });\n }\n\n this.transform = new TransformStream<Uint8Array, Uint8Array>(\n undefined,\n undefined,\n readableStrategy,\n );\n this.writer = this.transform.writable.getWriter();\n }\n\n /**\n * Whether `push` or `pipe` has been called at least once.\n */\n get activated(): boolean {\n return this._activated;\n }\n\n /**\n * The readable side of the underlying transform stream. Consumers (i.e. the\n * HTTP response) read SSE events from here.\n */\n get readable(): ReadableStream<Uint8Array> {\n return this.transform.readable;\n }\n\n /**\n * Resolve the current hashed step ID for stream events. Returns the\n * executing step's hashed ID (read from ALS), or undefined if outside a step.\n */\n private currentHashedStepId(): string | undefined {\n return getAsyncCtxSync()?.execution?.executingStep?.hashedId;\n }\n\n private activate(): void {\n if (!this._activated) {\n this._activated = true;\n this.onActivated?.();\n }\n }\n\n /**\n * Encode and write an SSE event string to the underlying writer.\n */\n private writeEncoded(sseEvent: string): Promise<void> {\n return this.writer.write(this.encoder.encode(sseEvent));\n }\n\n /**\n * Enqueue a pre-built SSE event string onto the write chain.\n */\n private enqueue(sseEvent: string): void {\n if (this._errored) return;\n\n this.writeChain = this.writeChain\n .then(() => this.writeEncoded(sseEvent))\n .catch((err) => {\n // Writer errored (e.g. stream closed) — swallow so the chain\n // doesn't break and subsequent writes fail gracefully.\n this._errored = true;\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Emit an `inngest.commit` SSE event indicating that uncommitted streamed data\n * should be committed (i.e. will not be rolled back). Internal use only.\n */\n commit(hashedStepId: string | null): void {\n this.enqueue(buildSseCommitEvent(hashedStepId));\n }\n\n /**\n * Emit an `inngest.rollback` SSE event indicating the uncommitted streamed\n * data should be discarded (e.g. step errored). Internal use only.\n */\n rollback(hashedStepId: string | null): void {\n this.enqueue(buildSseRollbackEvent(hashedStepId));\n }\n\n /**\n * Serialize `data` into an SSE stream event and enqueue it. Returns `false`\n * if serialization fails (e.g. circular reference) so callers can skip.\n */\n private enqueueStreamEvent(data: unknown, hashedStepId?: string): boolean {\n let sseEvent: string;\n try {\n sseEvent = buildSseStreamEvent(data, hashedStepId);\n } catch {\n return false;\n }\n\n this.enqueue(sseEvent);\n return true;\n }\n\n /**\n * Write a single SSE stream event containing `data`. The current step's\n * hashed ID is automatically included as stepId for rollback tracking.\n */\n push(data: unknown): void {\n this.activate();\n this.enqueueStreamEvent(data, this.currentHashedStepId());\n }\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Returns the concatenated content of all chunks.\n */\n async pipe(source: PipeSource): Promise<string> {\n this.activate();\n\n let iterable: AsyncIterable<string>;\n if (source instanceof ReadableStream) {\n iterable = this.readableToAsyncIterable(source);\n } else if (typeof source === \"function\") {\n iterable = source();\n } else {\n iterable = source;\n }\n\n return this.pipeIterable(iterable);\n }\n\n /**\n * Adapt a ReadableStream into an AsyncIterable<string>. TypeScript's\n * ReadableStream type doesn't declare Symbol.asyncIterator, so we use the\n * reader API for type safety.\n */\n private async *readableToAsyncIterable(\n readable: ReadableStream,\n ): AsyncIterable<string> {\n const reader = readable.getReader();\n const decoder = new TextDecoder();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield typeof value === \"string\"\n ? value\n : decoder.decode(value, { stream: true });\n }\n // flush any partially buffered multibyte characters from the decoder\n const final = decoder.decode();\n if (final) yield final;\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * Core pipe loop: iterate an async iterable, writing each chunk as an SSE\n * stream event and collecting the concatenated result.\n */\n private async pipeIterable(source: AsyncIterable<string>): Promise<string> {\n const hashedStepId = this.currentHashedStepId();\n const chunks: string[] = [];\n\n for await (const chunk of source) {\n if (this._errored) break;\n\n chunks.push(chunk);\n\n if (!this.enqueueStreamEvent(chunk, hashedStepId)) {\n continue;\n }\n\n await this.writeChain;\n }\n\n return chunks.join(\"\");\n }\n\n /**\n * Write a redirect info event. Tells the client where to reconnect if the\n * durable endpoint goes async. Does NOT close the writer — more stream\n * events may follow before the durable endpoint actually switches to async\n * mode. Internal use only.\n */\n sendRedirectInfo(data: { runId: string; url: string }): void {\n this.enqueue(buildSseRedirectEvent(data));\n }\n\n /**\n * Write a succeeded result event and close the writer. Internal use only.\n */\n closeSucceeded(response: SseResponse): void {\n let sseEvent: string;\n try {\n sseEvent = buildSseSucceededEvent(response);\n } catch {\n sseEvent = buildSseFailedEvent(\"Failed to serialize result\");\n }\n this.closeWriter(sseEvent);\n }\n\n /**\n * Write a failed result event and close the writer. Internal use only.\n */\n closeFailed(error: string): void {\n this.closeWriter(buildSseFailedEvent(error));\n }\n\n /**\n * Optionally write a final SSE event, then close the writer.\n */\n private closeWriter(finalEvent?: string): void {\n this.writeChain = this.writeChain\n .then(async () => {\n if (finalEvent) {\n await this.writeEncoded(finalEvent);\n }\n await this.writer.close();\n })\n .catch((err) => {\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Close the writer without writing a result event. Used when the durable endpoint goes\n * async and the real result will arrive on the redirected stream.\n */\n end(): void {\n this.closeWriter();\n }\n}\n\n/** Synchronous ALS lookup for the stream tools (fast path). */\nconst getStreamToolsSync = (): Stream | undefined => {\n const ctx = getAsyncCtxSync();\n return ctx?.execution?.stream;\n};\n\nconst getDeferredStreamTooling = async (): Promise<Stream | undefined> => {\n const ctx = await getAsyncCtx();\n return ctx?.execution?.stream;\n};\n\n/**\n * Stream tools that use ALS to resolve the current execution context.\n * Outside an Inngest execution, `push()` is a no-op and `pipe()` resolves immediately.\n */\nexport const stream: StreamTools = {\n push: (data) => {\n // Sync fast path: activate the stream before the next microtask tick.\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n syncStream.push(data);\n return;\n }\n\n // Fallback: ALS not yet initialized (first import still resolving).\n void getDeferredStreamTooling()\n .then((s) => {\n s?.push(data);\n })\n .catch(() => {\n // ALS initialization failure — already warned in als.ts.\n // push() is best-effort, so silently degrade.\n });\n },\n pipe: async (source) => {\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n return syncStream.pipe(source);\n }\n\n const s = await getDeferredStreamTooling();\n if (s) {\n return s.pipe(source);\n }\n return \"\";\n },\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StreamTools.js","names":["readableStrategy: QueuingStrategy<Uint8Array> | undefined","sseEvent: string","iterable: AsyncIterable<string>","chunks: string[]","stream: StreamTools"],"sources":["../../src/components/StreamTools.ts"],"sourcesContent":["import { getAsyncCtx, getAsyncCtxSync } from \"./execution/als.ts\";\nimport {\n buildSseCommitEvent,\n buildSseFailedEvent,\n buildSseRedirectEvent,\n buildSseRollbackEvent,\n buildSseStreamEvent,\n buildSseSucceededEvent,\n type SseResponse,\n} from \"./execution/streaming.ts\";\n\n/**\n * Accepted source types for `stream.pipe()`.\n *\n * - `ReadableStream` — piped directly\n * - `AsyncIterable<string>` — iterated; each yielded value becomes a chunk\n * - `() => AsyncIterable<string>` — factory invoked lazily, then iterated\n */\nexport type PipeSource =\n | ReadableStream\n | AsyncIterable<string>\n | (() => AsyncIterable<string>);\n\n/**\n * The public interface for stream tools available to user code.\n */\nexport interface StreamTools {\n /**\n * Push data to the client as an SSE stream event. Fire-and-forget from the\n * caller's perspective.\n *\n * Outside of an Inngest execution context this is a silent no-op (graceful\n * degradation).\n */\n push(data: unknown): void;\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Resolves with the concatenated content of all chunks when the source is\n * fully consumed.\n *\n * Accepts a `ReadableStream`, an `AsyncIterable<string>`, or a factory\n * function that returns an `AsyncIterable<string>` (e.g. an async\n * generator function).\n *\n * Outside of an Inngest execution context this resolves with an empty string.\n */\n pipe(source: PipeSource): Promise<string>;\n}\n\n/**\n * Wraps a `TransformStream<Uint8Array>` to provide push/pipe SSE streaming\n * capabilities within an Inngest execution.\n *\n * @internal\n */\nexport class Stream implements StreamTools {\n private transform: TransformStream<Uint8Array, Uint8Array>;\n private writer: WritableStreamDefaultWriter<Uint8Array>;\n private encoder = new TextEncoder();\n private _activated = false;\n private _errored = false;\n private writeChain: Promise<void> = Promise.resolve();\n\n /**\n * Optional callback invoked the first time `push` or `pipe` is called.\n * Used by the execution engine to fire a checkpoint that returns the SSE\n * Response to the client immediately.\n */\n private onActivated?: () => void;\n\n /**\n * Optional callback invoked when a write to the underlying stream fails\n * (e.g. the client disconnected or the transform stream errored). Used by\n * the execution engine to emit diagnostic logs.\n */\n private onWriteError?: (err: unknown) => void;\n\n constructor(opts?: {\n onActivated?: () => void;\n onWriteError?: (err: unknown) => void;\n }) {\n this.onActivated = opts?.onActivated;\n this.onWriteError = opts?.onWriteError;\n\n let readableStrategy: QueuingStrategy<Uint8Array> | undefined;\n\n // `CountQueuingStrategy` is not available in some runtimes (e.g. Next.js\n // Edge), where it may exist as a stub that throws on instantiation. Fall\n // back to a plain `TransformStream` when it's missing or broken.\n try {\n readableStrategy = new CountQueuingStrategy({\n // Use a generous high water mark so that writes don't block due to\n // backpressure before the consumer reads.\n highWaterMark: 1024,\n });\n } catch {\n // Leave `readableStrategy` undefined\n }\n\n this.transform = new TransformStream<Uint8Array, Uint8Array>(\n undefined,\n undefined,\n readableStrategy,\n );\n this.writer = this.transform.writable.getWriter();\n }\n\n /**\n * Whether `push` or `pipe` has been called at least once.\n */\n get activated(): boolean {\n return this._activated;\n }\n\n /**\n * The readable side of the underlying transform stream. Consumers (i.e. the\n * HTTP response) read SSE events from here.\n */\n get readable(): ReadableStream<Uint8Array> {\n return this.transform.readable;\n }\n\n /**\n * Resolve the current hashed step ID for stream events. Returns the\n * executing step's hashed ID (read from ALS), or undefined if outside a step.\n */\n private currentHashedStepId(): string | undefined {\n return getAsyncCtxSync()?.execution?.executingStep?.hashedId;\n }\n\n private activate(): void {\n if (!this._activated) {\n this._activated = true;\n this.onActivated?.();\n }\n }\n\n /**\n * Encode and write an SSE event string to the underlying writer.\n */\n private writeEncoded(sseEvent: string): Promise<void> {\n return this.writer.write(this.encoder.encode(sseEvent));\n }\n\n /**\n * Enqueue a pre-built SSE event string onto the write chain.\n */\n private enqueue(sseEvent: string): void {\n if (this._errored) return;\n\n this.writeChain = this.writeChain\n .then(() => this.writeEncoded(sseEvent))\n .catch((err) => {\n // Writer errored (e.g. stream closed) — swallow so the chain\n // doesn't break and subsequent writes fail gracefully.\n this._errored = true;\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Emit an `inngest.commit` SSE event indicating that uncommitted streamed data\n * should be committed (i.e. will not be rolled back). Internal use only.\n */\n commit(hashedStepId: string | null): void {\n this.enqueue(buildSseCommitEvent(hashedStepId));\n }\n\n /**\n * Emit an `inngest.rollback` SSE event indicating the uncommitted streamed\n * data should be discarded (e.g. step errored). Internal use only.\n */\n rollback(hashedStepId: string | null): void {\n this.enqueue(buildSseRollbackEvent(hashedStepId));\n }\n\n /**\n * Serialize `data` into an SSE stream event and enqueue it. Returns `false`\n * if serialization fails (e.g. circular reference) so callers can skip.\n */\n private enqueueStreamEvent(data: unknown, hashedStepId?: string): boolean {\n let sseEvent: string;\n try {\n sseEvent = buildSseStreamEvent(data, hashedStepId);\n } catch {\n return false;\n }\n\n this.enqueue(sseEvent);\n return true;\n }\n\n /**\n * Write a single SSE stream event containing `data`. The current step's\n * hashed ID is automatically included as stepId for rollback tracking.\n */\n push(data: unknown): void {\n this.activate();\n this.enqueueStreamEvent(data, this.currentHashedStepId());\n }\n\n /**\n * Pipe a source to the client, writing each chunk as an SSE stream event.\n * Returns the concatenated content of all chunks.\n */\n async pipe(source: PipeSource): Promise<string> {\n this.activate();\n\n let iterable: AsyncIterable<string>;\n if (source instanceof ReadableStream) {\n iterable = this.readableToAsyncIterable(source);\n } else if (typeof source === \"function\") {\n iterable = source();\n } else {\n iterable = source;\n }\n\n return this.pipeIterable(iterable);\n }\n\n /**\n * Adapt a ReadableStream into an AsyncIterable<string>. TypeScript's\n * ReadableStream type doesn't declare Symbol.asyncIterator, so we use the\n * reader API for type safety.\n */\n private async *readableToAsyncIterable(\n readable: ReadableStream,\n ): AsyncIterable<string> {\n const reader = readable.getReader();\n const decoder = new TextDecoder();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield typeof value === \"string\"\n ? value\n : decoder.decode(value, { stream: true });\n }\n // flush any partially buffered multibyte characters from the decoder\n const final = decoder.decode();\n if (final) yield final;\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * Core pipe loop: iterate an async iterable, writing each chunk as an SSE\n * stream event and collecting the concatenated result.\n */\n private async pipeIterable(source: AsyncIterable<string>): Promise<string> {\n const hashedStepId = this.currentHashedStepId();\n const chunks: string[] = [];\n\n for await (const chunk of source) {\n if (this._errored) break;\n\n chunks.push(chunk);\n\n if (!this.enqueueStreamEvent(chunk, hashedStepId)) {\n continue;\n }\n\n await this.writeChain;\n }\n\n return chunks.join(\"\");\n }\n\n /**\n * Write a redirect info event. Tells the client where to reconnect if the\n * durable endpoint goes async. Does NOT close the writer — more stream\n * events may follow before the durable endpoint actually switches to async\n * mode. Internal use only.\n */\n sendRedirectInfo(data: { runId: string; url: string }): void {\n this.enqueue(buildSseRedirectEvent(data));\n }\n\n /**\n * Write a succeeded result event and close the writer. Internal use only.\n */\n closeSucceeded(response: SseResponse): void {\n let sseEvent: string;\n try {\n sseEvent = buildSseSucceededEvent(response);\n } catch {\n sseEvent = buildSseFailedEvent(\"Failed to serialize result\");\n }\n this.closeWriter(sseEvent);\n }\n\n /**\n * Write a failed result event and close the writer. Internal use only.\n */\n closeFailed(error: string): void {\n this.closeWriter(buildSseFailedEvent(error));\n }\n\n /**\n * Optionally write a final SSE event, then close the writer.\n */\n private closeWriter(finalEvent?: string): void {\n this.writeChain = this.writeChain\n .then(async () => {\n if (finalEvent) {\n await this.writeEncoded(finalEvent);\n }\n await this.writer.close();\n })\n .catch((err) => {\n this.onWriteError?.(err);\n });\n }\n\n /**\n * Close the writer without writing a result event. Used when the durable endpoint goes\n * async and the real result will arrive on the redirected stream.\n */\n end(): void {\n this.closeWriter();\n }\n}\n\n/** Synchronous ALS lookup for the stream tools (fast path). */\nconst getStreamToolsSync = (): Stream | undefined => {\n const ctx = getAsyncCtxSync();\n return ctx?.execution?.stream;\n};\n\nconst getDeferredStreamTooling = async (): Promise<Stream | undefined> => {\n const ctx = await getAsyncCtx();\n return ctx?.execution?.stream;\n};\n\n/**\n * Stream tools that use ALS to resolve the current execution context.\n * Outside an Inngest execution, `push()` is a no-op and `pipe()` resolves immediately.\n */\nexport const stream: StreamTools = {\n push: (data) => {\n // Sync fast path: activate the stream before the next microtask tick.\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n syncStream.push(data);\n return;\n }\n\n // Fallback: ALS not yet initialized (first import still resolving).\n void getDeferredStreamTooling()\n .then((s) => {\n s?.push(data);\n })\n .catch(() => {\n // ALS initialization failure — already warned in als.ts.\n // push() is best-effort, so silently degrade.\n });\n },\n pipe: async (source) => {\n const syncStream = getStreamToolsSync();\n if (syncStream) {\n return syncStream.pipe(source);\n }\n\n const s = await getDeferredStreamTooling();\n if (s) {\n return s.pipe(source);\n }\n return \"\";\n },\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"als.cjs","names":["fallback: AsyncLocalStorageIsh"],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":["import type { Context, StepOptions } from \"../../types.ts\";\nimport type { Inngest } from \"../Inngest.ts\";\nimport type { Stream } from \"../StreamTools.ts\";\nimport type { IInngestExecution } from \"./InngestExecution.ts\";\n\n/**\n * Note - this structure can be used by other libraries, so cannot have breaking changes.\n */\nexport interface AsyncContext {\n /**\n * The Inngest App that is currently being used to execute the function.\n *\n * If this is defined, we are in the context of an Inngest function execution,\n * or a possible one.\n */\n app: Inngest.Like;\n\n /**\n * Details of the current function execution context. If this doesn't exist,\n * then we're not currently in a function execution context.\n */\n execution?: {\n /**\n * The execution instance that is currently running the function.\n */\n instance: IInngestExecution;\n\n /**\n * The `ctx` object that has been passed in to this function execution,\n * including values such as `step` and `event`.\n */\n ctx: Context.Any;\n\n /**\n * If present, this indicates we are currently executing a `step.run()` step's\n * callback. Useful to understand whether we are in the context of a step\n * execution or within the main function body.\n */\n executingStep?: StepOptions & { hashedId?: string };\n\n /**\n * If present, indicates the parallel mode that should be applied to steps\n * created within this context. Set by `group.parallel()`.\n */\n parallelMode?: \"race\";\n\n /**\n * The stream tools instance for this execution context. Used by the\n * `stream` singleton to push/pipe SSE data to the client.\n */\n stream?: Stream;\n\n /**\n * If present, indicates the variant callback is executing within an\n * experiment. Set by `group.experiment()`. Any `step.*()` call within\n * this context will include these fields in `OutgoingOp.opts`.\n */\n experimentContext?: {\n experimentStepID: string;\n experimentName: string;\n variant: string;\n };\n\n /**\n * A mutable tracker used to detect whether any step tool was invoked\n * during a variant callback. Set by `group.experiment()`, flipped by\n * `createTool` in `InngestStepTools.ts`.\n */\n experimentStepTracker?: { found: boolean };\n\n /**\n * If true, we are inside the `select()` callback of\n * `group.experiment()`. Any `step.*()` call here would create a\n * nested step, which is not allowed.\n */\n insideExperimentSelect?: boolean;\n };\n}\n\n/**\n * A local-only symbol used as a key in global state to store the async local\n * storage instance.\n */\nconst alsSymbol = Symbol.for(\"inngest:als\");\n\n/**\n * Cache structure that stores both the promise and resolved ALS instance.\n * This allows synchronous access after initialization.\n */\ntype ALSCache = {\n promise: Promise<AsyncLocalStorageIsh>;\n resolved?: AsyncLocalStorageIsh;\n isFallback?: boolean;\n};\n\n/**\n * A type that represents a partial, runtime-agnostic interface of\n * `AsyncLocalStorage`.\n */\ntype AsyncLocalStorageIsh = {\n getStore: () => AsyncContext | undefined;\n run: <R>(store: AsyncContext, fn: () => R) => R;\n};\n\nconst getCache = (): ALSCache => {\n const g = globalThis as Record<symbol, ALSCache | undefined>;\n\n if (!g[alsSymbol]) {\n g[alsSymbol] = createCache();\n }\n\n return g[alsSymbol];\n};\n\nconst createCache = (): ALSCache => {\n const cache = {} as ALSCache;\n cache.promise = initializeALS(cache);\n return cache;\n};\n\nconst initializeALS = async (\n cache: ALSCache,\n): Promise<AsyncLocalStorageIsh> => {\n try {\n const { AsyncLocalStorage } = await import(\"node:async_hooks\");\n const als = new AsyncLocalStorage<AsyncContext>();\n cache.resolved = als;\n cache.isFallback = false;\n return als;\n } catch {\n const fallback: AsyncLocalStorageIsh = {\n getStore: () => undefined,\n run: (_, fn) => fn(),\n };\n cache.resolved = fallback;\n cache.isFallback = true;\n console.warn(\n \"node:async_hooks is not supported in this runtime. Async context is disabled.\",\n );\n return fallback;\n }\n};\n\n/**\n * Check if AsyncLocalStorage is unavailable and we're using the fallback.\n * Returns `undefined` if ALS hasn't been initialized yet.\n */\nexport const isALSFallback = (): boolean | undefined => {\n return getCache().isFallback;\n};\n\n/**\n * Retrieve the async context for the current execution.\n */\nexport const getAsyncCtx = async (): Promise<AsyncContext | undefined> => {\n return getAsyncLocalStorage().then((als) => als.getStore());\n};\n\n/**\n * Retrieve the async context for the current execution synchronously.\n * Returns undefined if ALS hasn't been initialized yet.\n */\nexport const getAsyncCtxSync = (): AsyncContext | undefined => {\n return getCache().resolved?.getStore();\n};\n\n/**\n * Get a singleton instance of `AsyncLocalStorage` used to store and retrieve\n * async context for the current execution.\n */\nexport const getAsyncLocalStorage = async (): Promise<AsyncLocalStorageIsh> => {\n return getCache().promise;\n};\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"als.cjs","names":["fallback: AsyncLocalStorageIsh"],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":["import type { Context, StepOptions } from \"../../types.ts\";\nimport type { Inngest } from \"../Inngest.ts\";\nimport type { Stream } from \"../StreamTools.ts\";\nimport type { IInngestExecution } from \"./InngestExecution.ts\";\n\n/**\n * Note - this structure can be used by other libraries, so cannot have breaking changes.\n */\nexport interface AsyncContext {\n /**\n * The Inngest App that is currently being used to execute the function.\n *\n * If this is defined, we are in the context of an Inngest function execution,\n * or a possible one.\n */\n app: Inngest.Like;\n\n /**\n * Details of the current function execution context. If this doesn't exist,\n * then we're not currently in a function execution context.\n */\n execution?: {\n /**\n * The execution instance that is currently running the function.\n */\n instance: IInngestExecution;\n\n /**\n * The `ctx` object that has been passed in to this function execution,\n * including values such as `step` and `event`.\n */\n ctx: Context.Any;\n\n /**\n * If present, this indicates we are currently executing a `step.run()` step's\n * callback. Useful to understand whether we are in the context of a step\n * execution or within the main function body.\n */\n executingStep?: StepOptions & { hashedId?: string };\n\n /**\n * If present, indicates the parallel mode that should be applied to steps\n * created within this context. Set by `group.parallel()`.\n */\n parallelMode?: \"race\";\n\n /**\n * The stream tools instance for this execution context. Used by the\n * `stream` singleton to push/pipe SSE data to the client.\n */\n stream?: Stream;\n\n /**\n * If present, indicates the variant callback is executing within an\n * experiment. Set by `group.experiment()`. Any `step.*()` call within\n * this context will include these fields in `OutgoingOp.opts`.\n */\n experimentContext?: {\n experimentStepID: string;\n experimentName: string;\n variant: string;\n selectionStrategy: string;\n };\n\n /**\n * A mutable tracker used to detect whether any step tool was invoked\n * during a variant callback. Set by `group.experiment()`, flipped by\n * `createTool` in `InngestStepTools.ts`.\n */\n experimentStepTracker?: { found: boolean };\n\n /**\n * If true, we are inside the `select()` callback of\n * `group.experiment()`. Any `step.*()` call here would create a\n * nested step, which is not allowed.\n */\n insideExperimentSelect?: boolean;\n };\n}\n\n/**\n * A local-only symbol used as a key in global state to store the async local\n * storage instance.\n */\nconst alsSymbol = Symbol.for(\"inngest:als\");\n\n/**\n * Cache structure that stores both the promise and resolved ALS instance.\n * This allows synchronous access after initialization.\n */\ntype ALSCache = {\n promise: Promise<AsyncLocalStorageIsh>;\n resolved?: AsyncLocalStorageIsh;\n isFallback?: boolean;\n};\n\n/**\n * A type that represents a partial, runtime-agnostic interface of\n * `AsyncLocalStorage`.\n */\ntype AsyncLocalStorageIsh = {\n getStore: () => AsyncContext | undefined;\n run: <R>(store: AsyncContext, fn: () => R) => R;\n};\n\nconst getCache = (): ALSCache => {\n const g = globalThis as Record<symbol, ALSCache | undefined>;\n\n if (!g[alsSymbol]) {\n g[alsSymbol] = createCache();\n }\n\n return g[alsSymbol];\n};\n\nconst createCache = (): ALSCache => {\n const cache = {} as ALSCache;\n cache.promise = initializeALS(cache);\n return cache;\n};\n\nconst initializeALS = async (\n cache: ALSCache,\n): Promise<AsyncLocalStorageIsh> => {\n try {\n const { AsyncLocalStorage } = await import(\"node:async_hooks\");\n const als = new AsyncLocalStorage<AsyncContext>();\n cache.resolved = als;\n cache.isFallback = false;\n return als;\n } catch {\n const fallback: AsyncLocalStorageIsh = {\n getStore: () => undefined,\n run: (_, fn) => fn(),\n };\n cache.resolved = fallback;\n cache.isFallback = true;\n console.warn(\n \"node:async_hooks is not supported in this runtime. Async context is disabled.\",\n );\n return fallback;\n }\n};\n\n/**\n * Check if AsyncLocalStorage is unavailable and we're using the fallback.\n * Returns `undefined` if ALS hasn't been initialized yet.\n */\nexport const isALSFallback = (): boolean | undefined => {\n return getCache().isFallback;\n};\n\n/**\n * Retrieve the async context for the current execution.\n */\nexport const getAsyncCtx = async (): Promise<AsyncContext | undefined> => {\n return getAsyncLocalStorage().then((als) => als.getStore());\n};\n\n/**\n * Retrieve the async context for the current execution synchronously.\n * Returns undefined if ALS hasn't been initialized yet.\n */\nexport const getAsyncCtxSync = (): AsyncContext | undefined => {\n return getCache().resolved?.getStore();\n};\n\n/**\n * Get a singleton instance of `AsyncLocalStorage` used to store and retrieve\n * async context for the current execution.\n */\nexport const getAsyncLocalStorage = async (): Promise<AsyncLocalStorageIsh> => {\n return getCache().promise;\n};\n"],"mappings":";;;;;;AAoFA,MAAM,YAAY,OAAO,IAAI,cAAc;AAqB3C,MAAM,iBAA2B;CAC/B,MAAM,IAAI;AAEV,KAAI,CAAC,EAAE,WACL,GAAE,aAAa,aAAa;AAG9B,QAAO,EAAE;;AAGX,MAAM,oBAA8B;CAClC,MAAM,QAAQ,EAAE;AAChB,OAAM,UAAU,cAAc,MAAM;AACpC,QAAO;;AAGT,MAAM,gBAAgB,OACpB,UACkC;AAClC,KAAI;EACF,MAAM,EAAE,sBAAsB,MAAM,OAAO;EAC3C,MAAM,MAAM,IAAI,mBAAiC;AACjD,QAAM,WAAW;AACjB,QAAM,aAAa;AACnB,SAAO;SACD;EACN,MAAMA,WAAiC;GACrC,gBAAgB;GAChB,MAAM,GAAG,OAAO,IAAI;GACrB;AACD,QAAM,WAAW;AACjB,QAAM,aAAa;AACnB,UAAQ,KACN,gFACD;AACD,SAAO;;;;;;;AAQX,MAAa,sBAA2C;AACtD,QAAO,UAAU,CAAC;;;;;AAMpB,MAAa,cAAc,YAA+C;AACxE,QAAO,sBAAsB,CAAC,MAAM,QAAQ,IAAI,UAAU,CAAC;;;;;;AAO7D,MAAa,wBAAkD;AAC7D,QAAO,UAAU,CAAC,UAAU,UAAU;;;;;;AAOxC,MAAa,uBAAuB,YAA2C;AAC7E,QAAO,UAAU,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"als.d.cts","names":[],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAQA;AAA6B,UAAZ,YAAA,CAAY;;;;;;;
|
|
1
|
+
{"version":3,"file":"als.d.cts","names":[],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAQA;AAA6B,UAAZ,YAAA,CAAY;;;;;;;EAmJhB,GAAA,EA5IN,OAAA,CAAQ,IA8Id;EAAA;;;;;;;;cApIa;;;;;SAML,OAAA,CAAQ;;;;;;oBAOG;;;;;;;;;;;;aAYP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAyGA,mBAAwB,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"als.d.ts","names":[],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAQA;AAA6B,UAAZ,YAAA,CAAY;;;;;;;
|
|
1
|
+
{"version":3,"file":"als.d.ts","names":[],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAQA;AAA6B,UAAZ,YAAA,CAAY;;;;;;;EAmJhB,GAAA,EA5IN,OAAA,CAAQ,IA8Id;EAAA;;;;;;;;cApIa;;;;;SAML,OAAA,CAAQ;;;;;;oBAOG;;;;;;;;;;;;aAYP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAyGA,mBAAwB,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"als.js","names":["fallback: AsyncLocalStorageIsh"],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":["import type { Context, StepOptions } from \"../../types.ts\";\nimport type { Inngest } from \"../Inngest.ts\";\nimport type { Stream } from \"../StreamTools.ts\";\nimport type { IInngestExecution } from \"./InngestExecution.ts\";\n\n/**\n * Note - this structure can be used by other libraries, so cannot have breaking changes.\n */\nexport interface AsyncContext {\n /**\n * The Inngest App that is currently being used to execute the function.\n *\n * If this is defined, we are in the context of an Inngest function execution,\n * or a possible one.\n */\n app: Inngest.Like;\n\n /**\n * Details of the current function execution context. If this doesn't exist,\n * then we're not currently in a function execution context.\n */\n execution?: {\n /**\n * The execution instance that is currently running the function.\n */\n instance: IInngestExecution;\n\n /**\n * The `ctx` object that has been passed in to this function execution,\n * including values such as `step` and `event`.\n */\n ctx: Context.Any;\n\n /**\n * If present, this indicates we are currently executing a `step.run()` step's\n * callback. Useful to understand whether we are in the context of a step\n * execution or within the main function body.\n */\n executingStep?: StepOptions & { hashedId?: string };\n\n /**\n * If present, indicates the parallel mode that should be applied to steps\n * created within this context. Set by `group.parallel()`.\n */\n parallelMode?: \"race\";\n\n /**\n * The stream tools instance for this execution context. Used by the\n * `stream` singleton to push/pipe SSE data to the client.\n */\n stream?: Stream;\n\n /**\n * If present, indicates the variant callback is executing within an\n * experiment. Set by `group.experiment()`. Any `step.*()` call within\n * this context will include these fields in `OutgoingOp.opts`.\n */\n experimentContext?: {\n experimentStepID: string;\n experimentName: string;\n variant: string;\n };\n\n /**\n * A mutable tracker used to detect whether any step tool was invoked\n * during a variant callback. Set by `group.experiment()`, flipped by\n * `createTool` in `InngestStepTools.ts`.\n */\n experimentStepTracker?: { found: boolean };\n\n /**\n * If true, we are inside the `select()` callback of\n * `group.experiment()`. Any `step.*()` call here would create a\n * nested step, which is not allowed.\n */\n insideExperimentSelect?: boolean;\n };\n}\n\n/**\n * A local-only symbol used as a key in global state to store the async local\n * storage instance.\n */\nconst alsSymbol = Symbol.for(\"inngest:als\");\n\n/**\n * Cache structure that stores both the promise and resolved ALS instance.\n * This allows synchronous access after initialization.\n */\ntype ALSCache = {\n promise: Promise<AsyncLocalStorageIsh>;\n resolved?: AsyncLocalStorageIsh;\n isFallback?: boolean;\n};\n\n/**\n * A type that represents a partial, runtime-agnostic interface of\n * `AsyncLocalStorage`.\n */\ntype AsyncLocalStorageIsh = {\n getStore: () => AsyncContext | undefined;\n run: <R>(store: AsyncContext, fn: () => R) => R;\n};\n\nconst getCache = (): ALSCache => {\n const g = globalThis as Record<symbol, ALSCache | undefined>;\n\n if (!g[alsSymbol]) {\n g[alsSymbol] = createCache();\n }\n\n return g[alsSymbol];\n};\n\nconst createCache = (): ALSCache => {\n const cache = {} as ALSCache;\n cache.promise = initializeALS(cache);\n return cache;\n};\n\nconst initializeALS = async (\n cache: ALSCache,\n): Promise<AsyncLocalStorageIsh> => {\n try {\n const { AsyncLocalStorage } = await import(\"node:async_hooks\");\n const als = new AsyncLocalStorage<AsyncContext>();\n cache.resolved = als;\n cache.isFallback = false;\n return als;\n } catch {\n const fallback: AsyncLocalStorageIsh = {\n getStore: () => undefined,\n run: (_, fn) => fn(),\n };\n cache.resolved = fallback;\n cache.isFallback = true;\n console.warn(\n \"node:async_hooks is not supported in this runtime. Async context is disabled.\",\n );\n return fallback;\n }\n};\n\n/**\n * Check if AsyncLocalStorage is unavailable and we're using the fallback.\n * Returns `undefined` if ALS hasn't been initialized yet.\n */\nexport const isALSFallback = (): boolean | undefined => {\n return getCache().isFallback;\n};\n\n/**\n * Retrieve the async context for the current execution.\n */\nexport const getAsyncCtx = async (): Promise<AsyncContext | undefined> => {\n return getAsyncLocalStorage().then((als) => als.getStore());\n};\n\n/**\n * Retrieve the async context for the current execution synchronously.\n * Returns undefined if ALS hasn't been initialized yet.\n */\nexport const getAsyncCtxSync = (): AsyncContext | undefined => {\n return getCache().resolved?.getStore();\n};\n\n/**\n * Get a singleton instance of `AsyncLocalStorage` used to store and retrieve\n * async context for the current execution.\n */\nexport const getAsyncLocalStorage = async (): Promise<AsyncLocalStorageIsh> => {\n return getCache().promise;\n};\n"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"als.js","names":["fallback: AsyncLocalStorageIsh"],"sources":["../../../src/components/execution/als.ts"],"sourcesContent":["import type { Context, StepOptions } from \"../../types.ts\";\nimport type { Inngest } from \"../Inngest.ts\";\nimport type { Stream } from \"../StreamTools.ts\";\nimport type { IInngestExecution } from \"./InngestExecution.ts\";\n\n/**\n * Note - this structure can be used by other libraries, so cannot have breaking changes.\n */\nexport interface AsyncContext {\n /**\n * The Inngest App that is currently being used to execute the function.\n *\n * If this is defined, we are in the context of an Inngest function execution,\n * or a possible one.\n */\n app: Inngest.Like;\n\n /**\n * Details of the current function execution context. If this doesn't exist,\n * then we're not currently in a function execution context.\n */\n execution?: {\n /**\n * The execution instance that is currently running the function.\n */\n instance: IInngestExecution;\n\n /**\n * The `ctx` object that has been passed in to this function execution,\n * including values such as `step` and `event`.\n */\n ctx: Context.Any;\n\n /**\n * If present, this indicates we are currently executing a `step.run()` step's\n * callback. Useful to understand whether we are in the context of a step\n * execution or within the main function body.\n */\n executingStep?: StepOptions & { hashedId?: string };\n\n /**\n * If present, indicates the parallel mode that should be applied to steps\n * created within this context. Set by `group.parallel()`.\n */\n parallelMode?: \"race\";\n\n /**\n * The stream tools instance for this execution context. Used by the\n * `stream` singleton to push/pipe SSE data to the client.\n */\n stream?: Stream;\n\n /**\n * If present, indicates the variant callback is executing within an\n * experiment. Set by `group.experiment()`. Any `step.*()` call within\n * this context will include these fields in `OutgoingOp.opts`.\n */\n experimentContext?: {\n experimentStepID: string;\n experimentName: string;\n variant: string;\n selectionStrategy: string;\n };\n\n /**\n * A mutable tracker used to detect whether any step tool was invoked\n * during a variant callback. Set by `group.experiment()`, flipped by\n * `createTool` in `InngestStepTools.ts`.\n */\n experimentStepTracker?: { found: boolean };\n\n /**\n * If true, we are inside the `select()` callback of\n * `group.experiment()`. Any `step.*()` call here would create a\n * nested step, which is not allowed.\n */\n insideExperimentSelect?: boolean;\n };\n}\n\n/**\n * A local-only symbol used as a key in global state to store the async local\n * storage instance.\n */\nconst alsSymbol = Symbol.for(\"inngest:als\");\n\n/**\n * Cache structure that stores both the promise and resolved ALS instance.\n * This allows synchronous access after initialization.\n */\ntype ALSCache = {\n promise: Promise<AsyncLocalStorageIsh>;\n resolved?: AsyncLocalStorageIsh;\n isFallback?: boolean;\n};\n\n/**\n * A type that represents a partial, runtime-agnostic interface of\n * `AsyncLocalStorage`.\n */\ntype AsyncLocalStorageIsh = {\n getStore: () => AsyncContext | undefined;\n run: <R>(store: AsyncContext, fn: () => R) => R;\n};\n\nconst getCache = (): ALSCache => {\n const g = globalThis as Record<symbol, ALSCache | undefined>;\n\n if (!g[alsSymbol]) {\n g[alsSymbol] = createCache();\n }\n\n return g[alsSymbol];\n};\n\nconst createCache = (): ALSCache => {\n const cache = {} as ALSCache;\n cache.promise = initializeALS(cache);\n return cache;\n};\n\nconst initializeALS = async (\n cache: ALSCache,\n): Promise<AsyncLocalStorageIsh> => {\n try {\n const { AsyncLocalStorage } = await import(\"node:async_hooks\");\n const als = new AsyncLocalStorage<AsyncContext>();\n cache.resolved = als;\n cache.isFallback = false;\n return als;\n } catch {\n const fallback: AsyncLocalStorageIsh = {\n getStore: () => undefined,\n run: (_, fn) => fn(),\n };\n cache.resolved = fallback;\n cache.isFallback = true;\n console.warn(\n \"node:async_hooks is not supported in this runtime. Async context is disabled.\",\n );\n return fallback;\n }\n};\n\n/**\n * Check if AsyncLocalStorage is unavailable and we're using the fallback.\n * Returns `undefined` if ALS hasn't been initialized yet.\n */\nexport const isALSFallback = (): boolean | undefined => {\n return getCache().isFallback;\n};\n\n/**\n * Retrieve the async context for the current execution.\n */\nexport const getAsyncCtx = async (): Promise<AsyncContext | undefined> => {\n return getAsyncLocalStorage().then((als) => als.getStore());\n};\n\n/**\n * Retrieve the async context for the current execution synchronously.\n * Returns undefined if ALS hasn't been initialized yet.\n */\nexport const getAsyncCtxSync = (): AsyncContext | undefined => {\n return getCache().resolved?.getStore();\n};\n\n/**\n * Get a singleton instance of `AsyncLocalStorage` used to store and retrieve\n * async context for the current execution.\n */\nexport const getAsyncLocalStorage = async (): Promise<AsyncLocalStorageIsh> => {\n return getCache().promise;\n};\n"],"mappings":";;;;;AAoFA,MAAM,YAAY,OAAO,IAAI,cAAc;AAqB3C,MAAM,iBAA2B;CAC/B,MAAM,IAAI;AAEV,KAAI,CAAC,EAAE,WACL,GAAE,aAAa,aAAa;AAG9B,QAAO,EAAE;;AAGX,MAAM,oBAA8B;CAClC,MAAM,QAAQ,EAAE;AAChB,OAAM,UAAU,cAAc,MAAM;AACpC,QAAO;;AAGT,MAAM,gBAAgB,OACpB,UACkC;AAClC,KAAI;EACF,MAAM,EAAE,sBAAsB,MAAM,OAAO;EAC3C,MAAM,MAAM,IAAI,mBAAiC;AACjD,QAAM,WAAW;AACjB,QAAM,aAAa;AACnB,SAAO;SACD;EACN,MAAMA,WAAiC;GACrC,gBAAgB;GAChB,MAAM,GAAG,OAAO,IAAI;GACrB;AACD,QAAM,WAAW;AACjB,QAAM,aAAa;AACnB,UAAQ,KACN,gFACD;AACD,SAAO;;;;;;;AAQX,MAAa,sBAA2C;AACtD,QAAO,UAAU,CAAC;;;;;AAMpB,MAAa,cAAc,YAA+C;AACxE,QAAO,sBAAsB,CAAC,MAAM,QAAQ,IAAI,UAAU,CAAC;;;;;;AAO7D,MAAa,wBAAkD;AAC7D,QAAO,UAAU,CAAC,UAAU,UAAU;;;;;;AAOxC,MAAa,uBAAuB,YAA2C;AAC7E,QAAO,UAAU,CAAC"}
|
|
@@ -64,9 +64,9 @@ declare namespace Realtime {
|
|
|
64
64
|
stream_id: z.ZodOptional<z.ZodString>;
|
|
65
65
|
kind: z.ZodEnum<["step", "run", "data", "ping", "pong", "closing", "event", "sub", "unsub", "datastream-start", "datastream-end", "chunk"]>;
|
|
66
66
|
}, "strip", z.ZodTypeAny, {
|
|
67
|
-
kind: "data" | "event" | "run" | "step" | "
|
|
68
|
-
data?: any;
|
|
67
|
+
kind: "data" | "event" | "run" | "step" | "ping" | "pong" | "closing" | "sub" | "unsub" | "datastream-start" | "datastream-end" | "chunk";
|
|
69
68
|
channel?: string | undefined;
|
|
69
|
+
data?: any;
|
|
70
70
|
topic?: string | undefined;
|
|
71
71
|
run_id?: string | undefined;
|
|
72
72
|
fn_id?: string | undefined;
|
|
@@ -74,9 +74,9 @@ declare namespace Realtime {
|
|
|
74
74
|
env_id?: string | undefined;
|
|
75
75
|
stream_id?: string | undefined;
|
|
76
76
|
}, {
|
|
77
|
-
kind: "data" | "event" | "run" | "step" | "
|
|
78
|
-
data?: any;
|
|
77
|
+
kind: "data" | "event" | "run" | "step" | "ping" | "pong" | "closing" | "sub" | "unsub" | "datastream-start" | "datastream-end" | "chunk";
|
|
79
78
|
channel?: string | undefined;
|
|
79
|
+
data?: any;
|
|
80
80
|
topic?: string | undefined;
|
|
81
81
|
run_id?: string | undefined;
|
|
82
82
|
fn_id?: string | undefined;
|
|
@@ -85,7 +85,7 @@ declare namespace Realtime {
|
|
|
85
85
|
stream_id?: string | undefined;
|
|
86
86
|
}>, {
|
|
87
87
|
data: any;
|
|
88
|
-
kind: "data" | "event" | "run" | "step" | "
|
|
88
|
+
kind: "data" | "event" | "run" | "step" | "ping" | "pong" | "closing" | "sub" | "unsub" | "datastream-start" | "datastream-end" | "chunk";
|
|
89
89
|
channel?: string | undefined;
|
|
90
90
|
topic?: string | undefined;
|
|
91
91
|
run_id?: string | undefined;
|
|
@@ -94,9 +94,9 @@ declare namespace Realtime {
|
|
|
94
94
|
env_id?: string | undefined;
|
|
95
95
|
stream_id?: string | undefined;
|
|
96
96
|
}, {
|
|
97
|
-
kind: "data" | "event" | "run" | "step" | "
|
|
98
|
-
data?: any;
|
|
97
|
+
kind: "data" | "event" | "run" | "step" | "ping" | "pong" | "closing" | "sub" | "unsub" | "datastream-start" | "datastream-end" | "chunk";
|
|
99
98
|
channel?: string | undefined;
|
|
99
|
+
data?: any;
|
|
100
100
|
topic?: string | undefined;
|
|
101
101
|
run_id?: string | undefined;
|
|
102
102
|
fn_id?: string | undefined;
|