@elaraai/east-claude-plugin 1.0.98 → 1.0.100
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/.build/daemon/lsp.js +40 -3
- package/.build/daemon/server.js +40 -3
- package/.build/mcp/server.js +3 -2
- package/.claude-plugin/plugin.json +1 -1
- package/index.json +13340 -482
- package/package.json +1 -1
- package/skills/e3/SKILL.md +107 -676
- package/skills/e3/reference/browser.md +93 -0
- package/skills/e3/reference/cli.md +120 -0
- package/skills/e3/reference/from-code.md +48 -0
- package/skills/e3/reference/rack.md +139 -0
- package/skills/e3/reference/record-indexes.md +326 -0
- package/skills/e3/reference/record-migrations.md +63 -0
- package/skills/e3/reference/repository.md +25 -0
- package/skills/e3-ui/SKILL.md +426 -259
- package/skills/east/SKILL.md +33 -395
- package/skills/east/reference/beast2.md +71 -0
- package/skills/east/reference/cross-language-and-json-schema.md +87 -0
- package/skills/east/reference/queries.md +46 -0
- package/skills/east/reference/runners-and-ir.md +148 -0
- package/skills/east/reference/trees-and-recursion.md +56 -0
- package/skills/east/reference/values-in-typescript.md +62 -0
- package/skills/east-contribute/SKILL.md +0 -1
- package/skills/east-py/SKILL.md +36 -1613
- package/skills/east-py/reference/beast2.md +108 -0
- package/skills/east-py/reference/cli.md +51 -0
- package/skills/east-py/reference/columns-and-platform-functions.md +87 -0
- package/skills/east-py/reference/control-flow-and-queries.md +147 -0
- package/skills/east-py/reference/cross-language.md +70 -0
- package/skills/east-py/reference/ir-and-diagnostics.md +150 -0
- package/skills/east-py/reference/patterns.md +369 -0
- package/skills/east-py/reference/performance.md +70 -0
- package/skills/east-py/reference/value-methods.md +306 -0
- package/skills/east-py/reference/values-construction-and-json.md +88 -0
- package/skills/east-py/reference/writing-east-functions.md +275 -0
- package/skills/east-py-datascience/SKILL.md +11 -28
- package/skills/east-ui/SKILL.md +5 -5
package/skills/e3/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: e3
|
|
3
|
-
description: "East Execution Engine (e3) - durable, content-addressed dataflow for East programs. Use when: (1) authoring a package with @elaraai/e3 (e3.input, e3.task, e3.streamTask with e3.output and e3.partition for bounded memory and work split over an input, e3.customTask, e3.function, e3.package, e3.export, East.importFunction), (2) records - audited state written only through mutations
|
|
3
|
+
description: "East Execution Engine (e3) - durable, content-addressed dataflow for East programs. Use when: (1) authoring a package with @elaraai/e3 (e3.input, e3.task, e3.streamTask with e3.output and e3.partition for bounded memory and work split over an input, e3.customTask, e3.function, e3.package, e3.export, East.importFunction), (2) records - audited state written only through mutations, declared whole in one e3.record(name, type, { initial, indexes, mutations, migrations }) call: mutation.reduce/edit/patch; index.on/key/keys, index.interval / index.area for reads by a time window or a 2D window, index.series / index.summary for a block of keys over a span of time tiles and their totals, and index.nested for what a record's rows nest — jobs, a value a day, child rows, trees — filed item by item, with totals and parents' subtotals; and migrated between versions (migration.value/rows/rekey, in order), (3) the e3 CLI (repo, package, workspace, dataset, task, dataflow run, run, call, mutate, seed, history, reindex, compact, watch, convert, auth) and the cores and memory a run may use (-j, --memory), (4) driving e3 from code: @elaraai/e3-api-client against a server, @elaraai/e3-core on a local repository, e3-api-server, (5) caching, reactive re-runs and garbage collection, (6) delegating local task bodies to a rack: e3 rack enroll/policy, dataflow run --rack, watch --start --rack, optional e3-api-server --rack, (7) e3 in a browser, no server: @elaraai/e3-web (serveUnits, serveE3, createWebE3) and what a browser cannot do. UI tasks are e3-ui's ui()."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# East Execution Engine (e3)
|
|
@@ -29,6 +29,10 @@ writing or changing East code:
|
|
|
29
29
|
programs, exact and far cheaper, and the signatures omit the runtime rules
|
|
30
30
|
that make East code correct.
|
|
31
31
|
|
|
32
|
+
Tested e3 packages (inputs, tasks, stream tasks and partitions, custom tasks,
|
|
33
|
+
functions, records with their mutations and migrations) are in the index too:
|
|
34
|
+
search with `package: "e3"`.
|
|
35
|
+
|
|
32
36
|
Nothing is injected for you; the search is the step.
|
|
33
37
|
|
|
34
38
|
## Quick Start
|
|
@@ -66,160 +70,26 @@ What do you need?
|
|
|
66
70
|
│ │ └─ Split over an input, in pieces → wrap that input: e3.partition(dataset, { by? })
|
|
67
71
|
│ ├─ A shell command → e3.customTask(name, [inputs], outputType, command)
|
|
68
72
|
│ ├─ A named function (RPC) → e3.function(name, fn, config?) — called by name, persists nothing
|
|
69
|
-
│ ├─ Audited state → e3.record
|
|
70
|
-
│ │ ├─
|
|
71
|
-
│ │
|
|
72
|
-
│ ├─
|
|
73
|
+
│ ├─ Audited state → e3.record(name, type, { initial?, indexes?, mutations?, migrations? }), declared whole
|
|
74
|
+
│ │ ├─ Its writes → mutations: (mutation) => ({ name: mutation.reduce | edit | patch })
|
|
75
|
+
│ │ ├─ An order to page in → indexes: (index) => ({ name: index.on([...fields]) | index.key(fn) | index.keys(fn) }) (reference/record-indexes.md)
|
|
76
|
+
│ │ ├─ Read by a time window or a 2D window → index.interval | index.area
|
|
77
|
+
│ │ ├─ Read a block of keys over time tiles, or their totals → index.series | index.summary
|
|
78
|
+
│ │ └─ Read what rows nest (jobs, a value a day, child rows, trees) a tile at a time, with totals and subtotals
|
|
79
|
+
│ │ → index.nested({ collections, tile, summary? })
|
|
80
|
+
│ ├─ A record's type changes → migrations: (migration) => [migration.value | rows | rekey(name, fn), …], in order (reference/record-migrations.md)
|
|
73
81
|
│ ├─ A UI task → ui(name, [inputs], fn) from @elaraai/e3-ui (the e3-ui skill)
|
|
74
82
|
│ ├─ A function written in python or another node package → East.importFunction(pkg, name, FunctionType) in the body
|
|
75
83
|
│ └─ Bundle and export → e3.package(name, version, ...items); e3.export(pkg, zipPath)
|
|
76
|
-
├─ Operate it from the shell → the e3 CLI (
|
|
77
|
-
├─ Delegate local task bodies to a rack → e3 rack enroll + policy; dataflow run --rack, watch --start --rack
|
|
84
|
+
├─ Operate it from the shell → the e3 CLI (reference/cli.md): deploy, run, read and write datasets, mutate records, gc
|
|
85
|
+
├─ Delegate local task bodies to a rack → e3 rack enroll + policy; dataflow run --rack, watch --start --rack (reference/rack.md)
|
|
78
86
|
└─ Drive it from code
|
|
79
|
-
├─ A server, over HTTP → @elaraai/e3-api-client
|
|
87
|
+
├─ A server, over HTTP → @elaraai/e3-api-client (reference/from-code.md)
|
|
80
88
|
├─ A local repository → @elaraai/e3-core
|
|
81
89
|
├─ Serve repositories → e3-api-server --repos <dir>, or createServer()
|
|
82
|
-
└─ e3 in a browser, no server → @elaraai/e3-web: serveUnits, serveE3, createWebE3 (
|
|
90
|
+
└─ e3 in a browser, no server → @elaraai/e3-web: serveUnits, serveE3, createWebE3 (reference/browser.md)
|
|
83
91
|
```
|
|
84
92
|
|
|
85
|
-
## Rack delegation from a local repository
|
|
86
|
-
|
|
87
|
-
**Setup and routing**
|
|
88
|
-
|
|
89
|
-
| Signature | Description | Example |
|
|
90
|
-
| --- | --- | --- |
|
|
91
|
-
| `e3 rack enroll --listen <host:port> [--allow <ip,...>] [--advertise-url <origin>] [--label <label>] [--no-wait]` | Starts the user-wide hub and prints the source origin, the hub's certificate SHA-256, the matching release and a single-use token for the companion's hidden prompts. Waits for a new registration, not activation. Behind an SSH reverse tunnel, listen on the loopback and advertise the relay's origin. | `e3 rack enroll --listen 127.0.0.1:7331 --allow 127.0.0.1 --advertise-url https://relay.example:7401` |
|
|
92
|
-
| `e3 rack policy [repo] set <glob> rack\|local [--size <size>] [--timeout <min>] [--allow-host-access]` | First whole-task-name match wins; `*` and `?` are wildcards. Size: serverless, small, medium, large, xlarge. | `e3 rack policy . set 'train_*' rack --size large` |
|
|
93
|
-
| `e3 rack policy [repo] enable\|disable\|mode <opt-in\|auto>` | Enable/disable saved opt-in; `mode auto` selects eligible unmatched tasks. Defaults: disabled, opt-in. | `e3 rack policy . enable` |
|
|
94
|
-
| `e3 rack policy [repo] defaults [--no-spill] [--claim-timeout <sec>] [--retry-task-failures-locally]` | Defaults: large, 1440-minute timeout, spill on, 30-second claim recheck, no task-failure retry. | `e3 rack policy . defaults --no-spill` |
|
|
95
|
-
| `e3 rack policy [repo] show\|unset <glob>\|reset` | Inspect rules, remove an exact pattern, or delete the saved policy. | `e3 rack policy . show` |
|
|
96
|
-
| `e3 dataflow run <repo> <ws> --rack\|--no-rack\|--rack-only` | Select rack routing for this run; rack-only disables capacity spill but keeps infrastructure fallback. ❗ Refused for remote URLs. | `e3 dataflow run . dev --rack` |
|
|
97
|
-
| `e3 watch <source> <repo> <ws> --start --rack` | Reuses a rack session across watch runs and refreshes capacity; a second Ctrl-C stops the watch at once. ❗ `--rack` and `--rack-only` require `--start`. | `e3 watch src/main.ts . dev --start --rack` |
|
|
98
|
-
| `e3 rack status\|list\|remove <id-or-label>\|approve <id-or-label>\|stop` | Inspect fleet, revoke a rack, admit the boot a quarantined rack last reported, or stop the shared hub. Status never starts an absent hub. | `e3 rack status` |
|
|
99
|
-
| `e3 rack config [--listen <host:port>] [--allow <ip,...>] [--idle-exit <min>] [--advertise-url <origin>] [--tls\|--no-tls] [--require-approval\|--no-require-approval]` | Configure the shared listener (off by default; HTTPS with the hub's own certificate unless `--no-tls`); every rack and data URL is built on `--advertise-url` when set; zero idle-exit disables automatic shutdown; `--require-approval` quarantines a rack after each reboot until `e3 rack approve` admits it (enrollment admits the first boot). ❗ Plain HTTP only on the loopback or a private address, with `--allow`. | `e3 rack config --listen 10.0.0.5:7331` |
|
|
100
|
-
|
|
101
|
-
The local process owns graph planning, repository and cache. After a cache
|
|
102
|
-
miss, eligible plain task, map and merge bodies can execute on the rack; input
|
|
103
|
-
closures and every output object transfer by hash. The session verifies the
|
|
104
|
-
whole output graph before success. Placement never changes task/cache hashes.
|
|
105
|
-
Local bodies and fallback share the original cores/memory budget; rack bodies
|
|
106
|
-
use remote capacity. Calls, mutations and intake stay local. Stock runner
|
|
107
|
-
configurations qualify; commands, custom environments/platforms and unscannable
|
|
108
|
-
IR stay local. Host file/environment/network access stays local unless a rule
|
|
109
|
-
deliberately grants `--allow-host-access` and its resources exist on the rack.
|
|
110
|
-
|
|
111
|
-
The listener serves HTTPS with the hub's own self-signed certificate, made on
|
|
112
|
-
first use and kept in `hub-tls.beast2`; a rack pins it by its SHA-256, which
|
|
113
|
-
enrollment prints, so TLS runs end to end through any relay or tunnel. The
|
|
114
|
-
usual path is an SSH reverse tunnel from the developer machine to a relay the
|
|
115
|
-
rack reaches, with the hub on the loopback (`--allow 127.0.0.1`) and the relay's
|
|
116
|
-
origin advertised. Removing the certificate file, with the hub stopped, makes a
|
|
117
|
-
new one, which every worker must then pin again. `--no-tls` is for a TLS
|
|
118
|
-
terminator in front of the hub; the hub refuses plain HTTP on a wildcard or
|
|
119
|
-
public address, or without `--allow`. A listener on every address needs
|
|
120
|
-
`--advertise-url` to enroll.
|
|
121
|
-
|
|
122
|
-
The handoff targets a prepared independent worker VM with the matching verified
|
|
123
|
-
`e3-rack` release. Its `configure --allocation <host-provisioned-path>` prompts
|
|
124
|
-
for the source origin, the certificate SHA-256 for an HTTPS origin the hub
|
|
125
|
-
serves, the worker label and settings; `enroll --allocation <path>` prompts for
|
|
126
|
-
the one-time token. Origins, pins and credentials are encrypted together. A
|
|
127
|
-
plain-HTTP origin needs `--allow-vpn-http` on both commands and host
|
|
128
|
-
authorization for that exact endpoint, and belongs only on a path encrypted end
|
|
129
|
-
to end, such as an SSH tunnel, never across a VPN hub that decrypts traffic.
|
|
130
|
-
Never put a source address or token in worker command arguments or plaintext
|
|
131
|
-
files. The local mint intentionally displays its token; do not log that output.
|
|
132
|
-
|
|
133
|
-
Enrollment does not start services or authorize task claims. The local CLI
|
|
134
|
-
observes a new registration (optionally filtered by label); compare it with the
|
|
135
|
-
worker's result. Before a first heartbeat, status is `enrolled-not-started`.
|
|
136
|
-
After managed resume, a ready heartbeat advertises free plus occupied slots;
|
|
137
|
-
busy workers stay eligible. Fenced, draining or stopping workers withdraw their
|
|
138
|
-
capacity and show `unavailable`. After 45 seconds without a heartbeat, readiness
|
|
139
|
-
expires; lease polling cannot prolong it. Source boot approval, when required,
|
|
140
|
-
remains an additional gate. This advertisement does not authorize host resource
|
|
141
|
-
admission or removal of service/network quarantine.
|
|
142
|
-
Resources and mutable whole-GPU assignments belong to the host lifecycle, not
|
|
143
|
-
local enrollment flags. The companion accepts up to 32 mutually trusted sources
|
|
144
|
-
in one worker. After draining and stopping its services and sockets, use
|
|
145
|
-
`configure --allocation <path> --add-source`, confirm the shared trust boundary,
|
|
146
|
-
and enter the additional origin privately. It prints the new source UUID for
|
|
147
|
-
`enroll --allocation <path> --source <UUID>`; add `--allow-vpn-http` to both for a
|
|
148
|
-
plain-HTTP origin. All sources must be enrolled before startup. Each has its own identity,
|
|
149
|
-
client, cache and completion journal; actual execution shares one worker-wide
|
|
150
|
-
task/GPU limit. Per-source capacity reports are not additive reservations.
|
|
151
|
-
A worker is the trust boundary: a shared agent process can access all its
|
|
152
|
-
sources' credentials, so separate clients, and a cloud deployment and developer
|
|
153
|
-
hubs, need separate worker VMs. The hub keeps no task data; what a worker keeps
|
|
154
|
-
between tasks is its own policy, and an agent that keeps nothing works unchanged. Worker-local `source list --allocation <path>`
|
|
155
|
-
prints UUIDs/enrollment status; `--show-origins` adds origins only on a terminal.
|
|
156
|
-
After drain/stop and source-side revocation, `source remove` or `source reset`
|
|
157
|
-
with `--allocation <path> --source <UUID> --acknowledge-revocation` asks for UUID
|
|
158
|
-
confirmation. Reset prints a new UUID for fresh enrollment; old pending results
|
|
159
|
-
and caches stay in the retired namespace. Other sources and retained data are
|
|
160
|
-
preserved, and host endpoint policy must be reconciled before activation. The
|
|
161
|
-
flag acknowledges revocation; it does not revoke remotely.
|
|
162
|
-
|
|
163
|
-
Worker-local `registry status --allocation <path>` reports only whether encrypted
|
|
164
|
-
registry credentials are present; it can run while active and does not test access.
|
|
165
|
-
After drain/stop, `registry configure --allocation <path>` privately prompts for
|
|
166
|
-
username/token without re-entering settings. `registry clear --allocation <path>`
|
|
167
|
-
asks for the installation UUID and removes only the stored access. Both require
|
|
168
|
-
the same release, the writer lock and no unresolved enrollment/service transition;
|
|
169
|
-
they preserve current identities, rotations, settings, journals and cached data.
|
|
170
|
-
Credentials stay in the existing encrypted bundle, with no Docker login file.
|
|
171
|
-
A later activation receives them; services remain stopped and the next boot fenced.
|
|
172
|
-
Repeating configure with the same values or clear when absent does not write.
|
|
173
|
-
Clearing/replacing does not revoke a token remotely or purge cached images.
|
|
174
|
-
|
|
175
|
-
The companion's
|
|
176
|
-
`reconcile-services` applies a host-provided next allocation and service budgets
|
|
177
|
-
after a completed drain, with stopped units, the previous plan hash and exact
|
|
178
|
-
operation/boot binding. It retains ciphertext/journals and network quarantine;
|
|
179
|
-
it does not move a GPU, start a worker or change its executable release. For a
|
|
180
|
-
compatible release change, invoke the target verified executable's `switch-release`
|
|
181
|
-
with the same drain/plan bindings and unchanged resource budgets at the next
|
|
182
|
-
allocation generation. It checks both managed-state contracts, updates only the
|
|
183
|
-
runtime version in the current encrypted configuration, and preserves identities
|
|
184
|
-
and journals through interruption or downgrade. Both four-file releases must stay
|
|
185
|
-
available; a downgrade is a new operation, never a saved-state restore. Services
|
|
186
|
-
stay stopped and quarantined. If the selected release never resumed, the exact
|
|
187
|
-
recorded previous executable's `recover-release` binds the completed release
|
|
188
|
-
receipt, current plan and new operation at another generation increase. It
|
|
189
|
-
requires the original drain or a valid fenced journal with no operation at the
|
|
190
|
-
failed generation, verifies both releases' recorded hashes, and preserves current
|
|
191
|
-
credentials and journals. A prior resume attempt, even across reboot, refuses
|
|
192
|
-
recovery. General journal repair remains unfinished. Named host management, source aliases, network
|
|
193
|
-
activation, incompatible-state migrations and hardware acceptance remain
|
|
194
|
-
companion cloud #253 work. This and durable completion retry are implemented in
|
|
195
|
-
companion PR #254, separately from the original main protocol pin.
|
|
196
|
-
|
|
197
|
-
Policy is `<repo>/rack/policy.beast2`; hub state is `~/.e3/rack` (or
|
|
198
|
-
`E3_RACK_HOME`). A hub `dataflow run` starts exits after 15 idle minutes, and a
|
|
199
|
-
run waits up to 10 seconds for an enrolled rack to report healthy (with none
|
|
200
|
-
enrolled, or a policy that selects no task for a rack, it starts at once): run
|
|
201
|
-
`e3 rack hub` as a service, or set `--idle-exit 0`, for a hub racks can always
|
|
202
|
-
reach. A data URL lasts 15 minutes, which a transfer may use whole; an object is
|
|
203
|
-
at most 5 GiB. The control schema currently requires protocol 2 exactly.
|
|
204
|
-
Agents must report bundled e3 at least the project's release. Infrastructure
|
|
205
|
-
failure falls back locally with a separate execution attempt, as does an
|
|
206
|
-
attempt the rack stopped on its own; the run's own cancellation and the task's
|
|
207
|
-
timeout never do. One cancelled subscriber does not stop work shared by other
|
|
208
|
-
sessions of the same canonical repository and e3 release.
|
|
209
|
-
|
|
210
|
-
**Programmatic attachment** (`@elaraai/e3-rack`)
|
|
211
|
-
|
|
212
|
-
| Signature | Description | Example |
|
|
213
|
-
| --- | --- | --- |
|
|
214
|
-
| `openRackRunner({ repoPath, workspace, storage, label, budget?, settings?, policy?, rackOnly? })` | Opens a runner/session; preserve the caller's existing budget and process settings, use `rackSlots` to extend orchestrator width, and close in `finally`. | `const rack = await openRackRunner({ repoPath, workspace: 'dev', storage, label: 'run', budget });` |
|
|
215
|
-
| `createDataflowRunnerFactory(options?)` | Pass to `createServer({ dataflowRunner })`; one session per API dataflow, using the server's shared budget. Explicit server opt-in overrides policy.enabled; rules still apply. | `const server = await createServer({ reposDir: './repos', dataflowRunner: createDataflowRunnerFactory() });` |
|
|
216
|
-
|
|
217
|
-
The API server's optional rack package is needed only for
|
|
218
|
-
`e3-api-server --repos ./repos --rack`. Portable handlers receive a runner
|
|
219
|
-
factory and do not import hub code. Custom environments, remote function
|
|
220
|
-
delegation, automatic reconnect after hub restart and embedded UI integration
|
|
221
|
-
are deferred.
|
|
222
|
-
|
|
223
93
|
## Authoring a package
|
|
224
94
|
|
|
225
95
|
### Inputs — `e3.input(name, type, source?)`
|
|
@@ -335,17 +205,26 @@ const shout = e3.task('shout', [greet.output],
|
|
|
335
205
|
- **An East function written in python, or in another node package**:
|
|
336
206
|
`East.importFunction(pkg, name, FunctionType(...))` in the body. `e3.export`
|
|
337
207
|
resolves it and embeds its IR, so the task is pure IR on any runner, its
|
|
338
|
-
locations kept so an error or a profile names the function's own lines
|
|
208
|
+
locations kept so an error or a profile names the function's own lines (so
|
|
209
|
+
moving them re-runs the task: **Caching**, below). A uv
|
|
339
210
|
workspace member is found by name in `uv.lock`, and its root module's
|
|
340
|
-
`east_functions` dict is exported with `east-py export-functions` (
|
|
341
|
-
project's `.venv`, else
|
|
342
|
-
`package.json` name, and its built `./functions` export's `eastFunctions`
|
|
343
|
-
exported with `east-node export-functions` (`EAST_NODE`).
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
211
|
+
`east_functions` dict is exported with `east-py export-functions` (`EAST_PY`
|
|
212
|
+
when set, else the project's `.venv`, else PATH); an npm member is found by
|
|
213
|
+
its `package.json` name, and its built `./functions` export's `eastFunctions`
|
|
214
|
+
is exported with `east-node export-functions` (`EAST_NODE`). Each member is
|
|
215
|
+
exported in one process however many tasks import from it, the members side
|
|
216
|
+
by side, as many at once as the export has cores (a deploy's `-j`,
|
|
217
|
+
`e3.export`'s `cores`). A python member's manifests are kept in
|
|
218
|
+
`node_modules/.cache/e3/functions` under the directory the export runs in,
|
|
219
|
+
keyed on its sources and those of the local packages it depends on, the
|
|
220
|
+
workspace's `uv.lock` and `pyproject.toml`, the east-py exporting it and the
|
|
221
|
+
functions and providers each manifest is for, so a redeploy whose python has
|
|
222
|
+
not changed runs no export; deleting the directory clears it, and a node
|
|
223
|
+
member is exported every time. A package built elsewhere is passed as its
|
|
224
|
+
manifest — `{ functions: [...] }` / `--functions` — which wins. The declared
|
|
225
|
+
type must equal the exported one exactly, and each platform function it calls
|
|
226
|
+
must be provided by the task's runner, by name or by family (`east-py-std` ≡
|
|
227
|
+
`@elaraai/east-node-std` ≡ `east-c-std`; `east-py-io` ≡ `@elaraai/east-node-io`).
|
|
349
228
|
- **Environments** are derived at export from each `{ custom }` platform: the
|
|
350
229
|
providing workspace package's closure (`pyproject.toml` + `uv.lock` + sdists,
|
|
351
230
|
or `package.json` + lockfile + `npm pack`) is captured into the package and
|
|
@@ -501,7 +380,7 @@ const change = e3.streamTask('change', {
|
|
|
501
380
|
| `e3.customTask(name, inputs, outputType, command, { environment? })` | A task that runs a bash script: `command($, inputPaths, outputPath)` builds it from the staged beast2 input files, and it writes the output as a beast2 file. |
|
|
502
381
|
| `e3.function(name, fn, { runner?, environment? })` | A named function, called by name with arguments (`e3 call`, the API) and its result returned inline: not in the dataflow, and a call persists nothing. An argument is a value, or in a call through a workspace one of its datasets, read where it is stored. A server bounds a call by a timeout and a 1 MiB result, so long work belongs in a task. A `custom` runner must speak the runner CLI (`<command> -i <arg>… -o <out> <ir>`). |
|
|
503
382
|
| `e3.package(name, version, ...items)` | Bundles the items and everything they depend on — pass the last tasks, records and functions; a package passed in contributes its contents. The name and version must be valid file names. |
|
|
504
|
-
| `e3.export(pkg, zipPath, { functions? })` | Writes the package zip, resolving every `East.importFunction` (a workspace package is exported by the export itself; `functions` lists manifests built elsewhere). |
|
|
383
|
+
| `e3.export(pkg, zipPath, { functions?, cores? })` | Writes the package zip, resolving every `East.importFunction` (a workspace package is exported by the export itself, `cores` packages at once, the CPUs by default; `functions` lists manifests built elsewhere). |
|
|
505
384
|
|
|
506
385
|
```typescript
|
|
507
386
|
const process = e3.customTask('process', [rawData], StringType,
|
|
@@ -513,15 +392,19 @@ await e3.export(e3.package('tools', '1.0.0', process, add), '/tmp/tools.zip');
|
|
|
513
392
|
|
|
514
393
|
### Records — audited state, written only through mutations
|
|
515
394
|
|
|
516
|
-
`e3.record(name, type,
|
|
517
|
-
tasks read and react to like any input, but
|
|
395
|
+
`e3.record(name, type, { initial?, indexes?, mutations?, migrations? })` is a
|
|
396
|
+
dataset at `.records.<name>` that tasks read and react to like any input, but
|
|
397
|
+
that only its **mutations** write:
|
|
518
398
|
a raw `dataset set` is refused. A mutation is pure, synchronous East — no async
|
|
519
399
|
body and no platform call, since the compare-and-swap retry runs it again
|
|
520
400
|
against fresher state — named by an identifier. Each commit records its parent,
|
|
521
401
|
the state, the mutation, its arguments, the actor and the time. Deploy mints a
|
|
522
402
|
`$init` commit from the initial value, and a redeploy keeps state and history.
|
|
523
|
-
|
|
524
|
-
|
|
403
|
+
A large initial value is given as a function that makes it —
|
|
404
|
+
`initial: () => East.compile(generate, [])(50_000n)` — called once, as the
|
|
405
|
+
package is exported, never where the declaration is only read (a page binding
|
|
406
|
+
the record). The record's version vector carries the commit hash, so even a
|
|
407
|
+
mutation that leaves the state unchanged triggers downstream tasks.
|
|
525
408
|
|
|
526
409
|
Declared with no initial value, a record is **unassigned** until its first
|
|
527
410
|
state is committed — `e3 seed <repo> <record> <file.beast2> -w <ws>` from a
|
|
@@ -532,38 +415,54 @@ and a mutation on it is refused until then. So a record whose first state is
|
|
|
532
415
|
data that arrives after the deploy — a customer list, a reference table —
|
|
533
416
|
never runs its dependents on an empty value first.
|
|
534
417
|
|
|
418
|
+
A record is **declared whole**: its type, its initial value and its parts, in
|
|
419
|
+
one call, never added to after. Each part comes from a builder the call hands
|
|
420
|
+
its declaration, typed by the record's type: `indexes` gets `index` and returns
|
|
421
|
+
the indexes by name, `mutations` gets `mutation` and returns the mutations by
|
|
422
|
+
name, and `migrations` gets `migration` and returns its steps in order. Each is
|
|
423
|
+
checked where the record is declared, every refusal naming the record and the
|
|
424
|
+
part, and the record carries each by its name — `plans.mutations.add_plan`,
|
|
425
|
+
`plans.indexes.by_due` — which a package, a binding (`Record.bind(plans)`,
|
|
426
|
+
`Data.bindPaged(plans, { indexes: ['by_due'] })`) and TypeScript all read.
|
|
427
|
+
|
|
535
428
|
| Form | Body | Use it when |
|
|
536
429
|
|---|---|---|
|
|
537
|
-
| `
|
|
538
|
-
| `
|
|
539
|
-
| `
|
|
430
|
+
| `mutation.reduce(fn)` | `(state, …args) => state`, over the whole state | the rule is over the whole state, or the record is small; any record type |
|
|
431
|
+
| `mutation.edit(fn)` | `(state, …args, edit) => Null`: reads lazily, writes through `edit.set(k, v)`, `edit.delete(k)`, `edit.update(k, patch)` | server-side logic touches a few entries of a large Dict or Set record |
|
|
432
|
+
| `mutation.patch()` | none: the argument is a `PatchType(state)` | a view's save, or an integration sending diffs; on a record without indexes no program runs at all |
|
|
540
433
|
|
|
541
434
|
Every form commits a delta addressed by key, rewriting only the segments it
|
|
542
435
|
touches (a `reduce` of a record that is not a Dict or Set writes it whole), and
|
|
543
436
|
takes `{ runner? }`. In an `edit`, repeated edits of one key fold, a `set` of the
|
|
544
437
|
value a row already holds changes nothing, and a `delete` or `update` of a key
|
|
545
438
|
the record does not hold is a conflict naming the key: the caller's view of the
|
|
546
|
-
record is stale. `e3.mutation.editType(recordType)` is the type of `edit`.
|
|
439
|
+
record is stale. `e3.mutation.editType(recordType)` is the type of `edit`. A
|
|
440
|
+
mutation is named by its key, an identifier; a part's functions are typed by the
|
|
441
|
+
record's type, so name the type (`PlansType`) to write them, in the declaration
|
|
442
|
+
or beside it.
|
|
547
443
|
|
|
548
444
|
```typescript
|
|
549
445
|
const PlanType = StructType({
|
|
550
446
|
title: StringType, owner: StringType, due: DateTimeType, resources: SetType(StringType),
|
|
551
447
|
});
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
448
|
+
const PlansType = DictType(StringType, PlanType);
|
|
449
|
+
|
|
450
|
+
const plans = e3.record('plans', PlansType, {
|
|
451
|
+
initial: new Map(),
|
|
452
|
+
mutations: (mutation) => ({
|
|
453
|
+
add_plan: mutation.reduce(East.function([PlansType, StringType, PlanType], PlansType, ($, state, id, plan) => {
|
|
454
|
+
const next = $.let(state.copy()); // the state is frozen: copy first
|
|
455
|
+
$(next.insert(id, plan));
|
|
456
|
+
return next;
|
|
457
|
+
})),
|
|
458
|
+
reschedule: mutation.edit(East.function([PlansType, StringType, DateTimeType, e3.mutation.editType(PlansType)], NullType,
|
|
459
|
+
($, state, id, due, edit) => {
|
|
460
|
+
const plan = $.let(state.get(id)); // decodes one segment, not the record
|
|
461
|
+
$(edit.set(id, { title: plan.title, owner: plan.owner, due, resources: plan.resources }));
|
|
462
|
+
})),
|
|
463
|
+
patch: mutation.patch(), // a view's save
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
567
466
|
```
|
|
568
467
|
|
|
569
468
|
Costs count in segments: a Dict or Set record is cut by key and by each entry's
|
|
@@ -571,511 +470,43 @@ encoded size, so wide rows get segments of a few rows and a one-row edit stays
|
|
|
571
470
|
cheap. A view reads every field of the rows it shows, so keep a bulky payload it
|
|
572
471
|
does not display in a second record keyed the same way.
|
|
573
472
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
names row fields in index order: one field uses its value as the key, several
|
|
579
|
-
use a Struct in that order, and a Set field gives an entry per element. It
|
|
580
|
-
also carries field metadata for `Data.bindPaged(record, { indexes })` and
|
|
581
|
-
`Slice.bindRecord` (see **e3-ui**). `key` gives one computed entry per row;
|
|
582
|
-
`keys` returns a Set, one entry per element (a row
|
|
583
|
-
naming five resources appears five times, an empty set not at all); `value` is a
|
|
584
|
-
projection a view renders without touching the record. The functions are pure
|
|
585
|
-
and synchronous, since an index maintained commit by commit must equal its
|
|
586
|
-
rebuild to the byte. The name is an identifier, never `primary` (the record's own
|
|
587
|
-
order). Read through one with `index=<name>` on a page or key search; `e3
|
|
588
|
-
reindex` rebuilds. A deploy builds an index the package declares and the state
|
|
589
|
-
lacks, drops one it no longer declares, and runs nothing for the rest; a build
|
|
590
|
-
is a split task over the record.
|
|
591
|
-
|
|
592
|
-
```typescript
|
|
593
|
-
const byDue = e3.recordIndex('by_due', plans, { on: ['due'] });
|
|
594
|
-
const byOwnerDue = e3.recordIndex('by_owner_due', plans, { on: ['owner', 'due'] });
|
|
595
|
-
const byOwner = e3.recordIndex('by_owner', plans, {
|
|
596
|
-
key: East.function([StringType, PlanType], StringType, ($, _id, plan) => plan.owner),
|
|
597
|
-
value: East.function([StringType, PlanType], StringType, ($, _id, plan) => plan.title),
|
|
598
|
-
});
|
|
599
|
-
const byResource = e3.recordIndex('by_resource', plans, {
|
|
600
|
-
keys: East.function([StringType, PlanType], SetType(StringType), ($, _id, plan) => plan.resources),
|
|
601
|
-
});
|
|
602
|
-
const pkg = e3.package('planning', '1.0.0',
|
|
603
|
-
plans, addPlan, reschedule, e3.mutation.patch(plans), byDue, byOwnerDue, byOwner, byResource);
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
#### Interval and area indexes — `e3.recordIndex.interval`, `e3.recordIndex.area`
|
|
607
|
-
|
|
608
|
-
These indexes answer more than an order — as do the series and summary indexes
|
|
609
|
-
below — each with a constructor of its own whose keys the SDK generates;
|
|
610
|
-
`e3.recordIndex` refuses an `interval` option, naming its constructor. Underneath each is an ordinary index — the same build,
|
|
611
|
-
mutation, storage, paging and `e3 reindex` — and the record Slice reads a
|
|
612
|
-
window through either (see **e3-ui**). Their generated keys are query
|
|
613
|
-
machinery: edits and sessions always target the record's primary key.
|
|
614
|
-
|
|
615
|
-
**`e3.recordIndex.interval(name, record, { on?, from, to })`** keeps events
|
|
616
|
-
visible while they overlap a selected time window. `from` and `to` are the
|
|
617
|
-
row's DateTime or Option<DateTime> fields; optional `on` fields are exact
|
|
618
|
-
leading choices:
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
const byTime = e3.recordIndex.interval('by_time', events, { from: 'start', to: 'end' });
|
|
622
|
-
const byStatusTime = e3.recordIndex.interval('by_status_time', events, {
|
|
623
|
-
on: ['status'], from: 'start', to: 'end',
|
|
624
|
-
});
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
It stores at most three entries per event per leading-field combination,
|
|
628
|
-
independent of duration. The record Slice adapter resolves a half-open
|
|
629
|
-
`[from, to)` window with at most 55 disjoint bounded queries per exact prefix;
|
|
630
|
-
each matching original record occurs once, including events that started
|
|
631
|
-
earlier or span the whole window. It never scans the record or guesses a
|
|
632
|
-
maximum duration. An event ending at `from` is outside; an instant at `from`
|
|
633
|
-
is inside and one at `to` is outside. A missing or non-positive end is an
|
|
634
|
-
instant. A missing start remains selectable as Unset/backlog.
|
|
635
|
-
|
|
636
|
-
Use `on: ['start']` only when the intended query is **starts within**, since it
|
|
637
|
-
cannot preserve carry-in events. When both declarations are available, a Slice
|
|
638
|
-
range on the start field uses the interval declaration. The generated interval
|
|
639
|
-
keys are query machinery; record edits and sessions always target the original
|
|
640
|
-
primary key. Calendar's `Calendar.events(binding, { slice, entries, … })`
|
|
641
|
-
requires a matching interval declaration and the primary `entries` binding to
|
|
642
|
-
confirm saves even after an edit leaves the selected window.
|
|
643
|
-
|
|
644
|
-
**`e3.recordIndex.area(name, record, { on?, x, y, width?, height?, at?, cell })`**
|
|
645
|
-
reads the rows whose box meets a 2D window — what a floor plan's viewport
|
|
646
|
-
shows — the interval index's twin:
|
|
647
|
-
|
|
648
|
-
```typescript
|
|
649
|
-
// racks: Dict<String, { hall: String, x: Option<Float>, y: Option<Float>, w: Float, h: Float }>
|
|
650
|
-
const racksByArea = e3.recordIndex.area('racks_by_area', racks, {
|
|
651
|
-
x: 'x', y: 'y', width: 'w', height: 'h', at: 'centre', cell: 0.01,
|
|
652
|
-
});
|
|
653
|
-
const racksByHallArea = e3.recordIndex.area('racks_by_hall_area', racks, {
|
|
654
|
-
on: ['hall'], x: 'x', y: 'y', width: 'w', height: 'h', at: 'centre', cell: 0.01,
|
|
655
|
-
});
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
| Option | The box's |
|
|
659
|
-
|---|---|
|
|
660
|
-
| `x`, `y` | place: two Float fields, or two Option<Float> fields whose `none` is a row not yet placed |
|
|
661
|
-
| `width`, `height` | size: two Float fields, or neither for a point |
|
|
662
|
-
| `at` | `'corner'`, the default — `x`, `y` is the box's minimum corner, as a zone's is — or `'centre'`, as an item's is |
|
|
663
|
-
| `cell` | finest cell: its side in world units, about the smallest thing drawn — 0.01 for a floor in metres, 1 for one in millimetres. It is rounded down to a power of two, so the keys' arithmetic is exact on every runtime, and has no default: a wrong one costs a silent tenfold read |
|
|
664
|
-
| `on` | exact leading fields, as the interval index's; a Set field fans out |
|
|
665
|
-
|
|
666
|
-
Each option takes only the row's fields of its type, so a wrong one does not
|
|
667
|
-
compile, and the definition checks them again, naming the option, the field and
|
|
668
|
-
its type; a row field named `__area` is refused, the name being the key's. The
|
|
669
|
-
domain is the square ±`cell`·2²⁷ world units around zero (±1,048 km at a 0.01
|
|
670
|
-
cell, which rounds to 2⁻⁷), in 29 levels. Each row is filed once, under the
|
|
671
|
-
deepest level whose cell is at least twice its box's larger side, in the cell
|
|
672
|
-
holding its box's centre: the key `{ …on, __area: Integer }`. A row not yet
|
|
673
|
-
placed files under `__area` −1, which the record Slice reads as the unplaced
|
|
674
|
-
clause; one outside the domain, not finite, or larger than half of it files
|
|
675
|
-
under the root, which every window reads. The index **carries its rows**: each
|
|
676
|
-
entry's projection is the whole row, so a window reads the index alone and no
|
|
677
|
-
record segment, and it takes no `value`. The record Slice reads a viewport,
|
|
678
|
-
grown by an eighth of its larger side on each side, as at most 266 disjoint
|
|
679
|
-
searches per exact prefix, sent in one request, which together hold every row
|
|
680
|
-
whose box meets it, each once — beside rows of the cells searched that it
|
|
681
|
-
misses, which the reader draws or not.
|
|
682
|
-
|
|
683
|
-
#### Series and summary indexes — `e3.recordIndex.series`, `e3.recordIndex.summary`
|
|
684
|
-
|
|
685
|
-
What a planning board reads: the events on a block of keys — a Plan's
|
|
686
|
-
resources — over a span of time, and, zoomed out past what a screen can draw,
|
|
687
|
-
how busy each key is. Both file a row's event by the key it is on and by each
|
|
688
|
-
time tile it touches — `day`, `week` (ISO, from Monday), `month`, `quarter` or
|
|
689
|
-
`year`, in UTC, half-open — their key `{ …on, __tile, <key> }`. The tile leads
|
|
690
|
-
the key, so a block of keys in one tile is one contiguous range:
|
|
691
|
-
|
|
692
|
-
```typescript
|
|
693
|
-
// jobs: Dict<String, { press: String, start: Option<DateTime>, end: Option<DateTime>, … }>
|
|
694
|
-
const jobsByPressWeek = e3.recordIndex.series('jobs_by_press_week', jobs, {
|
|
695
|
-
key: 'press', from: 'start', to: 'end', tile: 'week',
|
|
696
|
-
});
|
|
697
|
-
const jobsByPressMonth = e3.recordIndex.summary('jobs_by_press_month', jobs, {
|
|
698
|
-
key: 'press', from: 'start', to: 'end', tile: 'month',
|
|
699
|
-
});
|
|
700
|
-
```
|
|
473
|
+
A record's indexes (`index.on` / `key` / `keys`, `interval`, `area`,
|
|
474
|
+
`series`, `summary`, `nested`) are in
|
|
475
|
+
[reference/record-indexes.md](reference/record-indexes.md), and its migrations
|
|
476
|
+
in [reference/record-migrations.md](reference/record-migrations.md).
|
|
701
477
|
|
|
702
|
-
|
|
703
|
-
|---|---|
|
|
704
|
-
| `key` | the field naming what an event is on: any field but a Set |
|
|
705
|
-
| `from`, `to` | the event's start and end: DateTime or Option<DateTime> fields. A row with no start is in no tile; no end, or one at or before the start, is an instant, in the one tile holding it |
|
|
706
|
-
| `tile` | `day`, `week`, `month`, `quarter` or `year` |
|
|
707
|
-
| `on` | exact leading fields, before the tile; a Set field fans out |
|
|
708
|
-
|
|
709
|
-
**A series index** is an ordinary index whose keys the SDK generates, an entry
|
|
710
|
-
per tile an event touches, and it **carries its rows**, as the area index does:
|
|
711
|
-
a block of keys over a span of tiles reads the index alone, a search a tile, in
|
|
712
|
-
one request. **A summary index** holds, under each key, `{ count, ms }`: how
|
|
713
|
-
many rows touch its tile, and the milliseconds of it they cover — a row's shares
|
|
714
|
-
add up to its length, and an instant's is none. Each entry stands for every row
|
|
715
|
-
of its key and names none, so its window's `key` is Null, a read of it joins
|
|
716
|
-
nothing, and a read that needs its rows — a joined read, a placement
|
|
717
|
-
(`datasetPlace`) or a membership read (`datasetWithin`) — is refused, naming the
|
|
718
|
-
index; the record's describe says it is a summary. A write
|
|
719
|
-
changes its totals by what its rows took away and gave — its program reads the
|
|
720
|
-
record, never the summary — and the apply adds that to each total, dropping one
|
|
721
|
-
whose count reaches zero; the totals are Integers, so a maintained summary is the
|
|
722
|
-
one `e3 reindex` sums, byte for byte, and a build split into pieces adds the
|
|
723
|
-
pieces' totals of a key. A change that would leave a total no rebuild sums — a
|
|
724
|
-
count below zero, or time with no row — fails the write, naming the total and
|
|
725
|
-
`e3 reindex`, which rebuilds it; a total past the largest Integer fails the
|
|
726
|
-
write or the build, naming a finer tile.
|
|
727
|
-
|
|
728
|
-
An event touching more than 4,096 tiles — eleven years of days — is refused
|
|
729
|
-
where it is written, naming it, its span and the tile: declare a coarser tile.
|
|
730
|
-
Each option takes only the row's fields of its type, as the area index's do,
|
|
731
|
-
and the definition checks them again; a row field named `__tile` is refused, the
|
|
732
|
-
name being the key's, and so are a `value` and an `on` field that is the key,
|
|
733
|
-
the start or the end: the key follows the tile, and the tiles are cut by the
|
|
734
|
-
start and the end.
|
|
735
|
-
|
|
736
|
-
#### Migrations — how a record's type changes
|
|
737
|
-
|
|
738
|
-
| Form | Function | Runs as |
|
|
739
|
-
|---|---|---|
|
|
740
|
-
| `e3.migration.value(name, rec, fn, { after? })` | `(Old) => New`, over any record | one unit; its runner opens the state lazily |
|
|
741
|
-
| `e3.migration.rows(name, rec, fn, { after? })` | a Dict's rows, `(K, V1) => V2` with the keys kept, or an Array's elements, `(T1) => T2` | a task split over the state: a piece at a time, in parallel, each piece cached |
|
|
742
|
-
| `e3.migration.rekey(name, rec, fn, { after? })` | a Dict's entries, `(K1, V1) => { key: K2, value: V2 }`, or a Set's elements, `(T1) => T2` | the same split task, its pieces merged by key: two Dict rows landing on one key fail the deploy, naming it; two Set elements are one |
|
|
743
|
-
|
|
744
|
-
A record's steps are one chain: each names the step before it with `after` (a
|
|
745
|
-
step of the same record, leaving it as this step's input type), passing the last
|
|
746
|
-
step passes the chain, and the last step leaves the record as its declared type.
|
|
747
|
-
A step's name is an identifier, unique on the record, and a workspace records the
|
|
748
|
-
steps it has applied by name — so moving a step's code, or re-exporting it with a
|
|
749
|
-
newer e3, does not run it again, and an edit to an applied step's body is not
|
|
750
|
-
detected. The functions are pure, synchronous East.
|
|
751
|
-
|
|
752
|
-
```typescript
|
|
753
|
-
const RowV1Type = StructType({ title: StringType });
|
|
754
|
-
const RowV2Type = StructType({ title: StringType, owner: StringType });
|
|
755
|
-
const plans = e3.record('plans', DictType(StringType, RowV2Type), new Map());
|
|
756
|
-
|
|
757
|
-
const addOwner = e3.migration.rows('add_owner', plans,
|
|
758
|
-
East.function([StringType, RowV1Type], RowV2Type, ($, _id, row) => ({ title: row.title, owner: 'unassigned' })));
|
|
759
|
-
// The next version adds its step after this one: e3.migration.value('…', plans, fn, { after: addOwner })
|
|
760
|
-
const pkg = e3.package('planning', '2.0.0', plans, addOwner);
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
What a deploy does with each record, decided before it writes anything:
|
|
764
|
-
|
|
765
|
-
| The workspace holds | Against the package's chain | The deploy |
|
|
766
|
-
|---|---|---|
|
|
767
|
-
| no state | — | mints `$init` from the initial value, the whole chain counted applied; declared with no initial value, leaves it unassigned (`unassigned`) for `e3 seed` |
|
|
768
|
-
| state | every step applied, the type unchanged | keeps it, with a `$deploy` commit when the package changed |
|
|
769
|
-
| state | a proper prefix applied | runs the rest, a `$migrate:<name>` commit each |
|
|
770
|
-
| state | every step applied, but the type changed | refused: a type change needs a migration, or `--schema reset` |
|
|
771
|
-
| state | applied steps the chain does not start with | refused, naming the steps applied and those declared |
|
|
772
|
-
| a record the package no longer declares | — | refused unless `--allow-drop-records` |
|
|
773
|
-
|
|
774
|
-
`--schema <policy>` on `e3 workspace deploy` and `e3 watch`: `migrate` (the
|
|
775
|
-
default), `fail` (run no migration; refuse) or `reset` (reset the record to its
|
|
776
|
-
initial value, with a `$reset` commit (`reset`); one declared with no initial
|
|
777
|
-
value goes back to unassigned (`unassign`), for `e3 seed` again). `--plan` prints what the deploy would do
|
|
778
|
-
to each record, index and input and writes nothing — from a zip or a source it
|
|
779
|
-
imports nothing, and a server plans only a package it holds. The steps run
|
|
780
|
-
before the deploy writes: a failed step leaves the workspace as it was, and a
|
|
781
|
-
deploy run again is served the steps that finished from the cache.
|
|
782
|
-
|
|
783
|
-
What a deploy does with each input — a dataset an `e3.input` declares, which
|
|
784
|
-
people set:
|
|
785
|
-
|
|
786
|
-
| The input now | Its type | The deploy |
|
|
787
|
-
|---|---|---|
|
|
788
|
-
| holds the value the deployed package gave it, or is new | — | the new package's value (`package`) |
|
|
789
|
-
| someone set it | — | the new package's value under `--inputs reset`, the default, and says it reset it (`reset`) |
|
|
790
|
-
| someone set it | the new package's | kept, under `--inputs keep-edited` (`keep`) |
|
|
791
|
-
| someone set it | changed | the new package's value, and says why (`reset`) |
|
|
792
|
-
| the deployed package took it from a file, and it holds a value | — | the new package's value under either policy, since a value set since cannot be told from the file's, and says so (`reset`) |
|
|
793
|
-
| the new package takes it from a file | — | its file, as every deploy does (`file`); a deploy that reads no file, a server's, leaves it unset for its client to send |
|
|
794
|
-
| the new package no longer declares it | — | gone, with its value (`drop`) |
|
|
795
|
-
|
|
796
|
-
## The e3 CLI
|
|
797
|
-
|
|
798
|
-
Every `<repo>` is a local path or a server URL, `http(s)://<host>/repos/<name>`;
|
|
799
|
-
where `[repo]` is optional it defaults to `$E3_REPO`, then `.`. Dataset paths are
|
|
800
|
-
flat, `<ws>.<name>` — an input or a task output, with "did you mean" suggestions;
|
|
801
|
-
a task is `<ws>.<task>` and a mutation `<record>.<mutation>`.
|
|
802
|
-
|
|
803
|
-
| Command | Does |
|
|
804
|
-
|---|---|
|
|
805
|
-
| **Repositories** | |
|
|
806
|
-
| `e3 repo create [repo] [--exist-ok]` | Create one (`--exist-ok`: succeed if it exists). |
|
|
807
|
-
| `e3 repo status [repo]` · `e3 repo remove [repo] [-r]` | Show objects, packages, workspaces · remove (`-r`: its workspaces first). |
|
|
808
|
-
| `e3 repo gc [repo] [--dry-run] [--keep-runs <n>] [--keep-days <d>] [--min-age <ms>]` | Drop old run history, then what nothing names (below). |
|
|
809
|
-
| `e3 repo list <server>` | The repositories a server holds. |
|
|
810
|
-
| **Packages** | |
|
|
811
|
-
| `e3 package import [repo] <zip>` · `export [repo] <pkg[@ver]> <zip>` | Import · export (`--quiet`: errors only). |
|
|
812
|
-
| `e3 package list [repo]` · `remove [repo] <pkg[@ver]>` | |
|
|
813
|
-
| **Workspaces** | |
|
|
814
|
-
| `e3 workspace create [repo] <ws>` · `list [repo]` · `status [repo] <ws>` · `remove [repo] <ws>` | `status` shows tasks, datasets and locks. |
|
|
815
|
-
| `e3 workspace copy [repo] <from> <to>` | Make `<to>` what `<from>` is now — its package and every input, record and task output, so nothing runs again — made, or replaced whole with its runs. It writes refs only, so its cost follows the number of datasets, never their size; a write to either afterwards leaves the other as it was. Refused while a dataflow runs in `<to>`. |
|
|
816
|
-
| `e3 workspace deploy [repo] <ws> <pkg[@ver]>` | Deploy an imported package. |
|
|
817
|
-
| `… --from-zip <zip>` · `… --from-source <src.ts> [--functions <manifest…>]` | Import the zip (or bundle the source) and deploy, creating the workspace. |
|
|
818
|
-
| `… [--schema <policy>] [--inputs <policy>] [--allow-drop-records] [--plan] [--skip-file-sources] [-j <n>] [--memory <size>] [--quiet]` | A record's policy, an input's, and `file` sources; says how far it has got (below), `--quiet` aside. `-j`: the `file` sources taken in at once, and the runner processes their intake units, the migrations and the index builds run on; `--memory`: what those may reserve (local). |
|
|
819
|
-
| `e3 workspace export [repo] <ws> <zip> [--name <n>] [--version <v>]` | The workspace's state as a package. |
|
|
820
|
-
| **Datasets** | |
|
|
821
|
-
| `e3 dataset get [repo] <ws.name> [-f east\|json\|beast2]` | Print a value. |
|
|
822
|
-
| `e3 dataset set [repo] <ws.name> <file> [--type <spec> \| --type-file <path>]` | Write from `.east`, `.beast2`, `.json` or `.csv` (JSON and CSV need the type). |
|
|
823
|
-
| `e3 dataset set [repo] <ws.name> --from-file <path.beast2> [-j <n>] [--memory <size>]` | Take a beast2 file in as the value, as a deploy takes a `file` source in, saying how far it has got; `-j`, `--memory`: the budget its intake units run under (local). |
|
|
824
|
-
| `e3 dataset list [repo] <ws> [-l]` · `status [repo] <ws.name>` · `find [repo] <ws> <pattern>` | Paths (`-l`: kind, type, status, size) · one dataset · by substring or glob. |
|
|
825
|
-
| **Running** | |
|
|
826
|
-
| `e3 dataflow run [repo] <ws> [--filter <task>] [--force \| --force-task <task>…] [-j <n>] [--memory <size>] [-v]` | Run what is stale, then print the outputs' paths. `--filter`: one task, by its exact name, and the tasks it depends on. `--force`: re-run every task the run runs (under `--filter`, its task); `--force-task`, repeated for each: re-run the tasks it names, whose dependents re-run when an output changes. A task the graph lacks, or one `--filter` leaves out, refuses the run. A task whose input nothing has set — an input never set, or a record declared with no initial value and not yet seeded — is left waiting, with its dependents (`[WAIT]`, `Waiting:`), and the run completes without them; they run once it is set. |
|
|
827
|
-
| `e3 task list [repo] <ws>` | Tasks with the status of each one's latest run. |
|
|
828
|
-
| `e3 task logs [repo] <ws.task> [-n <lines>] [--all] [--follow] [--execution <task>/<inputs>/<id>]` | The last 200 lines by default, of the latest attempt over the task's current inputs, or its latest run; `--follow` stays on that execution; `--execution`: one unit's log, as a split task's log names it (local). |
|
|
829
|
-
| `e3 run <repo> <pkg[@ver].task> [inputs.beast2…] -o <out> [--force] [-v] [-j <n>] [--memory <size>]` | Run one task ad hoc. |
|
|
830
|
-
| `e3 call <repo> <pkg[@ver].fn> [args…] [-o <out.beast2>] [-j <n>] [--memory <size>] [-v]` · `e3 call <repo> -w <ws> <fn> [args…]` | Call a function; each argument is an `.east` literal or a `.beast2`/`.json`/`.east` file, and with `-w` one written `@<keypath>` (`@.inputs.sales`) is that dataset of the workspace, pinned at its hash when the call starts. |
|
|
831
|
-
| **Records** (`-w <ws>` required) | |
|
|
832
|
-
| `e3 mutate <repo> <record.mutation> [args…] -w <ws> [-j <n>] [--memory <size>] [-v]` | Apply a mutation. |
|
|
833
|
-
| `e3 seed <repo> <record> <file.beast2> -w <ws> [-j <n>] [--memory <size>]` | Commit a record's first state from a beast2 file, as its `$init`: for a record declared with no initial value, unassigned until then (local). The file is taken in as a `file` source is, never read whole; a record that holds a state is refused. |
|
|
834
|
-
| `e3 history <repo> <record> -w <ws> [--limit <n>] [--from <hash>] [--delta]` | Commits, newest first (`--delta`: what each changed, per target — a summary's totals added to as `±n`; local). |
|
|
835
|
-
| `e3 reindex <repo> <record> -w <ws> [--index <name>] [-j <n>] [--memory <size>]` · `e3 compact <repo> <record> -w <ws>` | Rebuild indexes (local) · collapse the history to a `$compact` root, the state kept. |
|
|
836
|
-
| **Development** | |
|
|
837
|
-
| `e3 watch <src.ts> <repo> <ws> [--start] [--schema <p>] [--inputs <p>] [--abort-on-change] [--functions <manifest…>] [-j <n>] [--memory <size>]` | Redeploy on each change (and run, with `--start`). |
|
|
838
|
-
| `e3 convert [input] [--from <f>] [--to <f>] [--type <spec>] [-o <out>]` | Convert between `.east`, `.json` and `.beast2`. |
|
|
839
|
-
| `e3 completion install [--shell <s>]` · `uninstall` · `bash` \| `zsh` \| `fish` | Shell completion. |
|
|
840
|
-
| **Servers** | |
|
|
841
|
-
| `e3 auth login <server> [--no-browser]` · `logout <server>` · `status` · `token <server>` · `whoami [server]` | OAuth2 device flow; credentials per server (`token` prints one for curl). |
|
|
842
|
-
|
|
843
|
-
**The budget.** `-j`/`--jobs <n>` is the cores — runner processes in flight, a
|
|
844
|
-
task or a unit each — and `--memory <size>` (`8G`, `512M`) what they may reserve
|
|
845
|
-
between them. The defaults are `E3_JOBS` and `E3_MEMORY`, else what e3 may use:
|
|
846
|
-
its CPU affinity capped by a cgroup quota, and the cgroup's `memory.max` or
|
|
847
|
-
physical memory, less a reserve. A unit of a split task reserves the largest
|
|
848
|
-
peak a unit of its stage has reached in the run, so a stage runs its first unit
|
|
849
|
-
alone and then fans out; on Linux and macOS a guard stops the newest unit when
|
|
850
|
-
the runners together pass the budget, and runs it again once it fits. A deploy
|
|
851
|
-
takes its `file` sources in `-j` at a time, their pieces' intake units running
|
|
852
|
-
under the same budget. The commands that run East — `workspace deploy`,
|
|
853
|
-
`dataset set --from-file`, `dataflow run`, `run`, `call`, `mutate`, `seed`,
|
|
854
|
-
`reindex` and `watch` — take the flags for a local repository; against a server the CLI
|
|
855
|
-
refuses them, since the server runs the work under its own budget
|
|
856
|
-
(`e3-api-server -j`, `--memory`).
|
|
857
|
-
|
|
858
|
-
**A deploy says how far it has got**, on stderr. Each `file` source prints a
|
|
859
|
-
line once it is in: its size, time and rate, and how it was taken in
|
|
860
|
-
(`unchanged, already in the store`, `taken in by east-c`, or `carried` for a
|
|
861
|
-
value that is not a collection), with why the first time a runner fell back to
|
|
862
|
-
another. A terminal also keeps a live line for the files in flight, with their
|
|
863
|
-
pieces, the rate and the time left across them. Against a server, the deploy line
|
|
864
|
-
says what the job is doing, such as migrating a record or building an index;
|
|
865
|
-
the deliveries then upload a few at a time, each printing a line once it is in,
|
|
866
|
-
and the live line says what the server's commit is doing with each in flight.
|
|
867
|
-
While a deploy runs, its lock carries the same progress for `e3-ui` or any
|
|
868
|
-
client to read (`workspaceLockStatus`). A workspace deployed for the first time
|
|
869
|
-
has no status until its deploy ends, so the lock is the only place to see it.
|
|
870
|
-
|
|
871
|
-
**`-v`** passes `-v` to the runners, which print to the task's logs, identically
|
|
872
|
-
on every runtime, how each input was read — opened lazily, and what reading it
|
|
873
|
-
came to, or decoded whole, and the resident memory that added — and a timing
|
|
874
|
-
and peak-memory block. It never changes hashes or caching (add `--force` to see
|
|
875
|
-
it for a cached task), and works against a server.
|
|
876
|
-
**Where a task's time goes.** `EAST_PROFILE=1` in the environment of a local
|
|
877
|
-
`e3 dataflow run` (or `run`, `call`, `mutate`) has the east-c and east-py
|
|
878
|
-
runners profile each unit — the runners inherit e3's environment — and the
|
|
879
|
-
task's log (`e3 task logs`) then ends with every East function and platform
|
|
880
|
-
function it called, by self time, with its calls and where it was written and
|
|
881
|
-
called: the python lines of a function `East.importFunction` linked in
|
|
882
|
-
included. Each input read lazily is an entry too, named as `-v` names it
|
|
883
|
-
(`input 1`): the time its reads spent decoding segments, a call each — keyed
|
|
884
|
-
reads at random beyond the segments the runner keeps, or a scan repeated,
|
|
885
|
-
which `-v`'s account counts — and not the reading function's own time.
|
|
886
|
-
`EAST_PROFILE_INTERVAL=300` adds the profile so far every five minutes while a
|
|
887
|
-
long task runs. east-node does not profile. Like `-v` it changes no hash: add
|
|
888
|
-
`--force` for a cached task.
|
|
889
|
-
**`E3_SCRATCH_DIR`** moves a local run's per-execution scratch directories
|
|
890
|
-
(default `<repo>/tmp/scratch`, on the object store's disk; on tmpfs, outputs sit
|
|
891
|
-
in memory until stored).
|
|
892
|
-
|
|
893
|
-
**`repo gc`** keeps each workspace's last 10 runs (`--keep-runs`), every run
|
|
894
|
-
from the last 7 days (`--keep-days`) and the run its current state came from,
|
|
895
|
-
with every execution those runs used; every execution a workspace's current
|
|
896
|
-
state is served from, so a re-run stays cached; every execution from the last 7
|
|
897
|
-
days; and whatever is still running. An attempt recorded running that the local
|
|
898
|
-
runner says cannot finish is recorded interrupted, and goes as any that ended.
|
|
899
|
-
It then removes the objects nothing names, and staging files older than
|
|
900
|
-
`--min-age` (60 s). `--dry-run` reports what would go.
|
|
901
|
-
|
|
902
|
-
A package zip names the release of e3 that exported it, and an import refuses a
|
|
903
|
-
zip a newer release exported, naming that release: import it with an e3 at least
|
|
904
|
-
as new as the SDK that exported it. A dataset upload names the transfer protocol
|
|
905
|
-
its e3 speaks, and a server refuses one that speaks another, naming both
|
|
906
|
-
releases and which of them to upgrade.
|
|
907
|
-
|
|
908
|
-
## Driving e3 from code
|
|
909
|
-
|
|
910
|
-
**`@elaraai/e3-api-client`** talks to a server over HTTP. Every call is
|
|
911
|
-
`(url, repo, …, options)`, `options` being
|
|
912
|
-
`{ token: string | null, retry?, verbose?, fetch? }` (`e3 auth token <server>`
|
|
913
|
-
prints a token); `fetch` is the `fetch` every request of the call goes
|
|
914
|
-
through, the global one unless given — an in-page e3's `e3.fetch` (below).
|
|
915
|
-
Dataset paths are `TreePath`s — an input definition's `.path`, for one. A
|
|
916
|
-
request a workspace's lock refuses throws `ApiError` `workspace_locked`,
|
|
917
|
-
naming the holder: its pid, boot id, command and when it took the lock.
|
|
918
|
-
|
|
919
|
-
| Area | Functions |
|
|
920
|
-
|---|---|
|
|
921
|
-
| Repositories | `repoList(url, opts)`, `repoCreate(url, name, opts)`, `repoRemove(url, name, opts)`, `repoStatus(url, repo, opts)`, `repoRecord` (its release and upgrades), `repoGc(url, repo, gcRequest, opts)` — a job it polls; `repoGcStart` and `repoGcStatus` apart |
|
|
922
|
-
| Packages | `packageList(url, repo, opts)`, `packageGet(url, repo, name, version, opts)`, `packageImport(url, repo, zipBytes, opts)`, `packageExport(url, repo, name, version, opts)` → zip bytes, `packageRemove` |
|
|
923
|
-
| Workspaces | `workspaceList`, `workspaceCreate(url, repo, ws, opts)`, `workspaceCopy(url, repo, from, to, opts)` → the target, made or replaced whole, `workspaceGet`, `workspaceStatus(url, repo, ws, opts, { paths? })` — the whole workspace's, or the datasets named and the tasks producing them, `workspaceStatusSince(url, repo, ws, etag, opts, { paths? })` → `{ status, etag }`, or `null` while the status answered under `etag` is current (a poll sends the last answer's tag back; `null` asks in full), `workspaceLockStatus(url, repo, ws, opts)` → what holds the workspace and how far it says it has got (a deploy's files and records), or `null`, `workspaceRemove`, `workspaceDeploy(url, repo, ws, 'pkg@ver', opts, { schema?, inputs?, allowDropRecords?, plan?, onProgress? })` — a job it polls, whose progress while `deploying` is the deploy's own, answering what it did with each record, index and input, `workspaceExport(url, repo, ws, opts, { name?, version? })` → zip bytes. A poll's request takes `pollRequestOptions(opts)`: one attempt, its next poll being its retry |
|
|
924
|
-
| Datasets | `datasetGet(url, repo, ws, path, opts)` → `{ data, hash, size }` (a collection downloads as its segments), `datasetGetStream`, `datasetGetPage(…, window, opts)`, `datasetFindKey(…, query, opts)`, and several reads in one request (at most `DATASET_BATCH_MAX`, 10,000, each at one content hash, in a body of at most 16 MiB: a larger one is refused 413, `payload_too_large`): `datasetFindKeys(…, { queries, index?, hash? }, opts)` — key searches, `datasetGetRows(…, { keys, hash? }, opts)` — a Dict's rows by `.east` key, each with its position — or `datasetGetRows(…, { keysOf: { path, index, runs, hash }, hash? }, opts)`, by the keys the entries of some runs of another record's index name, which the server reads from the index so none is sent (a drawing's details for its window's cells), a stale `keysOf.hash` thrown as `RunsHashMismatchError` naming the record's content — and `datasetGetRuns(…, { runs, hash, index?, join? }, opts)` — the entries of several runs as one window, a rows or runs answer holding the first `count` the page budget reaches (by runs, of `runEntries`); and where a narrowing's rows sit in a record, from its index alone: `datasetPlace(…, { index, runs, offset?, limit?, hash }, opts)` → `{ segments, placed, runEntries, totalElements, hash }` — how many of the runs' entries each record segment holds (`{ segment, offset, count, rows }`), for the first `placed` of a window from `offset` (the next window from `offset + placed`), and `datasetWithin(…, { index, runs, keys, hash }, opts)` → `{ within, hash }` — whether each index entry key (`(ik=…, k=…)`) lies in one of the runs, `datasetSet(url, repo, ws, path, beast2Bytes, opts)`, `datasetSetStream(url, repo, ws, path, { size, hash, slice }, opts, { onCommitProgress? })` — a file of any size, the server's commit saying how far it has taken it in, `datasetList`, `datasetListAt`, `datasetListRecursive`, `datasetListWithStatus`, `datasetGetStatus` |
|
|
925
|
-
| Runs and tasks | `dataflowExecute(url, repo, ws, { force?, filter? }, opts, { pollInterval?, timeout? })` → the result (or `dataflowExecuteLaunch` and `dataflowExecutePoll`) — `force` is `true` or the names of the tasks to re-run, `filter` one task's exact name, `dataflowCancel` — with nothing running, `dataflow_error` ("No active execution for this workspace"), `dataflowGraph`, `dataflowBudget`, `taskList`, `taskGet`, `taskExecutionList(url, repo, ws, task, opts, { limit?, before? })` — a page of the task's runs, the latest first, each naming its `executionId`, the next page the runs `before` the last one's (100 a page unless `limit` says, 1,000 at most), `taskLogs(url, repo, ws, task, { stream?, offset?, limit?, execution? }, opts)` — a chunk of the log of the task's current execution, or of the one `execution` names (`{ inputsHash, executionId }`), the chunk naming its execution and whether it has `ended` |
|
|
926
|
-
| Functions | `functionList`, `functionDescribe`, `functionCall(url, repo, pkg, version, fn, { args, runner, limits }, opts)` — each argument `variant('value', beast2Bytes)`; the function runs on its own runner for any caller (`runner: none`); a runner the call names is never `custom`, and loads a platform package the function's does not only for an elevated caller; `workspaceFunctionList`, `…Describe`, `…Call(url, repo, ws, fn, request, opts)` — where an argument may also be `variant('dataset', path)`, a dataset of the workspace pinned at its hash, which the result names (`inputs`); `oneShotExecute` — a reader runs a platform-free one (stock runner, `platforms: []`, no platform call), and its result names the datasets it read (`inputs`); `splitCall(url, repo, ws, { bodyIr, args, output, then, runner, limits }, opts, { onProgress? })` — a program over a dataset's pieces, as a job it polls, under the same rule (or `splitCallLaunch`, `splitCallStatus` and `splitCallExplain`) |
|
|
927
|
-
| Records | `workspaceRecordDescribe`, `workspaceRecordMutate(url, repo, ws, record, mutation, { args, actor, limits }, opts, idempotencyKey?)`, `workspaceRecordHistory(url, repo, ws, record, limit, opts, from?)`, `workspaceRecordCompact` |
|
|
928
|
-
| From East | `Platform` and the `platform_*` functions (`platform_dataset_get`, `platform_dataflow_execute`, …): the same calls as platform functions, for an East program that drives a server |
|
|
929
|
-
|
|
930
|
-
**`@elaraai/e3-core`** does the same on a local repository, given a storage
|
|
931
|
-
backend (`new LocalStorage()`) and the repository's path; work that runs East
|
|
932
|
-
takes a runner, `new LocalTaskRunner(repo)`.
|
|
933
|
-
|
|
934
|
-
| Area | Functions |
|
|
935
|
-
|---|---|
|
|
936
|
-
| Repositories | `repoInit(path)`, `repoFind(startPath?)`, `repositoryOpen(storage, repo)` (checks the repository and applies the upgrades it owes), `repositoryUpgradeStep(storage, repo, { budgetMs, waitMs? })` → `{ owed }` — a host's job applies them so, a part per run under its time limit, each part taking a step up where the last stopped, `repoGc(storage, repo, { dryRun?, minAge?, keepRuns?, keepDays?, retention?, executionAlive? })` — holding the repository still, or, with `retention: { windowMs }`, beside running work: an object goes once unreachable for the window and not written or re-referenced since; given a runner's `executionAlive`, an attempt recorded running that cannot finish is recorded interrupted and pruned; `repoGcStep(storage, repo, step, { windowMs, pruneMs?, markMs?, concurrency?, executionAlive?, … })` runs that one step at a time, each returning the next (`GcStepType`), `pruneMs` and `markMs` bounding a prune step and a mark step for a host with a time limit |
|
|
937
|
-
| Packages | `packageImport(storage, repo, zipPath)`, `packageExport(storage, repo, name, version, zipPath)`, `packageList`, `packageRemove` |
|
|
938
|
-
| Workspaces | `workspaceCreate(storage, repo, ws)`, `workspaceCopy(storage, repo, from, to)`, `workspaceDeploy(storage, repo, ws, pkgName, pkgVersion, options?)` (`runner`: its migrations, index builds and intake units; `schema` and `inputs`: its policies; `onRecordPlan`, `onRecordIndex`, `onInputPlan`: what it decided; `sourceConcurrency`: the `file` sources taken in at once; `onSourceProgress`, `onDeployProgress`: how far it has got), `workspaceExport(storage, repo, ws, zipPath, name?, version?)`, `workspaceStatus(storage, runner, repo, ws)`, `workspaceStatusRevision(storage, repo, ws)` — a token that moves whenever the status might, which the API's `ETag` names, `workspaceLockStatus(storage, repo, ws)`, `workspaceListStates(storage, repo)` — every workspace with its state in one read of the store, `workspaceRemove(storage, repo, ws, { sweep? })` — the record and what a store removes at once go in one call, and the rest is swept in bounded batches unless `sweep: false` leaves them to `workspaceSweep(storage, repo, cursor?)`, which a host with a time limit runs from a job |
|
|
939
|
-
| Datasets | `workspaceGetDataset(storage, repo, ws, treePath)`, `workspaceSetDataset(storage, repo, ws, treePath, value, type)`, `datasetAdoptFile(storage, repo, ws, treePath, file, { runner, onProgress? })` → `{ hash, size, segments, rows, taken, runners? }`, `taken` being `known`, `carried` or `taken` (by the `runners` named); `deliveryAdoptStored(storage, repo, ws, { address, type }, sha256, { runner?, onProgress?, signal? })` — a delivery the store holds taken in as a stored value of the type, naming no dataset, for a record's first state |
|
|
940
|
-
| Runs | `dataflowExecute(storage, repo, ws, options?)`; `LocalOrchestrator` to start, poll and cancel a run |
|
|
941
|
-
| Records | `recordMutate(storage, runner, repo, ws, record, mutation, args, { actor })`, `recordHistory`, `recordDescribe`, `recordCompact`, `recordReindex`; `recordSystemCommit(storage, runner, repo, ws, record, { name, target, actor, args? })` — `$rollback` to a commit, `$restore` of a state at the record's migrations, and `$init` on a record declared with no initial value: its first state, from a stored value, with the whole chain applied |
|
|
942
|
-
|
|
943
|
-
`@elaraai/e3-core/test` exports the contract suites another storage backend runs
|
|
944
|
-
over itself.
|
|
945
|
-
|
|
946
|
-
**`e3-api-server`** serves repositories: `--repos <dir>` (each subdirectory a
|
|
947
|
-
repository) or `--repo <path>` (one, served as `default`), `-p`/`--port` (3000),
|
|
948
|
-
`-H`/`--host` (localhost), `--cors`, `-j`/`--memory` (the server's budget, for
|
|
949
|
-
every run and call it serves and every upload it takes in), and auth — `--oidc`, a built-in provider for `e3
|
|
950
|
-
auth login` (`--token-expiry`, `--refresh-token-expiry`), or an external JWT
|
|
951
|
-
issuer (`--auth-key`, `--auth-issuer`, `--auth-audience`). In code,
|
|
952
|
-
`createServer({ reposDir | singleRepoPath, port, host, … })` starts one, and its
|
|
953
|
-
route factories mount on another host.
|
|
954
|
-
|
|
955
|
-
## Running e3 in a browser — `@elaraai/e3-web`
|
|
956
|
-
|
|
957
|
-
e3-web runs e3 entirely in a page, with no server: a repository kept in the
|
|
958
|
-
browser's own storage (records in IndexedDB, objects in OPFS, locks through
|
|
959
|
-
Web Locks), every East program on a pool of Web Workers, and e3's whole API
|
|
960
|
-
answered in the page by e3-api-server's own routes. An app is three scripts:
|
|
961
|
-
|
|
962
|
-
```typescript
|
|
963
|
-
// unit.worker.ts — where East programs run: east-web-std answers for east-node-std's
|
|
964
|
-
// platform functions, and e3's own (e3-api-client's Platform) reach the e3 worker
|
|
965
|
-
import { serveUnits } from '@elaraai/e3-web/units';
|
|
966
|
-
serveUnits();
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
```typescript
|
|
970
|
-
// e3.worker.ts — e3 itself: its storage, orchestrator, runner and API
|
|
971
|
-
import { serveE3 } from '@elaraai/e3-web/worker';
|
|
972
|
-
serveE3({
|
|
973
|
-
units: () => new Worker(new URL('./unit.worker.ts', import.meta.url), { type: 'module' }),
|
|
974
|
-
});
|
|
975
|
-
```
|
|
976
|
-
|
|
977
|
-
```typescript
|
|
978
|
-
// main.ts — the page: e3-api-client's calls, every one answered by the e3 worker
|
|
979
|
-
import { createWebE3 } from '@elaraai/e3-web';
|
|
980
|
-
import {
|
|
981
|
-
dataflowExecute, packageImport, repoCreate, repoList, workspaceCreate, workspaceDeploy,
|
|
982
|
-
} from '@elaraai/e3-api-client';
|
|
983
|
-
|
|
984
|
-
const e3 = await createWebE3(new Worker(new URL('./e3.worker.ts', import.meta.url), { type: 'module' }));
|
|
985
|
-
const opts = { token: null, fetch: e3.fetch };
|
|
986
|
-
|
|
987
|
-
// The first visit makes the repository; later visits find it where they left it.
|
|
988
|
-
if (!(await repoList(e3.apiUrl, opts)).includes('default')) {
|
|
989
|
-
await repoCreate(e3.apiUrl, 'default', opts);
|
|
990
|
-
// The package zip e3.export wrote at build time, served beside the page
|
|
991
|
-
const zip = new Uint8Array(await (await fetch('/sales-planning-1.0.0.zip')).arrayBuffer());
|
|
992
|
-
const { name, version } = await packageImport(e3.apiUrl, 'default', zip, opts);
|
|
993
|
-
await workspaceCreate(e3.apiUrl, 'default', 'main', opts);
|
|
994
|
-
await workspaceDeploy(e3.apiUrl, 'default', 'main', `${name}@${version}`, opts);
|
|
995
|
-
await dataflowExecute(e3.apiUrl, 'default', 'main', {}, opts);
|
|
996
|
-
}
|
|
997
|
-
```
|
|
998
|
-
|
|
999
|
-
`e3.apiUrl` is `https://e3-web.invalid`, an origin reserved so that a request
|
|
1000
|
-
sent without `e3.fetch` fails rather than reaching the network, and
|
|
1001
|
-
`e3.fetch`, the global `fetch`'s signature, is answered by the e3 worker. Both
|
|
1002
|
-
go to e3-api-client (`RequestOptions.fetch`) and to e3-ui-components'
|
|
1003
|
-
`<E3Provider>` (`E3Config.fetch`: the **e3-ui** skill). An app's own platform
|
|
1004
|
-
package is served beside the standard ones:
|
|
1005
|
-
`serveUnits({ platforms: { '@acme/pricing': PricingPlatform } })`.
|
|
1006
|
-
|
|
1007
|
-
| `serveE3` option | Default | Effect |
|
|
1008
|
-
|---|---|---|
|
|
1009
|
-
| `units` | — | starts a unit worker: a Web Worker whose script calls `serveUnits()` |
|
|
1010
|
-
| `persist` | `true` | `false` keeps repositories in memory, gone with the page |
|
|
1011
|
-
| `name` | `'e3'` | the storage's name: e3 workers of one name share their repositories, in one tab or several |
|
|
1012
|
-
| `identify` | none | each request's caller, `{ sub, email?, roles }`, from the request; a request to a repository whose caller it does not identify is answered 401. None: the page's one caller may do everything |
|
|
1013
|
-
| `access` | `oneShotAccessByRoles()` with `identify`, else `any` | the grant a caller holds for a one-shot, a split call, or a function call naming its own runner |
|
|
1014
|
-
| `wholeIntakeLimit` | 256 MiB | the largest delivery one intake unit takes in whole |
|
|
1015
|
-
|
|
1016
|
-
What works is the API, as the local server answers it: every API suite of
|
|
1017
|
-
e3-api-tests passes against it in Chromium, as an admin and, in its reader
|
|
1018
|
-
cases, as a reader. Paged reads and key search, function calls, one-shot and
|
|
1019
|
-
split calls, record mutations, deploys that migrate records or build indexes,
|
|
1020
|
-
uploads, package import and export, gc, and dataflow runs — tasks in parallel
|
|
1021
|
-
up to the pool's width (the cores), split tasks in pieces, re-run reactively.
|
|
1022
|
-
A repository outlives a reload, and two tabs share an origin's repositories as
|
|
1023
|
-
two processes share a directory. A closed tab's locks are free; a run it left
|
|
1024
|
-
is taken as one whose process died, the next run served what finished from
|
|
1025
|
-
the cache; and a job or an upload's commit it left is recorded failed, never
|
|
1026
|
-
run again.
|
|
1027
|
-
|
|
1028
|
-
What a browser cannot do — each fails naming it, never a partial answer:
|
|
1029
|
-
|
|
1030
|
-
- **Commands.** A custom task, or a task on the `custom` runtime, is recorded
|
|
1031
|
-
`error`: "a browser runs no commands".
|
|
1032
|
-
- **Platform functions with no browser meaning.** east-web-std has no
|
|
1033
|
-
FileSystem, Env or large-JSON reader, and east-node-io and the python
|
|
1034
|
-
packages have no browser equivalent: a unit calling one fails naming the
|
|
1035
|
-
function, and one listing a package its worker does not serve fails naming
|
|
1036
|
-
the package.
|
|
1037
|
-
- **Segments on demand.** A unit's inputs are staged whole into its worker:
|
|
1038
|
-
waiting for a segment needs a cross-origin isolated page, which GitHub Pages
|
|
1039
|
-
cannot give. Split a large input into pieces with `e3.partition`, which
|
|
1040
|
-
bounds a unit's memory anywhere; a delivery that cannot be cut into pieces
|
|
1041
|
-
is refused above `wholeIntakeLimit`, naming the fix.
|
|
1042
|
-
- **A memory budget.** Memory is not measured: the pool is as wide as the
|
|
1043
|
-
cores, there is no `--memory`, and no record names a peak.
|
|
1044
|
-
- **The CLI.** `e3` drives e3 over HTTP, which an in-page e3 does not serve.
|
|
1045
|
-
|
|
1046
|
-
## A local repository
|
|
1047
|
-
|
|
1048
|
-
```
|
|
1049
|
-
repo/
|
|
1050
|
-
├── repository.beast2 # the release that last wrote it, and the upgrades it has had
|
|
1051
|
-
├── repository-upgrade.beast2 # while an upgrade is under way: where its last part stopped
|
|
1052
|
-
├── metadata.beast2 # its name and status
|
|
1053
|
-
├── objects/ # content-addressed: values, segments, manifests, programs
|
|
1054
|
-
├── packages/ # package refs
|
|
1055
|
-
├── workspaces/ # each workspace's state, dataset refs, and its latest run's state and events
|
|
1056
|
-
├── dataflows/ # run records
|
|
1057
|
-
├── executions/ # execution attempts: status, owner, logs
|
|
1058
|
-
├── running/ # an index of the attempts recorded running, by task: what a status reads
|
|
1059
|
-
├── runs/ # an index of each task's runs, its units left out: what its history and logs read
|
|
1060
|
-
├── adoptions/ # the manifest each delivered file, or piece of one, became
|
|
1061
|
-
├── locks/ # locks, their holders, and how far each says it has got
|
|
1062
|
-
├── gc/ # gc beside running work: unreachable notes, a stepped run's parts
|
|
1063
|
-
├── envs/ # built execution environments
|
|
1064
|
-
└── tmp/ # scratch and staged uploads
|
|
1065
|
-
```
|
|
1066
|
-
|
|
1067
|
-
An e3 opening a repository an older release wrote applies the upgrades it has
|
|
1068
|
-
not had, in place; one a newer e3 upgraded is refused, naming that release.
|
|
478
|
+
## Caching
|
|
1069
479
|
|
|
1070
480
|
**Caching.** An execution is keyed by its task — program, runner and
|
|
1071
481
|
environment — and its inputs' hashes, so a task re-runs only when one of those
|
|
1072
482
|
changes (`--force` bypasses the cache, and `--force-task <task>` for the tasks
|
|
1073
483
|
it names: a task whose code e3 cannot see changed, such as a platform
|
|
1074
|
-
function's, or one reading data from outside e3). A
|
|
484
|
+
function's, or one reading data from outside e3). A task's program holds the
|
|
485
|
+
source locations of its body and of every function it embeds, an imported
|
|
486
|
+
python or node function's too, since its errors, a caught error's `stack` and
|
|
487
|
+
the closures it returns name them: an edit that moves one of those lines — a
|
|
488
|
+
comment or an import added above a function — re-runs the task. One that leaves
|
|
489
|
+
them where they were — another function, the end of a module, another package —
|
|
490
|
+
re-runs only the tasks whose environment it is in, their runner's `{ custom }`
|
|
491
|
+
packages. A change to an input re-runs its
|
|
1075
492
|
consumers, even during a run. A split task is cached a unit at a time: each piece
|
|
1076
493
|
and each merge is its own execution, and pieces are cut by content, so an append
|
|
1077
494
|
or an insertion re-runs only the pieces it reaches and the merges above them.
|
|
1078
495
|
|
|
496
|
+
## Reference files
|
|
497
|
+
|
|
498
|
+
Read the one a task needs; each holds its subject whole.
|
|
499
|
+
|
|
500
|
+
| File | Holds |
|
|
501
|
+
|---|---|
|
|
502
|
+
| [reference/record-indexes.md](reference/record-indexes.md) | Secondary indexes, interval and area indexes, series and summary indexes, nested series indexes |
|
|
503
|
+
| [reference/record-migrations.md](reference/record-migrations.md) | `migration.value` / `rows` / `rekey`, and how a record's type changes |
|
|
504
|
+
| [reference/cli.md](reference/cli.md) | Every `e3` command: repositories, packages, workspaces, datasets, tasks, runs, calls, records, watch, convert, auth, gc |
|
|
505
|
+
| [reference/from-code.md](reference/from-code.md) | `@elaraai/e3-api-client`, `@elaraai/e3-core`, `e3-api-server` |
|
|
506
|
+
| [reference/browser.md](reference/browser.md) | `@elaraai/e3-web`: serveUnits, serveE3, createWebE3, and what a browser cannot do |
|
|
507
|
+
| [reference/rack.md](reference/rack.md) | `e3 rack enroll` / `policy`, routing a run to a rack, `@elaraai/e3-rack` |
|
|
508
|
+
| [reference/repository.md](reference/repository.md) | A local repository's layout, and upgrades |
|
|
509
|
+
|
|
1079
510
|
## Related skills
|
|
1080
511
|
|
|
1081
512
|
- **east** — the language task bodies are written in.
|