@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.
Files changed (158) hide show
  1. package/README.md +1 -1
  2. package/dist/src/api.d.ts +829 -36
  3. package/dist/src/api.d.ts.map +1 -1
  4. package/dist/src/api.js +301 -32
  5. package/dist/src/api.js.map +1 -1
  6. package/dist/src/collection-manifest.d.ts +37 -0
  7. package/dist/src/collection-manifest.d.ts.map +1 -0
  8. package/dist/src/collection-manifest.js +37 -0
  9. package/dist/src/collection-manifest.js.map +1 -0
  10. package/dist/src/dataflow.d.ts +264 -19
  11. package/dist/src/dataflow.d.ts.map +1 -1
  12. package/dist/src/dataflow.js +156 -15
  13. package/dist/src/dataflow.js.map +1 -1
  14. package/dist/src/dataflow.spec.js +184 -27
  15. package/dist/src/dataflow.spec.js.map +1 -1
  16. package/dist/src/dataset-blob.d.ts +75 -15
  17. package/dist/src/dataset-blob.d.ts.map +1 -1
  18. package/dist/src/dataset-blob.js +95 -28
  19. package/dist/src/dataset-blob.js.map +1 -1
  20. package/dist/src/dataset-blob.spec.d.ts +6 -0
  21. package/dist/src/dataset-blob.spec.d.ts.map +1 -0
  22. package/dist/src/dataset-blob.spec.js +92 -0
  23. package/dist/src/dataset-blob.spec.js.map +1 -0
  24. package/dist/src/dataset-ref.d.ts +5 -4
  25. package/dist/src/dataset-ref.d.ts.map +1 -1
  26. package/dist/src/dataset-ref.js +5 -4
  27. package/dist/src/dataset-ref.js.map +1 -1
  28. package/dist/src/environment.d.ts +3 -2
  29. package/dist/src/environment.d.ts.map +1 -1
  30. package/dist/src/environment.js +3 -2
  31. package/dist/src/environment.js.map +1 -1
  32. package/dist/src/execution.d.ts +112 -18
  33. package/dist/src/execution.d.ts.map +1 -1
  34. package/dist/src/execution.js +174 -45
  35. package/dist/src/execution.js.map +1 -1
  36. package/dist/src/execution.spec.d.ts +6 -0
  37. package/dist/src/execution.spec.d.ts.map +1 -0
  38. package/dist/src/execution.spec.js +108 -0
  39. package/dist/src/execution.spec.js.map +1 -0
  40. package/dist/src/function.d.ts +14 -12
  41. package/dist/src/function.d.ts.map +1 -1
  42. package/dist/src/function.js +17 -32
  43. package/dist/src/function.js.map +1 -1
  44. package/dist/src/index.d.ts +17 -12
  45. package/dist/src/index.d.ts.map +1 -1
  46. package/dist/src/index.js +28 -20
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/intake.d.ts +75 -0
  49. package/dist/src/intake.d.ts.map +1 -0
  50. package/dist/src/intake.js +46 -0
  51. package/dist/src/intake.js.map +1 -0
  52. package/dist/src/lock.d.ts +253 -21
  53. package/dist/src/lock.d.ts.map +1 -1
  54. package/dist/src/lock.js +55 -21
  55. package/dist/src/lock.js.map +1 -1
  56. package/dist/src/names.d.ts +21 -0
  57. package/dist/src/names.d.ts.map +1 -0
  58. package/dist/src/names.js +31 -0
  59. package/dist/src/names.js.map +1 -0
  60. package/dist/src/package.d.ts +11 -78
  61. package/dist/src/package.d.ts.map +1 -1
  62. package/dist/src/package.js +13 -66
  63. package/dist/src/package.js.map +1 -1
  64. package/dist/src/record.d.ts +582 -18
  65. package/dist/src/record.d.ts.map +1 -1
  66. package/dist/src/record.js +405 -7
  67. package/dist/src/record.js.map +1 -1
  68. package/dist/src/{stream.spec.d.ts → record.spec.d.ts} +1 -1
  69. package/dist/src/record.spec.d.ts.map +1 -0
  70. package/dist/src/record.spec.js +143 -0
  71. package/dist/src/record.spec.js.map +1 -0
  72. package/dist/src/release.d.ts +36 -0
  73. package/dist/src/release.d.ts.map +1 -0
  74. package/dist/src/release.js +71 -0
  75. package/dist/src/release.js.map +1 -0
  76. package/dist/src/release.spec.d.ts +6 -0
  77. package/dist/src/release.spec.d.ts.map +1 -0
  78. package/dist/src/release.spec.js +42 -0
  79. package/dist/src/release.spec.js.map +1 -0
  80. package/dist/src/repository.d.ts +64 -0
  81. package/dist/src/repository.d.ts.map +1 -0
  82. package/dist/src/repository.js +49 -0
  83. package/dist/src/repository.js.map +1 -0
  84. package/dist/src/runner.d.ts +16 -62
  85. package/dist/src/runner.d.ts.map +1 -1
  86. package/dist/src/runner.js +16 -79
  87. package/dist/src/runner.js.map +1 -1
  88. package/dist/src/runner.spec.js +11 -85
  89. package/dist/src/runner.spec.js.map +1 -1
  90. package/dist/src/structure.d.ts +0 -57
  91. package/dist/src/structure.d.ts.map +1 -1
  92. package/dist/src/structure.js +0 -3
  93. package/dist/src/structure.js.map +1 -1
  94. package/dist/src/task.d.ts +210 -318
  95. package/dist/src/task.d.ts.map +1 -1
  96. package/dist/src/task.js +117 -472
  97. package/dist/src/task.js.map +1 -1
  98. package/dist/src/task.spec.js +62 -154
  99. package/dist/src/task.spec.js.map +1 -1
  100. package/dist/src/transfer.d.ts +38 -26
  101. package/dist/src/transfer.d.ts.map +1 -1
  102. package/dist/src/transfer.js +22 -26
  103. package/dist/src/transfer.js.map +1 -1
  104. package/dist/src/transfer.spec.js +4 -34
  105. package/dist/src/transfer.spec.js.map +1 -1
  106. package/dist/src/unit-plan.d.ts +130 -0
  107. package/dist/src/unit-plan.d.ts.map +1 -0
  108. package/dist/src/unit-plan.js +107 -0
  109. package/dist/src/unit-plan.js.map +1 -0
  110. package/dist/src/unit-plan.spec.d.ts +6 -0
  111. package/dist/src/unit-plan.spec.d.ts.map +1 -0
  112. package/dist/src/unit-plan.spec.js +29 -0
  113. package/dist/src/unit-plan.spec.js.map +1 -0
  114. package/dist/src/workspace.d.ts +22 -11
  115. package/dist/src/workspace.d.ts.map +1 -1
  116. package/dist/src/workspace.js +10 -11
  117. package/dist/src/workspace.js.map +1 -1
  118. package/package.json +3 -3
  119. package/src/api.ts +326 -32
  120. package/src/collection-manifest.ts +53 -0
  121. package/src/dataflow.spec.ts +223 -31
  122. package/src/dataflow.ts +183 -26
  123. package/src/dataset-blob.spec.ts +117 -0
  124. package/src/dataset-blob.ts +157 -21
  125. package/src/dataset-ref.ts +5 -4
  126. package/src/environment.ts +3 -2
  127. package/src/execution.spec.ts +114 -0
  128. package/src/execution.ts +175 -51
  129. package/src/function.ts +19 -32
  130. package/src/index.ts +141 -39
  131. package/src/intake.ts +53 -0
  132. package/src/lock.ts +67 -21
  133. package/src/names.ts +39 -0
  134. package/src/package.ts +15 -71
  135. package/src/record.spec.ts +195 -0
  136. package/src/record.ts +489 -7
  137. package/src/release.spec.ts +47 -0
  138. package/src/release.ts +69 -0
  139. package/src/repository.ts +67 -0
  140. package/src/runner.spec.ts +11 -94
  141. package/src/runner.ts +16 -80
  142. package/src/structure.ts +0 -8
  143. package/src/task.spec.ts +67 -188
  144. package/src/task.ts +140 -522
  145. package/src/transfer.spec.ts +4 -51
  146. package/src/transfer.ts +22 -26
  147. package/src/unit-plan.spec.ts +31 -0
  148. package/src/unit-plan.ts +121 -0
  149. package/src/workspace.ts +13 -11
  150. package/dist/src/stream.d.ts +0 -97
  151. package/dist/src/stream.d.ts.map +0 -1
  152. package/dist/src/stream.js +0 -134
  153. package/dist/src/stream.js.map +0 -1
  154. package/dist/src/stream.spec.d.ts.map +0 -1
  155. package/dist/src/stream.spec.js +0 -60
  156. package/dist/src/stream.spec.js.map +0 -1
  157. package/src/stream.spec.ts +0 -88
  158. 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 provides exclusive access to a workspace. The state tracks:
10
- * - What operation acquired the lock (dataflow, deployment, etc.)
11
- * - Who holds the lock (East text-encoded string)
12
- * - When it was acquired
13
- * - Optional expiry (for cloud TTL-based locks)
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
- * Lock file location: workspaces/<name>.lock
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
- * The holder field is an East text-encoded string, allowing different backends
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
- * Process holder schema - for local filesystem backends.
54
- * Used to encode/decode the holder string.
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
- * Lock state stored in workspaces/<name>.lock
71
- *
72
- * Represents an advisory lock on a workspace. The actual locking mechanism
73
- * is platform-specific (flock on Linux, DynamoDB conditional writes in cloud),
74
- * but the lock content follows this schema.
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 holder field is an East text-encoded string representing a variant value.
77
- * Use `printFor`/`parseInferred` from @elaraai/east to encode/decode.
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 - East text-encoded variant (e.g., `.process (...)`) */
83
- holder: StringType,
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, tolerating the older wire
155
- * formats (dual-decode migration).
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
- * Every package-read path — local AND cloud — must use this instead of
158
- * `decodeBeast2For(PackageObjectType)` directly, so packages exported before
159
- * the `sources`/`records`/`functions` fields existed keep decoding. Older
160
- * bytes decode with the missing maps defaulted to empty.
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
- try {
167
- const recEra = decodeRecordsEra(data);
168
- return { ...recEra, sources: new Map() };
169
- } catch {
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
+ });