@sanity/runtime-cli 17.16.0-next.g8d59d6f → 17.16.0-next.gcb96a23
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/README.md +19 -19
- package/dist/cores/functions/logs.d.ts +7 -0
- package/dist/cores/functions/logs.js +17 -6
- package/dist/cores/functions/test.js +14 -3
- package/dist/utils/functions/child-runtime/durable.js +141 -23
- package/dist/utils/functions/child-runtime/getBatchResults.js +75 -0
- package/dist/utils/functions/child-runtime/step-history.d.ts +15 -0
- package/dist/utils/functions/child-runtime/step-history.js +52 -0
- package/dist/utils/functions/invoke/local.js +16 -6
- package/dist/utils/types.d.ts +4 -0
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ EXAMPLES
|
|
|
90
90
|
$ sanity-run blueprints add function --name my-function --fn-type document-create --fn-type document-update --lang js
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
_See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
93
|
+
_See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/add.ts)_
|
|
94
94
|
|
|
95
95
|
## `sanity-run blueprints config`
|
|
96
96
|
|
|
@@ -127,7 +127,7 @@ EXAMPLES
|
|
|
127
127
|
$ sanity-run blueprints config --edit --project-id <projectId> --stack <name-or-id>
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
_See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
130
|
+
_See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/config.ts)_
|
|
131
131
|
|
|
132
132
|
## `sanity-run blueprints deploy`
|
|
133
133
|
|
|
@@ -183,7 +183,7 @@ EXAMPLES
|
|
|
183
183
|
$ sanity-run blueprints deploy --new-stack-name <new-name>
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
_See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
186
|
+
_See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/deploy.ts)_
|
|
187
187
|
|
|
188
188
|
## `sanity-run blueprints destroy`
|
|
189
189
|
|
|
@@ -221,7 +221,7 @@ EXAMPLES
|
|
|
221
221
|
$ sanity-run blueprints destroy --stack <name-or-id> --project-id <projectId> --force --no-wait
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
_See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
224
|
+
_See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/destroy.ts)_
|
|
225
225
|
|
|
226
226
|
## `sanity-run blueprints doctor`
|
|
227
227
|
|
|
@@ -253,7 +253,7 @@ EXAMPLES
|
|
|
253
253
|
$ sanity-run blueprints doctor --fix
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
_See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
256
|
+
_See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/doctor.ts)_
|
|
257
257
|
|
|
258
258
|
## `sanity-run blueprints info`
|
|
259
259
|
|
|
@@ -291,7 +291,7 @@ EXAMPLES
|
|
|
291
291
|
$ sanity-run blueprints info --organization-id <orgId> --stack <name-or-id>
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
-
_See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
294
|
+
_See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/info.ts)_
|
|
295
295
|
|
|
296
296
|
## `sanity-run blueprints init [DIR]`
|
|
297
297
|
|
|
@@ -355,7 +355,7 @@ EXAMPLES
|
|
|
355
355
|
$ sanity-run blueprints init old-stack --type <json|js|ts> --project-id <projectId> --stack-id <existingStackId>
|
|
356
356
|
```
|
|
357
357
|
|
|
358
|
-
_See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
358
|
+
_See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/init.ts)_
|
|
359
359
|
|
|
360
360
|
## `sanity-run blueprints logs`
|
|
361
361
|
|
|
@@ -401,7 +401,7 @@ EXAMPLES
|
|
|
401
401
|
$ sanity-run blueprints logs --before 2026-05-01T00:00:00Z
|
|
402
402
|
```
|
|
403
403
|
|
|
404
|
-
_See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
404
|
+
_See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/logs.ts)_
|
|
405
405
|
|
|
406
406
|
## `sanity-run blueprints mint-deploy-token`
|
|
407
407
|
|
|
@@ -446,7 +446,7 @@ EXAMPLES
|
|
|
446
446
|
$ sanity-run blueprints mint-deploy-token --organization-id <orgId>
|
|
447
447
|
```
|
|
448
448
|
|
|
449
|
-
_See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
449
|
+
_See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/mint-deploy-token.ts)_
|
|
450
450
|
|
|
451
451
|
## `sanity-run blueprints plan`
|
|
452
452
|
|
|
@@ -479,7 +479,7 @@ EXAMPLES
|
|
|
479
479
|
$ sanity-run blueprints plan --organization-id <orgId> --stack <name-or-id>
|
|
480
480
|
```
|
|
481
481
|
|
|
482
|
-
_See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
482
|
+
_See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/plan.ts)_
|
|
483
483
|
|
|
484
484
|
## `sanity-run blueprints promote`
|
|
485
485
|
|
|
@@ -515,7 +515,7 @@ EXAMPLES
|
|
|
515
515
|
$ sanity-run blueprints promote --new-stack-name <new-name>
|
|
516
516
|
```
|
|
517
517
|
|
|
518
|
-
_See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
518
|
+
_See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/promote.ts)_
|
|
519
519
|
|
|
520
520
|
## `sanity-run blueprints stacks`
|
|
521
521
|
|
|
@@ -556,7 +556,7 @@ EXAMPLES
|
|
|
556
556
|
$ sanity-run blueprints stacks --organization-id <organizationId> --include-projects
|
|
557
557
|
```
|
|
558
558
|
|
|
559
|
-
_See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
559
|
+
_See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/blueprints/stacks.ts)_
|
|
560
560
|
|
|
561
561
|
## `sanity-run functions add`
|
|
562
562
|
|
|
@@ -608,7 +608,7 @@ EXAMPLES
|
|
|
608
608
|
$ sanity-run functions add --name my-function --type document-create --type document-update --lang js
|
|
609
609
|
```
|
|
610
610
|
|
|
611
|
-
_See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
611
|
+
_See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/functions/add.ts)_
|
|
612
612
|
|
|
613
613
|
## `sanity-run functions dev`
|
|
614
614
|
|
|
@@ -643,7 +643,7 @@ EXAMPLES
|
|
|
643
643
|
$ sanity-run functions dev --timeout 60
|
|
644
644
|
```
|
|
645
645
|
|
|
646
|
-
_See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
646
|
+
_See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/functions/dev.ts)_
|
|
647
647
|
|
|
648
648
|
## `sanity-run functions env add NAME KEY VALUE`
|
|
649
649
|
|
|
@@ -676,7 +676,7 @@ EXAMPLES
|
|
|
676
676
|
$ sanity-run functions env add --stack <name-or-id> MyFunction API_URL https://api.example.com/
|
|
677
677
|
```
|
|
678
678
|
|
|
679
|
-
_See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
679
|
+
_See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/functions/env/add.ts)_
|
|
680
680
|
|
|
681
681
|
## `sanity-run functions env list NAME`
|
|
682
682
|
|
|
@@ -706,7 +706,7 @@ EXAMPLES
|
|
|
706
706
|
$ sanity-run functions env list --stack <name-or-id> MyFunction
|
|
707
707
|
```
|
|
708
708
|
|
|
709
|
-
_See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
709
|
+
_See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/functions/env/list.ts)_
|
|
710
710
|
|
|
711
711
|
## `sanity-run functions env remove NAME KEY`
|
|
712
712
|
|
|
@@ -738,7 +738,7 @@ EXAMPLES
|
|
|
738
738
|
$ sanity-run functions env remove --stack <name-or-id> MyFunction API_URL
|
|
739
739
|
```
|
|
740
740
|
|
|
741
|
-
_See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
741
|
+
_See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/functions/env/remove.ts)_
|
|
742
742
|
|
|
743
743
|
## `sanity-run functions logs [NAME]`
|
|
744
744
|
|
|
@@ -778,7 +778,7 @@ EXAMPLES
|
|
|
778
778
|
$ sanity-run functions logs <name> --delete
|
|
779
779
|
```
|
|
780
780
|
|
|
781
|
-
_See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
781
|
+
_See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/functions/logs.ts)_
|
|
782
782
|
|
|
783
783
|
## `sanity-run functions test [NAME]`
|
|
784
784
|
|
|
@@ -836,7 +836,7 @@ EXAMPLES
|
|
|
836
836
|
$ sanity-run functions test <name> --event update --data-before '{ "title": "before" }' --data-after '{ "title": "after" }'
|
|
837
837
|
```
|
|
838
838
|
|
|
839
|
-
_See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.
|
|
839
|
+
_See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.gcb96a23/src/commands/functions/test.ts)_
|
|
840
840
|
|
|
841
841
|
## `sanity-run help [COMMAND]`
|
|
842
842
|
|
|
@@ -15,3 +15,10 @@ export interface FunctionLogsOptions extends DeployedBlueprintConfig {
|
|
|
15
15
|
helpText: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function functionLogsCore(options: FunctionLogsOptions): Promise<CoreResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Formats a date as `[YYYY-MM-DD, HH:mm:ss]` in either UTC or local time.
|
|
20
|
+
* The shape is identical in both cases so `--utc` only changes the timezone.
|
|
21
|
+
* @param date
|
|
22
|
+
* @param utc
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatTimestamp(date: Date, utc: boolean | undefined): [string, string];
|
|
@@ -45,7 +45,7 @@ async function deleteLogs({ name, externalId, auth, force, log, }) {
|
|
|
45
45
|
spinner.succeed('Logs deleted');
|
|
46
46
|
return { success: true };
|
|
47
47
|
}
|
|
48
|
-
async function streamLogs({ name, externalId, auth, log, }) {
|
|
48
|
+
async function streamLogs({ name, externalId, auth, utc, log, }) {
|
|
49
49
|
const spinner = log.ora(`Setting up streaming logs session for function ${niceId(name)}`).start();
|
|
50
50
|
try {
|
|
51
51
|
spinner.stop();
|
|
@@ -58,7 +58,7 @@ async function streamLogs({ name, externalId, auth, log, }) {
|
|
|
58
58
|
alreadyOpened = true;
|
|
59
59
|
};
|
|
60
60
|
const renderLog = (logEntry) => {
|
|
61
|
-
log(formatLog(logEntry,
|
|
61
|
+
log(formatLog(logEntry, utc));
|
|
62
62
|
};
|
|
63
63
|
streamLogsAction(externalId, auth, renderLog, onOpen, (error) => log.error(`${styleText('red', 'Error:')} ${error}`), log);
|
|
64
64
|
// Return a special key for streaming mode
|
|
@@ -121,10 +121,7 @@ async function getLogs({ name, externalId, auth, limit, json, utc, log, }) {
|
|
|
121
121
|
*/
|
|
122
122
|
function formatLog(entry, utc) {
|
|
123
123
|
const { time, level, message, operationName, attempt, executionArn } = entry;
|
|
124
|
-
const
|
|
125
|
-
const [dateString, timeString] = utc
|
|
126
|
-
? date.toISOString().slice(0, 19).split('T')
|
|
127
|
-
: [date.toLocaleDateString(), date.toLocaleTimeString()];
|
|
124
|
+
const [dateString, timeString] = formatTimestamp(new Date(time), utc);
|
|
128
125
|
const durableAnnotation = durableLogAnnotation({ operationName, attempt, executionArn });
|
|
129
126
|
return [
|
|
130
127
|
styleText('bold', dateString),
|
|
@@ -134,6 +131,20 @@ function formatLog(entry, utc) {
|
|
|
134
131
|
message,
|
|
135
132
|
].join(' ');
|
|
136
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Formats a date as `[YYYY-MM-DD, HH:mm:ss]` in either UTC or local time.
|
|
136
|
+
* The shape is identical in both cases so `--utc` only changes the timezone.
|
|
137
|
+
* @param date
|
|
138
|
+
* @param utc
|
|
139
|
+
*/
|
|
140
|
+
export function formatTimestamp(date, utc) {
|
|
141
|
+
if (utc)
|
|
142
|
+
return date.toISOString().slice(0, 19).split('T');
|
|
143
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
144
|
+
const dateString = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
145
|
+
const timeString = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
146
|
+
return [dateString, timeString];
|
|
147
|
+
}
|
|
137
148
|
/**
|
|
138
149
|
* Log annotation for durable functions.
|
|
139
150
|
* @param operationName
|
|
@@ -116,7 +116,9 @@ export async function functionTestCore(options) {
|
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
const logNestedRuns = () => {
|
|
119
|
-
|
|
119
|
+
// Remove buffered logs for durable runs
|
|
120
|
+
const runs = plan.isDurable ? nestedRuns.splice(0) : [...nestedRuns];
|
|
121
|
+
for (const run of runs.sort((a, b) => a.order - b.order)) {
|
|
120
122
|
// Skip if is Durable so log ordering is retained
|
|
121
123
|
if (run.isLiveDurable)
|
|
122
124
|
continue;
|
|
@@ -155,10 +157,19 @@ export async function functionTestCore(options) {
|
|
|
155
157
|
if (event.kind === 'delegate') {
|
|
156
158
|
logNestedRuns();
|
|
157
159
|
}
|
|
158
|
-
const
|
|
160
|
+
const willRetry = event.phase === 'error' && typeof event.retryDelayMs === 'number';
|
|
161
|
+
const glyph = willRetry ? '⟳' : (STEP_PHASE_GLYPH[event.phase] ?? '•');
|
|
159
162
|
const duration = typeof event.durationMs === 'number' ? ` (${Math.round(event.durationMs)}ms)` : '';
|
|
163
|
+
const retriesPerformed = (event.attempt ?? 1) - 1;
|
|
164
|
+
let retryNote = '';
|
|
165
|
+
if (willRetry) {
|
|
166
|
+
retryNote = ` retry ${event.attempt} of ${event.maxAttempts}`;
|
|
167
|
+
}
|
|
168
|
+
else if (event.phase === 'error' && retriesPerformed > 0) {
|
|
169
|
+
retryNote = ` failed after ${retriesPerformed} retries`;
|
|
170
|
+
}
|
|
160
171
|
const detail = event.phase === 'error' && event.error ? ` ${event.error.message}` : '';
|
|
161
|
-
lines.push(` ${glyph} ${event.name} [${event.kind}]${duration}${detail}`);
|
|
172
|
+
lines.push(` ${glyph} ${event.name} [${event.kind}]${duration}${detail}${retryNote}`);
|
|
162
173
|
}
|
|
163
174
|
return lines;
|
|
164
175
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {performance} from 'node:perf_hooks'
|
|
2
2
|
import {inspect} from 'node:util'
|
|
3
|
+
import {getBatchResults} from './getBatchResults.js'
|
|
4
|
+
import {createStepHistory} from './step-history.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
*
|
|
@@ -23,10 +25,13 @@ const formatArgs = (args) =>
|
|
|
23
25
|
* @param handler - callback being run
|
|
24
26
|
* @param context - data to send to the operation
|
|
25
27
|
* @param event - the step being run
|
|
28
|
+
* @param [branchName] - the name of the branch being run
|
|
26
29
|
* @param io
|
|
27
30
|
*/
|
|
28
|
-
export async function runDurable(handler, {context, event}, io) {
|
|
31
|
+
export async function runDurable(handler, {context, event, branchName}, io) {
|
|
32
|
+
const history = createStepHistory()
|
|
29
33
|
let currentStep = null
|
|
34
|
+
|
|
30
35
|
const streamLog = (level, args) => {
|
|
31
36
|
const date = new Date()
|
|
32
37
|
const timestamp = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
|
|
@@ -41,7 +46,6 @@ export async function runDurable(handler, {context, event}, io) {
|
|
|
41
46
|
})
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
// @todo: need to throw error for use of `console.log` in a durable function
|
|
45
49
|
const logger = {
|
|
46
50
|
log: (...a) => streamLog('info', a),
|
|
47
51
|
info: (...a) => streamLog('info', a),
|
|
@@ -49,24 +53,57 @@ export async function runDurable(handler, {context, event}, io) {
|
|
|
49
53
|
error: (...a) => streamLog('error', a),
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
const emitFailure = (pending) => {
|
|
57
|
+
history.settle(pending)
|
|
58
|
+
io.emit(pending.event)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const settleUnretried = () => {
|
|
62
|
+
const pending = history.getPending()
|
|
63
|
+
if (pending !== null) {
|
|
64
|
+
emitFailure(pending)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const track = async (name, kind, produce, retry) => {
|
|
69
|
+
settleUnretried()
|
|
70
|
+
const {index, saved} = history.nextStep()
|
|
71
|
+
|
|
72
|
+
if (saved?.phase === 'complete') {
|
|
73
|
+
return saved.output
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const maxAttempts = retry?.maxAttempts ?? 1
|
|
77
|
+
const retries = Math.max((retry?.maxAttempts ?? 1) - 1, 0)
|
|
78
|
+
const waitingOnRetry = typeof saved?.retryDelayMs === 'number'
|
|
79
|
+
|
|
80
|
+
if (saved?.phase === 'error' && !waitingOnRetry) {
|
|
81
|
+
throw saved.output
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const attempt = (saved?.attempt ?? 0) + 1
|
|
55
85
|
const prev = currentStep
|
|
56
86
|
currentStep = name
|
|
57
87
|
|
|
88
|
+
io.emit({name, kind, phase: 'start'})
|
|
89
|
+
const startedAt = performance.now()
|
|
90
|
+
|
|
58
91
|
try {
|
|
59
|
-
const value = await produce()
|
|
60
|
-
|
|
92
|
+
const value = await produce(attempt)
|
|
93
|
+
const completeEvent = {
|
|
61
94
|
name,
|
|
62
95
|
kind,
|
|
63
96
|
phase: 'complete',
|
|
64
97
|
output: value,
|
|
65
98
|
durationMs: performance.now() - startedAt,
|
|
66
|
-
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
history.recordSuccess(index, completeEvent)
|
|
102
|
+
io.emit(completeEvent)
|
|
103
|
+
|
|
67
104
|
return value
|
|
68
105
|
} catch (err) {
|
|
69
|
-
|
|
106
|
+
const errorEvent = {
|
|
70
107
|
name,
|
|
71
108
|
kind,
|
|
72
109
|
phase: 'error',
|
|
@@ -76,20 +113,81 @@ export async function runDurable(handler, {context, event}, io) {
|
|
|
76
113
|
stack: err?.stack ?? '',
|
|
77
114
|
},
|
|
78
115
|
durationMs: performance.now() - startedAt,
|
|
116
|
+
attempt,
|
|
117
|
+
retries,
|
|
118
|
+
maxAttempts,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const retryPending = history.recordFailure(index, {
|
|
122
|
+
event: errorEvent,
|
|
123
|
+
error: err,
|
|
124
|
+
delayMs: retry?.delay ? durationToMs(retry.delay) : 0,
|
|
79
125
|
})
|
|
126
|
+
|
|
127
|
+
if (!retryPending) {
|
|
128
|
+
io.emit(errorEvent)
|
|
129
|
+
}
|
|
130
|
+
|
|
80
131
|
throw err
|
|
81
132
|
} finally {
|
|
82
133
|
currentStep = prev
|
|
83
134
|
}
|
|
84
135
|
}
|
|
85
136
|
|
|
137
|
+
const FANOUT_STATUS = {
|
|
138
|
+
STARTED: 'STARTED',
|
|
139
|
+
ALL_COMPLETED: 'ALL_COMPLETED',
|
|
140
|
+
FAILURE_TOLERANCE_EXCEEDED: 'FAILURE_TOLERANCE_EXCEEDED',
|
|
141
|
+
SUCCEEDED: 'SUCCEEDED',
|
|
142
|
+
FAILED: 'FAILED',
|
|
143
|
+
}
|
|
144
|
+
|
|
86
145
|
const step = {
|
|
87
|
-
run: ({name, handler: fn
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
146
|
+
run: ({name, handler: fn, retry}) =>
|
|
147
|
+
track(name || '', 'run', (attempt) => fn({logger, attempt}), retry),
|
|
148
|
+
delegate: ({name, handler, event, retry}) =>
|
|
149
|
+
track(
|
|
150
|
+
name || '',
|
|
151
|
+
'delegate',
|
|
152
|
+
() => {
|
|
153
|
+
const fn = typeof handler === 'string' ? handler : handler?.name
|
|
154
|
+
if (!fn) {
|
|
155
|
+
throw new Error('No function name provided for delegation')
|
|
156
|
+
}
|
|
157
|
+
return io.invoke(fn, event?.data ?? {})
|
|
158
|
+
},
|
|
159
|
+
retry,
|
|
160
|
+
),
|
|
161
|
+
fanout: async ({name, branches}) => {
|
|
162
|
+
if (
|
|
163
|
+
!Array.isArray(branches) ||
|
|
164
|
+
Array.from(branches).some((branch) => typeof branch !== 'function')
|
|
165
|
+
) {
|
|
166
|
+
throw new TypeError('fanout branches must be an array of functions')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const outcomes = branches.map((_, index) => ({index, status: FANOUT_STATUS.STARTED}))
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const execs = branches.map(async (branch, index) => {
|
|
172
|
+
try {
|
|
173
|
+
const branchName = `${name || 'fanout'} / branch ${index}`
|
|
174
|
+
const result = await runDurable(branch, {context, event, branchName}, io)
|
|
175
|
+
outcomes[index] = {index, status: FANOUT_STATUS.SUCCEEDED, result}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
178
|
+
outcomes[index] = {index, status: FANOUT_STATUS.FAILED, error}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
Promise.allSettled(execs)
|
|
183
|
+
.then(() => {
|
|
184
|
+
resolve(getBatchResults(outcomes, FANOUT_STATUS.ALL_COMPLETED))
|
|
185
|
+
})
|
|
186
|
+
.catch(() => {
|
|
187
|
+
reject(reject)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
},
|
|
93
191
|
waitForCondition: ({name, initial, poller, next}) =>
|
|
94
192
|
track(name || '', 'wait-for-condition', async () => {
|
|
95
193
|
let state = initial
|
|
@@ -107,15 +205,35 @@ export async function runDurable(handler, {context, event}, io) {
|
|
|
107
205
|
}
|
|
108
206
|
return state
|
|
109
207
|
}),
|
|
110
|
-
|
|
111
|
-
track(name || '', '
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
throw new Error('No function name provided for delegation')
|
|
115
|
-
}
|
|
116
|
-
return io.invoke(fn, event)
|
|
208
|
+
wait: ({name, duration}) =>
|
|
209
|
+
track(name || '', 'wait', async () => {
|
|
210
|
+
await io.wait(durationToMs(duration))
|
|
211
|
+
return undefined
|
|
117
212
|
}),
|
|
118
213
|
}
|
|
119
214
|
|
|
120
|
-
|
|
215
|
+
let result
|
|
216
|
+
let done = false
|
|
217
|
+
const handlerArgs = branchName !== undefined ? {step, logger} : {context, event, step, logger}
|
|
218
|
+
while (!done) {
|
|
219
|
+
history.startRun()
|
|
220
|
+
try {
|
|
221
|
+
result = await handler(handlerArgs)
|
|
222
|
+
settleUnretried()
|
|
223
|
+
done = true
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const pending = history.getPending()
|
|
226
|
+
if (pending === null) {
|
|
227
|
+
throw err
|
|
228
|
+
}
|
|
229
|
+
if (err?.name === 'UnrecoverableError') {
|
|
230
|
+
emitFailure(pending)
|
|
231
|
+
throw err
|
|
232
|
+
}
|
|
233
|
+
io.emit({...pending.event, retryDelayMs: pending.delayMs})
|
|
234
|
+
await io.wait(pending.delayMs)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return result
|
|
121
239
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps to runtime APIs from @sanity/functions
|
|
3
|
+
* @param outcomes - array of outcomes from the batch execution, each outcome should have a status and result or error
|
|
4
|
+
* @param reason - a string indicating the reason for the batch result, e.g., 'ALL_COMPLETED' or 'FAILED'
|
|
5
|
+
*/
|
|
6
|
+
export const getBatchResults = (outcomes, reason) => {
|
|
7
|
+
/**
|
|
8
|
+
* Returns an array of all outcomes, regardless of their status.
|
|
9
|
+
*/
|
|
10
|
+
const all = outcomes.map((outcome) => ({...outcome}))
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns an array of outcomes that have failed (status 'FAILED').
|
|
14
|
+
*/
|
|
15
|
+
const failed = () =>
|
|
16
|
+
all.filter((outcome) => outcome.error !== undefined && outcome.status === 'FAILED')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns an array of outcomes that have succeeded (status 'SUCCEEDED').
|
|
20
|
+
*/
|
|
21
|
+
const succeeded = () =>
|
|
22
|
+
all.filter((outcome) => outcome.result !== undefined && outcome.status === 'SUCCEEDED')
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns an array of outcomes that are still in progress (status 'STARTED').
|
|
26
|
+
*/
|
|
27
|
+
const started = () => all.filter((outcome) => outcome.status === 'STARTED')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns the count of successful outcomes (status 'SUCCEEDED')
|
|
31
|
+
*/
|
|
32
|
+
const successCount = all.filter((item) => item.status === 'SUCCEEDED').length
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the count of failed outcomes (status 'FAILED')
|
|
36
|
+
*/
|
|
37
|
+
const failureCount = all.filter((item) => item.status === 'FAILED').length
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns an array of results from successful outcomes (status 'SUCCEEDED').
|
|
41
|
+
*/
|
|
42
|
+
const getResults = () => succeeded().map((item) => item.result)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns an array of errors from failed outcomes (status 'FAILED').
|
|
46
|
+
*/
|
|
47
|
+
const getErrors = () => failed().map((item) => item.error)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Throws the first error found in the failed outcomes (status 'FAILED').
|
|
51
|
+
*/
|
|
52
|
+
const throwIfError = () => {
|
|
53
|
+
const failure = all.find((outcome) => outcome.status === 'FAILED')
|
|
54
|
+
if (failure?.error) {
|
|
55
|
+
throw failure.error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
all,
|
|
61
|
+
status: failureCount > 0 ? 'FAILED' : 'SUCCEEDED',
|
|
62
|
+
hasFailure: failureCount > 0,
|
|
63
|
+
failed,
|
|
64
|
+
succeeded,
|
|
65
|
+
started,
|
|
66
|
+
reason,
|
|
67
|
+
successCount,
|
|
68
|
+
failureCount,
|
|
69
|
+
startedCount: started().length,
|
|
70
|
+
totalCount: all.length,
|
|
71
|
+
getResults,
|
|
72
|
+
getErrors,
|
|
73
|
+
throwIfError,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type {DurableStepEvent} from './types'
|
|
2
|
+
|
|
3
|
+
type PendingRetry = {index: number; event: DurableStepEvent; delayMs: number}
|
|
4
|
+
|
|
5
|
+
export function createStepHistory(): {
|
|
6
|
+
startRun: () => void
|
|
7
|
+
nextStep: () => {index: number; saved: DurableStepEvent | undefined}
|
|
8
|
+
recordSuccess: (index: number, event: DurableStepEvent) => void
|
|
9
|
+
recordFailure: (
|
|
10
|
+
index: number,
|
|
11
|
+
failure: {event: DurableStepEvent; error: unknown; delayMs: number},
|
|
12
|
+
) => boolean
|
|
13
|
+
getPending: () => PendingRetry | null
|
|
14
|
+
settle: (pending: PendingRetry) => void
|
|
15
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const createStepHistory = () => {
|
|
2
|
+
const history = new Map([])
|
|
3
|
+
let cursor = 0
|
|
4
|
+
let pending = null
|
|
5
|
+
|
|
6
|
+
const startRun = () => {
|
|
7
|
+
cursor = 0
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const nextStep = () => {
|
|
11
|
+
const index = cursor
|
|
12
|
+
cursor++
|
|
13
|
+
return {index, saved: history.get(index)}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const recordSuccess = (index, event) => {
|
|
17
|
+
history.set(index, event)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const recordFailure = (index, {event, error, delayMs}) => {
|
|
21
|
+
const retryable = event.attempt <= event.retries && error?.name !== 'UnrecoverableError'
|
|
22
|
+
|
|
23
|
+
if (retryable) {
|
|
24
|
+
history.set(index, {...event, output: error, retryDelayMs: delayMs})
|
|
25
|
+
pending = {index, event, delayMs}
|
|
26
|
+
} else {
|
|
27
|
+
history.set(index, {...event, output: error})
|
|
28
|
+
}
|
|
29
|
+
return retryable
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const getPending = () => {
|
|
33
|
+
const item = pending
|
|
34
|
+
pending = null
|
|
35
|
+
return item
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const settle = ({index, event}) => {
|
|
39
|
+
history.set(index, {...event, output: history.get(index).output})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
startRun,
|
|
44
|
+
nextStep,
|
|
45
|
+
recordSuccess,
|
|
46
|
+
recordFailure,
|
|
47
|
+
getPending,
|
|
48
|
+
settle,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {createStepHistory}
|
|
@@ -174,12 +174,22 @@ export default async function invoke(resource, payload, context, options) {
|
|
|
174
174
|
// of `nested.error`. The error isn't lost: it travels back in the returned
|
|
175
175
|
// result and is logged/streamed to the UI by the caller (app/test core).
|
|
176
176
|
if (child && !child.killed) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
// step.delegate errors need to be surfaced to the caller for retry behavior and for the durable step to be marked as failed.
|
|
178
|
+
const origin = metadata?.origin || undefined;
|
|
179
|
+
const error = nested.error || undefined;
|
|
180
|
+
const response = origin === 'step.delegate' && error
|
|
181
|
+
? {
|
|
182
|
+
type: 'invoke-response',
|
|
183
|
+
id,
|
|
184
|
+
ok: false,
|
|
185
|
+
error,
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
type: 'invoke-response',
|
|
189
|
+
id,
|
|
190
|
+
ok: true,
|
|
191
|
+
result: { json: nested.json },
|
|
192
|
+
};
|
|
183
193
|
child.send(JSON.stringify(response));
|
|
184
194
|
}
|
|
185
195
|
}
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -379,6 +379,10 @@ export interface DurableStepEvent {
|
|
|
379
379
|
output?: unknown;
|
|
380
380
|
error?: InvocationError;
|
|
381
381
|
durationMs?: number;
|
|
382
|
+
retryDelayMs?: number;
|
|
383
|
+
attempt?: number;
|
|
384
|
+
retries?: number;
|
|
385
|
+
maxAttempts?: number;
|
|
382
386
|
}
|
|
383
387
|
export interface DurableLogEvent {
|
|
384
388
|
kind: 'log';
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/runtime-cli",
|
|
3
3
|
"description": "Sanity's Runtime CLI for Blueprints and Functions",
|
|
4
|
-
"version": "17.16.0-next.
|
|
4
|
+
"version": "17.16.0-next.gcb96a23",
|
|
5
5
|
"author": "Sanity Runtime Team",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"@oclif/test": "^4.2.0",
|
|
101
101
|
"@playwright/test": "^1.63.0",
|
|
102
102
|
"@sanity-labs/design-tokens": "^0.0.2-alpha.2",
|
|
103
|
-
"@sanity/functions": "^1.
|
|
103
|
+
"@sanity/functions": "^1.10.1",
|
|
104
104
|
"@types/cardinal": "^2.1.1",
|
|
105
105
|
"@types/mime-types": "^3.0.1",
|
|
106
106
|
"@types/node": "^22",
|