inngest 4.0.0-beta.3 → 4.0.0
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 +34 -0
- package/components/ExperimentStrategies.cjs +107 -0
- package/components/ExperimentStrategies.cjs.map +1 -0
- package/components/ExperimentStrategies.d.cts +78 -0
- package/components/ExperimentStrategies.d.cts.map +1 -0
- package/components/ExperimentStrategies.d.ts +78 -0
- package/components/ExperimentStrategies.d.ts.map +1 -0
- package/components/ExperimentStrategies.js +105 -0
- package/components/ExperimentStrategies.js.map +1 -0
- package/components/Fetch.cjs +4 -4
- package/components/Fetch.cjs.map +1 -1
- package/components/Fetch.js +4 -4
- package/components/Fetch.js.map +1 -1
- package/components/InngestCommHandler.cjs +6 -5
- package/components/InngestCommHandler.cjs.map +1 -1
- package/components/InngestCommHandler.d.cts +1 -1
- package/components/InngestCommHandler.d.cts.map +1 -1
- package/components/InngestCommHandler.d.ts +1 -1
- package/components/InngestCommHandler.d.ts.map +1 -1
- package/components/InngestCommHandler.js +7 -6
- package/components/InngestCommHandler.js.map +1 -1
- package/components/InngestGroupTools.cjs +69 -2
- package/components/InngestGroupTools.cjs.map +1 -1
- package/components/InngestGroupTools.d.cts +101 -2
- package/components/InngestGroupTools.d.cts.map +1 -1
- package/components/InngestGroupTools.d.ts +101 -2
- package/components/InngestGroupTools.d.ts.map +1 -1
- package/components/InngestGroupTools.js +69 -2
- package/components/InngestGroupTools.js.map +1 -1
- package/components/InngestMetadata.cjs.map +1 -1
- package/components/InngestMetadata.d.cts +1 -1
- package/components/InngestMetadata.d.cts.map +1 -1
- package/components/InngestMetadata.d.ts +1 -1
- package/components/InngestMetadata.d.ts.map +1 -1
- package/components/InngestMetadata.js.map +1 -1
- package/components/InngestStepTools.cjs +30 -1
- package/components/InngestStepTools.cjs.map +1 -1
- package/components/InngestStepTools.d.cts +8 -1
- package/components/InngestStepTools.d.cts.map +1 -1
- package/components/InngestStepTools.d.ts +8 -1
- package/components/InngestStepTools.d.ts.map +1 -1
- package/components/InngestStepTools.js +29 -2
- package/components/InngestStepTools.js.map +1 -1
- package/components/connect/buffer.cjs +16 -13
- package/components/connect/buffer.cjs.map +1 -1
- package/components/connect/buffer.js +16 -11
- package/components/connect/buffer.js.map +1 -1
- package/components/connect/index.cjs +73 -76
- package/components/connect/index.cjs.map +1 -1
- package/components/connect/index.d.cts.map +1 -1
- package/components/connect/index.d.ts.map +1 -1
- package/components/connect/index.js +73 -74
- package/components/connect/index.js.map +1 -1
- package/components/connect/strategies/core/BaseStrategy.cjs +6 -9
- package/components/connect/strategies/core/BaseStrategy.cjs.map +1 -1
- package/components/connect/strategies/core/BaseStrategy.js +6 -7
- package/components/connect/strategies/core/BaseStrategy.js.map +1 -1
- package/components/connect/strategies/core/connection.cjs +87 -69
- package/components/connect/strategies/core/connection.cjs.map +1 -1
- package/components/connect/strategies/core/connection.js +87 -69
- package/components/connect/strategies/core/connection.js.map +1 -1
- package/components/connect/strategies/sameThread/index.cjs +11 -10
- package/components/connect/strategies/sameThread/index.cjs.map +1 -1
- package/components/connect/strategies/sameThread/index.js +11 -10
- package/components/connect/strategies/sameThread/index.js.map +1 -1
- package/components/connect/strategies/workerThread/index.cjs +62 -42
- package/components/connect/strategies/workerThread/index.cjs.map +1 -1
- package/components/connect/strategies/workerThread/index.js +62 -42
- package/components/connect/strategies/workerThread/index.js.map +1 -1
- package/components/connect/strategies/workerThread/runner.cjs +49 -17
- package/components/connect/strategies/workerThread/runner.cjs.map +1 -1
- package/components/connect/strategies/workerThread/runner.js +49 -17
- package/components/connect/strategies/workerThread/runner.js.map +1 -1
- package/components/connect/types.cjs.map +1 -1
- package/components/connect/types.d.cts +1 -1
- package/components/connect/types.d.cts.map +1 -1
- package/components/connect/types.d.ts +1 -1
- package/components/connect/types.d.ts.map +1 -1
- package/components/connect/types.js.map +1 -1
- package/components/execution/InngestExecution.cjs +2 -2
- package/components/execution/InngestExecution.cjs.map +1 -1
- package/components/execution/InngestExecution.d.cts +1 -1
- package/components/execution/InngestExecution.d.cts.map +1 -1
- package/components/execution/InngestExecution.d.ts +1 -1
- package/components/execution/InngestExecution.d.ts.map +1 -1
- package/components/execution/InngestExecution.js +2 -2
- package/components/execution/InngestExecution.js.map +1 -1
- package/components/execution/als.cjs.map +1 -1
- package/components/execution/als.d.cts +24 -0
- package/components/execution/als.d.cts.map +1 -1
- package/components/execution/als.d.ts +24 -0
- package/components/execution/als.d.ts.map +1 -1
- package/components/execution/als.js.map +1 -1
- package/components/execution/engine.cjs +27 -19
- package/components/execution/engine.cjs.map +1 -1
- package/components/execution/engine.d.cts.map +1 -1
- package/components/execution/engine.d.ts.map +1 -1
- package/components/execution/engine.js +28 -20
- package/components/execution/engine.js.map +1 -1
- package/components/execution/otel/consts.cjs +1 -0
- package/components/execution/otel/consts.cjs.map +1 -1
- package/components/execution/otel/consts.js +1 -0
- package/components/execution/otel/consts.js.map +1 -1
- package/components/execution/otel/middleware.cjs +7 -7
- package/components/execution/otel/middleware.cjs.map +1 -1
- package/components/execution/otel/middleware.d.cts +1 -1
- package/components/execution/otel/middleware.d.ts +1 -1
- package/components/execution/otel/middleware.js +7 -7
- package/components/execution/otel/middleware.js.map +1 -1
- package/components/execution/otel/processor.cjs +70 -19
- package/components/execution/otel/processor.cjs.map +1 -1
- package/components/execution/otel/processor.d.cts +11 -0
- package/components/execution/otel/processor.d.cts.map +1 -1
- package/components/execution/otel/processor.d.ts +11 -0
- package/components/execution/otel/processor.d.ts.map +1 -1
- package/components/execution/otel/processor.js +70 -19
- package/components/execution/otel/processor.js.map +1 -1
- package/components/middleware/middleware.cjs.map +1 -1
- package/components/middleware/middleware.d.cts +2 -1
- package/components/middleware/middleware.d.cts.map +1 -1
- package/components/middleware/middleware.d.ts +2 -1
- package/components/middleware/middleware.d.ts.map +1 -1
- package/components/middleware/middleware.js.map +1 -1
- package/components/middleware/utils.cjs +3 -1
- package/components/middleware/utils.cjs.map +1 -1
- package/components/middleware/utils.js +3 -1
- package/components/middleware/utils.js.map +1 -1
- package/components/triggers/typeHelpers.d.cts +1 -1
- package/components/triggers/typeHelpers.d.ts +1 -1
- package/helpers/deterministicId.cjs +298 -0
- package/helpers/deterministicId.cjs.map +1 -0
- package/helpers/deterministicId.js +296 -0
- package/helpers/deterministicId.js.map +1 -0
- package/helpers/env.cjs +8 -22
- package/helpers/env.cjs.map +1 -1
- package/helpers/env.js +8 -22
- package/helpers/env.js.map +1 -1
- package/helpers/functions.cjs +9 -5
- package/helpers/functions.cjs.map +1 -1
- package/helpers/functions.js +8 -5
- package/helpers/functions.js.map +1 -1
- package/helpers/url.cjs +2 -5
- package/helpers/url.cjs.map +1 -1
- package/helpers/url.js +2 -5
- package/helpers/url.js.map +1 -1
- package/index.cjs +3 -0
- package/index.d.cts +3 -2
- package/index.d.ts +3 -2
- package/index.js +3 -2
- package/package.json +1 -1
- package/types.d.cts +1 -1
- package/types.d.ts +1 -1
- package/types.d.ts.map +1 -1
- 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/helpers/devserver.cjs +0 -42
- package/helpers/devserver.cjs.map +0 -1
- package/helpers/devserver.js +0 -42
- package/helpers/devserver.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# inngest
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [#1346](https://github.com/inngest/inngest-js/pull/1346) [`e8024132`](https://github.com/inngest/inngest-js/commit/e80241321f1735dfe512dacacbfc5791c9f5da53) Thanks [@amh4r](https://github.com/amh4r)! - See list of changes in the migration guide: https://www.inngest.com/docs/reference/typescript/v4/migrations/v3-to-v4
|
|
8
|
+
|
|
9
|
+
## 3.52.7
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#1359](https://github.com/inngest/inngest-js/pull/1359) [`b4d9833f`](https://github.com/inngest/inngest-js/commit/b4d9833fe632f542aad10c6faf5da3fd6a6fc9b7) Thanks [@Linell](https://github.com/Linell)! - fix: use deterministic IDs for correct checkpointed parenting
|
|
14
|
+
|
|
15
|
+
- [#1370](https://github.com/inngest/inngest-js/pull/1370) [`037336dc`](https://github.com/inngest/inngest-js/commit/037336dce8731aa9fcf5d56ff3e2d8a48e5aee6f) Thanks [@amh4r](https://github.com/amh4r)! - Fix mishandling drain message (Connect only)
|
|
16
|
+
|
|
17
|
+
## 3.52.6
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#1350](https://github.com/inngest/inngest-js/pull/1350) [`470fdb98`](https://github.com/inngest/inngest-js/commit/470fdb9845514b6597e0bd7c6db469d3808f6dcf) Thanks [@amh4r](https://github.com/amh4r)! - Fix false NESTING_STEPS error
|
|
22
|
+
|
|
23
|
+
- [#1356](https://github.com/inngest/inngest-js/pull/1356) [`2e961c21`](https://github.com/inngest/inngest-js/commit/2e961c2169a451d60c3a14e8b9cc19e19ad0dec6) Thanks [@amh4r](https://github.com/amh4r)! - Fix checkpointing maxRuntime causing function run hang
|
|
24
|
+
|
|
25
|
+
## 3.52.5
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- [#1340](https://github.com/inngest/inngest-js/pull/1340) [`335703d7`](https://github.com/inngest/inngest-js/commit/335703d7372d092a865def4b2cfb4730e50d5fa9) Thanks [@Linell](https://github.com/Linell)! - fix: fallback to async flow on checkpoint error
|
|
30
|
+
|
|
31
|
+
## 3.52.4
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- [#1338](https://github.com/inngest/inngest-js/pull/1338) [`4f45adb7`](https://github.com/inngest/inngest-js/commit/4f45adb71fc7dfbc1ed7f941ddc4dd5e42f3523b) Thanks [@jakobevangelista](https://github.com/jakobevangelista)! - Fix signing key propagation from serve() options to InngestApi for outgoing API calls
|
|
36
|
+
|
|
3
37
|
## 3.52.3
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_als = require('./execution/als.cjs');
|
|
3
|
+
let hash_js = require("hash.js");
|
|
4
|
+
hash_js = require_rolldown_runtime.__toESM(hash_js);
|
|
5
|
+
|
|
6
|
+
//#region src/components/ExperimentStrategies.ts
|
|
7
|
+
const { sha256 } = hash_js.default;
|
|
8
|
+
/**
|
|
9
|
+
* Hash a string to a float in [0, 1) using SHA-256.
|
|
10
|
+
*/
|
|
11
|
+
const hashToFloat = (str) => {
|
|
12
|
+
const hex = sha256().update(str).digest("hex").slice(0, 8);
|
|
13
|
+
return Number.parseInt(hex, 16) / 4294967296;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Given a float in [0, 1) and a weights map, select the variant whose bucket
|
|
17
|
+
* the float falls into. Entries are sorted alphabetically for determinism.
|
|
18
|
+
*/
|
|
19
|
+
const selectByWeight = (hash01, weights) => {
|
|
20
|
+
const entries = Object.entries(weights).sort(([a], [b]) => a.localeCompare(b));
|
|
21
|
+
const total = entries.reduce((sum, [, w]) => sum + w, 0);
|
|
22
|
+
let cursor = 0;
|
|
23
|
+
for (const [name, weight] of entries) {
|
|
24
|
+
cursor += weight / total;
|
|
25
|
+
if (hash01 < cursor) return name;
|
|
26
|
+
}
|
|
27
|
+
return entries[entries.length - 1][0];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build equal weights from variant names: `{ a: 1, b: 1, ... }`.
|
|
31
|
+
*/
|
|
32
|
+
const equalWeights = (variantNames) => {
|
|
33
|
+
return Object.fromEntries(variantNames.map((name) => [name, 1]));
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Throw if all weights are zero.
|
|
37
|
+
*/
|
|
38
|
+
const validateWeights = (weights) => {
|
|
39
|
+
for (const [name, w] of Object.entries(weights)) {
|
|
40
|
+
if (!Number.isFinite(w)) throw new Error(`experiment.weighted(): weight for "${name}" is not a finite number (${w}); weights must be finite numbers >= 0`);
|
|
41
|
+
if (w < 0) throw new Error(`experiment.weighted(): weight for "${name}" is negative (${w}); weights must be >= 0`);
|
|
42
|
+
}
|
|
43
|
+
if (Object.values(weights).reduce((sum, w) => sum + w, 0) <= 0) throw new Error("experiment.weighted(): all weights are zero; at least one weight must be positive");
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Attach `__experimentConfig` to a select function, producing an
|
|
47
|
+
* `ExperimentSelectFn`.
|
|
48
|
+
*/
|
|
49
|
+
const createSelectFn = (fn, config) => {
|
|
50
|
+
return Object.assign(fn, { __experimentConfig: config });
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Factory functions for creating experiment selection strategies.
|
|
54
|
+
*
|
|
55
|
+
* Each factory returns an `ExperimentSelectFn` — a callable function with an
|
|
56
|
+
* `__experimentConfig` property carrying strategy metadata.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { experiment, group, step } from "inngest";
|
|
61
|
+
*
|
|
62
|
+
* const result = await group.experiment("checkout-flow", {
|
|
63
|
+
* variants: {
|
|
64
|
+
* control: () => step.run("old", () => oldCheckout()),
|
|
65
|
+
* new_flow: () => step.run("new", () => newCheckout()),
|
|
66
|
+
* },
|
|
67
|
+
* select: experiment.weighted({ control: 80, new_flow: 20 }),
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
const experiment = {
|
|
74
|
+
fixed(variantName) {
|
|
75
|
+
return createSelectFn(() => variantName, { strategy: "fixed" });
|
|
76
|
+
},
|
|
77
|
+
weighted(weights) {
|
|
78
|
+
validateWeights(weights);
|
|
79
|
+
const frozen = { ...weights };
|
|
80
|
+
return createSelectFn(() => {
|
|
81
|
+
return selectByWeight(hashToFloat(require_als.getAsyncCtxSync()?.execution?.ctx.runId ?? crypto.randomUUID()), frozen);
|
|
82
|
+
}, {
|
|
83
|
+
strategy: "weighted",
|
|
84
|
+
weights: frozen
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
bucket(value, options) {
|
|
88
|
+
if (options?.weights) validateWeights(options.weights);
|
|
89
|
+
const str = value == null ? "" : String(value);
|
|
90
|
+
return createSelectFn((variantNames) => {
|
|
91
|
+
const weights = options?.weights ?? (variantNames ? equalWeights(variantNames) : void 0);
|
|
92
|
+
if (!weights) throw new Error("experiment.bucket() requires either explicit weights or variant names from group.experiment()");
|
|
93
|
+
return selectByWeight(hashToFloat(str), weights);
|
|
94
|
+
}, {
|
|
95
|
+
strategy: "bucket",
|
|
96
|
+
weights: options?.weights,
|
|
97
|
+
...value == null && { nullishBucket: true }
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
custom(fn) {
|
|
101
|
+
return createSelectFn(fn, { strategy: "custom" });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
exports.experiment = experiment;
|
|
107
|
+
//# sourceMappingURL=ExperimentStrategies.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExperimentStrategies.cjs","names":["hashjs","getAsyncCtxSync"],"sources":["../../src/components/ExperimentStrategies.ts"],"sourcesContent":["import hashjs from \"hash.js\";\nimport { getAsyncCtxSync } from \"./execution/als.ts\";\nimport type { ExperimentSelectFn } from \"./InngestGroupTools.ts\";\n\nconst { sha256 } = hashjs;\n\n/**\n * Hash a string to a float in [0, 1) using SHA-256.\n */\nconst hashToFloat = (str: string): number => {\n const hex = sha256().update(str).digest(\"hex\").slice(0, 8);\n return Number.parseInt(hex, 16) / 0x100000000;\n};\n\n/**\n * Given a float in [0, 1) and a weights map, select the variant whose bucket\n * the float falls into. Entries are sorted alphabetically for determinism.\n */\nconst selectByWeight = (\n hash01: number,\n weights: Record<string, number>,\n): string => {\n const entries = Object.entries(weights).sort(([a], [b]) =>\n a.localeCompare(b),\n );\n const total = entries.reduce((sum, [, w]) => sum + w, 0);\n\n let cursor = 0;\n for (const [name, weight] of entries) {\n cursor += weight / total;\n if (hash01 < cursor) {\n return name;\n }\n }\n\n // Fallback to last entry (floating-point edge case)\n return entries[entries.length - 1]![0]!;\n};\n\n/**\n * Build equal weights from variant names: `{ a: 1, b: 1, ... }`.\n */\nconst equalWeights = (variantNames: string[]): Record<string, number> => {\n return Object.fromEntries(variantNames.map((name) => [name, 1]));\n};\n\n/**\n * Throw if all weights are zero.\n */\nconst validateWeights = (weights: Record<string, number>): void => {\n for (const [name, w] of Object.entries(weights)) {\n if (!Number.isFinite(w)) {\n throw new Error(\n `experiment.weighted(): weight for \"${name}\" is not a finite number (${w}); weights must be finite numbers >= 0`,\n );\n }\n if (w < 0) {\n throw new Error(\n `experiment.weighted(): weight for \"${name}\" is negative (${w}); weights must be >= 0`,\n );\n }\n }\n\n const total = Object.values(weights).reduce((sum, w) => sum + w, 0);\n if (total <= 0) {\n throw new Error(\n \"experiment.weighted(): all weights are zero; at least one weight must be positive\",\n );\n }\n};\n\n/**\n * Attach `__experimentConfig` to a select function, producing an\n * `ExperimentSelectFn`.\n */\nconst createSelectFn = (\n fn: (variantNames?: string[]) => Promise<string> | string,\n config: ExperimentSelectFn[\"__experimentConfig\"],\n): ExperimentSelectFn => {\n return Object.assign(fn, { __experimentConfig: config });\n};\n\n/**\n * Factory functions for creating experiment selection strategies.\n *\n * Each factory returns an `ExperimentSelectFn` — a callable function with an\n * `__experimentConfig` property carrying strategy metadata.\n *\n * @example\n * ```ts\n * import { experiment, group, step } from \"inngest\";\n *\n * const result = await group.experiment(\"checkout-flow\", {\n * variants: {\n * control: () => step.run(\"old\", () => oldCheckout()),\n * new_flow: () => step.run(\"new\", () => newCheckout()),\n * },\n * select: experiment.weighted({ control: 80, new_flow: 20 }),\n * });\n * ```\n *\n * @public\n */\nexport const experiment = {\n /**\n * Always selects the specified variant.\n *\n * @example\n * ```ts\n * select: experiment.fixed(\"control\")\n * ```\n */\n fixed(variantName: string): ExperimentSelectFn {\n return createSelectFn(() => variantName, { strategy: \"fixed\" });\n },\n\n /**\n * Weighted random selection, seeded with the current run ID for\n * determinism — the same run always gets the same variant.\n *\n * @example\n * ```ts\n * select: experiment.weighted({ gpt4: 50, claude: 50 })\n * ```\n *\n * @throws If all weights are zero (validated at creation time).\n */\n weighted(weights: Record<string, number>): ExperimentSelectFn {\n validateWeights(weights);\n\n // Snapshot so that later mutations to the caller's object can't silently\n // change runtime behaviour after validation has passed.\n const frozen = { ...weights };\n\n return createSelectFn(\n () => {\n const runId =\n getAsyncCtxSync()?.execution?.ctx.runId ?? crypto.randomUUID();\n return selectByWeight(hashToFloat(runId), frozen);\n },\n { strategy: \"weighted\", weights: frozen },\n );\n },\n\n /**\n * Consistent hashing — the same value always maps to the same variant.\n *\n * When `value` is `null` or `undefined`, an empty string is hashed instead.\n *\n * @example\n * ```ts\n * select: experiment.bucket(userId)\n * select: experiment.bucket(userId, { weights: { a: 70, b: 30 } })\n * ```\n */\n bucket(\n value: unknown,\n options?: { weights?: Record<string, number> },\n ): ExperimentSelectFn {\n if (options?.weights) {\n validateWeights(options.weights);\n }\n\n const str = value == null ? \"\" : String(value);\n\n return createSelectFn(\n (variantNames?: string[]) => {\n const weights =\n options?.weights ??\n (variantNames ? equalWeights(variantNames) : undefined);\n\n if (!weights) {\n throw new Error(\n \"experiment.bucket() requires either explicit weights or variant \" +\n \"names from group.experiment()\",\n );\n }\n\n return selectByWeight(hashToFloat(str), weights);\n },\n {\n strategy: \"bucket\",\n weights: options?.weights,\n ...(value == null && { nullishBucket: true }),\n },\n );\n },\n\n /**\n * User-provided selection function. The function is called inside the\n * memoized step, so it only runs once per run.\n *\n * @example\n * ```ts\n * select: experiment.custom(async () => {\n * const flag = await getFeatureFlag(\"checkout-variant\");\n * return flag;\n * })\n * ```\n */\n custom(fn: () => Promise<string> | string): ExperimentSelectFn {\n return createSelectFn(fn, { strategy: \"custom\" });\n },\n};\n"],"mappings":";;;;;;AAIA,MAAM,EAAE,WAAWA;;;;AAKnB,MAAM,eAAe,QAAwB;CAC3C,MAAM,MAAM,QAAQ,CAAC,OAAO,IAAI,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE;AAC1D,QAAO,OAAO,SAAS,KAAK,GAAG,GAAG;;;;;;AAOpC,MAAM,kBACJ,QACA,YACW;CACX,MAAM,UAAU,OAAO,QAAQ,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,OAClD,EAAE,cAAc,EAAE,CACnB;CACD,MAAM,QAAQ,QAAQ,QAAQ,KAAK,GAAG,OAAO,MAAM,GAAG,EAAE;CAExD,IAAI,SAAS;AACb,MAAK,MAAM,CAAC,MAAM,WAAW,SAAS;AACpC,YAAU,SAAS;AACnB,MAAI,SAAS,OACX,QAAO;;AAKX,QAAO,QAAQ,QAAQ,SAAS,GAAI;;;;;AAMtC,MAAM,gBAAgB,iBAAmD;AACvE,QAAO,OAAO,YAAY,aAAa,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;;;;;AAMlE,MAAM,mBAAmB,YAA0C;AACjE,MAAK,MAAM,CAAC,MAAM,MAAM,OAAO,QAAQ,QAAQ,EAAE;AAC/C,MAAI,CAAC,OAAO,SAAS,EAAE,CACrB,OAAM,IAAI,MACR,sCAAsC,KAAK,4BAA4B,EAAE,wCAC1E;AAEH,MAAI,IAAI,EACN,OAAM,IAAI,MACR,sCAAsC,KAAK,iBAAiB,EAAE,yBAC/D;;AAKL,KADc,OAAO,OAAO,QAAQ,CAAC,QAAQ,KAAK,MAAM,MAAM,GAAG,EAAE,IACtD,EACX,OAAM,IAAI,MACR,oFACD;;;;;;AAQL,MAAM,kBACJ,IACA,WACuB;AACvB,QAAO,OAAO,OAAO,IAAI,EAAE,oBAAoB,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwB1D,MAAa,aAAa;CASxB,MAAM,aAAyC;AAC7C,SAAO,qBAAqB,aAAa,EAAE,UAAU,SAAS,CAAC;;CAcjE,SAAS,SAAqD;AAC5D,kBAAgB,QAAQ;EAIxB,MAAM,SAAS,EAAE,GAAG,SAAS;AAE7B,SAAO,qBACC;AAGJ,UAAO,eAAe,YADpBC,6BAAiB,EAAE,WAAW,IAAI,SAAS,OAAO,YAAY,CACxB,EAAE,OAAO;KAEnD;GAAE,UAAU;GAAY,SAAS;GAAQ,CAC1C;;CAcH,OACE,OACA,SACoB;AACpB,MAAI,SAAS,QACX,iBAAgB,QAAQ,QAAQ;EAGlC,MAAM,MAAM,SAAS,OAAO,KAAK,OAAO,MAAM;AAE9C,SAAO,gBACJ,iBAA4B;GAC3B,MAAM,UACJ,SAAS,YACR,eAAe,aAAa,aAAa,GAAG;AAE/C,OAAI,CAAC,QACH,OAAM,IAAI,MACR,gGAED;AAGH,UAAO,eAAe,YAAY,IAAI,EAAE,QAAQ;KAElD;GACE,UAAU;GACV,SAAS,SAAS;GAClB,GAAI,SAAS,QAAQ,EAAE,eAAe,MAAM;GAC7C,CACF;;CAeH,OAAO,IAAwD;AAC7D,SAAO,eAAe,IAAI,EAAE,UAAU,UAAU,CAAC;;CAEpD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ExperimentSelectFn } from "./InngestGroupTools.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/components/ExperimentStrategies.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Factory functions for creating experiment selection strategies.
|
|
7
|
+
*
|
|
8
|
+
* Each factory returns an `ExperimentSelectFn` — a callable function with an
|
|
9
|
+
* `__experimentConfig` property carrying strategy metadata.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { experiment, group, step } from "inngest";
|
|
14
|
+
*
|
|
15
|
+
* const result = await group.experiment("checkout-flow", {
|
|
16
|
+
* variants: {
|
|
17
|
+
* control: () => step.run("old", () => oldCheckout()),
|
|
18
|
+
* new_flow: () => step.run("new", () => newCheckout()),
|
|
19
|
+
* },
|
|
20
|
+
* select: experiment.weighted({ control: 80, new_flow: 20 }),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
declare const experiment: {
|
|
27
|
+
/**
|
|
28
|
+
* Always selects the specified variant.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* select: experiment.fixed("control")
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
fixed(variantName: string): ExperimentSelectFn;
|
|
36
|
+
/**
|
|
37
|
+
* Weighted random selection, seeded with the current run ID for
|
|
38
|
+
* determinism — the same run always gets the same variant.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* select: experiment.weighted({ gpt4: 50, claude: 50 })
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @throws If all weights are zero (validated at creation time).
|
|
46
|
+
*/
|
|
47
|
+
weighted(weights: Record<string, number>): ExperimentSelectFn;
|
|
48
|
+
/**
|
|
49
|
+
* Consistent hashing — the same value always maps to the same variant.
|
|
50
|
+
*
|
|
51
|
+
* When `value` is `null` or `undefined`, an empty string is hashed instead.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* select: experiment.bucket(userId)
|
|
56
|
+
* select: experiment.bucket(userId, { weights: { a: 70, b: 30 } })
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
bucket(value: unknown, options?: {
|
|
60
|
+
weights?: Record<string, number>;
|
|
61
|
+
}): ExperimentSelectFn;
|
|
62
|
+
/**
|
|
63
|
+
* User-provided selection function. The function is called inside the
|
|
64
|
+
* memoized step, so it only runs once per run.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* select: experiment.custom(async () => {
|
|
69
|
+
* const flag = await getFeatureFlag("checkout-variant");
|
|
70
|
+
* return flag;
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
custom(fn: () => Promise<string> | string): ExperimentSelectFn;
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { experiment };
|
|
78
|
+
//# sourceMappingURL=ExperimentStrategies.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExperimentStrategies.d.cts","names":[],"sources":["../../src/components/ExperimentStrategies.ts"],"sourcesContent":[],"mappings":";;;;;;AAuGA;;;;;;;;;;;;;;;;;;;cAAa;;;;;;;;;8BASiB;;;;;;;;;;;;oBAeV,yBAAyB;;;;;;;;;;;;;cA8BnB;MACrB;;;;;;;;;;;;;mBA0Cc,2BAA2B"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ExperimentSelectFn } from "./InngestGroupTools.js";
|
|
2
|
+
|
|
3
|
+
//#region src/components/ExperimentStrategies.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Factory functions for creating experiment selection strategies.
|
|
7
|
+
*
|
|
8
|
+
* Each factory returns an `ExperimentSelectFn` — a callable function with an
|
|
9
|
+
* `__experimentConfig` property carrying strategy metadata.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { experiment, group, step } from "inngest";
|
|
14
|
+
*
|
|
15
|
+
* const result = await group.experiment("checkout-flow", {
|
|
16
|
+
* variants: {
|
|
17
|
+
* control: () => step.run("old", () => oldCheckout()),
|
|
18
|
+
* new_flow: () => step.run("new", () => newCheckout()),
|
|
19
|
+
* },
|
|
20
|
+
* select: experiment.weighted({ control: 80, new_flow: 20 }),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
declare const experiment: {
|
|
27
|
+
/**
|
|
28
|
+
* Always selects the specified variant.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* select: experiment.fixed("control")
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
fixed(variantName: string): ExperimentSelectFn;
|
|
36
|
+
/**
|
|
37
|
+
* Weighted random selection, seeded with the current run ID for
|
|
38
|
+
* determinism — the same run always gets the same variant.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* select: experiment.weighted({ gpt4: 50, claude: 50 })
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @throws If all weights are zero (validated at creation time).
|
|
46
|
+
*/
|
|
47
|
+
weighted(weights: Record<string, number>): ExperimentSelectFn;
|
|
48
|
+
/**
|
|
49
|
+
* Consistent hashing — the same value always maps to the same variant.
|
|
50
|
+
*
|
|
51
|
+
* When `value` is `null` or `undefined`, an empty string is hashed instead.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* select: experiment.bucket(userId)
|
|
56
|
+
* select: experiment.bucket(userId, { weights: { a: 70, b: 30 } })
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
bucket(value: unknown, options?: {
|
|
60
|
+
weights?: Record<string, number>;
|
|
61
|
+
}): ExperimentSelectFn;
|
|
62
|
+
/**
|
|
63
|
+
* User-provided selection function. The function is called inside the
|
|
64
|
+
* memoized step, so it only runs once per run.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* select: experiment.custom(async () => {
|
|
69
|
+
* const flag = await getFeatureFlag("checkout-variant");
|
|
70
|
+
* return flag;
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
custom(fn: () => Promise<string> | string): ExperimentSelectFn;
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { experiment };
|
|
78
|
+
//# sourceMappingURL=ExperimentStrategies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExperimentStrategies.d.ts","names":[],"sources":["../../src/components/ExperimentStrategies.ts"],"sourcesContent":[],"mappings":";;;;;;AAuGA;;;;;;;;;;;;;;;;;;;cAAa;;;;;;;;;8BASiB;;;;;;;;;;;;oBAeV,yBAAyB;;;;;;;;;;;;;cA8BnB;MACrB;;;;;;;;;;;;;mBA0Cc,2BAA2B"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getAsyncCtxSync } from "./execution/als.js";
|
|
2
|
+
import hashjs from "hash.js";
|
|
3
|
+
|
|
4
|
+
//#region src/components/ExperimentStrategies.ts
|
|
5
|
+
const { sha256 } = hashjs;
|
|
6
|
+
/**
|
|
7
|
+
* Hash a string to a float in [0, 1) using SHA-256.
|
|
8
|
+
*/
|
|
9
|
+
const hashToFloat = (str) => {
|
|
10
|
+
const hex = sha256().update(str).digest("hex").slice(0, 8);
|
|
11
|
+
return Number.parseInt(hex, 16) / 4294967296;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Given a float in [0, 1) and a weights map, select the variant whose bucket
|
|
15
|
+
* the float falls into. Entries are sorted alphabetically for determinism.
|
|
16
|
+
*/
|
|
17
|
+
const selectByWeight = (hash01, weights) => {
|
|
18
|
+
const entries = Object.entries(weights).sort(([a], [b]) => a.localeCompare(b));
|
|
19
|
+
const total = entries.reduce((sum, [, w]) => sum + w, 0);
|
|
20
|
+
let cursor = 0;
|
|
21
|
+
for (const [name, weight] of entries) {
|
|
22
|
+
cursor += weight / total;
|
|
23
|
+
if (hash01 < cursor) return name;
|
|
24
|
+
}
|
|
25
|
+
return entries[entries.length - 1][0];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Build equal weights from variant names: `{ a: 1, b: 1, ... }`.
|
|
29
|
+
*/
|
|
30
|
+
const equalWeights = (variantNames) => {
|
|
31
|
+
return Object.fromEntries(variantNames.map((name) => [name, 1]));
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Throw if all weights are zero.
|
|
35
|
+
*/
|
|
36
|
+
const validateWeights = (weights) => {
|
|
37
|
+
for (const [name, w] of Object.entries(weights)) {
|
|
38
|
+
if (!Number.isFinite(w)) throw new Error(`experiment.weighted(): weight for "${name}" is not a finite number (${w}); weights must be finite numbers >= 0`);
|
|
39
|
+
if (w < 0) throw new Error(`experiment.weighted(): weight for "${name}" is negative (${w}); weights must be >= 0`);
|
|
40
|
+
}
|
|
41
|
+
if (Object.values(weights).reduce((sum, w) => sum + w, 0) <= 0) throw new Error("experiment.weighted(): all weights are zero; at least one weight must be positive");
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Attach `__experimentConfig` to a select function, producing an
|
|
45
|
+
* `ExperimentSelectFn`.
|
|
46
|
+
*/
|
|
47
|
+
const createSelectFn = (fn, config) => {
|
|
48
|
+
return Object.assign(fn, { __experimentConfig: config });
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Factory functions for creating experiment selection strategies.
|
|
52
|
+
*
|
|
53
|
+
* Each factory returns an `ExperimentSelectFn` — a callable function with an
|
|
54
|
+
* `__experimentConfig` property carrying strategy metadata.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { experiment, group, step } from "inngest";
|
|
59
|
+
*
|
|
60
|
+
* const result = await group.experiment("checkout-flow", {
|
|
61
|
+
* variants: {
|
|
62
|
+
* control: () => step.run("old", () => oldCheckout()),
|
|
63
|
+
* new_flow: () => step.run("new", () => newCheckout()),
|
|
64
|
+
* },
|
|
65
|
+
* select: experiment.weighted({ control: 80, new_flow: 20 }),
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
const experiment = {
|
|
72
|
+
fixed(variantName) {
|
|
73
|
+
return createSelectFn(() => variantName, { strategy: "fixed" });
|
|
74
|
+
},
|
|
75
|
+
weighted(weights) {
|
|
76
|
+
validateWeights(weights);
|
|
77
|
+
const frozen = { ...weights };
|
|
78
|
+
return createSelectFn(() => {
|
|
79
|
+
return selectByWeight(hashToFloat(getAsyncCtxSync()?.execution?.ctx.runId ?? crypto.randomUUID()), frozen);
|
|
80
|
+
}, {
|
|
81
|
+
strategy: "weighted",
|
|
82
|
+
weights: frozen
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
bucket(value, options) {
|
|
86
|
+
if (options?.weights) validateWeights(options.weights);
|
|
87
|
+
const str = value == null ? "" : String(value);
|
|
88
|
+
return createSelectFn((variantNames) => {
|
|
89
|
+
const weights = options?.weights ?? (variantNames ? equalWeights(variantNames) : void 0);
|
|
90
|
+
if (!weights) throw new Error("experiment.bucket() requires either explicit weights or variant names from group.experiment()");
|
|
91
|
+
return selectByWeight(hashToFloat(str), weights);
|
|
92
|
+
}, {
|
|
93
|
+
strategy: "bucket",
|
|
94
|
+
weights: options?.weights,
|
|
95
|
+
...value == null && { nullishBucket: true }
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
custom(fn) {
|
|
99
|
+
return createSelectFn(fn, { strategy: "custom" });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { experiment };
|
|
105
|
+
//# sourceMappingURL=ExperimentStrategies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExperimentStrategies.js","names":[],"sources":["../../src/components/ExperimentStrategies.ts"],"sourcesContent":["import hashjs from \"hash.js\";\nimport { getAsyncCtxSync } from \"./execution/als.ts\";\nimport type { ExperimentSelectFn } from \"./InngestGroupTools.ts\";\n\nconst { sha256 } = hashjs;\n\n/**\n * Hash a string to a float in [0, 1) using SHA-256.\n */\nconst hashToFloat = (str: string): number => {\n const hex = sha256().update(str).digest(\"hex\").slice(0, 8);\n return Number.parseInt(hex, 16) / 0x100000000;\n};\n\n/**\n * Given a float in [0, 1) and a weights map, select the variant whose bucket\n * the float falls into. Entries are sorted alphabetically for determinism.\n */\nconst selectByWeight = (\n hash01: number,\n weights: Record<string, number>,\n): string => {\n const entries = Object.entries(weights).sort(([a], [b]) =>\n a.localeCompare(b),\n );\n const total = entries.reduce((sum, [, w]) => sum + w, 0);\n\n let cursor = 0;\n for (const [name, weight] of entries) {\n cursor += weight / total;\n if (hash01 < cursor) {\n return name;\n }\n }\n\n // Fallback to last entry (floating-point edge case)\n return entries[entries.length - 1]![0]!;\n};\n\n/**\n * Build equal weights from variant names: `{ a: 1, b: 1, ... }`.\n */\nconst equalWeights = (variantNames: string[]): Record<string, number> => {\n return Object.fromEntries(variantNames.map((name) => [name, 1]));\n};\n\n/**\n * Throw if all weights are zero.\n */\nconst validateWeights = (weights: Record<string, number>): void => {\n for (const [name, w] of Object.entries(weights)) {\n if (!Number.isFinite(w)) {\n throw new Error(\n `experiment.weighted(): weight for \"${name}\" is not a finite number (${w}); weights must be finite numbers >= 0`,\n );\n }\n if (w < 0) {\n throw new Error(\n `experiment.weighted(): weight for \"${name}\" is negative (${w}); weights must be >= 0`,\n );\n }\n }\n\n const total = Object.values(weights).reduce((sum, w) => sum + w, 0);\n if (total <= 0) {\n throw new Error(\n \"experiment.weighted(): all weights are zero; at least one weight must be positive\",\n );\n }\n};\n\n/**\n * Attach `__experimentConfig` to a select function, producing an\n * `ExperimentSelectFn`.\n */\nconst createSelectFn = (\n fn: (variantNames?: string[]) => Promise<string> | string,\n config: ExperimentSelectFn[\"__experimentConfig\"],\n): ExperimentSelectFn => {\n return Object.assign(fn, { __experimentConfig: config });\n};\n\n/**\n * Factory functions for creating experiment selection strategies.\n *\n * Each factory returns an `ExperimentSelectFn` — a callable function with an\n * `__experimentConfig` property carrying strategy metadata.\n *\n * @example\n * ```ts\n * import { experiment, group, step } from \"inngest\";\n *\n * const result = await group.experiment(\"checkout-flow\", {\n * variants: {\n * control: () => step.run(\"old\", () => oldCheckout()),\n * new_flow: () => step.run(\"new\", () => newCheckout()),\n * },\n * select: experiment.weighted({ control: 80, new_flow: 20 }),\n * });\n * ```\n *\n * @public\n */\nexport const experiment = {\n /**\n * Always selects the specified variant.\n *\n * @example\n * ```ts\n * select: experiment.fixed(\"control\")\n * ```\n */\n fixed(variantName: string): ExperimentSelectFn {\n return createSelectFn(() => variantName, { strategy: \"fixed\" });\n },\n\n /**\n * Weighted random selection, seeded with the current run ID for\n * determinism — the same run always gets the same variant.\n *\n * @example\n * ```ts\n * select: experiment.weighted({ gpt4: 50, claude: 50 })\n * ```\n *\n * @throws If all weights are zero (validated at creation time).\n */\n weighted(weights: Record<string, number>): ExperimentSelectFn {\n validateWeights(weights);\n\n // Snapshot so that later mutations to the caller's object can't silently\n // change runtime behaviour after validation has passed.\n const frozen = { ...weights };\n\n return createSelectFn(\n () => {\n const runId =\n getAsyncCtxSync()?.execution?.ctx.runId ?? crypto.randomUUID();\n return selectByWeight(hashToFloat(runId), frozen);\n },\n { strategy: \"weighted\", weights: frozen },\n );\n },\n\n /**\n * Consistent hashing — the same value always maps to the same variant.\n *\n * When `value` is `null` or `undefined`, an empty string is hashed instead.\n *\n * @example\n * ```ts\n * select: experiment.bucket(userId)\n * select: experiment.bucket(userId, { weights: { a: 70, b: 30 } })\n * ```\n */\n bucket(\n value: unknown,\n options?: { weights?: Record<string, number> },\n ): ExperimentSelectFn {\n if (options?.weights) {\n validateWeights(options.weights);\n }\n\n const str = value == null ? \"\" : String(value);\n\n return createSelectFn(\n (variantNames?: string[]) => {\n const weights =\n options?.weights ??\n (variantNames ? equalWeights(variantNames) : undefined);\n\n if (!weights) {\n throw new Error(\n \"experiment.bucket() requires either explicit weights or variant \" +\n \"names from group.experiment()\",\n );\n }\n\n return selectByWeight(hashToFloat(str), weights);\n },\n {\n strategy: \"bucket\",\n weights: options?.weights,\n ...(value == null && { nullishBucket: true }),\n },\n );\n },\n\n /**\n * User-provided selection function. The function is called inside the\n * memoized step, so it only runs once per run.\n *\n * @example\n * ```ts\n * select: experiment.custom(async () => {\n * const flag = await getFeatureFlag(\"checkout-variant\");\n * return flag;\n * })\n * ```\n */\n custom(fn: () => Promise<string> | string): ExperimentSelectFn {\n return createSelectFn(fn, { strategy: \"custom\" });\n },\n};\n"],"mappings":";;;;AAIA,MAAM,EAAE,WAAW;;;;AAKnB,MAAM,eAAe,QAAwB;CAC3C,MAAM,MAAM,QAAQ,CAAC,OAAO,IAAI,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE;AAC1D,QAAO,OAAO,SAAS,KAAK,GAAG,GAAG;;;;;;AAOpC,MAAM,kBACJ,QACA,YACW;CACX,MAAM,UAAU,OAAO,QAAQ,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,OAClD,EAAE,cAAc,EAAE,CACnB;CACD,MAAM,QAAQ,QAAQ,QAAQ,KAAK,GAAG,OAAO,MAAM,GAAG,EAAE;CAExD,IAAI,SAAS;AACb,MAAK,MAAM,CAAC,MAAM,WAAW,SAAS;AACpC,YAAU,SAAS;AACnB,MAAI,SAAS,OACX,QAAO;;AAKX,QAAO,QAAQ,QAAQ,SAAS,GAAI;;;;;AAMtC,MAAM,gBAAgB,iBAAmD;AACvE,QAAO,OAAO,YAAY,aAAa,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;;;;;AAMlE,MAAM,mBAAmB,YAA0C;AACjE,MAAK,MAAM,CAAC,MAAM,MAAM,OAAO,QAAQ,QAAQ,EAAE;AAC/C,MAAI,CAAC,OAAO,SAAS,EAAE,CACrB,OAAM,IAAI,MACR,sCAAsC,KAAK,4BAA4B,EAAE,wCAC1E;AAEH,MAAI,IAAI,EACN,OAAM,IAAI,MACR,sCAAsC,KAAK,iBAAiB,EAAE,yBAC/D;;AAKL,KADc,OAAO,OAAO,QAAQ,CAAC,QAAQ,KAAK,MAAM,MAAM,GAAG,EAAE,IACtD,EACX,OAAM,IAAI,MACR,oFACD;;;;;;AAQL,MAAM,kBACJ,IACA,WACuB;AACvB,QAAO,OAAO,OAAO,IAAI,EAAE,oBAAoB,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwB1D,MAAa,aAAa;CASxB,MAAM,aAAyC;AAC7C,SAAO,qBAAqB,aAAa,EAAE,UAAU,SAAS,CAAC;;CAcjE,SAAS,SAAqD;AAC5D,kBAAgB,QAAQ;EAIxB,MAAM,SAAS,EAAE,GAAG,SAAS;AAE7B,SAAO,qBACC;AAGJ,UAAO,eAAe,YADpB,iBAAiB,EAAE,WAAW,IAAI,SAAS,OAAO,YAAY,CACxB,EAAE,OAAO;KAEnD;GAAE,UAAU;GAAY,SAAS;GAAQ,CAC1C;;CAcH,OACE,OACA,SACoB;AACpB,MAAI,SAAS,QACX,iBAAgB,QAAQ,QAAQ;EAGlC,MAAM,MAAM,SAAS,OAAO,KAAK,OAAO,MAAM;AAE9C,SAAO,gBACJ,iBAA4B;GAC3B,MAAM,UACJ,SAAS,YACR,eAAe,aAAa,aAAa,GAAG;AAE/C,OAAI,CAAC,QACH,OAAM,IAAI,MACR,gGAED;AAGH,UAAO,eAAe,YAAY,IAAI,EAAE,QAAQ;KAElD;GACE,UAAU;GACV,SAAS,SAAS;GAClB,GAAI,SAAS,QAAQ,EAAE,eAAe,MAAM;GAC7C,CACF;;CAeH,OAAO,IAAwD;AAC7D,SAAO,eAAe,IAAI,EAAE,UAAU,UAAU,CAAC;;CAEpD"}
|
package/components/Fetch.cjs
CHANGED
|
@@ -6,23 +6,23 @@ debug = require_rolldown_runtime.__toESM(debug);
|
|
|
6
6
|
|
|
7
7
|
//#region src/components/Fetch.ts
|
|
8
8
|
const globalFetch = globalThis.fetch;
|
|
9
|
-
const
|
|
9
|
+
const devDebug = (0, debug.default)("inngest:fetch");
|
|
10
10
|
const createFetchShim = () => {
|
|
11
11
|
let stepFetch;
|
|
12
12
|
const fetch$1 = async (input, init) => {
|
|
13
13
|
const ctx = await require_als.getAsyncCtx();
|
|
14
14
|
if (!ctx?.execution) {
|
|
15
15
|
if (!stepFetch.fallback) throw new Error("step.fetch() called outside of a function and had no fallback set");
|
|
16
|
-
|
|
16
|
+
devDebug("step.fetch() called outside of a function; falling back to global fetch");
|
|
17
17
|
return stepFetch.fallback(input, init);
|
|
18
18
|
}
|
|
19
19
|
if (ctx.execution.executingStep) {
|
|
20
20
|
if (!stepFetch.fallback) throw new Error(`step.fetch() called inside step "${ctx.execution.executingStep.id}" and had no fallback set`);
|
|
21
|
-
|
|
21
|
+
devDebug(`step.fetch() called inside step "${ctx.execution.executingStep.id}"; falling back to global fetch`);
|
|
22
22
|
return stepFetch.fallback(input, init);
|
|
23
23
|
}
|
|
24
24
|
const targetUrl = new URL(input instanceof Request ? input.url : input.toString());
|
|
25
|
-
|
|
25
|
+
devDebug("step.fetch() shimming request to", targetUrl.hostname);
|
|
26
26
|
const jsonRes = await ctx.execution.ctx.step[require_InngestStepTools.gatewaySymbol](`step.fetch: ${targetUrl.hostname}`, input, init);
|
|
27
27
|
return new Response(jsonRes.body, {
|
|
28
28
|
headers: jsonRes.headers,
|
package/components/Fetch.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Fetch.cjs","names":["
|
|
1
|
+
{"version":3,"file":"Fetch.cjs","names":["stepFetch: StepFetch","fetch: Fetch","getAsyncCtx","gatewaySymbol","optionsRef: StepFetch.Options","extras: StepFetch.Extras","fetch"],"sources":["../../src/components/Fetch.ts"],"sourcesContent":["import Debug from \"debug\";\nimport type { Simplify } from \"../helpers/types.ts\";\nimport { getAsyncCtx } from \"./execution/als.ts\";\nimport { gatewaySymbol, type InternalStepTools } from \"./InngestStepTools.ts\";\n\nconst globalFetch = globalThis.fetch;\ntype Fetch = typeof globalFetch;\n\nexport type StepFetch = Fetch &\n Simplify<\n {\n config: (options: StepFetch.Options) => StepFetch;\n } & Readonly<StepFetch.Options>\n >;\n\nexport namespace StepFetch {\n export interface Options {\n fallback?: Fetch | undefined;\n }\n\n export interface Extras extends Options {\n config: (options: Options) => StepFetch;\n }\n}\n\nconst devDebug = Debug(\"inngest:fetch\");\n\nconst createFetchShim = (): StepFetch => {\n // biome-ignore lint/style/useConst: need this to allow fns to be defined\n let stepFetch: StepFetch;\n\n const fetch: Fetch = async (input, init) => {\n const ctx = await getAsyncCtx();\n if (!ctx?.execution) {\n // Not in a function run\n if (!stepFetch.fallback) {\n // TODO Tell the user how to solve\n throw new Error(\n \"step.fetch() called outside of a function and had no fallback set\",\n );\n }\n\n devDebug(\n \"step.fetch() called outside of a function; falling back to global fetch\",\n );\n\n return stepFetch.fallback(input, init);\n }\n\n // In a function run\n if (ctx.execution.executingStep) {\n // Inside a step\n if (!stepFetch.fallback) {\n // TODO Tell the user how to solve\n throw new Error(\n `step.fetch() called inside step \"${ctx.execution.executingStep.id}\" and had no fallback set`,\n );\n }\n\n devDebug(\n `step.fetch() called inside step \"${ctx.execution.executingStep.id}\"; falling back to global fetch`,\n );\n\n return stepFetch.fallback(input, init);\n }\n\n // TODO Do we need to make this better with deferred (global) step tooling?\n // hmmmmm\n\n const targetUrl = new URL(\n input instanceof Request ? input.url : input.toString(),\n );\n\n devDebug(\"step.fetch() shimming request to\", targetUrl.hostname);\n\n // Purposefully do not try/cacth this; if it throws then we treat that as a\n // regular `fetch()` throw, which also would not return a `Response`.\n const jsonRes = await (ctx.execution.ctx.step as InternalStepTools)[\n gatewaySymbol\n ](`step.fetch: ${targetUrl.hostname}`, input, init);\n\n return new Response(jsonRes.body, {\n headers: jsonRes.headers,\n status: jsonRes.status_code,\n });\n };\n\n const optionsRef: StepFetch.Options = {\n fallback: globalFetch,\n };\n\n const extras: StepFetch.Extras = {\n config: (options) => {\n Object.assign(optionsRef, options);\n Object.assign(stepFetch, optionsRef);\n\n return stepFetch;\n },\n ...optionsRef,\n };\n\n stepFetch = Object.assign(fetch, extras);\n\n return stepFetch;\n};\n\n/**\n * `fetch` is a Fetch API-compatible function that can be used to make any HTTP\n * code durable if it's called within an Inngest function.\n *\n * It will gracefully fall back to the global `fetch` if called outside of this\n * context, and a custom fallback can be set using the `config` method.\n *\n * @example Basic usage\n * ```ts\n * import { fetch } from \"inngest\";\n *\n * const api = new MyProductApi({ fetch });\n * ```\n *\n * @example Setting a custom fallback\n * ```ts\n * import { fetch } from \"inngest\";\n *\n * const api = new MyProductApi({\n * fetch: fetch.config({ fallback: myCustomFetch }),\n * });\n * ```\n *\n * @example Do not allow fallback\n * ```ts\n * import { fetch } from \"inngest\";\n *\n * const api = new MyProductApi({\n * fetch: fetch.config({ fallback: undefined }),\n * });\n * ```\n */\nexport const fetch = createFetchShim();\n"],"mappings":";;;;;;;AAKA,MAAM,cAAc,WAAW;AAoB/B,MAAM,8BAAiB,gBAAgB;AAEvC,MAAM,wBAAmC;CAEvC,IAAIA;CAEJ,MAAMC,UAAe,OAAO,OAAO,SAAS;EAC1C,MAAM,MAAM,MAAMC,yBAAa;AAC/B,MAAI,CAAC,KAAK,WAAW;AAEnB,OAAI,CAAC,UAAU,SAEb,OAAM,IAAI,MACR,oEACD;AAGH,YACE,0EACD;AAED,UAAO,UAAU,SAAS,OAAO,KAAK;;AAIxC,MAAI,IAAI,UAAU,eAAe;AAE/B,OAAI,CAAC,UAAU,SAEb,OAAM,IAAI,MACR,oCAAoC,IAAI,UAAU,cAAc,GAAG,2BACpE;AAGH,YACE,oCAAoC,IAAI,UAAU,cAAc,GAAG,iCACpE;AAED,UAAO,UAAU,SAAS,OAAO,KAAK;;EAMxC,MAAM,YAAY,IAAI,IACpB,iBAAiB,UAAU,MAAM,MAAM,MAAM,UAAU,CACxD;AAED,WAAS,oCAAoC,UAAU,SAAS;EAIhE,MAAM,UAAU,MAAO,IAAI,UAAU,IAAI,KACvCC,wCACA,eAAe,UAAU,YAAY,OAAO,KAAK;AAEnD,SAAO,IAAI,SAAS,QAAQ,MAAM;GAChC,SAAS,QAAQ;GACjB,QAAQ,QAAQ;GACjB,CAAC;;CAGJ,MAAMC,aAAgC,EACpC,UAAU,aACX;CAED,MAAMC,SAA2B;EAC/B,SAAS,YAAY;AACnB,UAAO,OAAO,YAAY,QAAQ;AAClC,UAAO,OAAO,WAAW,WAAW;AAEpC,UAAO;;EAET,GAAG;EACJ;AAED,aAAY,OAAO,OAAOC,SAAO,OAAO;AAExC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCT,MAAa,QAAQ,iBAAiB"}
|
package/components/Fetch.js
CHANGED
|
@@ -4,23 +4,23 @@ import Debug from "debug";
|
|
|
4
4
|
|
|
5
5
|
//#region src/components/Fetch.ts
|
|
6
6
|
const globalFetch = globalThis.fetch;
|
|
7
|
-
const
|
|
7
|
+
const devDebug = Debug("inngest:fetch");
|
|
8
8
|
const createFetchShim = () => {
|
|
9
9
|
let stepFetch;
|
|
10
10
|
const fetch$1 = async (input, init) => {
|
|
11
11
|
const ctx = await getAsyncCtx();
|
|
12
12
|
if (!ctx?.execution) {
|
|
13
13
|
if (!stepFetch.fallback) throw new Error("step.fetch() called outside of a function and had no fallback set");
|
|
14
|
-
|
|
14
|
+
devDebug("step.fetch() called outside of a function; falling back to global fetch");
|
|
15
15
|
return stepFetch.fallback(input, init);
|
|
16
16
|
}
|
|
17
17
|
if (ctx.execution.executingStep) {
|
|
18
18
|
if (!stepFetch.fallback) throw new Error(`step.fetch() called inside step "${ctx.execution.executingStep.id}" and had no fallback set`);
|
|
19
|
-
|
|
19
|
+
devDebug(`step.fetch() called inside step "${ctx.execution.executingStep.id}"; falling back to global fetch`);
|
|
20
20
|
return stepFetch.fallback(input, init);
|
|
21
21
|
}
|
|
22
22
|
const targetUrl = new URL(input instanceof Request ? input.url : input.toString());
|
|
23
|
-
|
|
23
|
+
devDebug("step.fetch() shimming request to", targetUrl.hostname);
|
|
24
24
|
const jsonRes = await ctx.execution.ctx.step[gatewaySymbol](`step.fetch: ${targetUrl.hostname}`, input, init);
|
|
25
25
|
return new Response(jsonRes.body, {
|
|
26
26
|
headers: jsonRes.headers,
|
package/components/Fetch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Fetch.js","names":["stepFetch: StepFetch","fetch: Fetch","optionsRef: StepFetch.Options","extras: StepFetch.Extras","fetch"],"sources":["../../src/components/Fetch.ts"],"sourcesContent":["import Debug from \"debug\";\nimport type { Simplify } from \"../helpers/types.ts\";\nimport { getAsyncCtx } from \"./execution/als.ts\";\nimport { gatewaySymbol, type InternalStepTools } from \"./InngestStepTools.ts\";\n\nconst globalFetch = globalThis.fetch;\ntype Fetch = typeof globalFetch;\n\nexport type StepFetch = Fetch &\n Simplify<\n {\n config: (options: StepFetch.Options) => StepFetch;\n } & Readonly<StepFetch.Options>\n >;\n\nexport namespace StepFetch {\n export interface Options {\n fallback?: Fetch | undefined;\n }\n\n export interface Extras extends Options {\n config: (options: Options) => StepFetch;\n }\n}\n\nconst
|
|
1
|
+
{"version":3,"file":"Fetch.js","names":["stepFetch: StepFetch","fetch: Fetch","optionsRef: StepFetch.Options","extras: StepFetch.Extras","fetch"],"sources":["../../src/components/Fetch.ts"],"sourcesContent":["import Debug from \"debug\";\nimport type { Simplify } from \"../helpers/types.ts\";\nimport { getAsyncCtx } from \"./execution/als.ts\";\nimport { gatewaySymbol, type InternalStepTools } from \"./InngestStepTools.ts\";\n\nconst globalFetch = globalThis.fetch;\ntype Fetch = typeof globalFetch;\n\nexport type StepFetch = Fetch &\n Simplify<\n {\n config: (options: StepFetch.Options) => StepFetch;\n } & Readonly<StepFetch.Options>\n >;\n\nexport namespace StepFetch {\n export interface Options {\n fallback?: Fetch | undefined;\n }\n\n export interface Extras extends Options {\n config: (options: Options) => StepFetch;\n }\n}\n\nconst devDebug = Debug(\"inngest:fetch\");\n\nconst createFetchShim = (): StepFetch => {\n // biome-ignore lint/style/useConst: need this to allow fns to be defined\n let stepFetch: StepFetch;\n\n const fetch: Fetch = async (input, init) => {\n const ctx = await getAsyncCtx();\n if (!ctx?.execution) {\n // Not in a function run\n if (!stepFetch.fallback) {\n // TODO Tell the user how to solve\n throw new Error(\n \"step.fetch() called outside of a function and had no fallback set\",\n );\n }\n\n devDebug(\n \"step.fetch() called outside of a function; falling back to global fetch\",\n );\n\n return stepFetch.fallback(input, init);\n }\n\n // In a function run\n if (ctx.execution.executingStep) {\n // Inside a step\n if (!stepFetch.fallback) {\n // TODO Tell the user how to solve\n throw new Error(\n `step.fetch() called inside step \"${ctx.execution.executingStep.id}\" and had no fallback set`,\n );\n }\n\n devDebug(\n `step.fetch() called inside step \"${ctx.execution.executingStep.id}\"; falling back to global fetch`,\n );\n\n return stepFetch.fallback(input, init);\n }\n\n // TODO Do we need to make this better with deferred (global) step tooling?\n // hmmmmm\n\n const targetUrl = new URL(\n input instanceof Request ? input.url : input.toString(),\n );\n\n devDebug(\"step.fetch() shimming request to\", targetUrl.hostname);\n\n // Purposefully do not try/cacth this; if it throws then we treat that as a\n // regular `fetch()` throw, which also would not return a `Response`.\n const jsonRes = await (ctx.execution.ctx.step as InternalStepTools)[\n gatewaySymbol\n ](`step.fetch: ${targetUrl.hostname}`, input, init);\n\n return new Response(jsonRes.body, {\n headers: jsonRes.headers,\n status: jsonRes.status_code,\n });\n };\n\n const optionsRef: StepFetch.Options = {\n fallback: globalFetch,\n };\n\n const extras: StepFetch.Extras = {\n config: (options) => {\n Object.assign(optionsRef, options);\n Object.assign(stepFetch, optionsRef);\n\n return stepFetch;\n },\n ...optionsRef,\n };\n\n stepFetch = Object.assign(fetch, extras);\n\n return stepFetch;\n};\n\n/**\n * `fetch` is a Fetch API-compatible function that can be used to make any HTTP\n * code durable if it's called within an Inngest function.\n *\n * It will gracefully fall back to the global `fetch` if called outside of this\n * context, and a custom fallback can be set using the `config` method.\n *\n * @example Basic usage\n * ```ts\n * import { fetch } from \"inngest\";\n *\n * const api = new MyProductApi({ fetch });\n * ```\n *\n * @example Setting a custom fallback\n * ```ts\n * import { fetch } from \"inngest\";\n *\n * const api = new MyProductApi({\n * fetch: fetch.config({ fallback: myCustomFetch }),\n * });\n * ```\n *\n * @example Do not allow fallback\n * ```ts\n * import { fetch } from \"inngest\";\n *\n * const api = new MyProductApi({\n * fetch: fetch.config({ fallback: undefined }),\n * });\n * ```\n */\nexport const fetch = createFetchShim();\n"],"mappings":";;;;;AAKA,MAAM,cAAc,WAAW;AAoB/B,MAAM,WAAW,MAAM,gBAAgB;AAEvC,MAAM,wBAAmC;CAEvC,IAAIA;CAEJ,MAAMC,UAAe,OAAO,OAAO,SAAS;EAC1C,MAAM,MAAM,MAAM,aAAa;AAC/B,MAAI,CAAC,KAAK,WAAW;AAEnB,OAAI,CAAC,UAAU,SAEb,OAAM,IAAI,MACR,oEACD;AAGH,YACE,0EACD;AAED,UAAO,UAAU,SAAS,OAAO,KAAK;;AAIxC,MAAI,IAAI,UAAU,eAAe;AAE/B,OAAI,CAAC,UAAU,SAEb,OAAM,IAAI,MACR,oCAAoC,IAAI,UAAU,cAAc,GAAG,2BACpE;AAGH,YACE,oCAAoC,IAAI,UAAU,cAAc,GAAG,iCACpE;AAED,UAAO,UAAU,SAAS,OAAO,KAAK;;EAMxC,MAAM,YAAY,IAAI,IACpB,iBAAiB,UAAU,MAAM,MAAM,MAAM,UAAU,CACxD;AAED,WAAS,oCAAoC,UAAU,SAAS;EAIhE,MAAM,UAAU,MAAO,IAAI,UAAU,IAAI,KACvC,eACA,eAAe,UAAU,YAAY,OAAO,KAAK;AAEnD,SAAO,IAAI,SAAS,QAAQ,MAAM;GAChC,SAAS,QAAQ;GACjB,QAAQ,QAAQ;GACjB,CAAC;;CAGJ,MAAMC,aAAgC,EACpC,UAAU,aACX;CAED,MAAMC,SAA2B;EAC/B,SAAS,YAAY;AACnB,UAAO,OAAO,YAAY,QAAQ;AAClC,UAAO,OAAO,WAAW,WAAW;AAEpC,UAAO;;EAET,GAAG;EACJ;AAED,aAAY,OAAO,OAAOC,SAAO,OAAO;AAExC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCT,MAAa,QAAQ,iBAAiB"}
|
|
@@ -626,6 +626,7 @@ var InngestCommHandler = class {
|
|
|
626
626
|
fn,
|
|
627
627
|
handler: innerHandler,
|
|
628
628
|
middleware: mwInstances,
|
|
629
|
+
requestArgs: args,
|
|
629
630
|
requestInfo,
|
|
630
631
|
runId
|
|
631
632
|
})();
|
|
@@ -1354,11 +1355,11 @@ var InngestCommHandler = class {
|
|
|
1354
1355
|
*/
|
|
1355
1356
|
checkModeConfiguration() {
|
|
1356
1357
|
this.client.setEnvVars(this.env);
|
|
1357
|
-
|
|
1358
|
-
this.client
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1358
|
+
return require_env.checkModeConfiguration({
|
|
1359
|
+
mode: this.client.mode,
|
|
1360
|
+
signingKey: this.client.signingKey,
|
|
1361
|
+
internalLogger: this.client[require_Inngest.internalLoggerSymbol]
|
|
1362
|
+
});
|
|
1362
1363
|
}
|
|
1363
1364
|
/**
|
|
1364
1365
|
* Validate the signature of a request and return the signing key used to
|