@volter/world-core 3.0.123 → 3.0.125
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/src/derived-real.js +10 -10
- package/dist/src/head.d.ts +5 -1
- package/dist/src/head.js +3 -2
- package/dist/src/pack-fetch.js +7 -0
- package/package.json +1 -1
- package/src/derived-real.ts +9 -9
- package/src/head.ts +8 -5
- package/src/pack-fetch.ts +6 -0
package/dist/src/derived-real.js
CHANGED
|
@@ -417,7 +417,7 @@ function performOf(units) {
|
|
|
417
417
|
adopted.push({ actionId: entry.id, externalId, data });
|
|
418
418
|
}
|
|
419
419
|
const primary = adopted.shift();
|
|
420
|
-
return primary ? { ...primary, ...(adopted.length ? { also: adopted } : {}) } : { externalId: '', actionId: group.at(-1).id };
|
|
420
|
+
return primary ? { ...primary, response: answer, ...(adopted.length ? { also: adopted } : {}) } : { externalId: '', actionId: group.at(-1).id, response: answer };
|
|
421
421
|
}
|
|
422
422
|
// an XML answer is read through its document element (S3's `InitiateMultipartUploadResult`), as a JSON one under its key
|
|
423
423
|
const documentElement = () => {
|
|
@@ -434,7 +434,7 @@ function performOf(units) {
|
|
|
434
434
|
// CreateSecretResponse, S3's CreateMultipartUploadOutput) is the group's subject's, its id read by that resource
|
|
435
435
|
const subjectDecl = () => Object.entries(m.resources).find(([r]) => storedType(m, r) === group.at(-1).subject.type)?.[1];
|
|
436
436
|
const decl = resource ? m.resources[resource] : subjectDecl();
|
|
437
|
-
const idOf = (o, d) =>
|
|
437
|
+
const idOf = (o, d) => o ? storedIdOf(d ?? { idPrefix: '' }, o) : undefined;
|
|
438
438
|
// the group's entry the answer is: of the answered resource's stored type (a cascade writes children first), else
|
|
439
439
|
// the last; a delete answers its subject's own vendor id
|
|
440
440
|
const answered = resource ? [...group].reverse().find((e) => e.subject.type === storedType(m, resource)) : undefined;
|
|
@@ -443,7 +443,7 @@ function performOf(units) {
|
|
|
443
443
|
// the membership and its channel_join message) gives no entry its id or its fields: each settles deployed with no
|
|
444
444
|
// vendor id, and refresh adopts it ("Perform", step 2)
|
|
445
445
|
if (resource && !answered)
|
|
446
|
-
return { externalId: '', actionId: primary.id };
|
|
446
|
+
return { externalId: '', actionId: primary.id, response: answer };
|
|
447
447
|
const deleted = primary.fields?.deleted === true;
|
|
448
448
|
// a keyed subject's id is its key filled from the answer (its envelope's fields, Slack's `channel` and `ts`, among them)
|
|
449
449
|
const keyed = decl?.key ? decl : Object.entries(m.resources).find(([r]) => storedType(m, r) === primary.subject.type)?.[1];
|
|
@@ -453,10 +453,10 @@ function performOf(units) {
|
|
|
453
453
|
for (const [companion, where] of Object.entries(decl?.companions ?? keyed?.companions ?? {})) {
|
|
454
454
|
const id = at(parsed, where) ?? at(obj, where);
|
|
455
455
|
const entry = group.find((e) => e !== primary && e.subject.type === storedType(m, companion));
|
|
456
|
-
if (entry && typeof id === 'string')
|
|
457
|
-
also.push({ actionId: entry.id, externalId: id });
|
|
456
|
+
if (entry && (typeof id === 'string' || typeof id === 'number'))
|
|
457
|
+
also.push({ actionId: entry.id, externalId: String(id) });
|
|
458
458
|
}
|
|
459
|
-
return { externalId, actionId: primary.id, data: obj, ...(also.length ? { also } : {}) };
|
|
459
|
+
return { externalId, actionId: primary.id, data: obj, response: answer, ...(also.length ? { also } : {}) };
|
|
460
460
|
};
|
|
461
461
|
return async (execute, group, ctx) => {
|
|
462
462
|
await adoptAccount(units, execute, ctx);
|
|
@@ -620,7 +620,7 @@ function refreshOf(units, service) {
|
|
|
620
620
|
if (answer.status === 404)
|
|
621
621
|
continue;
|
|
622
622
|
if (answer.status >= 400 || refusedIn(m, parse(answer)))
|
|
623
|
-
throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
623
|
+
throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
624
624
|
const full = object(op.answers?.key ? at(parse(answer), op.answers.key) : parse(answer)) ?? {};
|
|
625
625
|
const local = m.worldFields?.[resource] ?? {};
|
|
626
626
|
const fields = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
|
|
@@ -703,7 +703,7 @@ function refreshOf(units, service) {
|
|
|
703
703
|
break;
|
|
704
704
|
}
|
|
705
705
|
if (answer.status >= 400 || refusedIn(m, parse(answer)))
|
|
706
|
-
throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
706
|
+
throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
707
707
|
answered += 1;
|
|
708
708
|
const parsed = parse(answer);
|
|
709
709
|
const shape = 'list' in scope ? scope.shape : undefined;
|
|
@@ -753,7 +753,7 @@ function refreshOf(units, service) {
|
|
|
753
753
|
const detailSent = detailRpc && Object.keys(withValues).length ? encode(detail, withValues) : undefined;
|
|
754
754
|
const answer = await execute({ method: detail.method.toUpperCase(), path: sentTo(m, marked(detail, Object.keys(detailQuery).length ? `${path}?${new URLSearchParams(detailQuery)}` : path), { ...item, ...under?.params }), ...onLane, headers: { ...fixedHeaders(detail), ...(detailSent?.type ? { 'content-type': detailSent.type } : {}) }, ...(detailSent?.body !== undefined ? { body: detailSent.body } : {}) });
|
|
755
755
|
if (answer.status >= 400 || refusedIn(m, parse(answer)))
|
|
756
|
-
throw new Error(`${answer.status}: ${refusalWords(m, parse(answer)) || 'the vendor refused the refresh detail; nothing folded'}`);
|
|
756
|
+
throw new Error(`${m.vendor}: ${resource} refresh detail ${detail.id} (${detail.method.toUpperCase()} ${detail.path}): ${answer.status}: ${refusalWords(m, parse(answer)) || 'the vendor refused the refresh detail; nothing folded'}`);
|
|
757
757
|
const detailBody = parse(answer);
|
|
758
758
|
const detailKey = scope.detailMerge ?? detail.answers?.key;
|
|
759
759
|
const fragment = object(detailKey ? at(detailBody, detailKey) : detailBody);
|
|
@@ -857,7 +857,7 @@ function refreshOf(units, service) {
|
|
|
857
857
|
// parent is never the account's whole list there
|
|
858
858
|
if (whole && !decl.parent && !opts.scope)
|
|
859
859
|
complete.push(type);
|
|
860
|
-
else if (whole && parents.length)
|
|
860
|
+
else if (whole && parents.length && decl.parent)
|
|
861
861
|
complete.push({ type, field: decl.parent.field, within: parents.map((p) => p.value) });
|
|
862
862
|
}
|
|
863
863
|
}
|
package/dist/src/head.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TwinAction } from './actions.js';
|
|
2
2
|
import { type CredentialPayload, type Sealer } from './credential.js';
|
|
3
3
|
import { type CredentialCustody } from './executor.js';
|
|
4
|
-
import type { RemoteExecute } from './remote-execute.js';
|
|
4
|
+
import type { RemoteExecute, RemoteExecuteResponse } from './remote-execute.js';
|
|
5
5
|
import { type Check, type DeployPolicy, type RootConfig, type StateSystemAdapters } from './state-system.js';
|
|
6
6
|
/** What a perform answers: the id the vendor minted (the local id when it kept ours), its URL, and
|
|
7
7
|
* the vendor's answer as fields for the landed copy. */
|
|
@@ -9,6 +9,8 @@ export type PushOutcome = {
|
|
|
9
9
|
externalId: string;
|
|
10
10
|
url?: string;
|
|
11
11
|
data?: Record<string, unknown>;
|
|
12
|
+
/** the successful vendor response for the caller, distinct from the resource fields stored in data */
|
|
13
|
+
response?: RemoteExecuteResponse;
|
|
12
14
|
/** the group's entry the id is for, when it is not the group's first (a cascade writes children first) */
|
|
13
15
|
actionId?: string;
|
|
14
16
|
/** the group's other entries the answer named an id for (a resource's `companions`) */
|
|
@@ -123,6 +125,7 @@ export type DeployReport = {
|
|
|
123
125
|
externalId?: string;
|
|
124
126
|
at: string; /** what the vendor answered, as the adapter returned it */
|
|
125
127
|
data?: unknown;
|
|
128
|
+
response?: RemoteExecuteResponse;
|
|
126
129
|
}>;
|
|
127
130
|
/** the first refusal; nothing after it was attempted */
|
|
128
131
|
refused?: {
|
|
@@ -187,6 +190,7 @@ export declare function deferPerform<T>(request: string, fn: () => Promise<T>):
|
|
|
187
190
|
* reverts one. Nothing when the head is not real and auto, or the request wrote nothing the vendor is offered. */
|
|
188
191
|
export declare function performRequestGroup(service: string, request: string, root?: string): Promise<{
|
|
189
192
|
ids: Map<string, string>;
|
|
193
|
+
response?: RemoteExecuteResponse;
|
|
190
194
|
} | RefusedWriteError | VendorWriteError>;
|
|
191
195
|
/** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
|
|
192
196
|
* vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
|
package/dist/src/head.js
CHANGED
|
@@ -412,7 +412,7 @@ async function performNow(opts) {
|
|
|
412
412
|
settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, { ...action.fields, ...(action === primary ? answered : also.get(action.id)?.data ?? {}) }, externalId || undefined);
|
|
413
413
|
const data = action === primary ? outcome.data : also.get(action.id)?.data;
|
|
414
414
|
report.pushed += 1;
|
|
415
|
-
report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}) });
|
|
415
|
+
report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}), ...(action === primary && outcome.response ? { response: outcome.response } : {}) });
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
catch (error) {
|
|
@@ -514,7 +514,8 @@ export async function performRequestGroup(service, request, root) {
|
|
|
514
514
|
if (e && d.externalId && d.externalId !== e.subject.id)
|
|
515
515
|
ids.set(e.subject.id, d.externalId);
|
|
516
516
|
}
|
|
517
|
-
|
|
517
|
+
const response = report.deployed.find((entry) => entry.response)?.response;
|
|
518
|
+
return { ids, ...(response ? { response } : {}) };
|
|
518
519
|
}
|
|
519
520
|
/** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
|
|
520
521
|
* vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
|
package/dist/src/pack-fetch.js
CHANGED
|
@@ -557,6 +557,13 @@ export function createPackFetch(parts, options = {}) {
|
|
|
557
557
|
}
|
|
558
558
|
if (performed instanceof VendorWriteError)
|
|
559
559
|
return vendorError(m, { status: 502, message: performed.message });
|
|
560
|
+
if (performed.response) {
|
|
561
|
+
const { status, headers: received, body, bodyBytes } = performed.response;
|
|
562
|
+
const headers = new Headers(received);
|
|
563
|
+
for (const name of ['content-length', 'content-encoding', 'transfer-encoding', 'connection', 'keep-alive'])
|
|
564
|
+
headers.delete(name);
|
|
565
|
+
return new Response([204, 205, 304].includes(status) ? null : bodyBytes ? new Uint8Array(bodyBytes).buffer : body, { status, headers });
|
|
566
|
+
}
|
|
560
567
|
if (!performed.ids.size || !(answer.headers.get('content-type') ?? '').includes('json'))
|
|
561
568
|
return answer;
|
|
562
569
|
let text = await answer.text();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.125",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/derived-real.ts
CHANGED
|
@@ -348,7 +348,7 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
348
348
|
adopted.push({ actionId: entry.id, externalId, data });
|
|
349
349
|
}
|
|
350
350
|
const primary = adopted.shift();
|
|
351
|
-
return primary ? { ...primary, ...(adopted.length ? { also: adopted } : {}) } : { externalId: '', actionId: group.at(-1)!.id };
|
|
351
|
+
return primary ? { ...primary, response: answer, ...(adopted.length ? { also: adopted } : {}) } : { externalId: '', actionId: group.at(-1)!.id, response: answer };
|
|
352
352
|
}
|
|
353
353
|
// an XML answer is read through its document element (S3's `InitiateMultipartUploadResult`), as a JSON one under its key
|
|
354
354
|
const documentElement = (): Row | undefined => {
|
|
@@ -365,7 +365,7 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
365
365
|
// CreateSecretResponse, S3's CreateMultipartUploadOutput) is the group's subject's, its id read by that resource
|
|
366
366
|
const subjectDecl = (): ResourceDecl | undefined => Object.entries(m.resources).find(([r]) => storedType(m, r) === group.at(-1)!.subject.type)?.[1];
|
|
367
367
|
const decl: ResourceDecl | undefined = resource ? m.resources[resource] : subjectDecl();
|
|
368
|
-
const idOf = (o: Row | undefined, d: ResourceDecl | undefined): string | undefined =>
|
|
368
|
+
const idOf = (o: Row | undefined, d: ResourceDecl | undefined): string | undefined => o ? storedIdOf(d ?? { idPrefix: '' }, o) : undefined;
|
|
369
369
|
// the group's entry the answer is: of the answered resource's stored type (a cascade writes children first), else
|
|
370
370
|
// the last; a delete answers its subject's own vendor id
|
|
371
371
|
const answered = resource ? [...group].reverse().find((e) => e.subject.type === storedType(m, resource)) : undefined;
|
|
@@ -373,7 +373,7 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
373
373
|
// an answer about a subject no entry of the group is (Slack's conversations.join answers the channel; the group is
|
|
374
374
|
// the membership and its channel_join message) gives no entry its id or its fields: each settles deployed with no
|
|
375
375
|
// vendor id, and refresh adopts it ("Perform", step 2)
|
|
376
|
-
if (resource && !answered) return { externalId: '', actionId: primary.id };
|
|
376
|
+
if (resource && !answered) return { externalId: '', actionId: primary.id, response: answer };
|
|
377
377
|
const deleted = primary.fields?.deleted === true;
|
|
378
378
|
// a keyed subject's id is its key filled from the answer (its envelope's fields, Slack's `channel` and `ts`, among them)
|
|
379
379
|
const keyed = decl?.key ? decl : Object.entries(m.resources).find(([r]) => storedType(m, r) === primary.subject.type)?.[1];
|
|
@@ -383,9 +383,9 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
383
383
|
for (const [companion, where] of Object.entries(decl?.companions ?? keyed?.companions ?? {})) {
|
|
384
384
|
const id = at(parsed, where) ?? at(obj, where);
|
|
385
385
|
const entry = group.find((e) => e !== primary && e.subject.type === storedType(m, companion));
|
|
386
|
-
if (entry && typeof id === 'string') also.push({ actionId: entry.id, externalId: id });
|
|
386
|
+
if (entry && (typeof id === 'string' || typeof id === 'number')) also.push({ actionId: entry.id, externalId: String(id) });
|
|
387
387
|
}
|
|
388
|
-
return { externalId, actionId: primary.id, data: obj, ...(also.length ? { also } : {}) };
|
|
388
|
+
return { externalId, actionId: primary.id, data: obj, response: answer, ...(also.length ? { also } : {}) };
|
|
389
389
|
};
|
|
390
390
|
return async (execute: RemoteExecute, group: TwinAction[], ctx: PerformContext): Promise<PushOutcome> => {
|
|
391
391
|
await adoptAccount(units, execute, ctx);
|
|
@@ -529,7 +529,7 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
|
|
|
529
529
|
read.push(String(subject.id));
|
|
530
530
|
answered += 1;
|
|
531
531
|
if (answer.status === 404) continue;
|
|
532
|
-
if (answer.status >= 400 || refusedIn(m, parse(answer))) throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
532
|
+
if (answer.status >= 400 || refusedIn(m, parse(answer))) throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
533
533
|
const full = object(op.answers?.key ? at(parse(answer), op.answers.key) : parse(answer)) ?? {};
|
|
534
534
|
const local = (m as { worldFields?: Record<string, Record<string, unknown>> }).worldFields?.[resource] ?? {};
|
|
535
535
|
const fields = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
|
|
@@ -596,7 +596,7 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
|
|
|
596
596
|
// one subject read by its parent and not there (an account with no workers.dev subdomain): nothing to observe. A
|
|
597
597
|
// path missing a parameter (an empty segment) is the kernel's mistake, never the vendor's absence
|
|
598
598
|
if ('get' in scope && answer.status === 404 && !unfilled) { answered += 1; break; }
|
|
599
|
-
if (answer.status >= 400 || refusedIn(m, parse(answer))) throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
599
|
+
if (answer.status >= 400 || refusedIn(m, parse(answer))) throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
600
600
|
answered += 1;
|
|
601
601
|
const parsed = parse(answer);
|
|
602
602
|
const shape = 'list' in scope ? scope.shape : undefined;
|
|
@@ -643,7 +643,7 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
|
|
|
643
643
|
const detailQuery = { ...(scope.query ?? {}), ...(detailRpc ? {} : withValues) };
|
|
644
644
|
const detailSent = detailRpc && Object.keys(withValues).length ? encode(detail, withValues) : undefined;
|
|
645
645
|
const answer = await execute({ method: detail.method.toUpperCase(), path: sentTo(m, marked(detail, Object.keys(detailQuery).length ? `${path}?${new URLSearchParams(detailQuery)}` : path), { ...item, ...under?.params }), ...onLane, headers: { ...fixedHeaders(detail), ...(detailSent?.type ? { 'content-type': detailSent.type } : {}) }, ...(detailSent?.body !== undefined ? { body: detailSent.body } : {}) });
|
|
646
|
-
if (answer.status >= 400 || refusedIn(m, parse(answer))) throw new Error(`${answer.status}: ${refusalWords(m, parse(answer)) || 'the vendor refused the refresh detail; nothing folded'}`);
|
|
646
|
+
if (answer.status >= 400 || refusedIn(m, parse(answer))) throw new Error(`${m.vendor}: ${resource} refresh detail ${detail.id} (${detail.method.toUpperCase()} ${detail.path}): ${answer.status}: ${refusalWords(m, parse(answer)) || 'the vendor refused the refresh detail; nothing folded'}`);
|
|
647
647
|
const detailBody = parse(answer);
|
|
648
648
|
const detailKey = scope.detailMerge ?? detail.answers?.key;
|
|
649
649
|
const fragment = object(detailKey ? at(detailBody, detailKey) : detailBody);
|
|
@@ -718,7 +718,7 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
|
|
|
718
718
|
// scoped to one resource (`RootConfig.scope`, GitHub's `repos/o/r`) lists only what is under it, so a type with no
|
|
719
719
|
// parent is never the account's whole list there
|
|
720
720
|
if (whole && !decl.parent && !opts.scope) complete.push(type);
|
|
721
|
-
else if (whole && parents.length) complete.push({ type, field: decl.parent
|
|
721
|
+
else if (whole && parents.length && decl.parent) complete.push({ type, field: decl.parent.field, within: parents.map((p) => p!.value) });
|
|
722
722
|
}
|
|
723
723
|
}
|
|
724
724
|
// every read refused is a credential the vendor refuses, never an account that holds nothing
|
package/src/head.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { confirmAction, currentCorrelationId, isTwinBookkeeping, resolveSubjectI
|
|
|
14
14
|
import { keySealer, type CredentialPayload, type SealedCredential, type Sealer } from './credential.ts';
|
|
15
15
|
import { buildRemoteExecute, validateRemoteOrigin, type CredentialCustody } from './executor.ts';
|
|
16
16
|
import { getPack } from './packRegistry.ts';
|
|
17
|
-
import type { RemoteExecute } from './remote-execute.ts';
|
|
17
|
+
import type { RemoteExecute, RemoteExecuteResponse } from './remote-execute.ts';
|
|
18
18
|
import { aliasesFrom, branchEntries, parentEntries, readTree, type Entry, type Receipt } from './log.ts';
|
|
19
19
|
import { resolveReferences } from './references.ts';
|
|
20
20
|
import { authStrategyFor, NO_SECRETS_CHECK, readRoot, runChecks, stateSystemFor, type Check, type DeployPolicy, type RootConfig, type StateSystemAdapters } from './state-system.ts';
|
|
@@ -28,6 +28,8 @@ import { getActiveWorldStore } from './world-store.ts';
|
|
|
28
28
|
* the vendor's answer as fields for the landed copy. */
|
|
29
29
|
export type PushOutcome = {
|
|
30
30
|
externalId: string; url?: string; data?: Record<string, unknown>;
|
|
31
|
+
/** the successful vendor response for the caller, distinct from the resource fields stored in data */
|
|
32
|
+
response?: RemoteExecuteResponse;
|
|
31
33
|
/** the group's entry the id is for, when it is not the group's first (a cascade writes children first) */
|
|
32
34
|
actionId?: string;
|
|
33
35
|
/** the group's other entries the answer named an id for (a resource's `companions`) */
|
|
@@ -253,7 +255,7 @@ export class VendorWriteError extends Error {
|
|
|
253
255
|
export type DeployReport = {
|
|
254
256
|
/** entries performed by this call */
|
|
255
257
|
pushed: number;
|
|
256
|
-
deployed: Array<{ actionId: string; externalId?: string; at: string; /** what the vendor answered, as the adapter returned it */ data?: unknown }>;
|
|
258
|
+
deployed: Array<{ actionId: string; externalId?: string; at: string; /** what the vendor answered, as the adapter returned it */ data?: unknown; response?: RemoteExecuteResponse }>;
|
|
257
259
|
/** the first refusal; nothing after it was attempted */
|
|
258
260
|
refused?: { actionId: string; check: string; reason: string };
|
|
259
261
|
/** the first failure, with the vendor's words; nothing after it was attempted */
|
|
@@ -417,7 +419,7 @@ async function performNow(opts: Parameters<typeof performEntries>[0]): Promise<D
|
|
|
417
419
|
settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, { ...action.fields, ...(action === primary ? answered : also.get(action.id)?.data ?? {}) }, externalId || undefined);
|
|
418
420
|
const data = action === primary ? outcome.data : also.get(action.id)?.data;
|
|
419
421
|
report.pushed += 1;
|
|
420
|
-
report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}) });
|
|
422
|
+
report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}), ...(action === primary && outcome.response ? { response: outcome.response } : {}) });
|
|
421
423
|
}
|
|
422
424
|
} catch (error) {
|
|
423
425
|
// a write the vendor refuses (a 4xx: it can never be sent as it is) settles the group refused, as a check's
|
|
@@ -475,7 +477,7 @@ export async function deferPerform<T>(request: string, fn: () => Promise<T>): Pr
|
|
|
475
477
|
/** Under `auto`, perform the entries one request made as one call (the real-system adapters, "Perform"): the ids the
|
|
476
478
|
* vendor gave them by local id, or the vendor's refusal or failure, the request's entries reverted as performAtHead
|
|
477
479
|
* reverts one. Nothing when the head is not real and auto, or the request wrote nothing the vendor is offered. */
|
|
478
|
-
export async function performRequestGroup(service: string, request: string, root?: string): Promise<{ ids: Map<string, string
|
|
480
|
+
export async function performRequestGroup(service: string, request: string, root?: string): Promise<{ ids: Map<string, string>; response?: RemoteExecuteResponse } | RefusedWriteError | VendorWriteError> {
|
|
479
481
|
const head = headOf(service, root);
|
|
480
482
|
const ids = new Map<string, string>();
|
|
481
483
|
if (head.kind !== 'real' || head.deploy !== 'auto') return { ids };
|
|
@@ -492,7 +494,8 @@ export async function performRequestGroup(service: string, request: string, root
|
|
|
492
494
|
const e = group.find((g) => g.id === d.actionId);
|
|
493
495
|
if (e && d.externalId && d.externalId !== e.subject.id) ids.set(e.subject.id, d.externalId);
|
|
494
496
|
}
|
|
495
|
-
|
|
497
|
+
const response = report.deployed.find((entry) => entry.response)?.response;
|
|
498
|
+
return { ids, ...(response ? { response } : {}) };
|
|
496
499
|
}
|
|
497
500
|
|
|
498
501
|
/** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
|
package/src/pack-fetch.ts
CHANGED
|
@@ -563,6 +563,12 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
563
563
|
// the vendor's refusal, as its status and words (perform names them `<status>: <message>`), in the vendor's error body
|
|
564
564
|
if (performed instanceof RefusedWriteError) { const said = /^(\d{3}): ([\s\S]*)$/.exec(performed.reason); return vendorError(m, { status: said ? Number(said[1]) : 400, message: said ? said[2]! : performed.reason }); }
|
|
565
565
|
if (performed instanceof VendorWriteError) return vendorError(m, { status: 502, message: performed.message });
|
|
566
|
+
if (performed.response) {
|
|
567
|
+
const { status, headers: received, body, bodyBytes } = performed.response;
|
|
568
|
+
const headers = new Headers(received);
|
|
569
|
+
for (const name of ['content-length', 'content-encoding', 'transfer-encoding', 'connection', 'keep-alive']) headers.delete(name);
|
|
570
|
+
return new Response([204, 205, 304].includes(status) ? null : bodyBytes ? new Uint8Array(bodyBytes).buffer : body, { status, headers });
|
|
571
|
+
}
|
|
566
572
|
if (!performed.ids.size || !(answer.headers.get('content-type') ?? '').includes('json')) return answer;
|
|
567
573
|
let text = await answer.text();
|
|
568
574
|
for (const [local, vendor] of performed.ids) text = text.split(JSON.stringify(local)).join(JSON.stringify(vendor));
|