@elaraai/e3-types 1.0.79 → 1.0.81
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 +1 -1
- package/dist/src/api.d.ts +829 -36
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +301 -32
- package/dist/src/api.js.map +1 -1
- package/dist/src/collection-manifest.d.ts +37 -0
- package/dist/src/collection-manifest.d.ts.map +1 -0
- package/dist/src/collection-manifest.js +37 -0
- package/dist/src/collection-manifest.js.map +1 -0
- package/dist/src/dataflow.d.ts +264 -19
- package/dist/src/dataflow.d.ts.map +1 -1
- package/dist/src/dataflow.js +156 -15
- package/dist/src/dataflow.js.map +1 -1
- package/dist/src/dataflow.spec.js +184 -27
- package/dist/src/dataflow.spec.js.map +1 -1
- package/dist/src/dataset-blob.d.ts +75 -15
- package/dist/src/dataset-blob.d.ts.map +1 -1
- package/dist/src/dataset-blob.js +95 -28
- package/dist/src/dataset-blob.js.map +1 -1
- package/dist/src/dataset-blob.spec.d.ts +6 -0
- package/dist/src/dataset-blob.spec.d.ts.map +1 -0
- package/dist/src/dataset-blob.spec.js +92 -0
- package/dist/src/dataset-blob.spec.js.map +1 -0
- package/dist/src/dataset-ref.d.ts +5 -4
- package/dist/src/dataset-ref.d.ts.map +1 -1
- package/dist/src/dataset-ref.js +5 -4
- package/dist/src/dataset-ref.js.map +1 -1
- package/dist/src/environment.d.ts +3 -2
- package/dist/src/environment.d.ts.map +1 -1
- package/dist/src/environment.js +3 -2
- package/dist/src/environment.js.map +1 -1
- package/dist/src/execution.d.ts +112 -18
- package/dist/src/execution.d.ts.map +1 -1
- package/dist/src/execution.js +174 -45
- package/dist/src/execution.js.map +1 -1
- package/dist/src/execution.spec.d.ts +6 -0
- package/dist/src/execution.spec.d.ts.map +1 -0
- package/dist/src/execution.spec.js +108 -0
- package/dist/src/execution.spec.js.map +1 -0
- package/dist/src/function.d.ts +14 -12
- package/dist/src/function.d.ts.map +1 -1
- package/dist/src/function.js +17 -32
- package/dist/src/function.js.map +1 -1
- package/dist/src/index.d.ts +17 -12
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +28 -20
- package/dist/src/index.js.map +1 -1
- package/dist/src/intake.d.ts +75 -0
- package/dist/src/intake.d.ts.map +1 -0
- package/dist/src/intake.js +46 -0
- package/dist/src/intake.js.map +1 -0
- package/dist/src/lock.d.ts +253 -21
- package/dist/src/lock.d.ts.map +1 -1
- package/dist/src/lock.js +55 -21
- package/dist/src/lock.js.map +1 -1
- package/dist/src/names.d.ts +21 -0
- package/dist/src/names.d.ts.map +1 -0
- package/dist/src/names.js +31 -0
- package/dist/src/names.js.map +1 -0
- package/dist/src/package.d.ts +11 -78
- package/dist/src/package.d.ts.map +1 -1
- package/dist/src/package.js +13 -66
- package/dist/src/package.js.map +1 -1
- package/dist/src/record.d.ts +582 -18
- package/dist/src/record.d.ts.map +1 -1
- package/dist/src/record.js +405 -7
- package/dist/src/record.js.map +1 -1
- package/dist/src/{stream.spec.d.ts → record.spec.d.ts} +1 -1
- package/dist/src/record.spec.d.ts.map +1 -0
- package/dist/src/record.spec.js +143 -0
- package/dist/src/record.spec.js.map +1 -0
- package/dist/src/release.d.ts +36 -0
- package/dist/src/release.d.ts.map +1 -0
- package/dist/src/release.js +71 -0
- package/dist/src/release.js.map +1 -0
- package/dist/src/release.spec.d.ts +6 -0
- package/dist/src/release.spec.d.ts.map +1 -0
- package/dist/src/release.spec.js +42 -0
- package/dist/src/release.spec.js.map +1 -0
- package/dist/src/repository.d.ts +64 -0
- package/dist/src/repository.d.ts.map +1 -0
- package/dist/src/repository.js +49 -0
- package/dist/src/repository.js.map +1 -0
- package/dist/src/runner.d.ts +16 -62
- package/dist/src/runner.d.ts.map +1 -1
- package/dist/src/runner.js +16 -79
- package/dist/src/runner.js.map +1 -1
- package/dist/src/runner.spec.js +11 -85
- package/dist/src/runner.spec.js.map +1 -1
- package/dist/src/structure.d.ts +0 -57
- package/dist/src/structure.d.ts.map +1 -1
- package/dist/src/structure.js +0 -3
- package/dist/src/structure.js.map +1 -1
- package/dist/src/task.d.ts +210 -318
- package/dist/src/task.d.ts.map +1 -1
- package/dist/src/task.js +117 -472
- package/dist/src/task.js.map +1 -1
- package/dist/src/task.spec.js +62 -154
- package/dist/src/task.spec.js.map +1 -1
- package/dist/src/transfer.d.ts +38 -26
- package/dist/src/transfer.d.ts.map +1 -1
- package/dist/src/transfer.js +22 -26
- package/dist/src/transfer.js.map +1 -1
- package/dist/src/transfer.spec.js +4 -34
- package/dist/src/transfer.spec.js.map +1 -1
- package/dist/src/unit-plan.d.ts +130 -0
- package/dist/src/unit-plan.d.ts.map +1 -0
- package/dist/src/unit-plan.js +107 -0
- package/dist/src/unit-plan.js.map +1 -0
- package/dist/src/unit-plan.spec.d.ts +6 -0
- package/dist/src/unit-plan.spec.d.ts.map +1 -0
- package/dist/src/unit-plan.spec.js +29 -0
- package/dist/src/unit-plan.spec.js.map +1 -0
- package/dist/src/workspace.d.ts +22 -11
- package/dist/src/workspace.d.ts.map +1 -1
- package/dist/src/workspace.js +10 -11
- package/dist/src/workspace.js.map +1 -1
- package/package.json +3 -3
- package/src/api.ts +326 -32
- package/src/collection-manifest.ts +53 -0
- package/src/dataflow.spec.ts +223 -31
- package/src/dataflow.ts +183 -26
- package/src/dataset-blob.spec.ts +117 -0
- package/src/dataset-blob.ts +157 -21
- package/src/dataset-ref.ts +5 -4
- package/src/environment.ts +3 -2
- package/src/execution.spec.ts +114 -0
- package/src/execution.ts +175 -51
- package/src/function.ts +19 -32
- package/src/index.ts +141 -39
- package/src/intake.ts +53 -0
- package/src/lock.ts +67 -21
- package/src/names.ts +39 -0
- package/src/package.ts +15 -71
- package/src/record.spec.ts +195 -0
- package/src/record.ts +489 -7
- package/src/release.spec.ts +47 -0
- package/src/release.ts +69 -0
- package/src/repository.ts +67 -0
- package/src/runner.spec.ts +11 -94
- package/src/runner.ts +16 -80
- package/src/structure.ts +0 -8
- package/src/task.spec.ts +67 -188
- package/src/task.ts +140 -522
- package/src/transfer.spec.ts +4 -51
- package/src/transfer.ts +22 -26
- package/src/unit-plan.spec.ts +31 -0
- package/src/unit-plan.ts +121 -0
- package/src/workspace.ts +13 -11
- package/dist/src/stream.d.ts +0 -97
- package/dist/src/stream.d.ts.map +0 -1
- package/dist/src/stream.js +0 -134
- package/dist/src/stream.js.map +0 -1
- package/dist/src/stream.spec.d.ts.map +0 -1
- package/dist/src/stream.spec.js +0 -60
- package/dist/src/stream.spec.js.map +0 -1
- package/src/stream.spec.ts +0 -88
- package/src/stream.ts +0 -170
package/src/intake.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How far a file being taken into the store has got: a deploy's file source,
|
|
8
|
+
* or an upload's delivery as its commit takes it in.
|
|
9
|
+
*
|
|
10
|
+
* A file is hashed first, since its SHA-256 is how the store knows a delivery
|
|
11
|
+
* it already holds, and then taken in: a collection by intake units on the
|
|
12
|
+
* runners, a piece of its segments each, and any other value as the object the
|
|
13
|
+
* file is.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ArrayType, IntegerType, NullType, StringType, StructType, VariantType, type ValueTypeOf } from '@elaraai/east';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The step a file being taken in is at.
|
|
20
|
+
*/
|
|
21
|
+
export const IntakeStepType = VariantType({
|
|
22
|
+
/** Not started: a deploy takes a bounded number of files in at once */
|
|
23
|
+
waiting: NullType,
|
|
24
|
+
/** Being read for its SHA-256, by which the store knows a delivery it
|
|
25
|
+
* already holds */
|
|
26
|
+
hashing: NullType,
|
|
27
|
+
/** Being taken in by intake units: its `pieces`, and how many are `done` */
|
|
28
|
+
taking_in: StructType({ pieces: IntegerType, done: IntegerType }),
|
|
29
|
+
/** In the store: one it already held; a value that is not a collection,
|
|
30
|
+
* carried as the object the file is; or a collection, taken in by the
|
|
31
|
+
* runners named, in the order each first took a piece in — none when every
|
|
32
|
+
* piece was taken in before, by an intake that stopped part way */
|
|
33
|
+
done: VariantType({ known: NullType, carried: NullType, taken: ArrayType(StringType) }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export type IntakeStep = ValueTypeOf<typeof IntakeStepType>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A file being taken into the store.
|
|
40
|
+
*/
|
|
41
|
+
export const IntakeFileType = StructType({
|
|
42
|
+
/** The dataset it becomes, as `inputs/<name>` */
|
|
43
|
+
path: StringType,
|
|
44
|
+
/** The step it is at */
|
|
45
|
+
step: IntakeStepType,
|
|
46
|
+
/** Bytes of the file its step has covered: read for its hash, or covered by
|
|
47
|
+
* the pieces taken in; its size once it is done */
|
|
48
|
+
bytes: IntegerType,
|
|
49
|
+
/** The file's size */
|
|
50
|
+
total: IntegerType,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type IntakeFile = ValueTypeOf<typeof IntakeFileType>;
|
package/src/lock.ts
CHANGED
|
@@ -6,18 +6,19 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Lock state type definitions.
|
|
8
8
|
*
|
|
9
|
-
* A lock
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
9
|
+
* A lock gives exclusive or shared access to a resource: a workspace, a
|
|
10
|
+
* workspace's dataflow, the repository's tasks, or one dataset's ref. The state
|
|
11
|
+
* records:
|
|
12
|
+
* - what operation acquired the lock (dataflow, deployment, etc.);
|
|
13
|
+
* - who holds it: a local process, or a cloud function;
|
|
14
|
+
* - when it was acquired;
|
|
15
|
+
* - when it expires, for a cloud backend's lease.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
17
|
+
* An exclusive holder may also report how far its operation has got, for
|
|
18
|
+
* another process to read while it holds the lock: a deploy taking its file
|
|
19
|
+
* sources in and deploying its records.
|
|
16
20
|
*
|
|
17
|
-
*
|
|
18
|
-
* to encode their own holder identification. Common patterns:
|
|
19
|
-
* - Local: `.process (pid=1234, bootId="abc-123", startTime=98765, command="e3 start")`
|
|
20
|
-
* - Cloud: `.lambda (requestId="req-123", functionName="e3-api")`
|
|
21
|
+
* A local repository keeps each resource's locks in `locks/<resource>/`.
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
import {
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
NullType,
|
|
31
32
|
ValueTypeOf,
|
|
32
33
|
} from '@elaraai/east';
|
|
34
|
+
import { DeployProgressType } from './api.js';
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Lock operation - what acquired the lock.
|
|
@@ -50,8 +52,8 @@ export const LockOperationType = VariantType({
|
|
|
50
52
|
export type LockOperation = ValueTypeOf<typeof LockOperationType>;
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
+
* A local process holding a lock: alive while a process of this pid and start
|
|
56
|
+
* time runs under this boot.
|
|
55
57
|
*/
|
|
56
58
|
export const ProcessHolderType = StructType({
|
|
57
59
|
/** Process ID */
|
|
@@ -67,20 +69,40 @@ export const ProcessHolderType = StructType({
|
|
|
67
69
|
export type ProcessHolder = ValueTypeOf<typeof ProcessHolderType>;
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
* A cloud function holding a lock, which the lock's lease bounds.
|
|
73
|
+
*/
|
|
74
|
+
export const LambdaHolderType = StructType({
|
|
75
|
+
/** The invocation's request ID */
|
|
76
|
+
requestId: StringType,
|
|
77
|
+
/** The function's name */
|
|
78
|
+
functionName: StringType,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type LambdaHolder = ValueTypeOf<typeof LambdaHolderType>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Who holds a lock.
|
|
85
|
+
*/
|
|
86
|
+
export const LockHolderVariantType = VariantType({
|
|
87
|
+
/** A local process */
|
|
88
|
+
process: ProcessHolderType,
|
|
89
|
+
/** A cloud function */
|
|
90
|
+
lambda: LambdaHolderType,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type LockHolderVariant = ValueTypeOf<typeof LockHolderVariantType>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The state of a held lock.
|
|
75
97
|
*
|
|
76
|
-
* The
|
|
77
|
-
*
|
|
98
|
+
* The locking itself is the backend's — an atomically created file locally, a
|
|
99
|
+
* conditional write in the cloud — and every backend records this state.
|
|
78
100
|
*/
|
|
79
101
|
export const LockStateType = StructType({
|
|
80
102
|
/** What operation acquired the lock */
|
|
81
103
|
operation: LockOperationType,
|
|
82
|
-
/** Who holds the lock
|
|
83
|
-
holder:
|
|
104
|
+
/** Who holds the lock */
|
|
105
|
+
holder: LockHolderVariantType,
|
|
84
106
|
/** When the lock was acquired */
|
|
85
107
|
acquiredAt: DateTimeType,
|
|
86
108
|
/** When the lock expires (for cloud TTL-based locks) */
|
|
@@ -88,3 +110,27 @@ export const LockStateType = StructType({
|
|
|
88
110
|
});
|
|
89
111
|
|
|
90
112
|
export type LockState = ValueTypeOf<typeof LockStateType>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How far the operation holding a lock exclusively has got, as its holder
|
|
116
|
+
* reports it for another process to read while it holds the lock.
|
|
117
|
+
*/
|
|
118
|
+
export const LockProgressType = VariantType({
|
|
119
|
+
/** A deploy: its file sources and its records */
|
|
120
|
+
deployment: DeployProgressType,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export type LockProgress = ValueTypeOf<typeof LockProgressType>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A resource's exclusive lock as another process reads it while it is held:
|
|
127
|
+
* what holds it, and how far that operation says it has got.
|
|
128
|
+
*/
|
|
129
|
+
export const LockStatusType = StructType({
|
|
130
|
+
/** The lock's state: the operation, its holder, and when it was acquired */
|
|
131
|
+
state: LockStateType,
|
|
132
|
+
/** What the holder last reported of its progress; `none` until it reports */
|
|
133
|
+
progress: OptionType(LockProgressType),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export type LockStatus = ValueTypeOf<typeof LockStatusType>;
|
package/src/names.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The rule for the names e3 makes paths of: a repository's name where a server
|
|
8
|
+
* keeps several, a workspace's name, a package's name and version, and a
|
|
9
|
+
* lock's resource. One place holds it, so the SDK, the CLI, the API server and
|
|
10
|
+
* the stores refuse the same names.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** What a name names. */
|
|
14
|
+
export type NamedKind = 'repository' | 'workspace' | 'package' | 'package version' | 'lock';
|
|
15
|
+
|
|
16
|
+
/** What no name may hold: a path separator, a character a Windows file name
|
|
17
|
+
* refuses, or a control character. */
|
|
18
|
+
const REFUSED = /[/\\:*?"<>|\u0000-\u001f]/;
|
|
19
|
+
|
|
20
|
+
/** What a workspace's name may not hold besides: the characters that join the
|
|
21
|
+
* parts of its locks' names. */
|
|
22
|
+
const REFUSED_IN_WORKSPACE = /[#~]/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why a name cannot be one path segment, or `null` when it can.
|
|
26
|
+
*
|
|
27
|
+
* @param kind - What the name names
|
|
28
|
+
* @param name - The name
|
|
29
|
+
* @returns The problem, in words that follow the name, or `null`
|
|
30
|
+
*/
|
|
31
|
+
export function nameProblem(kind: NamedKind, name: string): string | null {
|
|
32
|
+
if (name === '') return 'is empty';
|
|
33
|
+
if (name === '.' || name === '..') return 'is a path of its own';
|
|
34
|
+
const refused = REFUSED.exec(name);
|
|
35
|
+
if (refused !== null) return `holds ${JSON.stringify(refused[0])}, which a file name cannot`;
|
|
36
|
+
const joiner = kind === 'workspace' ? REFUSED_IN_WORKSPACE.exec(name) : null;
|
|
37
|
+
if (joiner !== null) return `holds ${JSON.stringify(joiner[0])}, which joins the parts of a lock's name`;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
package/src/package.ts
CHANGED
|
@@ -39,14 +39,6 @@ export type PackageDataType = typeof PackageDataType;
|
|
|
39
39
|
|
|
40
40
|
export type PackageData = ValueTypeOf<typeof PackageDataType>;
|
|
41
41
|
|
|
42
|
-
// Backwards compatibility alias
|
|
43
|
-
/** @deprecated Use PackageDataType instead */
|
|
44
|
-
export const PackageDatasetsType = PackageDataType;
|
|
45
|
-
/** @deprecated Use PackageData instead */
|
|
46
|
-
export type PackageDatasetsType = PackageDataType;
|
|
47
|
-
/** @deprecated Use PackageData instead */
|
|
48
|
-
export type PackageDatasets = PackageData;
|
|
49
|
-
|
|
50
42
|
/**
|
|
51
43
|
* Package object stored in the object store.
|
|
52
44
|
*
|
|
@@ -100,85 +92,37 @@ export const PackageObjectType = StructType({
|
|
|
100
92
|
data: PackageDataType,
|
|
101
93
|
/** Functions defined in this package: name -> FunctionObject hash. */
|
|
102
94
|
functions: DictType(StringType, StringType),
|
|
103
|
-
/** Records defined in this package: name -> RecordObject hash.
|
|
104
|
-
* BEAST2 encodes struct fields positionally in declaration order, so new
|
|
105
|
-
* fields MUST be appended LAST — never inserted between existing fields. */
|
|
95
|
+
/** Records defined in this package: name -> RecordObject hash. */
|
|
106
96
|
records: DictType(StringType, StringType),
|
|
107
|
-
/** Unresolved dataset sources: refPath (e.g. "inputs/table") -> descriptor.
|
|
108
|
-
* Appended LAST, per the positional rule above. */
|
|
97
|
+
/** Unresolved dataset sources: refPath (e.g. "inputs/table") -> descriptor. */
|
|
109
98
|
sources: DictType(StringType, DatasetSourceWireType),
|
|
110
99
|
});
|
|
111
100
|
export type PackageObjectType = typeof PackageObjectType;
|
|
112
101
|
|
|
113
102
|
export type PackageObject = ValueTypeOf<typeof PackageObjectType>;
|
|
114
103
|
|
|
115
|
-
/**
|
|
116
|
-
* The pre-`sources` package object wire shape (tasks, data, functions,
|
|
117
|
-
* records), kept only so {@link decodePackageObject} can read packages exported
|
|
118
|
-
* before path-initialised inputs existed.
|
|
119
|
-
*/
|
|
120
|
-
const RecordsEraPackageObjectType = StructType({
|
|
121
|
-
tasks: DictType(StringType, StringType),
|
|
122
|
-
data: PackageDataType,
|
|
123
|
-
functions: DictType(StringType, StringType),
|
|
124
|
-
records: DictType(StringType, StringType),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* The pre-`records` package object wire shape (tasks, data, functions), kept
|
|
129
|
-
* only so {@link decodePackageObject} can read packages exported before records
|
|
130
|
-
* existed.
|
|
131
|
-
*/
|
|
132
|
-
const FunctionsEraPackageObjectType = StructType({
|
|
133
|
-
tasks: DictType(StringType, StringType),
|
|
134
|
-
data: PackageDataType,
|
|
135
|
-
functions: DictType(StringType, StringType),
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* The pre-`functions` package object wire shape (tasks, data), kept only so
|
|
140
|
-
* {@link decodePackageObject} can read packages exported before functions
|
|
141
|
-
* existed.
|
|
142
|
-
*/
|
|
143
|
-
const LegacyPackageObjectType = StructType({
|
|
144
|
-
tasks: DictType(StringType, StringType),
|
|
145
|
-
data: PackageDataType,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
104
|
const decodeCurrent = decodeBeast2For(PackageObjectType);
|
|
149
|
-
const decodeRecordsEra = decodeBeast2For(RecordsEraPackageObjectType);
|
|
150
|
-
const decodeFunctionsEra = decodeBeast2For(FunctionsEraPackageObjectType);
|
|
151
|
-
const decodeLegacy = decodeBeast2For(LegacyPackageObjectType);
|
|
152
105
|
|
|
153
106
|
/**
|
|
154
|
-
* Decode a `PackageObject` from BEAST2 bytes
|
|
155
|
-
*
|
|
107
|
+
* Decode a `PackageObject` from BEAST2 bytes.
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* A package-borne wire, so it changes by hard cutover: a package exported by
|
|
111
|
+
* an older SDK is re-exported with the current one, and this says so.
|
|
156
112
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* the
|
|
160
|
-
*
|
|
113
|
+
* @param data - the stored bytes
|
|
114
|
+
* @returns the package object
|
|
115
|
+
* @throws {Error} When the bytes are not a current package object — a package
|
|
116
|
+
* exported by an older SDK, which is re-exported with the current one.
|
|
161
117
|
*/
|
|
162
118
|
export function decodePackageObject(data: Uint8Array): PackageObject {
|
|
163
119
|
try {
|
|
164
120
|
return decodeCurrent(data);
|
|
165
121
|
} catch (err) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const fnEra = decodeFunctionsEra(data);
|
|
172
|
-
return { tasks: fnEra.tasks, data: fnEra.data, functions: fnEra.functions, records: new Map(), sources: new Map() };
|
|
173
|
-
} catch {
|
|
174
|
-
try {
|
|
175
|
-
const legacy = decodeLegacy(data);
|
|
176
|
-
return { tasks: legacy.tasks, data: legacy.data, functions: new Map(), records: new Map(), sources: new Map() };
|
|
177
|
-
} catch {
|
|
178
|
-
throw err; // no known shape — surface the current-format error
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
122
|
+
throw new Error(
|
|
123
|
+
`the package object does not decode: the package was exported by an older e3 SDK — re-export it with the current one ` +
|
|
124
|
+
`(${err instanceof Error ? err.message : String(err)})`,
|
|
125
|
+
);
|
|
182
126
|
}
|
|
183
127
|
}
|
|
184
128
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The record wire types' decoders: each reads the current shape, and refuses
|
|
8
|
+
* an older one naming the fix — a commit an older e3 wrote, whose repository
|
|
9
|
+
* is re-created, and a mutation, migration or record object an older SDK
|
|
10
|
+
* exported, whose package is re-exported.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { DateTimeType, DictType, IntegerType, OptionType, StringType, StructType, ArrayType, EastTypeType, encodeBeast2For, isTypeValueEqual, some, toEastTypeValue, variant } from '@elaraai/east';
|
|
16
|
+
import { RunnerType } from './runner.js';
|
|
17
|
+
import {
|
|
18
|
+
MigrationObjectType,
|
|
19
|
+
MutationObjectType,
|
|
20
|
+
RecordCommitType,
|
|
21
|
+
RecordObjectType,
|
|
22
|
+
decodeMigrationObject,
|
|
23
|
+
decodeMutationObject,
|
|
24
|
+
decodeRecordCommit,
|
|
25
|
+
decodeRecordObject,
|
|
26
|
+
type MigrationObject,
|
|
27
|
+
type MutationObject,
|
|
28
|
+
type RecordCommit,
|
|
29
|
+
type RecordObject,
|
|
30
|
+
} from './record.js';
|
|
31
|
+
|
|
32
|
+
describe('decodeRecordCommit', () => {
|
|
33
|
+
const commit: RecordCommit = {
|
|
34
|
+
parent: some('a'.repeat(64)),
|
|
35
|
+
state: 'b'.repeat(64),
|
|
36
|
+
mutation: 'increment',
|
|
37
|
+
args: some('c'.repeat(64)),
|
|
38
|
+
actor: 'cli:test',
|
|
39
|
+
at: new Date(0),
|
|
40
|
+
delta: some('d'.repeat(64)),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
it('round-trips a current commit', () => {
|
|
44
|
+
assert.deepEqual(decodeRecordCommit(encodeBeast2For(RecordCommitType)(commit)), commit);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('refuses a commit an older e3 wrote before deltas existed, naming the fix', () => {
|
|
48
|
+
const PreDeltaCommitType = StructType({
|
|
49
|
+
parent: OptionType(StringType), state: StringType, mutation: StringType,
|
|
50
|
+
args: OptionType(StringType), actor: StringType, at: DateTimeType,
|
|
51
|
+
});
|
|
52
|
+
const { delta: _delta, ...older } = commit;
|
|
53
|
+
assert.throws(
|
|
54
|
+
() => decodeRecordCommit(encodeBeast2For(PreDeltaCommitType)(older)),
|
|
55
|
+
/^Error: the record commit does not decode: an older e3 wrote this repository — re-create it: deploy again and import its data again \(/,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('refuses bytes of no commit shape', () => {
|
|
60
|
+
assert.throws(() => decodeRecordCommit(encodeBeast2For(IntegerType)(7n)), /the record commit does not decode/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('decodeMutationObject', () => {
|
|
65
|
+
const mutation: MutationObject = {
|
|
66
|
+
bodyIr: 'a'.repeat(64),
|
|
67
|
+
argTypes: [toEastTypeValue(IntegerType)],
|
|
68
|
+
runner: variant('east_node', { platforms: ['@elaraai/east-node-std'] }),
|
|
69
|
+
form: variant('edit', null),
|
|
70
|
+
programIr: 'b'.repeat(64),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** A decoded mutation with its arg types checked off separately: a decoded
|
|
74
|
+
* type value carries no memoized identity, so only the East comparison
|
|
75
|
+
* says whether two of them are the same type. */
|
|
76
|
+
function read(bytes: Uint8Array): Omit<MutationObject, 'argTypes'> {
|
|
77
|
+
const decoded = decodeMutationObject(bytes);
|
|
78
|
+
assert.equal(decoded.argTypes.length, 1);
|
|
79
|
+
assert.ok(isTypeValueEqual(decoded.argTypes[0]!, toEastTypeValue(IntegerType)));
|
|
80
|
+
const { argTypes: _argTypes, ...rest } = decoded;
|
|
81
|
+
return rest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it('round-trips a current mutation', () => {
|
|
85
|
+
const { argTypes: _argTypes, ...expected } = mutation;
|
|
86
|
+
assert.deepEqual(read(encodeBeast2For(MutationObjectType)(mutation)), expected);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('refuses a mutation an older SDK exported before the delta, naming the fix', () => {
|
|
90
|
+
const PreDeltaMutationType = StructType({
|
|
91
|
+
bodyIr: StringType, argTypes: ArrayType(EastTypeType), runner: RunnerType,
|
|
92
|
+
});
|
|
93
|
+
const { form: _form, programIr: _programIr, argTypes: _argTypes, ...older } = mutation;
|
|
94
|
+
assert.throws(
|
|
95
|
+
() => decodeMutationObject(encodeBeast2For(PreDeltaMutationType)({ ...older, argTypes: mutation.argTypes })),
|
|
96
|
+
/^Error: the mutation object does not decode: the package was exported by an older e3 SDK — re-export it with the current one \(/,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('refuses a mutation whose form is a String, as an older SDK wrote it, whatever the string', () => {
|
|
101
|
+
// A String form decoded as any string, and a form e3 does not know ran as
|
|
102
|
+
// whichever form its checks fell through to.
|
|
103
|
+
const StringFormMutationType = StructType({
|
|
104
|
+
bodyIr: StringType, argTypes: ArrayType(EastTypeType), runner: RunnerType, form: StringType, programIr: StringType,
|
|
105
|
+
});
|
|
106
|
+
for (const form of ['edit', 'bogus']) {
|
|
107
|
+
assert.throws(
|
|
108
|
+
() => decodeMutationObject(encodeBeast2For(StringFormMutationType)({ ...mutation, form })),
|
|
109
|
+
/^Error: the mutation object does not decode: the package was exported by an older e3 SDK — re-export it with the current one \(/,
|
|
110
|
+
form,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('decodeMigrationObject', () => {
|
|
117
|
+
const RowV1Type = StructType({ name: StringType });
|
|
118
|
+
const RowV2Type = StructType({ name: StringType, shift: IntegerType });
|
|
119
|
+
const migration: MigrationObject = {
|
|
120
|
+
form: variant('rows', null),
|
|
121
|
+
from: toEastTypeValue(DictType(StringType, RowV1Type)),
|
|
122
|
+
to: toEastTypeValue(DictType(StringType, RowV2Type)),
|
|
123
|
+
bodyIr: 'a'.repeat(64),
|
|
124
|
+
programIr: 'b'.repeat(64),
|
|
125
|
+
runner: variant('east_node', { platforms: ['@elaraai/east-node-std'] }),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
it('round-trips a current migration, its types among it', () => {
|
|
129
|
+
const decoded = decodeMigrationObject(encodeBeast2For(MigrationObjectType)(migration));
|
|
130
|
+
// A decoded type value carries no memoized identity, so only the East
|
|
131
|
+
// comparison says whether two of them are the same type.
|
|
132
|
+
assert.ok(isTypeValueEqual(decoded.from, migration.from));
|
|
133
|
+
assert.ok(isTypeValueEqual(decoded.to, migration.to));
|
|
134
|
+
assert.ok(!isTypeValueEqual(decoded.from, decoded.to));
|
|
135
|
+
const { from: _from, to: _to, ...rest } = decoded;
|
|
136
|
+
const { from: _f, to: _t, ...expected } = migration;
|
|
137
|
+
assert.deepEqual(rest, expected);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('refuses the bytes of another object, naming the re-export', () => {
|
|
141
|
+
const mutation: MutationObject = {
|
|
142
|
+
bodyIr: 'a'.repeat(64), argTypes: [], runner: migration.runner, form: variant('reduce', null), programIr: '',
|
|
143
|
+
};
|
|
144
|
+
assert.throws(
|
|
145
|
+
() => decodeMigrationObject(encodeBeast2For(MutationObjectType)(mutation)),
|
|
146
|
+
/^Error: the migration object does not decode: the package was exported by an older e3 SDK — re-export it with the current one \(/,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('refuses a migration whose form is a String, as an older SDK wrote it, whatever the string', () => {
|
|
151
|
+
// A deploy ran a String form it did not know as a split step.
|
|
152
|
+
const StringFormMigrationType = StructType({
|
|
153
|
+
form: StringType, from: EastTypeType, to: EastTypeType, bodyIr: StringType, programIr: StringType, runner: RunnerType,
|
|
154
|
+
});
|
|
155
|
+
for (const form of ['rows', 'bogus']) {
|
|
156
|
+
assert.throws(
|
|
157
|
+
() => decodeMigrationObject(encodeBeast2For(StringFormMigrationType)({ ...migration, form })),
|
|
158
|
+
/^Error: the migration object does not decode: the package was exported by an older e3 SDK — re-export it with the current one \(/,
|
|
159
|
+
form,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('decodeRecordObject', () => {
|
|
166
|
+
const record: RecordObject = {
|
|
167
|
+
path: 'records/orders',
|
|
168
|
+
mutations: new Map([['place', 'a'.repeat(64)]]),
|
|
169
|
+
indexes: new Map([['by_status', 'b'.repeat(64)]]),
|
|
170
|
+
migrations: [{ name: 'add_shift', migration: 'c'.repeat(64) }, { name: 'widen_row', migration: 'd'.repeat(64) }],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** A decoded record object with its dicts as entry lists: a decoded Dict is
|
|
174
|
+
* a SortedMap, which is the same mapping and not the same container. */
|
|
175
|
+
const read = (bytes: Uint8Array) => {
|
|
176
|
+
const decoded = decodeRecordObject(bytes);
|
|
177
|
+
return { path: decoded.path, mutations: [...decoded.mutations], indexes: [...decoded.indexes], migrations: decoded.migrations };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
it('round-trips a current record object, its migrations in their order', () => {
|
|
181
|
+
assert.deepEqual(read(encodeBeast2For(RecordObjectType)(record)),
|
|
182
|
+
{ path: record.path, mutations: [...record.mutations], indexes: [...record.indexes], migrations: record.migrations });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('refuses a record object an older SDK exported before migrations, naming the fix', () => {
|
|
186
|
+
const IndexesEraRecordType = StructType({
|
|
187
|
+
path: StringType, mutations: DictType(StringType, StringType), indexes: DictType(StringType, StringType),
|
|
188
|
+
});
|
|
189
|
+
const { migrations: _migrations, ...older } = record;
|
|
190
|
+
assert.throws(
|
|
191
|
+
() => decodeRecordObject(encodeBeast2For(IndexesEraRecordType)(older)),
|
|
192
|
+
/^Error: the record object does not decode: the package was exported by an older e3 SDK — re-export it with the current one \(/,
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
});
|