@sanity/runtime-cli 17.16.0 → 17.17.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/README.md +19 -19
- package/dist/actions/blueprints/assets.d.ts +3 -3
- package/dist/actions/blueprints/assets.js +3 -3
- package/dist/actions/blueprints/pre-deploy.js +3 -3
- package/dist/actions/blueprints/process-assets.js +3 -2
- package/dist/config.js +4 -1
- package/dist/cores/functions/logs.d.ts +7 -0
- package/dist/cores/functions/logs.js +29 -16
- package/dist/cores/functions/test.js +15 -4
- package/dist/server/app.js +2 -2
- package/dist/server/static/vendor/vendor.bundle.js +21 -42
- package/dist/utils/blueprints/group-resources.d.ts +2 -1
- package/dist/utils/blueprints/group-resources.js +3 -3
- package/dist/utils/blueprints/resolve-deployed-resources.d.ts +2 -2
- package/dist/utils/blueprints/resolve-deployed-resources.js +4 -4
- package/dist/utils/bundles/prepare-asset.d.ts +3 -3
- package/dist/utils/bundles/prepare-asset.js +46 -12
- 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 +19 -8
- package/dist/utils/functions/invoke/resolve-invoke-type.d.ts +2 -2
- package/dist/utils/functions/invoke/resolve-invoke-type.js +2 -2
- package/dist/utils/functions/transpile/function.js +3 -0
- package/dist/utils/types.d.ts +21 -13
- package/dist/utils/types.js +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +5 -5
|
@@ -8,7 +8,8 @@ const BLUEPRINT_TYPE_TO_KEY = {
|
|
|
8
8
|
};
|
|
9
9
|
export function groupBlueprintResources(blueprintResources) {
|
|
10
10
|
const grouped = {};
|
|
11
|
-
for (const
|
|
11
|
+
for (const resolvedResource of blueprintResources) {
|
|
12
|
+
const resource = resolvedResource.resource;
|
|
12
13
|
if (resource.type === 'sanity.access.robot')
|
|
13
14
|
continue;
|
|
14
15
|
const key = (resource.type?.startsWith('sanity.function.')
|
|
@@ -18,9 +19,8 @@ export function groupBlueprintResources(blueprintResources) {
|
|
|
18
19
|
continue;
|
|
19
20
|
if (!grouped[key])
|
|
20
21
|
grouped[key] = [];
|
|
21
|
-
const externalId = resource.externalId;
|
|
22
22
|
grouped[key].push({
|
|
23
|
-
id: externalId || 'not-deployed',
|
|
23
|
+
id: resolvedResource.externalId || 'not-deployed',
|
|
24
24
|
name: resource.name,
|
|
25
25
|
type: resource.type,
|
|
26
26
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlueprintResource } from '@sanity/blueprints';
|
|
2
|
-
import type { AuthParams } from '../types.js';
|
|
2
|
+
import type { AuthParams, ResolvedResource } from '../types.js';
|
|
3
3
|
interface ResolveOptions {
|
|
4
4
|
resources: BlueprintResource[];
|
|
5
5
|
stackId?: string;
|
|
@@ -12,5 +12,5 @@ interface ResolveOptions {
|
|
|
12
12
|
* resources that aren't yet deployed are preserved without an externalId.
|
|
13
13
|
* Any failure to fetch the stack falls back to the raw local resources.
|
|
14
14
|
*/
|
|
15
|
-
export declare function resolveDeployedResources({ resources, stackId, auth, fetchFn, }: ResolveOptions): Promise<BlueprintResource[]>;
|
|
15
|
+
export declare function resolveDeployedResources({ resources, stackId, auth, fetchFn, }: ResolveOptions): Promise<ResolvedResource<BlueprintResource>[]>;
|
|
16
16
|
export {};
|
|
@@ -7,11 +7,11 @@ import { getStack } from '../../actions/blueprints/stacks.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export async function resolveDeployedResources({ resources, stackId, auth, fetchFn, }) {
|
|
9
9
|
if (!stackId || !auth?.token || !auth.scopeType || !auth.scopeId)
|
|
10
|
-
return resources;
|
|
10
|
+
return resources.map((r) => ({ resource: r }));
|
|
11
11
|
try {
|
|
12
12
|
const response = await getStack({ stackId, auth, fetchFn });
|
|
13
13
|
if (!response.ok || !Array.isArray(response.stack?.resources))
|
|
14
|
-
return resources;
|
|
14
|
+
return resources.map((r) => ({ resource: r }));
|
|
15
15
|
const deployedByKey = new Map();
|
|
16
16
|
for (const deployed of response.stack.resources) {
|
|
17
17
|
if (deployed.externalId) {
|
|
@@ -20,10 +20,10 @@ export async function resolveDeployedResources({ resources, stackId, auth, fetch
|
|
|
20
20
|
}
|
|
21
21
|
return resources.map((resource) => {
|
|
22
22
|
const externalId = deployedByKey.get(`${resource.type}:${resource.name}`);
|
|
23
|
-
return
|
|
23
|
+
return { resource, externalId };
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
catch {
|
|
27
|
-
return resources;
|
|
27
|
+
return resources.map((r) => ({ resource: r }));
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BlueprintApplicationResource, BlueprintInstallationConfigResource, BlueprintStudioResource } from '@sanity/blueprints';
|
|
2
2
|
import { type BuildStudioEventListener } from '@sanity/cli-build/_internal/build';
|
|
3
3
|
import type { Output } from '@sanity/cli-core';
|
|
4
|
-
import type { AssetPrepResult } from '../types.js';
|
|
4
|
+
import type { AssetPrepResult, ResolvedResource } from '../types.js';
|
|
5
5
|
export type BundleResource = BlueprintStudioResource | BlueprintApplicationResource | BlueprintInstallationConfigResource;
|
|
6
6
|
type BuildContext<R extends BundleResource> = {
|
|
7
7
|
resource: R;
|
|
@@ -28,9 +28,9 @@ export declare const BUNDLE_SPECS: {
|
|
|
28
28
|
build: typeof buildInstallationConfigBundle;
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
|
-
export declare function prepareBundleAsset<R extends BundleResource>({ blueprintFileDir,
|
|
31
|
+
export declare function prepareBundleAsset<R extends BundleResource>({ blueprintFileDir, resolvedResource, }: {
|
|
32
32
|
blueprintFileDir: string;
|
|
33
|
-
|
|
33
|
+
resolvedResource: ResolvedResource<R>;
|
|
34
34
|
}): Promise<AssetPrepResult>;
|
|
35
35
|
declare function buildStudioBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintStudioResource>): Promise<BuildResult>;
|
|
36
36
|
declare function buildApplicationBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintApplicationResource>): Promise<BuildResult>;
|
|
@@ -5,6 +5,7 @@ import path, { basename, dirname, join } from 'node:path';
|
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
6
|
import { createGzip } from 'node:zlib';
|
|
7
7
|
import { buildApp, buildStudio, compareDependencyVersions, } from '@sanity/cli-build/_internal/build';
|
|
8
|
+
import { buildStudioSchemasAndManifests } from '@sanity/cli-build/_internal/extract';
|
|
8
9
|
import { getLocalPackageVersion } from '@sanity/cli-core';
|
|
9
10
|
import { pack } from 'tar-fs';
|
|
10
11
|
export const BUNDLE_SPECS = {
|
|
@@ -12,10 +13,11 @@ export const BUNDLE_SPECS = {
|
|
|
12
13
|
'sanity.application': { requiresAppId: true, build: buildApplicationBundle },
|
|
13
14
|
'sanity.installation.config': { requiresAppId: false, build: buildInstallationConfigBundle },
|
|
14
15
|
};
|
|
15
|
-
export async function prepareBundleAsset({ blueprintFileDir,
|
|
16
|
+
export async function prepareBundleAsset({ blueprintFileDir, resolvedResource, }) {
|
|
17
|
+
const resource = resolvedResource.resource;
|
|
16
18
|
// The lookup can't prove it yields the spec matching this R, so it's asserted once here.
|
|
17
19
|
const spec = BUNDLE_SPECS[resource.type];
|
|
18
|
-
const appId = getAppId(
|
|
20
|
+
const appId = getAppId(resolvedResource);
|
|
19
21
|
if (spec.requiresAppId && !appId) {
|
|
20
22
|
return {
|
|
21
23
|
success: false,
|
|
@@ -104,8 +106,15 @@ export async function prepareBundleAsset({ blueprintFileDir, resource, }) {
|
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
async function buildStudioBundle({ resource, appId, workDir, outDir, output, eventListener, }) {
|
|
109
|
+
// this has been checked before we got here but this helps the type checker
|
|
110
|
+
if (!appId)
|
|
111
|
+
throw new Error(`Unable to determine appId for ${resource.type} resource '${resource.name}'`);
|
|
112
|
+
// currently we only support hard-coded project, support for dynamic project id to be added
|
|
113
|
+
const projectId = resource.project;
|
|
114
|
+
if (!projectId || projectId.startsWith('$.'))
|
|
115
|
+
throw new Error(`Unable to determine project for ${resource.type} resource '${resource.name}'`);
|
|
107
116
|
let mismatchedPackages;
|
|
108
|
-
await buildStudio({
|
|
117
|
+
const { mfManifest } = await buildStudio({
|
|
109
118
|
appId,
|
|
110
119
|
autoUpdatesEnabled: resource.autoUpdates.enabled,
|
|
111
120
|
checkAppId() { },
|
|
@@ -117,10 +126,12 @@ async function buildStudioBundle({ resource, appId, workDir, outDir, output, eve
|
|
|
117
126
|
determineBasePath() {
|
|
118
127
|
return resource.basePath ?? '/';
|
|
119
128
|
},
|
|
120
|
-
// a studio is a standalone app, not a workbench module-federation app
|
|
121
129
|
isApp: false,
|
|
122
130
|
isBlueprints: true,
|
|
123
|
-
|
|
131
|
+
// federated so the build emits mf-manifest.json for Brett; like `sanity build` for a
|
|
132
|
+
// `defineApplication` studio, this drops the auto-updates import map
|
|
133
|
+
isWorkbenchApp: true,
|
|
134
|
+
workbenchAppId: appId,
|
|
124
135
|
minify: resource.minify ?? true,
|
|
125
136
|
outDir,
|
|
126
137
|
output,
|
|
@@ -134,6 +145,21 @@ async function buildStudioBundle({ resource, appId, workDir, outDir, output, eve
|
|
|
134
145
|
workDir,
|
|
135
146
|
eventListener,
|
|
136
147
|
});
|
|
148
|
+
if (mfManifest) {
|
|
149
|
+
resource.mfManifest = mfManifest;
|
|
150
|
+
}
|
|
151
|
+
const { schemaInputs } = await buildStudioSchemasAndManifests(appId, {
|
|
152
|
+
configPath: resource.root,
|
|
153
|
+
isExternal: false,
|
|
154
|
+
outPath: outDir,
|
|
155
|
+
projectId,
|
|
156
|
+
schemaRequired: false,
|
|
157
|
+
verbose: false,
|
|
158
|
+
workDir,
|
|
159
|
+
});
|
|
160
|
+
if (schemaInputs) {
|
|
161
|
+
resource.schemaInputs = schemaInputs;
|
|
162
|
+
}
|
|
137
163
|
return { mismatchedPackages };
|
|
138
164
|
}
|
|
139
165
|
async function buildApplicationBundle({ resource, appId, workDir, outDir, output, eventListener, }) {
|
|
@@ -142,7 +168,7 @@ async function buildApplicationBundle({ resource, appId, workDir, outDir, output
|
|
|
142
168
|
if (extraWindow) {
|
|
143
169
|
throw new Error(`Application resource '${resource.name}' declares more than one window view; at most one is allowed`);
|
|
144
170
|
}
|
|
145
|
-
await buildApp({
|
|
171
|
+
const { mfManifest } = await buildApp({
|
|
146
172
|
...sharedBuildAppOptions({ appId, workDir, outDir, output, eventListener }),
|
|
147
173
|
appId,
|
|
148
174
|
appTitle: resource.title,
|
|
@@ -153,11 +179,14 @@ async function buildApplicationBundle({ resource, appId, workDir, outDir, output
|
|
|
153
179
|
webWorkers: resource.webWorkers,
|
|
154
180
|
},
|
|
155
181
|
});
|
|
182
|
+
if (mfManifest) {
|
|
183
|
+
resource.mfManifest = mfManifest;
|
|
184
|
+
}
|
|
156
185
|
// a workbench app never auto-updates, so no dependency mismatches are compared
|
|
157
186
|
return {};
|
|
158
187
|
}
|
|
159
188
|
async function buildInstallationConfigBundle({ resource, workDir, outDir, output, eventListener, }) {
|
|
160
|
-
await buildApp({
|
|
189
|
+
const { mfManifest } = await buildApp({
|
|
161
190
|
...sharedBuildAppOptions({ appId: undefined, workDir, outDir, output, eventListener }),
|
|
162
191
|
appId: undefined,
|
|
163
192
|
appTitle: undefined,
|
|
@@ -165,6 +194,9 @@ async function buildInstallationConfigBundle({ resource, workDir, outDir, output
|
|
|
165
194
|
workbenchAppId: undefined,
|
|
166
195
|
exposes: { config: { appType: resource.appType, fields: resource.fields } },
|
|
167
196
|
});
|
|
197
|
+
if (mfManifest) {
|
|
198
|
+
resource.mfManifest = mfManifest;
|
|
199
|
+
}
|
|
168
200
|
return {};
|
|
169
201
|
}
|
|
170
202
|
// Options a workbench build (application or installation config) always sets the
|
|
@@ -247,6 +279,8 @@ function createSilentOutput() {
|
|
|
247
279
|
return undefined;
|
|
248
280
|
},
|
|
249
281
|
log() { },
|
|
282
|
+
logDiagnostic(_message) { },
|
|
283
|
+
logJson(_json) { },
|
|
250
284
|
warn(input) {
|
|
251
285
|
return input;
|
|
252
286
|
},
|
|
@@ -260,12 +294,12 @@ async function getInstalledSanityVersion(workDir) {
|
|
|
260
294
|
return undefined;
|
|
261
295
|
}
|
|
262
296
|
}
|
|
263
|
-
function getAppId(
|
|
264
|
-
if (
|
|
265
|
-
return
|
|
297
|
+
function getAppId(resolvedResource) {
|
|
298
|
+
if (resolvedResource.externalId) {
|
|
299
|
+
return resolvedResource.externalId;
|
|
266
300
|
}
|
|
267
|
-
else if (resource.lifecycle?.ownershipAction?.type === 'attach') {
|
|
268
|
-
return resource.lifecycle.ownershipAction.id;
|
|
301
|
+
else if (resolvedResource.resource.lifecycle?.ownershipAction?.type === 'attach') {
|
|
302
|
+
return resolvedResource.resource.lifecycle.ownershipAction.id;
|
|
269
303
|
}
|
|
270
304
|
return undefined;
|
|
271
305
|
}
|
|
@@ -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}
|
|
@@ -64,10 +64,11 @@ export default async function invoke(resource, payload, context, options) {
|
|
|
64
64
|
// Apply the GROQ rule before preparing a build so a filtered-out event skips
|
|
65
65
|
// the (potentially expensive) transpile entirely.
|
|
66
66
|
if (isGroqInvocation(resource)) {
|
|
67
|
-
if (!
|
|
67
|
+
if (!('clientOptions' in context)) {
|
|
68
68
|
throw new Error('GROQ-based functions require a context with clientOptions');
|
|
69
69
|
}
|
|
70
|
-
const
|
|
70
|
+
const projectId = 'projectId' in context.clientOptions ? context.clientOptions.projectId : undefined;
|
|
71
|
+
const dataset = 'dataset' in context.clientOptions ? context.clientOptions.dataset : undefined;
|
|
71
72
|
if ('before' in payload) {
|
|
72
73
|
filteredData = await applyGroqRule(resource, payload, projectId, dataset);
|
|
73
74
|
const filter = resource.event && 'filter' in resource.event ? resource.event.filter : '';
|
|
@@ -173,12 +174,22 @@ export default async function invoke(resource, payload, context, options) {
|
|
|
173
174
|
// of `nested.error`. The error isn't lost: it travels back in the returned
|
|
174
175
|
// result and is logged/streamed to the UI by the caller (app/test core).
|
|
175
176
|
if (child && !child.killed) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
};
|
|
182
193
|
child.send(JSON.stringify(response));
|
|
183
194
|
}
|
|
184
195
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BlueprintResource } from '@sanity/blueprints';
|
|
2
2
|
import type { Logger } from '../../logger.js';
|
|
3
|
-
import type { EventType, FunctionGroqResource, FunctionResource, FunctionTestFlags, InvokeContextOptions, InvokePayloadOptions } from '../../types.js';
|
|
3
|
+
import type { EventType, FunctionGroqResource, FunctionResource, FunctionTestFlags, InvokeContextOptions, InvokePayloadOptions, ResolvedResource } from '../../types.js';
|
|
4
4
|
import buildPayload from '../build-payload.js';
|
|
5
5
|
import { fetchAsset, fetchDocument } from '../fetch-document.js';
|
|
6
6
|
export type InvocationPlan = {
|
|
@@ -23,7 +23,7 @@ export type InputContext = {
|
|
|
23
23
|
apiHost: string;
|
|
24
24
|
token?: string;
|
|
25
25
|
withUserToken?: boolean;
|
|
26
|
-
|
|
26
|
+
resolvedResources: ResolvedResource<BlueprintResource>[];
|
|
27
27
|
blueprintFileDir: string;
|
|
28
28
|
log: Logger;
|
|
29
29
|
};
|
|
@@ -51,7 +51,7 @@ export const buildContext = (plan, input) => {
|
|
|
51
51
|
if (plan.kind === 'plain') {
|
|
52
52
|
return {
|
|
53
53
|
clientOptions: { ...withToken },
|
|
54
|
-
resources: input.
|
|
54
|
+
resources: input.resolvedResources,
|
|
55
55
|
blueprintFileDir: input.blueprintFileDir,
|
|
56
56
|
};
|
|
57
57
|
}
|
|
@@ -69,7 +69,7 @@ export const buildContext = (plan, input) => {
|
|
|
69
69
|
'',
|
|
70
70
|
functionResourceType: 'project',
|
|
71
71
|
functionResourceId: input.projectId || '',
|
|
72
|
-
resources: input.
|
|
72
|
+
resources: input.resolvedResources,
|
|
73
73
|
blueprintFileDir: input.blueprintFileDir,
|
|
74
74
|
};
|
|
75
75
|
};
|