@exaudeus/workrail 1.2.0 → 1.3.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/dist/manifest.json +32 -0
- package/dist/v2/durable-core/domain/bundle-builder.d.ts +27 -0
- package/dist/v2/durable-core/domain/bundle-builder.js +93 -0
- package/dist/v2/durable-core/domain/bundle-validator.d.ts +4 -0
- package/dist/v2/durable-core/domain/bundle-validator.js +129 -0
- package/dist/v2/usecases/export-session.d.ts +47 -0
- package/dist/v2/usecases/export-session.js +92 -0
- package/dist/v2/usecases/import-session.d.ts +34 -0
- package/dist/v2/usecases/import-session.js +57 -0
- package/package.json +1 -1
package/dist/manifest.json
CHANGED
|
@@ -1081,6 +1081,22 @@
|
|
|
1081
1081
|
"sha256": "f3c86be68aaa045c57c942459031a72df2b2d514e32a57905c1a57f8374e947f",
|
|
1082
1082
|
"bytes": 1718
|
|
1083
1083
|
},
|
|
1084
|
+
"v2/durable-core/domain/bundle-builder.d.ts": {
|
|
1085
|
+
"sha256": "4038983872901590c2aaaa6a27ad2af2f381e2816627f01f8d4276435c8c8a87",
|
|
1086
|
+
"bytes": 1221
|
|
1087
|
+
},
|
|
1088
|
+
"v2/durable-core/domain/bundle-builder.js": {
|
|
1089
|
+
"sha256": "f5422dda0c2a6591d7adc6460be49eb3967298658b51ebf0afd4f041a5395305",
|
|
1090
|
+
"bytes": 3468
|
|
1091
|
+
},
|
|
1092
|
+
"v2/durable-core/domain/bundle-validator.d.ts": {
|
|
1093
|
+
"sha256": "549df7b80ab5c2adcdb7c14e2ae7c668ca3531dfd53aab32bdd364180ef4cecd",
|
|
1094
|
+
"bytes": 334
|
|
1095
|
+
},
|
|
1096
|
+
"v2/durable-core/domain/bundle-validator.js": {
|
|
1097
|
+
"sha256": "b68d6fbbbe5bbb207d566118f9d7080ce39f51ba3eb8568a588861ef5639341c",
|
|
1098
|
+
"bytes": 5925
|
|
1099
|
+
},
|
|
1084
1100
|
"v2/durable-core/domain/context-merge.d.ts": {
|
|
1085
1101
|
"sha256": "e3068d49237d2ae351cf88c6829bf612dc7ced6f726b618e7f8aeb7462d34256",
|
|
1086
1102
|
"bytes": 381
|
|
@@ -1840,6 +1856,22 @@
|
|
|
1840
1856
|
"v2/usecases/execution-session-gate.js": {
|
|
1841
1857
|
"sha256": "41a25f3046ed474c6c6223655ca1c21d0666aea171c024f51b85071619abfa52",
|
|
1842
1858
|
"bytes": 7635
|
|
1859
|
+
},
|
|
1860
|
+
"v2/usecases/export-session.d.ts": {
|
|
1861
|
+
"sha256": "037c6efb463c121feb41eff43d3b035e8443a12d25fcffb5387453ac5a49ae44",
|
|
1862
|
+
"bytes": 2049
|
|
1863
|
+
},
|
|
1864
|
+
"v2/usecases/export-session.js": {
|
|
1865
|
+
"sha256": "a91125866d50fe0a00bc8ef7d1a29bd43958b8e4eaff2e73a8247384e2d113db",
|
|
1866
|
+
"bytes": 4092
|
|
1867
|
+
},
|
|
1868
|
+
"v2/usecases/import-session.d.ts": {
|
|
1869
|
+
"sha256": "30c470707f5370f2b90d786f1d959a0a01bc4fecf3296a523a49518c626f99a8",
|
|
1870
|
+
"bytes": 1514
|
|
1871
|
+
},
|
|
1872
|
+
"v2/usecases/import-session.js": {
|
|
1873
|
+
"sha256": "9b9c8922cc2e6d6904f564c35ff0e402a2e755187f36ee7e54e76826f51bed5c",
|
|
1874
|
+
"bytes": 2788
|
|
1843
1875
|
}
|
|
1844
1876
|
}
|
|
1845
1877
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Result } from 'neverthrow';
|
|
2
|
+
import type { DomainEventV1 } from '../schemas/session/events.js';
|
|
3
|
+
import type { ManifestRecordV1 } from '../schemas/session/index.js';
|
|
4
|
+
import type { ExecutionSnapshotFileV1 } from '../schemas/execution-snapshot/index.js';
|
|
5
|
+
import type { ExportBundleV1 } from '../schemas/export-bundle/index.js';
|
|
6
|
+
import type { Sha256Digest } from '../ids/index.js';
|
|
7
|
+
export interface BuildExportBundleArgs {
|
|
8
|
+
readonly bundleId: string;
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly events: readonly DomainEventV1[];
|
|
11
|
+
readonly manifest: readonly ManifestRecordV1[];
|
|
12
|
+
readonly snapshots: ReadonlyMap<string, ExecutionSnapshotFileV1>;
|
|
13
|
+
readonly pinnedWorkflows: ReadonlyMap<string, unknown>;
|
|
14
|
+
readonly producer: {
|
|
15
|
+
readonly appVersion: string;
|
|
16
|
+
readonly appliedConfigHash?: string;
|
|
17
|
+
};
|
|
18
|
+
readonly sha256: (bytes: Uint8Array) => Sha256Digest;
|
|
19
|
+
}
|
|
20
|
+
export type BundleBuilderError = {
|
|
21
|
+
readonly code: 'BUNDLE_BUILD_EMPTY_EVENTS';
|
|
22
|
+
readonly message: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly code: 'BUNDLE_BUILD_CANONICALIZE_FAILED';
|
|
25
|
+
readonly message: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function buildExportBundle(args: BuildExportBundleArgs): Result<ExportBundleV1, BundleBuilderError>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildExportBundle = buildExportBundle;
|
|
4
|
+
const neverthrow_1 = require("neverthrow");
|
|
5
|
+
const jcs_js_1 = require("../canonical/jcs.js");
|
|
6
|
+
function buildExportBundle(args) {
|
|
7
|
+
if (args.events.length === 0) {
|
|
8
|
+
return (0, neverthrow_1.err)({
|
|
9
|
+
code: 'BUNDLE_BUILD_EMPTY_EVENTS',
|
|
10
|
+
message: 'Cannot export a session with no events',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const snapshotsRecord = {};
|
|
14
|
+
for (const [ref, snapshot] of args.snapshots) {
|
|
15
|
+
snapshotsRecord[ref] = snapshot;
|
|
16
|
+
}
|
|
17
|
+
const pinnedWorkflowsRecord = {};
|
|
18
|
+
for (const [hash, compiled] of args.pinnedWorkflows) {
|
|
19
|
+
pinnedWorkflowsRecord[hash] = compiled;
|
|
20
|
+
}
|
|
21
|
+
const integrityResult = computeIntegrityEntries({
|
|
22
|
+
events: args.events,
|
|
23
|
+
manifest: args.manifest,
|
|
24
|
+
snapshots: snapshotsRecord,
|
|
25
|
+
pinnedWorkflows: pinnedWorkflowsRecord,
|
|
26
|
+
sha256: args.sha256,
|
|
27
|
+
});
|
|
28
|
+
if (integrityResult.isErr())
|
|
29
|
+
return (0, neverthrow_1.err)(integrityResult.error);
|
|
30
|
+
const bundle = {
|
|
31
|
+
bundleSchemaVersion: 1,
|
|
32
|
+
bundleId: args.bundleId,
|
|
33
|
+
exportedAt: new Date().toISOString(),
|
|
34
|
+
producer: {
|
|
35
|
+
appVersion: args.producer.appVersion,
|
|
36
|
+
...(args.producer.appliedConfigHash != null
|
|
37
|
+
? { appliedConfigHash: args.producer.appliedConfigHash }
|
|
38
|
+
: {}),
|
|
39
|
+
},
|
|
40
|
+
integrity: {
|
|
41
|
+
kind: 'sha256_manifest_v1',
|
|
42
|
+
entries: integrityResult.value,
|
|
43
|
+
},
|
|
44
|
+
session: {
|
|
45
|
+
sessionId: args.sessionId,
|
|
46
|
+
events: args.events,
|
|
47
|
+
manifest: args.manifest,
|
|
48
|
+
snapshots: snapshotsRecord,
|
|
49
|
+
pinnedWorkflows: pinnedWorkflowsRecord,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
return (0, neverthrow_1.ok)(bundle);
|
|
53
|
+
}
|
|
54
|
+
function computeIntegrityEntries(args) {
|
|
55
|
+
const entries = [];
|
|
56
|
+
const eventsEntry = computeEntry('session/events', args.events, args.sha256);
|
|
57
|
+
if (eventsEntry.isErr())
|
|
58
|
+
return (0, neverthrow_1.err)(eventsEntry.error);
|
|
59
|
+
entries.push(eventsEntry.value);
|
|
60
|
+
const manifestEntry = computeEntry('session/manifest', args.manifest, args.sha256);
|
|
61
|
+
if (manifestEntry.isErr())
|
|
62
|
+
return (0, neverthrow_1.err)(manifestEntry.error);
|
|
63
|
+
entries.push(manifestEntry.value);
|
|
64
|
+
for (const [ref, snapshot] of Object.entries(args.snapshots)) {
|
|
65
|
+
const entry = computeEntry(`session/snapshots/${ref}`, snapshot, args.sha256);
|
|
66
|
+
if (entry.isErr())
|
|
67
|
+
return (0, neverthrow_1.err)(entry.error);
|
|
68
|
+
entries.push(entry.value);
|
|
69
|
+
}
|
|
70
|
+
for (const [hash, compiled] of Object.entries(args.pinnedWorkflows)) {
|
|
71
|
+
const entry = computeEntry(`session/pinnedWorkflows/${hash}`, compiled, args.sha256);
|
|
72
|
+
if (entry.isErr())
|
|
73
|
+
return (0, neverthrow_1.err)(entry.error);
|
|
74
|
+
entries.push(entry.value);
|
|
75
|
+
}
|
|
76
|
+
return (0, neverthrow_1.ok)(entries);
|
|
77
|
+
}
|
|
78
|
+
function computeEntry(path, value, sha256) {
|
|
79
|
+
const canonicalResult = (0, jcs_js_1.toCanonicalBytes)(value);
|
|
80
|
+
if (canonicalResult.isErr()) {
|
|
81
|
+
return (0, neverthrow_1.err)({
|
|
82
|
+
code: 'BUNDLE_BUILD_CANONICALIZE_FAILED',
|
|
83
|
+
message: `Failed to canonicalize ${path}: ${canonicalResult.error.message}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const bytes = canonicalResult.value;
|
|
87
|
+
const digest = sha256(bytes);
|
|
88
|
+
return (0, neverthrow_1.ok)({
|
|
89
|
+
path,
|
|
90
|
+
sha256: digest,
|
|
91
|
+
bytes: bytes.byteLength,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Result } from 'neverthrow';
|
|
2
|
+
import { type ExportBundleV1, type BundleImportError } from '../schemas/export-bundle/index.js';
|
|
3
|
+
import type { Sha256Digest } from '../ids/index.js';
|
|
4
|
+
export declare function validateBundle(raw: unknown, sha256: (bytes: Uint8Array) => Sha256Digest): Result<ExportBundleV1, BundleImportError>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateBundle = validateBundle;
|
|
4
|
+
const neverthrow_1 = require("neverthrow");
|
|
5
|
+
const index_js_1 = require("../schemas/export-bundle/index.js");
|
|
6
|
+
const jcs_js_1 = require("../canonical/jcs.js");
|
|
7
|
+
function validateBundle(raw, sha256) {
|
|
8
|
+
const schemaResult = validateSchema(raw);
|
|
9
|
+
if (schemaResult.isErr())
|
|
10
|
+
return schemaResult;
|
|
11
|
+
const bundle = schemaResult.value;
|
|
12
|
+
const integrityResult = validateIntegrity(bundle, sha256);
|
|
13
|
+
if (integrityResult.isErr())
|
|
14
|
+
return (0, neverthrow_1.err)(integrityResult.error);
|
|
15
|
+
const orderingResult = validateOrdering(bundle);
|
|
16
|
+
if (orderingResult.isErr())
|
|
17
|
+
return (0, neverthrow_1.err)(orderingResult.error);
|
|
18
|
+
const referencesResult = validateReferences(bundle);
|
|
19
|
+
if (referencesResult.isErr())
|
|
20
|
+
return (0, neverthrow_1.err)(referencesResult.error);
|
|
21
|
+
return (0, neverthrow_1.ok)(bundle);
|
|
22
|
+
}
|
|
23
|
+
function validateSchema(raw) {
|
|
24
|
+
if (typeof raw === 'object' &&
|
|
25
|
+
raw !== null &&
|
|
26
|
+
'bundleSchemaVersion' in raw &&
|
|
27
|
+
raw.bundleSchemaVersion !== 1) {
|
|
28
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_UNSUPPORTED_VERSION', `Unsupported bundle schema version: ${String(raw.bundleSchemaVersion)}. Expected 1.`));
|
|
29
|
+
}
|
|
30
|
+
const parsed = index_js_1.ExportBundleV1Schema.safeParse(raw);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_INVALID_FORMAT', `Bundle schema validation failed: ${parsed.error.issues.map(i => i.message).join('; ')}`));
|
|
33
|
+
}
|
|
34
|
+
return (0, neverthrow_1.ok)(parsed.data);
|
|
35
|
+
}
|
|
36
|
+
function validateIntegrity(bundle, sha256) {
|
|
37
|
+
for (const entry of bundle.integrity.entries) {
|
|
38
|
+
const value = resolveIntegrityPath(bundle, entry.path);
|
|
39
|
+
if (value === undefined) {
|
|
40
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_INTEGRITY_FAILED', `Integrity path not found in bundle: ${entry.path}`));
|
|
41
|
+
}
|
|
42
|
+
const canonicalResult = (0, jcs_js_1.toCanonicalBytes)(value);
|
|
43
|
+
if (canonicalResult.isErr()) {
|
|
44
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_INTEGRITY_FAILED', `Failed to canonicalize ${entry.path}: ${canonicalResult.error.message}`));
|
|
45
|
+
}
|
|
46
|
+
const bytes = canonicalResult.value;
|
|
47
|
+
const computedDigest = sha256(bytes);
|
|
48
|
+
if (computedDigest !== entry.sha256) {
|
|
49
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_INTEGRITY_FAILED', `Integrity mismatch for ${entry.path}: expected ${entry.sha256}, got ${computedDigest}`));
|
|
50
|
+
}
|
|
51
|
+
if (bytes.byteLength !== entry.bytes) {
|
|
52
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_INTEGRITY_FAILED', `Byte length mismatch for ${entry.path}: expected ${String(entry.bytes)}, got ${String(bytes.byteLength)}`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return (0, neverthrow_1.ok)(undefined);
|
|
56
|
+
}
|
|
57
|
+
function resolveIntegrityPath(bundle, path) {
|
|
58
|
+
if (path === 'session/events')
|
|
59
|
+
return bundle.session.events;
|
|
60
|
+
if (path === 'session/manifest')
|
|
61
|
+
return bundle.session.manifest;
|
|
62
|
+
const snapshotPrefix = 'session/snapshots/';
|
|
63
|
+
if (path.startsWith(snapshotPrefix)) {
|
|
64
|
+
const ref = path.slice(snapshotPrefix.length);
|
|
65
|
+
return bundle.session.snapshots[ref];
|
|
66
|
+
}
|
|
67
|
+
const workflowPrefix = 'session/pinnedWorkflows/';
|
|
68
|
+
if (path.startsWith(workflowPrefix)) {
|
|
69
|
+
const hash = path.slice(workflowPrefix.length);
|
|
70
|
+
return bundle.session.pinnedWorkflows[hash];
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function validateOrdering(bundle) {
|
|
75
|
+
const events = bundle.session.events;
|
|
76
|
+
for (let i = 1; i < events.length; i++) {
|
|
77
|
+
const prev = events[i - 1].eventIndex;
|
|
78
|
+
const curr = events[i].eventIndex;
|
|
79
|
+
if (curr <= prev) {
|
|
80
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_EVENT_ORDER_INVALID', `Events not in ascending eventIndex order: index ${String(i)} has eventIndex ${String(curr)} ≤ previous ${String(prev)}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const manifest = bundle.session.manifest;
|
|
84
|
+
for (let i = 1; i < manifest.length; i++) {
|
|
85
|
+
const prev = manifest[i - 1].manifestIndex;
|
|
86
|
+
const curr = manifest[i].manifestIndex;
|
|
87
|
+
if (curr <= prev) {
|
|
88
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_MANIFEST_ORDER_INVALID', `Manifest records not in ascending manifestIndex order: index ${String(i)} has manifestIndex ${String(curr)} ≤ previous ${String(prev)}`));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return (0, neverthrow_1.ok)(undefined);
|
|
92
|
+
}
|
|
93
|
+
function validateReferences(bundle) {
|
|
94
|
+
const referencedSnapshots = new Set();
|
|
95
|
+
const referencedWorkflows = new Set();
|
|
96
|
+
for (const event of bundle.session.events) {
|
|
97
|
+
const evt = event;
|
|
98
|
+
if (evt.kind === 'node_created') {
|
|
99
|
+
if (typeof evt.data.snapshotRef === 'string') {
|
|
100
|
+
referencedSnapshots.add(evt.data.snapshotRef);
|
|
101
|
+
}
|
|
102
|
+
if (typeof evt.data.workflowHash === 'string') {
|
|
103
|
+
referencedWorkflows.add(evt.data.workflowHash);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (evt.kind === 'run_started' && typeof evt.data.workflowHash === 'string') {
|
|
107
|
+
referencedWorkflows.add(evt.data.workflowHash);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const ref of referencedSnapshots) {
|
|
111
|
+
if (!(ref in bundle.session.snapshots)) {
|
|
112
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_MISSING_SNAPSHOT', `Referenced snapshotRef not found in bundle: ${ref}`, { missingRef: ref }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const hash of referencedWorkflows) {
|
|
116
|
+
if (!(hash in bundle.session.pinnedWorkflows)) {
|
|
117
|
+
return (0, neverthrow_1.err)(importError('BUNDLE_MISSING_PINNED_WORKFLOW', `Referenced workflowHash not found in bundle: ${hash}`, { missingHash: hash }));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return (0, neverthrow_1.ok)(undefined);
|
|
121
|
+
}
|
|
122
|
+
function importError(code, message, details) {
|
|
123
|
+
return {
|
|
124
|
+
code,
|
|
125
|
+
message,
|
|
126
|
+
retry: 'no',
|
|
127
|
+
...(details != null ? { details } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ResultAsync } from 'neverthrow';
|
|
2
|
+
import type { SessionId, Sha256Digest } from '../durable-core/ids/index.js';
|
|
3
|
+
import type { SessionEventLogReadonlyStorePortV2, SessionEventLogStoreError } from '../ports/session-event-log-store.port.js';
|
|
4
|
+
import type { SnapshotStorePortV2, SnapshotStoreError } from '../ports/snapshot-store.port.js';
|
|
5
|
+
import type { PinnedWorkflowStorePortV2, PinnedWorkflowStoreError } from '../ports/pinned-workflow-store.port.js';
|
|
6
|
+
import type { ExportBundleV1 } from '../durable-core/schemas/export-bundle/index.js';
|
|
7
|
+
import { type BundleBuilderError } from '../durable-core/domain/bundle-builder.js';
|
|
8
|
+
export interface ExportSessionPorts {
|
|
9
|
+
readonly sessionStore: SessionEventLogReadonlyStorePortV2;
|
|
10
|
+
readonly snapshotStore: SnapshotStorePortV2;
|
|
11
|
+
readonly pinnedWorkflowStore: PinnedWorkflowStorePortV2;
|
|
12
|
+
readonly sha256: (bytes: Uint8Array) => Sha256Digest;
|
|
13
|
+
}
|
|
14
|
+
export interface ExportSessionArgs {
|
|
15
|
+
readonly sessionId: SessionId;
|
|
16
|
+
readonly bundleId: string;
|
|
17
|
+
readonly producer: {
|
|
18
|
+
readonly appVersion: string;
|
|
19
|
+
readonly appliedConfigHash?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export type ExportSessionError = {
|
|
23
|
+
readonly code: 'EXPORT_SESSION_STORE_ERROR';
|
|
24
|
+
readonly message: string;
|
|
25
|
+
readonly cause: SessionEventLogStoreError;
|
|
26
|
+
} | {
|
|
27
|
+
readonly code: 'EXPORT_SNAPSHOT_STORE_ERROR';
|
|
28
|
+
readonly message: string;
|
|
29
|
+
readonly cause: SnapshotStoreError;
|
|
30
|
+
} | {
|
|
31
|
+
readonly code: 'EXPORT_PINNED_WORKFLOW_STORE_ERROR';
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly cause: PinnedWorkflowStoreError;
|
|
34
|
+
} | {
|
|
35
|
+
readonly code: 'EXPORT_MISSING_SNAPSHOT';
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly snapshotRef: string;
|
|
38
|
+
} | {
|
|
39
|
+
readonly code: 'EXPORT_MISSING_PINNED_WORKFLOW';
|
|
40
|
+
readonly message: string;
|
|
41
|
+
readonly workflowHash: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly code: 'EXPORT_BUILD_FAILED';
|
|
44
|
+
readonly message: string;
|
|
45
|
+
readonly cause: BundleBuilderError;
|
|
46
|
+
};
|
|
47
|
+
export declare function exportSession(args: ExportSessionArgs, ports: ExportSessionPorts): ResultAsync<ExportBundleV1, ExportSessionError>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exportSession = exportSession;
|
|
4
|
+
const neverthrow_1 = require("neverthrow");
|
|
5
|
+
const bundle_builder_js_1 = require("../durable-core/domain/bundle-builder.js");
|
|
6
|
+
function exportSession(args, ports) {
|
|
7
|
+
return ports.sessionStore
|
|
8
|
+
.load(args.sessionId)
|
|
9
|
+
.mapErr((cause) => ({
|
|
10
|
+
code: 'EXPORT_SESSION_STORE_ERROR',
|
|
11
|
+
message: `Failed to load session ${args.sessionId}: ${cause.message}`,
|
|
12
|
+
cause,
|
|
13
|
+
}))
|
|
14
|
+
.andThen((truth) => {
|
|
15
|
+
const snapshotRefs = new Set();
|
|
16
|
+
const workflowHashes = new Set();
|
|
17
|
+
for (const event of truth.events) {
|
|
18
|
+
const evt = event;
|
|
19
|
+
if (evt.kind === 'node_created') {
|
|
20
|
+
if (typeof evt.data.snapshotRef === 'string')
|
|
21
|
+
snapshotRefs.add(evt.data.snapshotRef);
|
|
22
|
+
if (typeof evt.data.workflowHash === 'string')
|
|
23
|
+
workflowHashes.add(evt.data.workflowHash);
|
|
24
|
+
}
|
|
25
|
+
if (evt.kind === 'run_started' && typeof evt.data.workflowHash === 'string') {
|
|
26
|
+
workflowHashes.add(evt.data.workflowHash);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const snapshotEntries = [...snapshotRefs].map((ref) => ports.snapshotStore
|
|
30
|
+
.getExecutionSnapshotV1(ref)
|
|
31
|
+
.mapErr((cause) => ({
|
|
32
|
+
code: 'EXPORT_SNAPSHOT_STORE_ERROR',
|
|
33
|
+
message: `Failed to load snapshot ${ref}: ${cause.message}`,
|
|
34
|
+
cause,
|
|
35
|
+
}))
|
|
36
|
+
.andThen((snapshot) => {
|
|
37
|
+
if (snapshot === null) {
|
|
38
|
+
return (0, neverthrow_1.errAsync)({
|
|
39
|
+
code: 'EXPORT_MISSING_SNAPSHOT',
|
|
40
|
+
message: `Snapshot not found: ${ref}`,
|
|
41
|
+
snapshotRef: ref,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return (0, neverthrow_1.okAsync)([ref, snapshot]);
|
|
45
|
+
}));
|
|
46
|
+
const workflowEntries = [...workflowHashes].map((hash) => ports.pinnedWorkflowStore
|
|
47
|
+
.get(hash)
|
|
48
|
+
.mapErr((cause) => ({
|
|
49
|
+
code: 'EXPORT_PINNED_WORKFLOW_STORE_ERROR',
|
|
50
|
+
message: `Failed to load pinned workflow ${hash}: ${cause.message}`,
|
|
51
|
+
cause,
|
|
52
|
+
}))
|
|
53
|
+
.andThen((compiled) => {
|
|
54
|
+
if (compiled === null) {
|
|
55
|
+
return (0, neverthrow_1.errAsync)({
|
|
56
|
+
code: 'EXPORT_MISSING_PINNED_WORKFLOW',
|
|
57
|
+
message: `Pinned workflow not found: ${hash}`,
|
|
58
|
+
workflowHash: hash,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return (0, neverthrow_1.okAsync)([hash, compiled]);
|
|
62
|
+
}));
|
|
63
|
+
const allSnapshots = snapshotEntries.length > 0
|
|
64
|
+
? snapshotEntries.reduce((acc, next) => acc.andThen((list) => next.map((entry) => [...list, entry])), (0, neverthrow_1.okAsync)([]))
|
|
65
|
+
: (0, neverthrow_1.okAsync)([]);
|
|
66
|
+
const allWorkflows = workflowEntries.length > 0
|
|
67
|
+
? workflowEntries.reduce((acc, next) => acc.andThen((list) => next.map((entry) => [...list, entry])), (0, neverthrow_1.okAsync)([]))
|
|
68
|
+
: (0, neverthrow_1.okAsync)([]);
|
|
69
|
+
return allSnapshots.andThen((snapshotPairs) => allWorkflows.andThen((workflowPairs) => {
|
|
70
|
+
const snapshots = new Map(snapshotPairs);
|
|
71
|
+
const pinnedWorkflows = new Map(workflowPairs);
|
|
72
|
+
const buildResult = (0, bundle_builder_js_1.buildExportBundle)({
|
|
73
|
+
bundleId: args.bundleId,
|
|
74
|
+
sessionId: args.sessionId,
|
|
75
|
+
events: truth.events,
|
|
76
|
+
manifest: truth.manifest,
|
|
77
|
+
snapshots,
|
|
78
|
+
pinnedWorkflows,
|
|
79
|
+
producer: args.producer,
|
|
80
|
+
sha256: ports.sha256,
|
|
81
|
+
});
|
|
82
|
+
if (buildResult.isErr()) {
|
|
83
|
+
return (0, neverthrow_1.errAsync)({
|
|
84
|
+
code: 'EXPORT_BUILD_FAILED',
|
|
85
|
+
message: `Bundle build failed: ${buildResult.error.message}`,
|
|
86
|
+
cause: buildResult.error,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return (0, neverthrow_1.okAsync)(buildResult.value);
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResultAsync } from 'neverthrow';
|
|
2
|
+
import type { Sha256Digest } from '../durable-core/ids/index.js';
|
|
3
|
+
import type { SnapshotStorePortV2, SnapshotStoreError } from '../ports/snapshot-store.port.js';
|
|
4
|
+
import type { PinnedWorkflowStorePortV2, PinnedWorkflowStoreError } from '../ports/pinned-workflow-store.port.js';
|
|
5
|
+
import type { BundleImportError, ExportBundleV1 } from '../durable-core/schemas/export-bundle/index.js';
|
|
6
|
+
export interface ImportSessionPorts {
|
|
7
|
+
readonly snapshotStore: SnapshotStorePortV2;
|
|
8
|
+
readonly pinnedWorkflowStore: PinnedWorkflowStorePortV2;
|
|
9
|
+
readonly generateSessionId: () => string;
|
|
10
|
+
readonly sha256: (bytes: Uint8Array) => Sha256Digest;
|
|
11
|
+
}
|
|
12
|
+
export interface ImportResult {
|
|
13
|
+
readonly sessionId: string;
|
|
14
|
+
readonly eventCount: number;
|
|
15
|
+
readonly snapshotCount: number;
|
|
16
|
+
readonly pinnedWorkflowCount: number;
|
|
17
|
+
readonly validatedBundle: ExportBundleV1;
|
|
18
|
+
}
|
|
19
|
+
export type ImportSessionError = BundleImportError | {
|
|
20
|
+
readonly code: 'IMPORT_SNAPSHOT_STORE_ERROR';
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly retry: 'no';
|
|
23
|
+
readonly cause: SnapshotStoreError;
|
|
24
|
+
} | {
|
|
25
|
+
readonly code: 'IMPORT_PINNED_WORKFLOW_STORE_ERROR';
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly retry: 'no';
|
|
28
|
+
readonly cause: PinnedWorkflowStoreError;
|
|
29
|
+
} | {
|
|
30
|
+
readonly code: 'IMPORT_SNAPSHOT_PARSE_ERROR';
|
|
31
|
+
readonly message: string;
|
|
32
|
+
readonly retry: 'no';
|
|
33
|
+
};
|
|
34
|
+
export declare function importSession(raw: unknown, ports: ImportSessionPorts): ResultAsync<ImportResult, ImportSessionError>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.importSession = importSession;
|
|
4
|
+
const neverthrow_1 = require("neverthrow");
|
|
5
|
+
const index_js_1 = require("../durable-core/ids/index.js");
|
|
6
|
+
const index_js_2 = require("../durable-core/schemas/execution-snapshot/index.js");
|
|
7
|
+
const bundle_validator_js_1 = require("../durable-core/domain/bundle-validator.js");
|
|
8
|
+
function importSession(raw, ports) {
|
|
9
|
+
const validationResult = (0, bundle_validator_js_1.validateBundle)(raw, ports.sha256);
|
|
10
|
+
if (validationResult.isErr()) {
|
|
11
|
+
return (0, neverthrow_1.errAsync)(validationResult.error);
|
|
12
|
+
}
|
|
13
|
+
const bundle = validationResult.value;
|
|
14
|
+
const newSessionId = ports.generateSessionId();
|
|
15
|
+
const workflowKeys = Object.keys(bundle.session.pinnedWorkflows);
|
|
16
|
+
const storeWorkflows = workflowKeys.length > 0
|
|
17
|
+
? workflowKeys.reduce((chain, hash) => chain.andThen(() => ports.pinnedWorkflowStore
|
|
18
|
+
.put((0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(hash)), bundle.session.pinnedWorkflows[hash])
|
|
19
|
+
.mapErr((cause) => ({
|
|
20
|
+
code: 'IMPORT_PINNED_WORKFLOW_STORE_ERROR',
|
|
21
|
+
message: `Failed to store pinned workflow ${hash}: ${cause.message}`,
|
|
22
|
+
retry: 'no',
|
|
23
|
+
cause,
|
|
24
|
+
}))), (0, neverthrow_1.okAsync)(undefined))
|
|
25
|
+
: (0, neverthrow_1.okAsync)(undefined);
|
|
26
|
+
const snapshotKeys = Object.keys(bundle.session.snapshots);
|
|
27
|
+
const storeSnapshots = snapshotKeys.length > 0
|
|
28
|
+
? snapshotKeys.reduce((chain, ref) => chain.andThen(() => {
|
|
29
|
+
const rawSnapshot = bundle.session.snapshots[ref];
|
|
30
|
+
const parsed = index_js_2.ExecutionSnapshotFileV1Schema.safeParse(rawSnapshot);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return (0, neverthrow_1.errAsync)({
|
|
33
|
+
code: 'IMPORT_SNAPSHOT_PARSE_ERROR',
|
|
34
|
+
message: `Snapshot ${ref} failed schema validation: ${parsed.error.issues.map(i => i.message).join('; ')}`,
|
|
35
|
+
retry: 'no',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return ports.snapshotStore
|
|
39
|
+
.putExecutionSnapshotV1(parsed.data)
|
|
40
|
+
.mapErr((cause) => ({
|
|
41
|
+
code: 'IMPORT_SNAPSHOT_STORE_ERROR',
|
|
42
|
+
message: `Failed to store snapshot ${ref}: ${cause.message}`,
|
|
43
|
+
retry: 'no',
|
|
44
|
+
cause,
|
|
45
|
+
}));
|
|
46
|
+
}), (0, neverthrow_1.okAsync)(undefined))
|
|
47
|
+
: (0, neverthrow_1.okAsync)(undefined);
|
|
48
|
+
return storeWorkflows
|
|
49
|
+
.andThen(() => storeSnapshots)
|
|
50
|
+
.map(() => ({
|
|
51
|
+
sessionId: newSessionId,
|
|
52
|
+
eventCount: bundle.session.events.length,
|
|
53
|
+
snapshotCount: snapshotKeys.length,
|
|
54
|
+
pinnedWorkflowCount: workflowKeys.length,
|
|
55
|
+
validatedBundle: bundle,
|
|
56
|
+
}));
|
|
57
|
+
}
|