@elaraai/e3-types 1.0.79 → 1.0.80
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 +569 -36
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +257 -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 +16 -12
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +25 -20
- package/dist/src/index.js.map +1 -1
- package/dist/src/lock.d.ts +68 -22
- package/dist/src/lock.d.ts.map +1 -1
- package/dist/src/lock.js +33 -22
- 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 +16 -22
- package/dist/src/transfer.d.ts.map +1 -1
- package/dist/src/transfer.js +16 -22
- 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 +276 -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 +122 -39
- package/src/lock.ts +39 -22
- 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 +16 -22
- 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/record.ts
CHANGED
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
* a content-addressed commit object, so a record's history is a git-style chain
|
|
12
12
|
* — the audit trail of who changed what, when, superseding which state.
|
|
13
13
|
*
|
|
14
|
-
* The record's current state is an ordinary `value` dataset ref
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* write protocol
|
|
14
|
+
* The record's current state is an ordinary `value` dataset ref. Its `hash`
|
|
15
|
+
* names the state or, once the record declares an index, a `$record` state
|
|
16
|
+
* naming it, and every read path resolves it, so task inputs, `e3 get` and the
|
|
17
|
+
* UI `Data.bind` read work on records unchanged. Only the write protocol
|
|
18
|
+
* differs.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ArrayType, BooleanType, DateTimeType, DictType, EastTypeType, FunctionType, NullType,
|
|
23
|
+
OptionType, PatchType, StringType, StructType, VariantType,
|
|
24
|
+
decodeBeast2For, dictPatchOpsType, setPatchOpsType, toEastTypeValue,
|
|
25
|
+
type EastType, type EastTypeValue, type PatchTypeOf, type ValueTypeOf,
|
|
26
|
+
} from '@elaraai/east';
|
|
21
27
|
import { RunnerType } from './runner.js';
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -41,10 +47,73 @@ export const RecordCommitType = StructType({
|
|
|
41
47
|
actor: StringType,
|
|
42
48
|
/** Commit wall-clock time. */
|
|
43
49
|
at: DateTimeType,
|
|
50
|
+
/** The mutation delta this commit applied: one sorted collection of the
|
|
51
|
+
* primary and index changes, addressed by key. History then shows WHAT
|
|
52
|
+
* changed without diffing two states, and an undo can be synthesised from
|
|
53
|
+
* it. No state depends on it — every state past and present reads
|
|
54
|
+
* identically if every delta were deleted — but it is a collection like any
|
|
55
|
+
* other, a manifest naming segment objects, so a collector walks it as a
|
|
56
|
+
* value rather than marking it as a leaf the way it marks `args`. */
|
|
57
|
+
delta: OptionType(StringType),
|
|
44
58
|
});
|
|
45
59
|
export type RecordCommitType = typeof RecordCommitType;
|
|
46
60
|
export type RecordCommit = ValueTypeOf<typeof RecordCommitType>;
|
|
47
61
|
|
|
62
|
+
const decodeCurrentCommit = decodeBeast2For(RecordCommitType);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Decode a `RecordCommit`.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* Stored state, so it changes by hard cutover: a repository an older e3 wrote
|
|
69
|
+
* is re-created rather than read, and this says so.
|
|
70
|
+
*
|
|
71
|
+
* @param data - the stored bytes
|
|
72
|
+
* @returns the commit
|
|
73
|
+
* @throws {Error} When the bytes are not a current commit — one an older e3
|
|
74
|
+
* wrote, whose repository is re-created.
|
|
75
|
+
*/
|
|
76
|
+
export function decodeRecordCommit(data: Uint8Array): RecordCommit {
|
|
77
|
+
try {
|
|
78
|
+
return decodeCurrentCommit(data);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`the record commit does not decode: an older e3 wrote this repository — re-create it: deploy again and import its data again ` +
|
|
82
|
+
`(${err instanceof Error ? err.message : String(err)})`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* How a mutation says what it changed.
|
|
89
|
+
*
|
|
90
|
+
* - `reduce` — `(State, …Args) => State`, the original surface. Its body and
|
|
91
|
+
* its diff see the whole state, so its RUNNER cost stays O(state); only its
|
|
92
|
+
* write, on a Dict or Set record, is O(touched).
|
|
93
|
+
* - `edit` — `(State, …Args, Edit) => Null`, the lazy write: the body reads the
|
|
94
|
+
* state it is given, lazily, and writes through an `edit` capability, so the
|
|
95
|
+
* body reads only the entries it touches and the commit rewrites only the
|
|
96
|
+
* segments they live in. The state reaches the runner as its manifest, the
|
|
97
|
+
* segments linked, so nothing on the way reads it whole either.
|
|
98
|
+
* - `patch` — no body; the argument is `PatchType(State)`. What an interactive
|
|
99
|
+
* edit from a view sends. On a record with no index a patch of per-key
|
|
100
|
+
* changes runs nothing at all, the only write whose cost is independent of
|
|
101
|
+
* the record's size.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* A variant, so a mutation object decodes only with one of these: a form this
|
|
105
|
+
* e3 does not know is refused where the object is read, not run as another.
|
|
106
|
+
*/
|
|
107
|
+
export const MutationFormType = VariantType({
|
|
108
|
+
edit: NullType,
|
|
109
|
+
patch: NullType,
|
|
110
|
+
reduce: NullType,
|
|
111
|
+
});
|
|
112
|
+
export type MutationFormType = typeof MutationFormType;
|
|
113
|
+
|
|
114
|
+
/** A {@link MutationFormType} by its name, as the SDK's definitions carry it. */
|
|
115
|
+
export type MutationForm = ValueTypeOf<typeof MutationFormType>['type'];
|
|
116
|
+
|
|
48
117
|
/**
|
|
49
118
|
* A mutation: the write half of the function machinery (CQRS — `e3.function`
|
|
50
119
|
* is the read/query half). A mutation is a pure East reducer
|
|
@@ -57,21 +126,434 @@ export const MutationObjectType = StructType({
|
|
|
57
126
|
bodyIr: StringType,
|
|
58
127
|
/** The EXTRA positional parameter types after the state parameter. */
|
|
59
128
|
argTypes: ArrayType(EastTypeType),
|
|
60
|
-
/** Author-chosen runtime
|
|
129
|
+
/** Author-chosen runtime the mutation's unit runs on. */
|
|
61
130
|
runner: RunnerType,
|
|
131
|
+
/** Which write form this is — see {@link MutationFormType}. */
|
|
132
|
+
form: MutationFormType,
|
|
133
|
+
/** Hash of the generated program's IR bundle: the one thing that actually
|
|
134
|
+
* runs, whatever the form. For the `patch` form `bodyIr` names it too,
|
|
135
|
+
* there being no author body. */
|
|
136
|
+
programIr: StringType,
|
|
62
137
|
});
|
|
63
138
|
export type MutationObjectType = typeof MutationObjectType;
|
|
64
139
|
export type MutationObject = ValueTypeOf<typeof MutationObjectType>;
|
|
65
140
|
|
|
141
|
+
const decodeCurrentMutation = decodeBeast2For(MutationObjectType);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Decode a `MutationObject`.
|
|
145
|
+
*
|
|
146
|
+
* @remarks
|
|
147
|
+
* A package-borne wire, so it changes by hard cutover: a package exported by
|
|
148
|
+
* an older SDK is re-exported with the current one, and this says so.
|
|
149
|
+
*
|
|
150
|
+
* @param data - the stored bytes
|
|
151
|
+
* @returns the mutation object
|
|
152
|
+
* @throws {Error} When the bytes are not a current mutation object — a package
|
|
153
|
+
* exported by an older SDK, which is re-exported with the current one.
|
|
154
|
+
*/
|
|
155
|
+
export function decodeMutationObject(data: Uint8Array): MutationObject {
|
|
156
|
+
try {
|
|
157
|
+
return decodeCurrentMutation(data);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`the mutation object does not decode: the package was exported by an older e3 SDK — re-export it with the current one ` +
|
|
161
|
+
`(${err instanceof Error ? err.message : String(err)})`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A secondary index over a record: an East function of an entry, and the
|
|
168
|
+
* collection it builds.
|
|
169
|
+
*
|
|
170
|
+
* An index is a second canonical collection whose sort order IS the query
|
|
171
|
+
* order, stored as a segment manifest exactly like the primary and maintained
|
|
172
|
+
* inside the same commit — so a view by an attribute of the row, or by a
|
|
173
|
+
* related entity a row names many of, is a page rather than a scan.
|
|
174
|
+
*
|
|
175
|
+
* The functions are pure for the reason a reducer is: they run again on every
|
|
176
|
+
* commit and in every bulk build, and a maintained index must equal a rebuilt
|
|
177
|
+
* one to the byte.
|
|
178
|
+
*/
|
|
179
|
+
export const RecordIndexObjectType = StructType({
|
|
180
|
+
/** Hash of the encoded EastIR bundle: `(K, V) -> IK`, or `-> Set<IK>` when
|
|
181
|
+
* {@link multi}. */
|
|
182
|
+
keyIr: StringType,
|
|
183
|
+
/** Declared with `keys` (a Set return) rather than `key`: one entry per
|
|
184
|
+
* element, so a row that names five resources appears under five keys. */
|
|
185
|
+
multi: BooleanType,
|
|
186
|
+
/** Hash of the covering projection's IR bundle, `(K, V) -> P`; none when the
|
|
187
|
+
* index carries no value and `P` is Null. */
|
|
188
|
+
valueIr: OptionType(StringType),
|
|
189
|
+
/** `IK` — the index key an entry sorts under. */
|
|
190
|
+
keyType: EastTypeType,
|
|
191
|
+
/** `P` — the covering projection's type; Null when there is none. */
|
|
192
|
+
valueType: EastTypeType,
|
|
193
|
+
/** Hash of the generated build program's IR bundle: `(piece, emit) =>
|
|
194
|
+
* Null`, which emits the index entries of a piece of the primary, in any
|
|
195
|
+
* order. */
|
|
196
|
+
buildIr: StringType,
|
|
197
|
+
/** Author-chosen runtime the index's functions run on. */
|
|
198
|
+
runner: RunnerType,
|
|
199
|
+
});
|
|
200
|
+
export type RecordIndexObjectType = typeof RecordIndexObjectType;
|
|
201
|
+
export type RecordIndexObject = ValueTypeOf<typeof RecordIndexObjectType>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* How a migration says what it changes.
|
|
205
|
+
*
|
|
206
|
+
* - `value` — `(Old) => New`, the whole state: one unit, whose runner opens the
|
|
207
|
+
* state lazily, so it costs what the body reads.
|
|
208
|
+
* - `rows` — a Dict's rows, `(K, V1) => V2` with the keys unchanged, or an
|
|
209
|
+
* Array's elements, `(T1) => T2` in order: a task split over the state, a
|
|
210
|
+
* piece at a time.
|
|
211
|
+
* - `rekey` — a Dict's entries, `(K1, V1) => { key: K2, value: V2 }`, or a
|
|
212
|
+
* Set's elements, `(T1) => T2`: the same split task, whose pieces' outputs
|
|
213
|
+
* merge by key. Two rows landing on one key fail, naming it; two elements
|
|
214
|
+
* landing on one are one element.
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* A variant, so a migration object decodes only with one of these: a deploy
|
|
218
|
+
* never runs a step of a form it does not know as another.
|
|
219
|
+
*/
|
|
220
|
+
export const MigrationFormType = VariantType({
|
|
221
|
+
rekey: NullType,
|
|
222
|
+
rows: NullType,
|
|
223
|
+
value: NullType,
|
|
224
|
+
});
|
|
225
|
+
export type MigrationFormType = typeof MigrationFormType;
|
|
226
|
+
|
|
227
|
+
/** A {@link MigrationFormType} by its name, as the SDK's definitions carry it. */
|
|
228
|
+
export type MigrationForm = ValueTypeOf<typeof MigrationFormType>['type'];
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A migration: one step of a record's declared chain, which a deploy runs over
|
|
232
|
+
* a workspace's state that has not applied it.
|
|
233
|
+
*
|
|
234
|
+
* @remarks
|
|
235
|
+
* A step changes the record's type, or keeps the type and changes its values.
|
|
236
|
+
* The chain is the record object's `migrations`, in order, and a workspace's
|
|
237
|
+
* record ref names the steps it has applied by name, never by this object's
|
|
238
|
+
* hash: the hash covers the step's IR, which carries source locations and
|
|
239
|
+
* changes with the SDK that exported it, so an unchanged step would read as
|
|
240
|
+
* edited.
|
|
241
|
+
*/
|
|
242
|
+
export const MigrationObjectType = StructType({
|
|
243
|
+
/** Which form this is — see {@link MigrationFormType}. */
|
|
244
|
+
form: MigrationFormType,
|
|
245
|
+
/** The record's type before the step. */
|
|
246
|
+
from: EastTypeType,
|
|
247
|
+
/** The record's type after it. */
|
|
248
|
+
to: EastTypeType,
|
|
249
|
+
/** Hash of the author's function's IR bundle, which a `value` step runs. */
|
|
250
|
+
bodyIr: StringType,
|
|
251
|
+
/** Hash of the generated program's IR bundle, `(piece, emit) => Null`,
|
|
252
|
+
* which a `rows` or `rekey` step runs over each piece of the state; empty
|
|
253
|
+
* for a `value` step. */
|
|
254
|
+
programIr: StringType,
|
|
255
|
+
/** Author-chosen runtime the step runs on. */
|
|
256
|
+
runner: RunnerType,
|
|
257
|
+
});
|
|
258
|
+
export type MigrationObjectType = typeof MigrationObjectType;
|
|
259
|
+
export type MigrationObject = ValueTypeOf<typeof MigrationObjectType>;
|
|
260
|
+
|
|
261
|
+
const decodeCurrentMigration = decodeBeast2For(MigrationObjectType);
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Decode a `MigrationObject`.
|
|
265
|
+
*
|
|
266
|
+
* @remarks
|
|
267
|
+
* A package-borne wire, so it changes by hard cutover: a package exported by
|
|
268
|
+
* an older SDK is re-exported with the current one, and this says so.
|
|
269
|
+
*
|
|
270
|
+
* @param data - the stored bytes
|
|
271
|
+
* @returns the migration object
|
|
272
|
+
* @throws {Error} When the bytes are not a current migration object — a
|
|
273
|
+
* package exported by an older SDK, which is re-exported with the current
|
|
274
|
+
* one.
|
|
275
|
+
*/
|
|
276
|
+
export function decodeMigrationObject(data: Uint8Array): MigrationObject {
|
|
277
|
+
try {
|
|
278
|
+
return decodeCurrentMigration(data);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`the migration object does not decode: the package was exported by an older e3 SDK — re-export it with the current one ` +
|
|
282
|
+
`(${err instanceof Error ? err.message : String(err)})`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
66
287
|
/**
|
|
67
288
|
* Record object stored in the object store, referenced by name from
|
|
68
|
-
* `PackageObject.records`. Carries the record's dataset path
|
|
289
|
+
* `PackageObject.records`. Carries the record's dataset path, its mutations,
|
|
290
|
+
* its secondary indexes and its migration chain.
|
|
69
291
|
*/
|
|
70
292
|
export const RecordObjectType = StructType({
|
|
71
293
|
/** refPath of the record's dataset, e.g. `records/orders`. */
|
|
72
294
|
path: StringType,
|
|
73
295
|
/** Mutations by name -> MutationObject hash. */
|
|
74
296
|
mutations: DictType(StringType, StringType),
|
|
297
|
+
/** Secondary indexes by name -> RecordIndexObject hash. */
|
|
298
|
+
indexes: DictType(StringType, StringType),
|
|
299
|
+
/** The declared migration chain, in order: each step's name, an identifier
|
|
300
|
+
* unique on the record, and its MigrationObject hash. Empty when the
|
|
301
|
+
* record declares none. */
|
|
302
|
+
migrations: ArrayType(StructType({ name: StringType, migration: StringType })),
|
|
75
303
|
});
|
|
76
304
|
export type RecordObjectType = typeof RecordObjectType;
|
|
77
305
|
export type RecordObject = ValueTypeOf<typeof RecordObjectType>;
|
|
306
|
+
|
|
307
|
+
const decodeCurrentRecord = decodeBeast2For(RecordObjectType);
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Decode a `RecordObject`.
|
|
311
|
+
*
|
|
312
|
+
* @remarks
|
|
313
|
+
* A package-borne wire, so it changes by hard cutover: a package exported by
|
|
314
|
+
* an older SDK is re-exported with the current one, and this says so.
|
|
315
|
+
*
|
|
316
|
+
* @param data - the stored bytes
|
|
317
|
+
* @returns the record object
|
|
318
|
+
* @throws {Error} When the bytes are not a current record object — a package
|
|
319
|
+
* exported by an older SDK, which is re-exported with the current one.
|
|
320
|
+
*/
|
|
321
|
+
export function decodeRecordObject(data: Uint8Array): RecordObject {
|
|
322
|
+
try {
|
|
323
|
+
return decodeCurrentRecord(data);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`the record object does not decode: the package was exported by an older e3 SDK — re-export it with the current one ` +
|
|
327
|
+
`(${err instanceof Error ? err.message : String(err)})`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** The `kind` tag a record's state object carries once the record has an
|
|
333
|
+
* index: what tells a reader that the ref names a table of manifests rather
|
|
334
|
+
* than one of them. */
|
|
335
|
+
export const RECORD_STATE_KIND = '$record';
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* A record's state when it has secondary indexes: one small object naming the
|
|
339
|
+
* primary's manifest and every index's.
|
|
340
|
+
*
|
|
341
|
+
* @remarks
|
|
342
|
+
* One state object, one commit, one conditional ref write — so the primary and
|
|
343
|
+
* every index are consistent at every commit, history carries them, reading at
|
|
344
|
+
* an older commit resolves them, compaction keeps them by copying one hash,
|
|
345
|
+
* and the collector walks them. A record with no index keeps the plain
|
|
346
|
+
* manifest, so this is additive: the door that resolves a record's state
|
|
347
|
+
* accepts either.
|
|
348
|
+
*/
|
|
349
|
+
export const RecordStateType = StructType({
|
|
350
|
+
/** Always {@link RECORD_STATE_KIND}. */
|
|
351
|
+
kind: StringType,
|
|
352
|
+
/** CollectionManifest hash of the primary. */
|
|
353
|
+
primary: StringType,
|
|
354
|
+
/** Index name -> the manifest it is held in and the index object it was
|
|
355
|
+
* built under. Deploy compares the second against the package's to decide
|
|
356
|
+
* whether an index is built, dropped or kept. */
|
|
357
|
+
indexes: DictType(StringType, StructType({
|
|
358
|
+
/** CollectionManifest hash of the index collection. */
|
|
359
|
+
manifest: StringType,
|
|
360
|
+
/** RecordIndexObject hash the index was built under. */
|
|
361
|
+
index: StringType,
|
|
362
|
+
})),
|
|
363
|
+
});
|
|
364
|
+
export type RecordStateType = typeof RecordStateType;
|
|
365
|
+
export type RecordState = ValueTypeOf<typeof RecordStateType>;
|
|
366
|
+
|
|
367
|
+
/** The record state's field names, in wire order — read from the type itself
|
|
368
|
+
* so the recognizer and the type cannot drift. */
|
|
369
|
+
const RECORD_STATE_FIELDS: readonly string[] =
|
|
370
|
+
(toEastTypeValue(RecordStateType).value as { name: string }[]).map((f) => f.name);
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Whether a decoded object is a record's `$record` state.
|
|
374
|
+
*
|
|
375
|
+
* @remarks
|
|
376
|
+
* Matched on the exact field set plus the tag, as every object recognizer is:
|
|
377
|
+
* a record's own state is an arbitrary user struct flowing through the same
|
|
378
|
+
* dispatch, and misreading one as a state table would probe its fields as
|
|
379
|
+
* object hashes.
|
|
380
|
+
*
|
|
381
|
+
* @param typeValue - the object's decoded root type
|
|
382
|
+
* @param value - the decoded value, when the tag is to be checked too
|
|
383
|
+
* @returns whether the object is a record state
|
|
384
|
+
*/
|
|
385
|
+
export function isRecordStateType(typeValue: EastTypeValue, value?: unknown): boolean {
|
|
386
|
+
if (typeValue.type !== 'Struct') return false;
|
|
387
|
+
const fields = typeValue.value as { name: string }[];
|
|
388
|
+
if (fields.length !== RECORD_STATE_FIELDS.length
|
|
389
|
+
|| !fields.every((f, i) => f.name === RECORD_STATE_FIELDS[i])) return false;
|
|
390
|
+
return value === undefined || (value as RecordState | null)?.kind === RECORD_STATE_KIND;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The collection an index is held in: `Dict<{ik, k}, P>`.
|
|
395
|
+
*
|
|
396
|
+
* @remarks
|
|
397
|
+
* `ik` first, so every entry with one index key is a contiguous run — ordered
|
|
398
|
+
* by primary key inside it — and a RANGE of index keys is one contiguous run
|
|
399
|
+
* too. `k` makes the entry unique and gives the run its inner order.
|
|
400
|
+
*
|
|
401
|
+
* @param keyType - the record's primary key type, `K`
|
|
402
|
+
* @param indexKeyType - the index key the function computes, `IK`
|
|
403
|
+
* @param valueType - the covering projection's type, `P`
|
|
404
|
+
* @returns the index collection's East type
|
|
405
|
+
*/
|
|
406
|
+
export function indexCollectionType(keyType: EastType, indexKeyType: EastType, valueType: EastType): EastType {
|
|
407
|
+
return DictType(StructType({ ik: indexKeyType, k: keyType }), valueType);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The op type of one target's changes: what one touched key carries.
|
|
412
|
+
*
|
|
413
|
+
* @remarks
|
|
414
|
+
* Exactly the op type of that target's own `PatchType`, which is what makes
|
|
415
|
+
* applying a delta's run for one target literally
|
|
416
|
+
* `applyFor(targetType)(segment, variant('patch', ops))` — `ConflictError` and
|
|
417
|
+
* all — rather than a second apply implementation that could disagree with the
|
|
418
|
+
* first.
|
|
419
|
+
*
|
|
420
|
+
* @param collectionType - the target's collection type (a Dict or a Set)
|
|
421
|
+
* @returns the op variant for one key of it
|
|
422
|
+
* @throws {Error} When the type is not a Dict or a Set — the kinds whose
|
|
423
|
+
* patches are sparse and addressed by key.
|
|
424
|
+
*/
|
|
425
|
+
export function patchOpsType(collectionType: EastType): EastType {
|
|
426
|
+
const collection = collectionType as unknown as { type: string; key: EastType; value: EastType };
|
|
427
|
+
if (collection.type === 'Dict') return dictPatchOpsType(collection.value);
|
|
428
|
+
if (collection.type === 'Set') return setPatchOpsType(collection.key);
|
|
429
|
+
throw new Error(
|
|
430
|
+
`a mutation delta addresses Dict and Set targets by key; this one holds ${collection.type}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** One target of a mutation delta: its name, its key and its op type. */
|
|
435
|
+
export interface DeltaTarget {
|
|
436
|
+
/** `primary`, or an index name. */
|
|
437
|
+
name: string;
|
|
438
|
+
/** The key the target is addressed by — `K` for the primary, `{ik, k}` for
|
|
439
|
+
* an index. */
|
|
440
|
+
keyType: EastType;
|
|
441
|
+
/** The target's own collection type, whose patch ops the arm carries. */
|
|
442
|
+
collectionType: EastType;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The case of a mutation delta that says the write went stale: its key says
|
|
447
|
+
* what went stale, in words, and its op is Null.
|
|
448
|
+
*
|
|
449
|
+
* @remarks
|
|
450
|
+
* A write reaches the record two ways — the program emits a delta and the
|
|
451
|
+
* engine applies it, or the engine applies the client's patch directly — and
|
|
452
|
+
* both meet the same situation: an op that disagrees with the state it lands
|
|
453
|
+
* on. The apply reports it as a conflict, which a caller retries. A program
|
|
454
|
+
* that meets it — a patch whose `before` the record no longer holds, an
|
|
455
|
+
* update of a row that no longer matches, a delete of a key the record does
|
|
456
|
+
* not hold — emits one of these rather than failing, which a caller would not
|
|
457
|
+
* retry. The name sorts before every target's, since index names are
|
|
458
|
+
* identifiers, so a stale delta says so in its first entry.
|
|
459
|
+
*/
|
|
460
|
+
export const DELTA_CONFLICT = '$conflict';
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The type of a mutation delta: every target's changes in ONE sorted
|
|
464
|
+
* collection.
|
|
465
|
+
*
|
|
466
|
+
* @remarks
|
|
467
|
+
* One variant case per target — `primary` plus one per declared index, which
|
|
468
|
+
* is why `primary` is a reserved index name — and {@link DELTA_CONFLICT}, which
|
|
469
|
+
* sorts first. Canonical order puts every target's ops in one contiguous run,
|
|
470
|
+
* in that target's own key order, so the apply takes the delta one target at
|
|
471
|
+
* a time, a segment at a time, and a delta is a pageable collection like any
|
|
472
|
+
* other.
|
|
473
|
+
*
|
|
474
|
+
* Derived on demand and never stored as a type: a delta blob is
|
|
475
|
+
* self-describing, and deriving it means it cannot drift from the record's
|
|
476
|
+
* declarations.
|
|
477
|
+
*
|
|
478
|
+
* @param targets - the primary and each index, in canonical case order
|
|
479
|
+
* @returns `Dict<DeltaKey, DeltaOp>`
|
|
480
|
+
*/
|
|
481
|
+
export function mutationDeltaType(targets: readonly DeltaTarget[]): EastType {
|
|
482
|
+
const keys: Record<string, EastType> = { [DELTA_CONFLICT]: StringType };
|
|
483
|
+
const ops: Record<string, EastType> = { [DELTA_CONFLICT]: NullType };
|
|
484
|
+
for (const target of targets) {
|
|
485
|
+
keys[target.name] = target.keyType;
|
|
486
|
+
ops[target.name] = patchOpsType(target.collectionType);
|
|
487
|
+
}
|
|
488
|
+
return DictType(VariantType(keys), VariantType(ops));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* The `edit` capability an edit-form mutation writes through.
|
|
493
|
+
*
|
|
494
|
+
* @remarks
|
|
495
|
+
* An edit body reads the state it is given — lazily, the frozen pager-backed
|
|
496
|
+
* value every runner already serves — and writes through these three
|
|
497
|
+
* functions; it never returns a state, so its commit rewrites only the
|
|
498
|
+
* segments the entries it touched live in.
|
|
499
|
+
*
|
|
500
|
+
* Repeated edits of one key fold: `set` after anything is that `set`; `update`
|
|
501
|
+
* after `set` applies to the set value; `update` after `update` composes;
|
|
502
|
+
* `delete` after anything is `delete`. A `set` of the value the state already
|
|
503
|
+
* holds is no change, and a `delete` or `update` of a key the state does not
|
|
504
|
+
* hold is a conflict naming the key, as an apply's would be.
|
|
505
|
+
*
|
|
506
|
+
* The Dict overload carries the struct's shape into TypeScript, so a body that
|
|
507
|
+
* declares `editTypeOf(record.type)` as its last parameter gets `edit.set`,
|
|
508
|
+
* `edit.delete` and `edit.update` typed against the record's own key and row.
|
|
509
|
+
*
|
|
510
|
+
* @param recordType - the record's state type, a Dict
|
|
511
|
+
* @returns the struct of edit functions
|
|
512
|
+
* @throws {Error} When the record's root is not a Dict.
|
|
513
|
+
*/
|
|
514
|
+
export function editTypeOf<K extends EastType, V extends EastType>(recordType: DictType<K, V>): StructType<{
|
|
515
|
+
set: FunctionType<[K, V], NullType>;
|
|
516
|
+
delete: FunctionType<[K], NullType>;
|
|
517
|
+
update: FunctionType<[K, PatchTypeOf<V>], NullType>;
|
|
518
|
+
}>;
|
|
519
|
+
export function editTypeOf(recordType: EastType): EastType;
|
|
520
|
+
export function editTypeOf(recordType: EastType): EastType {
|
|
521
|
+
const dict = recordType as unknown as { type: string; key: EastType; value: EastType };
|
|
522
|
+
if (dict.type !== 'Dict') {
|
|
523
|
+
throw new Error(`an edit mutation writes a Dict record; this one holds ${dict.type}`);
|
|
524
|
+
}
|
|
525
|
+
return StructType({
|
|
526
|
+
set: FunctionType([dict.key, dict.value], NullType),
|
|
527
|
+
delete: FunctionType([dict.key], NullType),
|
|
528
|
+
update: FunctionType([dict.key, PatchType(dict.value)], NullType),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* One window of an index read: an ORDERED array, never a dict.
|
|
534
|
+
*
|
|
535
|
+
* @remarks
|
|
536
|
+
* A Dict value re-sorts by its own key, which would throw away the index order
|
|
537
|
+
* the page was asked for — so the wire type of an index window is positional,
|
|
538
|
+
* in index order, carrying every key the client may need. Whatever renders an
|
|
539
|
+
* index-ordered view consumes this shape; it is a contract the data layer sets
|
|
540
|
+
* and the UI meets, not the reverse.
|
|
541
|
+
*
|
|
542
|
+
* @param keyType - the record's primary key type, `K`
|
|
543
|
+
* @param indexKeyType - the index key, `IK`
|
|
544
|
+
* @param valueType - the covering projection's type, `P`
|
|
545
|
+
* @param rowType - the record's row type, `V`
|
|
546
|
+
* @returns the window's East type
|
|
547
|
+
*/
|
|
548
|
+
export function indexWindowType(keyType: EastType, indexKeyType: EastType, valueType: EastType, rowType: EastType): EastType {
|
|
549
|
+
return ArrayType(StructType({
|
|
550
|
+
/** The index key. */
|
|
551
|
+
ik: indexKeyType,
|
|
552
|
+
/** The primary key, so a client can address the row it came from. */
|
|
553
|
+
key: keyType,
|
|
554
|
+
/** The covering projection; Null when the index carries none. */
|
|
555
|
+
value: valueType,
|
|
556
|
+
/** The primary row — `some(...)` when the read asked for a join. */
|
|
557
|
+
row: OptionType(rowType),
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 order of releases, by which an e3 tells a newer release's zip or
|
|
8
|
+
* execution state from an older one's.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { E3_RELEASE, compareReleases } from './release.js';
|
|
14
|
+
|
|
15
|
+
describe('compareReleases', () => {
|
|
16
|
+
it('orders releases by semantic-version precedence', () => {
|
|
17
|
+
// Each precedes the next: the core numerically, a pre-release before its
|
|
18
|
+
// core's release, numeric identifiers before words and numerically, and a
|
|
19
|
+
// list before a longer one it begins.
|
|
20
|
+
const ordered = [
|
|
21
|
+
'0.9.0', '1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-alpha.beta', '1.0.0-beta', '1.0.0-beta.2', '1.0.0-beta.11',
|
|
22
|
+
'1.0.0-rc.1', '1.0.0', '1.0.9', '1.0.10', '1.0.79', '1.0.80-beta.1', '1.0.80', '1.1.0', '2.0.0',
|
|
23
|
+
];
|
|
24
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
25
|
+
for (let j = 0; j < ordered.length; j++) {
|
|
26
|
+
const order = Math.sign(compareReleases(ordered[i]!, ordered[j]!));
|
|
27
|
+
assert.equal(order, Math.sign(i - j), `${ordered[i]} against ${ordered[j]}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('orders nothing by build metadata', () => {
|
|
33
|
+
assert.equal(compareReleases('1.0.79+ci.7', '1.0.79'), 0);
|
|
34
|
+
assert.equal(compareReleases('1.0.80-beta.1+a', '1.0.80-beta.1+b'), 0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('refuses what is not a semantic version, naming it', () => {
|
|
38
|
+
for (const release of ['1.0', 'v1.0.79', '1.0.079', '1.0.79-', 'latest', '']) {
|
|
39
|
+
assert.throws(() => compareReleases(release, E3_RELEASE), { message: new RegExp(`^${JSON.stringify(release).replace(/[.+]/g, '\\$&')} is not a release`) });
|
|
40
|
+
assert.throws(() => compareReleases(E3_RELEASE, release), /is not a release/);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is a release itself', () => {
|
|
45
|
+
assert.equal(compareReleases(E3_RELEASE, E3_RELEASE), 0);
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/release.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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 release of e3 this build is, and how two releases order.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A release is the version the release scripts write into every package.
|
|
11
|
+
* `scripts/set-npm-version.mjs` writes it here with them, and
|
|
12
|
+
* `scripts/check-version-drift.mjs` refuses a tree where it differs from
|
|
13
|
+
* theirs. What e3 keeps or ships records the release that wrote it: a
|
|
14
|
+
* repository's record, an execution state and a package zip, and a transfer
|
|
15
|
+
* request the release that sent it, so a refusal of any of them names that
|
|
16
|
+
* release beside this one.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The release of e3 this build is. */
|
|
20
|
+
export const E3_RELEASE = '1.0.79';
|
|
21
|
+
|
|
22
|
+
/** A semantic version: its core, a pre-release, and build metadata. */
|
|
23
|
+
const RELEASE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Orders two releases by semantic-version precedence.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* The core, `major.minor.patch`, orders numerically. A pre-release
|
|
30
|
+
* (`1.1.0-beta.2`) precedes the release of its core, and two pre-releases of
|
|
31
|
+
* one core order by their dot-separated identifiers in turn: numbers
|
|
32
|
+
* numerically and before words, words in ASCII order, and a list that is a
|
|
33
|
+
* prefix of the other first. Build metadata (`+…`) orders nothing.
|
|
34
|
+
*
|
|
35
|
+
* @param a - A release, such as `1.0.79` or `1.1.0-beta.2`
|
|
36
|
+
* @param b - Another release
|
|
37
|
+
* @returns A negative number when `a` precedes `b`, a positive one when it
|
|
38
|
+
* follows it, and 0 when neither does
|
|
39
|
+
* @throws {Error} When either is not a semantic version
|
|
40
|
+
*/
|
|
41
|
+
export function compareReleases(a: string, b: string): number {
|
|
42
|
+
const x = parseRelease(a);
|
|
43
|
+
const y = parseRelease(b);
|
|
44
|
+
for (let i = 0; i < 3; i++) {
|
|
45
|
+
if (x.core[i] !== y.core[i]) return x.core[i]! < y.core[i]! ? -1 : 1;
|
|
46
|
+
}
|
|
47
|
+
if (x.pre.length === 0 || y.pre.length === 0) return y.pre.length - x.pre.length;
|
|
48
|
+
for (let i = 0; i < Math.min(x.pre.length, y.pre.length); i++) {
|
|
49
|
+
const p = x.pre[i]!;
|
|
50
|
+
const q = y.pre[i]!;
|
|
51
|
+
if (p === q) continue;
|
|
52
|
+
const pNumber = /^\d+$/.test(p);
|
|
53
|
+
const qNumber = /^\d+$/.test(q);
|
|
54
|
+
if (pNumber && qNumber) return BigInt(p) < BigInt(q) ? -1 : 1;
|
|
55
|
+
if (pNumber !== qNumber) return pNumber ? -1 : 1;
|
|
56
|
+
return p < q ? -1 : 1;
|
|
57
|
+
}
|
|
58
|
+
return x.pre.length - y.pre.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A release's core numbers and pre-release identifiers. */
|
|
62
|
+
function parseRelease(release: string): { core: number[]; pre: string[] } {
|
|
63
|
+
const match = RELEASE.exec(release);
|
|
64
|
+
if (match === null) throw new Error(`${JSON.stringify(release)} is not a release: a release is a semantic version, such as ${E3_RELEASE}`);
|
|
65
|
+
return {
|
|
66
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
67
|
+
pre: match[4] === undefined ? [] : match[4].split('.'),
|
|
68
|
+
};
|
|
69
|
+
}
|