atmn 2.0.33 → 2.0.35
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/dist/bin.js +1409 -188
- package/dist/index.js +220 -19
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/actions/pull/webhooks/appendWebhook.ts +84 -0
- package/src/actions/pull/webhooks/applyWebhooksPull.ts +111 -0
- package/src/actions/pull/webhooks/locateWebhook.ts +22 -0
- package/src/actions/pull/webhooks/removeWebhookEnv.ts +141 -0
- package/src/actions/pull/webhooks/types.ts +20 -0
- package/src/actions/pull/webhooks/updateWebhook.ts +138 -0
- package/src/actions/pull.ts +32 -2
- package/src/actions/push/pushLanesError.ts +47 -0
- package/src/actions/push.ts +220 -60
- package/src/actions/webhooks/applyWebhooks.ts +52 -0
- package/src/actions/webhooks/isDashboardWebhook.ts +9 -0
- package/src/actions/webhooks/newWebhookId.ts +4 -0
- package/src/actions/webhooks/previewWebhooks.ts +40 -0
- package/src/actions/webhooks/resolveWebhookEnv.ts +34 -0
- package/src/actions/webhooks/resolveWebhooksForEnv.ts +29 -0
- package/src/actions/webhooks/types/webhookEnv.ts +9 -0
- package/src/actions/webhooks/webhookSecretName.ts +22 -0
- package/src/actions/webhooks/writeWebhookSecrets.ts +55 -0
- package/src/cli.ts +20 -1
- package/src/config/loadConfig.ts +3 -0
- package/src/generated/apiRoutes.ts +165 -1
- package/src/generated/client.ts +239 -1
- package/src/generated/emit.ts +20 -1
- package/src/generated/emitRuntime.ts +8 -0
- package/src/generated/isLocalWebhookUrl.ts +64 -0
- package/src/generated/lintRules.ts +95 -0
- package/src/generated/lintRuntime.ts +113 -10
- package/src/generated/sandboxName.ts +52 -0
- package/src/generated/skills.ts +10 -10
- package/src/generated/webhooks.ts +36 -0
- package/src/generated/wire.ts +32 -5
- package/src/index.ts +1 -0
- package/src/render/renderPreview.ts +17 -2
- package/src/render/renderWebhooks.ts +92 -0
- package/src/surgery/appendPropertyEdit.ts +9 -3
- package/src/surgery/patchFixturePaths.ts +57 -5
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { SYNCED_LISTS } from "../../../generated/emit";
|
|
2
|
+
import { deleteFixtureLiteral } from "../../../surgery/deleteFixtureLiteral";
|
|
3
|
+
import { deleteReference } from "../../../surgery/deleteReference";
|
|
4
|
+
import { fixtureObjectOf } from "../../../surgery/findFixture";
|
|
5
|
+
import { patchFixturePaths } from "../../../surgery/patchFixturePaths";
|
|
6
|
+
import {
|
|
7
|
+
fixtureKeyOf,
|
|
8
|
+
readFixtureValue,
|
|
9
|
+
} from "../../../surgery/readFixtureValue";
|
|
10
|
+
import { resolveCollectionTarget } from "../resolveCollectionTarget";
|
|
11
|
+
import { locateWebhook } from "./locateWebhook";
|
|
12
|
+
import type { PullFiles, StatedWebhook, WebhookEditResult } from "./types";
|
|
13
|
+
import { nonLiteralUrlWarning } from "./updateWebhook";
|
|
14
|
+
|
|
15
|
+
const SPEC = SYNCED_LISTS.webhooks;
|
|
16
|
+
|
|
17
|
+
/** Read from source, not the evaluated config: a key whose code evaluates to
|
|
18
|
+
* undefined is still a key the user wrote. */
|
|
19
|
+
const urlMapIsEmpty = ({
|
|
20
|
+
pull,
|
|
21
|
+
id,
|
|
22
|
+
}: {
|
|
23
|
+
pull: PullFiles;
|
|
24
|
+
id: string;
|
|
25
|
+
}): boolean => {
|
|
26
|
+
const located = locateWebhook({ pull, id });
|
|
27
|
+
const object = located === null ? null : fixtureObjectOf(located.node);
|
|
28
|
+
const member = object
|
|
29
|
+
?.namedChildren()
|
|
30
|
+
.find(
|
|
31
|
+
(child) =>
|
|
32
|
+
child.kind() === "pair" &&
|
|
33
|
+
child.field("key") !== null &&
|
|
34
|
+
fixtureKeyOf({ node: child.field("key") as never }) === "url",
|
|
35
|
+
);
|
|
36
|
+
const value = member?.field("value");
|
|
37
|
+
if (value === null || value === undefined) return false;
|
|
38
|
+
const read = readFixtureValue({ node: value });
|
|
39
|
+
return (
|
|
40
|
+
typeof read === "object" && read !== null && Object.keys(read).length === 0
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** The whole `webhook({...})`, and any export reference left pointing at it. */
|
|
45
|
+
const deleteWebhook = ({
|
|
46
|
+
pull,
|
|
47
|
+
id,
|
|
48
|
+
}: {
|
|
49
|
+
pull: PullFiles;
|
|
50
|
+
id: string;
|
|
51
|
+
}): boolean => {
|
|
52
|
+
const located = locateWebhook({ pull, id });
|
|
53
|
+
if (located === null) return false;
|
|
54
|
+
const removed = deleteFixtureLiteral({
|
|
55
|
+
source: located.source,
|
|
56
|
+
builder: SPEC.builder,
|
|
57
|
+
idField: SPEC.idField,
|
|
58
|
+
id,
|
|
59
|
+
});
|
|
60
|
+
if (removed === null) return false;
|
|
61
|
+
pull.files.set(located.file, removed.source);
|
|
62
|
+
if (removed.exportedName !== undefined) {
|
|
63
|
+
const referenceFiles = new Set([pull.configPath]);
|
|
64
|
+
const target = resolveCollectionTarget({
|
|
65
|
+
configPath: pull.configPath,
|
|
66
|
+
files: pull.files,
|
|
67
|
+
collection: "webhooks",
|
|
68
|
+
});
|
|
69
|
+
if (target !== null) referenceFiles.add(target.file);
|
|
70
|
+
for (const file of referenceFiles)
|
|
71
|
+
pull.files.set(
|
|
72
|
+
file,
|
|
73
|
+
deleteReference({
|
|
74
|
+
source: pull.files.get(file) ?? "",
|
|
75
|
+
name: removed.exportedName,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Rule 4: the server has no such webhook in this env, so the env's key goes;
|
|
84
|
+
* a map left empty registers the webhook nowhere, so the webhook goes too.
|
|
85
|
+
*/
|
|
86
|
+
export const removeWebhookEnv = ({
|
|
87
|
+
pull,
|
|
88
|
+
stated,
|
|
89
|
+
envKey,
|
|
90
|
+
}: {
|
|
91
|
+
pull: PullFiles;
|
|
92
|
+
stated: StatedWebhook;
|
|
93
|
+
envKey: string;
|
|
94
|
+
}): WebhookEditResult => {
|
|
95
|
+
const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
|
|
96
|
+
const located = locateWebhook({ pull, id: stated.id });
|
|
97
|
+
const patched =
|
|
98
|
+
located === null
|
|
99
|
+
? null
|
|
100
|
+
: patchFixturePaths({
|
|
101
|
+
source: located.source,
|
|
102
|
+
builder: SPEC.builder,
|
|
103
|
+
idField: SPEC.idField,
|
|
104
|
+
id: stated.id,
|
|
105
|
+
assignments: [{ path: ["url", envKey], text: null }],
|
|
106
|
+
});
|
|
107
|
+
if (located === null || patched === null) {
|
|
108
|
+
result.unlocated.push({
|
|
109
|
+
id: stated.id,
|
|
110
|
+
action: `remove url.${envKey} by hand`,
|
|
111
|
+
});
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
if (patched.skipped.length > 0) {
|
|
115
|
+
result.warnings.push(
|
|
116
|
+
nonLiteralUrlWarning({
|
|
117
|
+
id: stated.id,
|
|
118
|
+
envKey,
|
|
119
|
+
server: undefined,
|
|
120
|
+
config: stated.url?.[envKey],
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
if (patched.source === located.source) return result;
|
|
126
|
+
pull.files.set(located.file, patched.source);
|
|
127
|
+
if (urlMapIsEmpty({ pull, id: stated.id })) {
|
|
128
|
+
if (deleteWebhook({ pull, id: stated.id })) {
|
|
129
|
+
result.lines.push(`- webhook ${stated.id}`);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
// Only a literal can be deleted; saying so beats leaving `url: {}` behind.
|
|
133
|
+
result.unlocated.push({
|
|
134
|
+
id: stated.id,
|
|
135
|
+
action: "delete the webhook by hand: its url map is now empty",
|
|
136
|
+
});
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
result.lines.push(`- webhook ${stated.id} url.${envKey}`);
|
|
140
|
+
return result;
|
|
141
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ListWebhooksResponse } from "../../../generated/client";
|
|
2
|
+
|
|
3
|
+
export type RemoteWebhook = ListWebhooksResponse["list"][number];
|
|
4
|
+
|
|
5
|
+
/** A config webhook as evaluated: `url` still holds every env's value. */
|
|
6
|
+
export type StatedWebhook = Record<string, unknown> & {
|
|
7
|
+
id: string;
|
|
8
|
+
url?: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** The in-memory sources a pull edits, plus where the config lives. */
|
|
12
|
+
export type PullFiles = { configPath: string; files: Map<string, string> };
|
|
13
|
+
|
|
14
|
+
export type WebhookEditResult = {
|
|
15
|
+
/** Printed as they land, in the `+ / ~ / -` style of the other lanes. */
|
|
16
|
+
lines: string[];
|
|
17
|
+
/** Values the config states in code pull must not rewrite. */
|
|
18
|
+
warnings: string[];
|
|
19
|
+
unlocated: { id: string; action: string }[];
|
|
20
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { SYNCED_LISTS } from "../../../generated/emit";
|
|
3
|
+
import { patchFixturePaths } from "../../../surgery/patchFixturePaths";
|
|
4
|
+
import { locateWebhook } from "./locateWebhook";
|
|
5
|
+
import type {
|
|
6
|
+
PullFiles,
|
|
7
|
+
RemoteWebhook,
|
|
8
|
+
StatedWebhook,
|
|
9
|
+
WebhookEditResult,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
const SPEC = SYNCED_LISTS.webhooks;
|
|
13
|
+
|
|
14
|
+
type Assignment = { path: string[]; text: string | null };
|
|
15
|
+
|
|
16
|
+
const sameEvents = (left: unknown, right: readonly string[]): boolean =>
|
|
17
|
+
Array.isArray(left) && isDeepStrictEqual([...left].sort(), [...right].sort());
|
|
18
|
+
|
|
19
|
+
/** Settings-style: a field moves only when the server holds something else. */
|
|
20
|
+
const fieldAssignments = ({
|
|
21
|
+
webhook,
|
|
22
|
+
stated,
|
|
23
|
+
}: {
|
|
24
|
+
webhook: RemoteWebhook;
|
|
25
|
+
stated: StatedWebhook;
|
|
26
|
+
}): Assignment[] => {
|
|
27
|
+
const assignments: Assignment[] = [];
|
|
28
|
+
// `[]` is a dashboard webhook receiving every event: not a list a config can state.
|
|
29
|
+
if (webhook.events.length > 0 && !sameEvents(stated.events, webhook.events))
|
|
30
|
+
assignments.push({
|
|
31
|
+
path: ["events"],
|
|
32
|
+
text: `[${webhook.events.map((event) => JSON.stringify(event)).join(", ")}]`,
|
|
33
|
+
});
|
|
34
|
+
if ((stated.description ?? "") !== (webhook.description ?? ""))
|
|
35
|
+
assignments.push({
|
|
36
|
+
path: ["description"],
|
|
37
|
+
text: webhook.description ? JSON.stringify(webhook.description) : null,
|
|
38
|
+
});
|
|
39
|
+
if ((stated.disabled === true) !== webhook.disabled)
|
|
40
|
+
assignments.push({
|
|
41
|
+
path: ["disabled"],
|
|
42
|
+
text: webhook.disabled ? "true" : null,
|
|
43
|
+
});
|
|
44
|
+
return assignments;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Rule 3's wording: what the server holds against what the code evaluates to. */
|
|
48
|
+
export const nonLiteralUrlWarning = ({
|
|
49
|
+
id,
|
|
50
|
+
envKey,
|
|
51
|
+
server,
|
|
52
|
+
config,
|
|
53
|
+
}: {
|
|
54
|
+
id: string;
|
|
55
|
+
envKey: string;
|
|
56
|
+
server: string | undefined;
|
|
57
|
+
config: unknown;
|
|
58
|
+
}): string => {
|
|
59
|
+
const pad = " ".repeat(id.length + 4);
|
|
60
|
+
return [
|
|
61
|
+
`⚠ ${id} url.${envKey} isn't a plain string, so pull left it untouched`,
|
|
62
|
+
`${pad}server: ${server ?? "(not registered)"}`,
|
|
63
|
+
`${pad}your config: ${typeof config === "string" ? config : "(no value)"}`,
|
|
64
|
+
`${pad}Update it by hand, or make it a string and pull will manage it.`,
|
|
65
|
+
].join("\n");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Rules 2 and 3: set or replace `url[envKey]` when it is a string literal (or
|
|
70
|
+
* absent), and bring the other fields in line. Code is never rewritten; a
|
|
71
|
+
* value that evaluates differently from the server earns a warning instead.
|
|
72
|
+
*/
|
|
73
|
+
export const updateWebhook = ({
|
|
74
|
+
pull,
|
|
75
|
+
webhook,
|
|
76
|
+
stated,
|
|
77
|
+
envKey,
|
|
78
|
+
}: {
|
|
79
|
+
pull: PullFiles;
|
|
80
|
+
webhook: RemoteWebhook;
|
|
81
|
+
stated: StatedWebhook;
|
|
82
|
+
envKey: string;
|
|
83
|
+
}): WebhookEditResult => {
|
|
84
|
+
const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
|
|
85
|
+
if (webhook.events.length === 0 && Array.isArray(stated.events))
|
|
86
|
+
result.warnings.push(
|
|
87
|
+
`⚠ ${webhook.id} the dashboard webhook at this url receives every event; your next push narrows it to ${stated.events.join(", ")}`,
|
|
88
|
+
);
|
|
89
|
+
const located = locateWebhook({ pull, id: webhook.id });
|
|
90
|
+
if (located === null) {
|
|
91
|
+
result.unlocated.push({
|
|
92
|
+
id: webhook.id,
|
|
93
|
+
action: "update from the server by hand",
|
|
94
|
+
});
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
const patched = patchFixturePaths({
|
|
98
|
+
source: located.source,
|
|
99
|
+
builder: SPEC.builder,
|
|
100
|
+
idField: SPEC.idField,
|
|
101
|
+
id: webhook.id,
|
|
102
|
+
assignments: [
|
|
103
|
+
{ path: ["url", envKey], text: JSON.stringify(webhook.url) },
|
|
104
|
+
...fieldAssignments({ webhook, stated }),
|
|
105
|
+
],
|
|
106
|
+
bareKeys: true,
|
|
107
|
+
});
|
|
108
|
+
if (patched === null) {
|
|
109
|
+
result.unlocated.push({
|
|
110
|
+
id: webhook.id,
|
|
111
|
+
action: "update from the server by hand",
|
|
112
|
+
});
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
for (const { path } of patched.skipped) {
|
|
116
|
+
const field = path.join(".");
|
|
117
|
+
if (field !== `url.${envKey}`) {
|
|
118
|
+
result.warnings.push(
|
|
119
|
+
`⚠ ${webhook.id} ${field} isn't a plain literal, so pull left it untouched`,
|
|
120
|
+
);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const config = stated.url?.[envKey];
|
|
124
|
+
if (config === webhook.url) continue;
|
|
125
|
+
result.warnings.push(
|
|
126
|
+
nonLiteralUrlWarning({
|
|
127
|
+
id: webhook.id,
|
|
128
|
+
envKey,
|
|
129
|
+
server: webhook.url,
|
|
130
|
+
config,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (patched.source === located.source) return result;
|
|
135
|
+
pull.files.set(located.file, patched.source);
|
|
136
|
+
result.lines.push(`~ webhook ${webhook.id}`);
|
|
137
|
+
return result;
|
|
138
|
+
};
|
package/src/actions/pull.ts
CHANGED
|
@@ -30,11 +30,14 @@ import {
|
|
|
30
30
|
packageImports,
|
|
31
31
|
scaffoldConfig,
|
|
32
32
|
} from "./pull/scaffoldConfig";
|
|
33
|
+
import { applyWebhooksPull } from "./pull/webhooks/applyWebhooksPull";
|
|
34
|
+
import type { StatedWebhook } from "./pull/webhooks/types";
|
|
33
35
|
import { configSearchDirs } from "./push";
|
|
34
36
|
import {
|
|
35
37
|
backfillInternalIds,
|
|
36
38
|
identityRowsFromCatalog,
|
|
37
39
|
} from "./push/backfillInternalIds";
|
|
40
|
+
import type { WebhookEnv } from "./webhooks/types/webhookEnv";
|
|
38
41
|
|
|
39
42
|
export type PullResult = {
|
|
40
43
|
configPath: string;
|
|
@@ -64,6 +67,8 @@ export type PullOptions = {
|
|
|
64
67
|
yes?: boolean;
|
|
65
68
|
/** Asks where a first pull should put the config; headless by default. */
|
|
66
69
|
prompter?: Prompter;
|
|
70
|
+
/** The target env for the webhook lane; without it webhooks are not pulled. */
|
|
71
|
+
webhookEnv?: () => Promise<WebhookEnv>;
|
|
67
72
|
};
|
|
68
73
|
|
|
69
74
|
const OVERWRITE_HINT =
|
|
@@ -224,6 +229,7 @@ export const runPull = async ({
|
|
|
224
229
|
overwrite = false,
|
|
225
230
|
yes = false,
|
|
226
231
|
prompter = createPrompter({ interactive: false, write }),
|
|
232
|
+
webhookEnv,
|
|
227
233
|
}: PullOptions): Promise<PullResult> => {
|
|
228
234
|
const project = resolveProject({ cwd, configFlag });
|
|
229
235
|
if (overwrite && !yes) {
|
|
@@ -268,9 +274,9 @@ export const runPull = async ({
|
|
|
268
274
|
overwrite,
|
|
269
275
|
write,
|
|
270
276
|
});
|
|
271
|
-
const { catalog: wire, singletons } = splitWire(document);
|
|
277
|
+
const { catalog: wire, singletons, lists } = splitWire(document);
|
|
272
278
|
|
|
273
|
-
const [preview, catalog, settingsPreview] = await Promise.all([
|
|
279
|
+
const [preview, catalog, settingsPreview, webhooks] = await Promise.all([
|
|
274
280
|
diffOrExplain({ client, wire }),
|
|
275
281
|
// Every version: each is a row in plans, with `active` on it.
|
|
276
282
|
client.get({ include_versions: true }),
|
|
@@ -279,6 +285,10 @@ export const runPull = async ({
|
|
|
279
285
|
client.previewUpdateOrganization(
|
|
280
286
|
singletons.settings ?? { config: {} },
|
|
281
287
|
) as Promise<SettingsPreview>,
|
|
288
|
+
// Always listed, like settings: a webhook the config never named is pulled in.
|
|
289
|
+
webhookEnv === undefined
|
|
290
|
+
? undefined
|
|
291
|
+
: Promise.all([webhookEnv(), client.listWebhooks({})]),
|
|
282
292
|
]);
|
|
283
293
|
|
|
284
294
|
const files = new Map<string, string>();
|
|
@@ -364,6 +374,25 @@ export const runPull = async ({
|
|
|
364
374
|
);
|
|
365
375
|
}
|
|
366
376
|
|
|
377
|
+
const webhookWarnings: string[] = [];
|
|
378
|
+
if (webhooks !== undefined) {
|
|
379
|
+
const [env, { list }] = webhooks;
|
|
380
|
+
const applied = applyWebhooksPull({
|
|
381
|
+
pull: { configPath, files },
|
|
382
|
+
remote: list,
|
|
383
|
+
stated: lists.webhooks as StatedWebhook[] | undefined,
|
|
384
|
+
envKey: env.key,
|
|
385
|
+
});
|
|
386
|
+
lines.push(...applied.lines);
|
|
387
|
+
webhookWarnings.push(...applied.warnings);
|
|
388
|
+
unlocated.push(
|
|
389
|
+
...applied.unlocated.map((entry) => ({
|
|
390
|
+
collection: "webhooks",
|
|
391
|
+
...entry,
|
|
392
|
+
})),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
367
396
|
if (includeMappings) {
|
|
368
397
|
const managedCatalog = Object.fromEntries(
|
|
369
398
|
Object.entries(COLLECTIONS)
|
|
@@ -416,6 +445,7 @@ export const runPull = async ({
|
|
|
416
445
|
`↳ wrote versionSlug into ${slugged.length} fixture${slugged.length === 1 ? "" : "s"}`,
|
|
417
446
|
);
|
|
418
447
|
|
|
448
|
+
if (webhookWarnings.length > 0) write(`${webhookWarnings.join("\n\n")}\n\n`);
|
|
419
449
|
write(
|
|
420
450
|
lines.length === 0
|
|
421
451
|
? "Nothing to pull.\n"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** One lane of a push that failed: settings, catalog or webhooks. */
|
|
2
|
+
export type LaneFailure = { lane: string; error: unknown };
|
|
3
|
+
|
|
4
|
+
const messageOf = (error: unknown): string =>
|
|
5
|
+
error instanceof Error ? error.message : String(error);
|
|
6
|
+
|
|
7
|
+
/** Every failing lane at once: fixing one and re-running to meet the next is the cost this avoids. */
|
|
8
|
+
export class PushLanesError extends Error {
|
|
9
|
+
readonly failures: LaneFailure[];
|
|
10
|
+
|
|
11
|
+
constructor({
|
|
12
|
+
stage,
|
|
13
|
+
failures,
|
|
14
|
+
}: {
|
|
15
|
+
stage: "preview" | "apply";
|
|
16
|
+
failures: LaneFailure[];
|
|
17
|
+
}) {
|
|
18
|
+
super(
|
|
19
|
+
[
|
|
20
|
+
`push ${stage} failed in ${failures.length} of its lanes:`,
|
|
21
|
+
...failures.map(
|
|
22
|
+
({ lane, error }) =>
|
|
23
|
+
`\n ${lane}\n ${messageOf(error).replace(/\n/g, "\n ")}`,
|
|
24
|
+
),
|
|
25
|
+
].join("\n"),
|
|
26
|
+
);
|
|
27
|
+
this.name = "PushLanesError";
|
|
28
|
+
this.failures = failures;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The settled lanes' values; a rejected lane becomes a failure instead. */
|
|
33
|
+
export const settleLanes = async <T extends Record<string, unknown>>(
|
|
34
|
+
lanes: { [K in keyof T]: Promise<T[K]> },
|
|
35
|
+
): Promise<{ values: Partial<T>; failures: LaneFailure[] }> => {
|
|
36
|
+
const names = Object.keys(lanes) as (keyof T & string)[];
|
|
37
|
+
const settled = await Promise.allSettled(names.map((name) => lanes[name]));
|
|
38
|
+
const values: Partial<T> = {};
|
|
39
|
+
const failures: LaneFailure[] = [];
|
|
40
|
+
settled.forEach((result, index) => {
|
|
41
|
+
const lane = names[index] as keyof T & string;
|
|
42
|
+
if (result.status === "fulfilled")
|
|
43
|
+
values[lane] = result.value as T[typeof lane];
|
|
44
|
+
else failures.push({ lane, error: result.reason });
|
|
45
|
+
});
|
|
46
|
+
return { values, failures };
|
|
47
|
+
};
|