@cruba/crubal-postgres 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -2
- package/dist/bin/crubal-pg.d.ts +2 -0
- package/dist/bin/crubal-pg.js +115 -0
- package/dist/bin/crubal-pg.js.map +1 -0
- package/dist/src/border.d.ts +122 -0
- package/dist/src/border.js +432 -0
- package/dist/src/border.js.map +1 -0
- package/dist/src/db.d.ts +19 -0
- package/dist/src/db.js +60 -0
- package/dist/src/db.js.map +1 -0
- package/dist/src/definitions.d.ts +11 -0
- package/dist/src/definitions.js +119 -0
- package/dist/src/definitions.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/projector.d.ts +24 -0
- package/dist/src/projector.js +148 -0
- package/dist/src/projector.js.map +1 -0
- package/dist/src/schema.sql +273 -0
- package/dist/src/state.d.ts +16 -0
- package/dist/src/state.js +138 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/store.d.ts +269 -0
- package/dist/src/store.js +1003 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/working.d.ts +64 -0
- package/dist/src/working.js +312 -0
- package/dist/src/working.js.map +1 -0
- package/package.json +44 -4
package/README.md
CHANGED
|
@@ -1,3 +1,127 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cruba/crubal-postgres
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The persistent CRUBAL store on Postgres: the log as the truth, every other table a cache rebuilt from it.
|
|
4
|
+
The design is in [docs/design/store.md](../../docs/design/store.md); this package is its first version.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
- **One owner, one chain, one `PgStore`.** `commit(by, mutate, at?, extra?)` runs an operation on a draft
|
|
9
|
+
of the owner's state, turns what changed into one entry, applies it with the engine's own `applyEntry`,
|
|
10
|
+
persists the difference and appends the entry, all in one transaction with the chain row locked. A
|
|
11
|
+
refusal rolls everything back. `append(entry)` takes an entry made elsewhere and links it the same way.
|
|
12
|
+
- **The schema** (`src/schema.sql`) is the design's tables, every one keyed by owner. The log table's only
|
|
13
|
+
update is redaction; a trigger refuses every other update and every delete.
|
|
14
|
+
- **`rebuild()`** empties the owner's state tables and applies the log again. **`verify(keys?)`** checks
|
|
15
|
+
the chain's links and signatures. **`redact(logId, writeIndex?)`** replaces one write by its hash, or
|
|
16
|
+
an entry by its hash (spec 8.5.2), then rebuilds, so the cache forgets what the log forgot.
|
|
17
|
+
- **Nothing in here decides anything.** Folding, rules, access, lifecycle and views are `@cruba/crubal`'s
|
|
18
|
+
and run unchanged; this package loads a working set, persists a difference and keeps the chain.
|
|
19
|
+
|
|
20
|
+
## Proof
|
|
21
|
+
|
|
22
|
+
`test/pg.test.ts` runs against a real Postgres when `CRUBAL_PG_URL` is set (a local Postgres 17 is
|
|
23
|
+
enough: `CRUBAL_PG_URL=postgres:///postgres npm test`), in a schema of its own that it drops afterwards,
|
|
24
|
+
and is skipped otherwise. It checks that every corpus log rebuilds to the same state in Postgres as in
|
|
25
|
+
memory, that journaled operations (attach, merge, archive with cascade) write the same entries as the
|
|
26
|
+
memory journal and leave the same state, that redacting a write keeps the chain valid and the rebuilt
|
|
27
|
+
state without the value while the trigger refuses any other change, and that owners never see each other.
|
|
28
|
+
|
|
29
|
+
## The working set
|
|
30
|
+
|
|
31
|
+
An operation runs over a working set that loads as it is needed (`src/working.ts`). The engine is
|
|
32
|
+
synchronous and pure, so the operation runs on a set that answers what it has and notes what it lacks (a
|
|
33
|
+
record by ID, the records at a level, a record's children, a `for`'s targets, the holders of a key, a
|
|
34
|
+
binding, a run); the store loads what was lacking and runs the operation again on a fresh copy, until a
|
|
35
|
+
run asks for nothing new. Edges, instructions and definitions are loaded whole, being small. The entry's
|
|
36
|
+
application settles the same way, so an attach among sixty records loads the two it touches and their
|
|
37
|
+
neighbours, never the sixty. `store.trace` says what the last operation loaded. An operation passed to
|
|
38
|
+
`commit` MUST therefore be a pure function of the draft it is given: it may be run more than once.
|
|
39
|
+
|
|
40
|
+
## Projections and the stream
|
|
41
|
+
|
|
42
|
+
A store given `projecting: { dict, views, perms? }` marks, inside each write's transaction, the rows the
|
|
43
|
+
entry made stale: for every record it changed, in every view, what the engine's `staleAfter` names, worked
|
|
44
|
+
out on the state before the entry (`src/projector.ts`). `project(view?, audience?)` publishes outside any
|
|
45
|
+
write transaction: the first time (and after a new version) it rebuilds every row, afterwards only the
|
|
46
|
+
marked ones, through the engine's `Views` over a working set that loads what the view reads; it compares
|
|
47
|
+
them with the stored projection, appends one stream entry per row that changed or left, numbered per view
|
|
48
|
+
and audience, and clears the marks. `entriesSince(view, seq)` is what an index reads from its cursor.
|
|
49
|
+
With `fresh: "write"` every commit publishes as soon as it returns. The test replays the corpus stream case
|
|
50
|
+
(views/010) and the erasure removal (views/013) and gets the memory `Views.stream` entries byte for byte.
|
|
51
|
+
|
|
52
|
+
## The border
|
|
53
|
+
|
|
54
|
+
`PgBorder` runs contracts over the store (`src/border.ts`): each run builds the engine's `Exchange` over
|
|
55
|
+
the commit's draft, with the store's edges, its run count and the keys it has acted on, so a run is one
|
|
56
|
+
entry and never acts twice across restarts. `receive` and `arrive` keep the intake record with its bytes
|
|
57
|
+
and hash in the `intake` table; `send` acts at most once per contract, record and payload; `streamRun`
|
|
58
|
+
reads the projection stream from the store's tables since the index's cursor (or the caller's), refuses a
|
|
59
|
+
view above public to a party without `run`, and logs each batch as a run; `ack` keeps the cursor;
|
|
60
|
+
`tick` runs a scheduled contract's due ticks from the `schedules` table, each keyed by contract and
|
|
61
|
+
tick. `store.erase(record)` redacts every value write to a record across the log and leaves its shell.
|
|
62
|
+
|
|
63
|
+
## The command
|
|
64
|
+
|
|
65
|
+
`bin/crubal-pg.ts`, with `CRUBAL_PG_URL` set: `migrate`, `rebuild <owner>`, `verify <owner>`,
|
|
66
|
+
`export <owner>`, `redact <owner> <log> [<write>]`, `erase <owner> <record>`, `project <owner> --defs
|
|
67
|
+
<file>…` and `tick <owner> <contract> --now <moment> --defs <file>…`.
|
|
68
|
+
|
|
69
|
+
## Installing, and definitions from the store
|
|
70
|
+
|
|
71
|
+
`store.install(bundle, keys)` takes a published version (docs/design/publishing.md): it refuses an unsigned
|
|
72
|
+
bundle unless the store allows them for development, a bundle whose hash or signature doesn't verify, a
|
|
73
|
+
language version above the store's, and a bundle whose `extends` aren't installed at their pinned
|
|
74
|
+
versions; it checks the definitions that would result with every publish check; then it writes one
|
|
75
|
+
`install` entry, keeps the bundle in `bundles`, and, when an earlier version of the package was installed,
|
|
76
|
+
restamps every record through the bundle's recipes from that version, each write `by: migration`, and
|
|
77
|
+
leaves every view to be rebuilt. `store.define(member)` is the owner's own change: checked with the rest,
|
|
78
|
+
then one `define` write. `store.definitions()` builds what the engine runs with from the installed bundles
|
|
79
|
+
in order and then the define writes, cached per chain head; the projector and `PgBorder.over(store)` take
|
|
80
|
+
their dictionary, views, contracts and permissions from it, so nothing is handed in any more except in
|
|
81
|
+
tests.
|
|
82
|
+
|
|
83
|
+
## Sends with a real act
|
|
84
|
+
|
|
85
|
+
A send over the store is two entries with the act between them (docs/design/runs.md): `prepare` logs
|
|
86
|
+
`started` with the idempotency key and the payload's hash, the payload itself kept in `outtake`; the
|
|
87
|
+
contract's adapter (`adapters: { [contract]: (contract, payload, run) => … }`, host code, never a
|
|
88
|
+
package's) acts outside any transaction; then `acted` or `failed` with its attempt. A run still open
|
|
89
|
+
(`started`, or `failed` with attempts left under `retry`) is seeded into every fresh border from the runs
|
|
90
|
+
table, so a restart never acts twice and `retry(run)` acts again from the kept payload without re-mapping.
|
|
91
|
+
After the last attempt a review item `run:<contract>` opens. An answer the adapter brings back closes the
|
|
92
|
+
run as a receive with `via` that run; `undo(run)` runs the reversing contract naming its cause; `erase`
|
|
93
|
+
takes the record's outtake rows with its values.
|
|
94
|
+
|
|
95
|
+
## Review decisions and replay
|
|
96
|
+
|
|
97
|
+
`store.review()` lists the open items; `store.decide(decision, by)` makes one as one entry (the review
|
|
98
|
+
write with the define, value or release it causes), then replays every kept intake the decision reaches (a
|
|
99
|
+
facet's new value, a contract's unknown field, a unit's spelling), each its own entry `by: migration:
|
|
100
|
+
replay`, superseding what that intake landed before and leaving the rest to the trust order. A `units`
|
|
101
|
+
define feeds the owner's extension into the units table in force (one owner per process for now). A dead
|
|
102
|
+
run's `retry` releases its key, so a fresh border sends the payload again. An observation run loads the
|
|
103
|
+
observations supporting the record and its parent, and the open split items on them, apart from the draft;
|
|
104
|
+
at the package's `split` threshold the border opens `split:<record>`, and the decision `split` mints the
|
|
105
|
+
version and moves the instances.
|
|
106
|
+
|
|
107
|
+
## What a commit refuses
|
|
108
|
+
|
|
109
|
+
Every entry is checked in the same run that applies it (so what the check reads loads too): a person
|
|
110
|
+
owning what the package's `ownership` doesn't allow (14.1), a record wearing a class not for its kind or
|
|
111
|
+
classes whose meet is empty (5.5), `within` forming a cycle or crossing owners (14.2), and every edge
|
|
112
|
+
written (14.12). An emergency edge is the host's to check against its published terms (14.13).
|
|
113
|
+
|
|
114
|
+
## Hosted persons (14.1)
|
|
115
|
+
|
|
116
|
+
A record owned `<owner>:<id>` is a hosted person's: the writes to it leave the owner's entry and become
|
|
117
|
+
one entry per person in that person's chain, linked and signed (with the `hostedSigner` the host hands
|
|
118
|
+
the store) in the same transaction; the chains lock in a fixed order. A rebuild applies the owner's chain
|
|
119
|
+
and the hosted chains in the order they were written (`log.ord`); erase redacts in every chain.
|
|
120
|
+
|
|
121
|
+
`store.drain(rate)` and `crubal-pg drain <owner> [--rate N]` apply pending instructions to at most that
|
|
122
|
+
many records per call, each its own entry `by: migration: drain`.
|
|
123
|
+
|
|
124
|
+
## Not yet here
|
|
125
|
+
|
|
126
|
+
The host: authentication, secrets, named adapters, signing, the loops and the HTTP surface
|
|
127
|
+
(docs/design/host.md); forms as data (docs/design/index-and-forms.md).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// crubal-pg: operate a CRUBAL store on Postgres (docs/design/store.md). CRUBAL_PG_URL names the database.
|
|
3
|
+
// crubal-pg migrate create or update the tables
|
|
4
|
+
// crubal-pg rebuild <owner> empty the owner's state tables and apply the log again
|
|
5
|
+
// crubal-pg verify <owner> [--keys <file>] check the chain's links and signatures
|
|
6
|
+
// crubal-pg export <owner> the owner's log, as JSON
|
|
7
|
+
// crubal-pg redact <owner> <log> [<write>] redact one write (by index) or the whole entry
|
|
8
|
+
// crubal-pg erase <owner> <record> redact every value write to a record
|
|
9
|
+
// crubal-pg dry-run <owner> <bundle.json> what installing the version would restamp, without writing
|
|
10
|
+
// crubal-pg drain <owner> [--rate N] apply pending instructions to at most N records (default 100)
|
|
11
|
+
// crubal-pg project <owner> --defs <file>... publish every view's stale rows
|
|
12
|
+
// crubal-pg tick <owner> <contract> --now <moment> --defs <file>... run a scheduled contract's due ticks
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { emptyDictionary, readDefinitions } from "@cruba/crubal";
|
|
15
|
+
import { readContracts } from "@cruba/crubal";
|
|
16
|
+
import { readViews } from "@cruba/crubal";
|
|
17
|
+
import { readAccess } from "@cruba/crubal";
|
|
18
|
+
import { PgBorder } from "../src/border.js";
|
|
19
|
+
import { connect, migrate, transaction } from "../src/db.js";
|
|
20
|
+
import { PgStore } from "../src/store.js";
|
|
21
|
+
const [cmd, owner, ...rest] = process.argv.slice(2);
|
|
22
|
+
const flag = (name) => rest.flatMap((a, i) => (a === name ? [rest[i + 1]] : []));
|
|
23
|
+
const flags = (name) => {
|
|
24
|
+
const out = [];
|
|
25
|
+
let on = false;
|
|
26
|
+
for (const a of rest) {
|
|
27
|
+
if (a.startsWith("--"))
|
|
28
|
+
on = a === name;
|
|
29
|
+
else if (on)
|
|
30
|
+
out.push(a);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
};
|
|
34
|
+
function definitions() {
|
|
35
|
+
const dict = emptyDictionary();
|
|
36
|
+
const texts = flags("--defs").map((f) => readFileSync(f, "utf8"));
|
|
37
|
+
for (const t of texts)
|
|
38
|
+
readDefinitions(t, dict);
|
|
39
|
+
const all = texts.join("\n");
|
|
40
|
+
const views = readViews(all);
|
|
41
|
+
const { perms } = readAccess(all);
|
|
42
|
+
return { dict, views, perms, contracts: readContracts(all) };
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
const pool = connect();
|
|
46
|
+
try {
|
|
47
|
+
if (cmd === "migrate") {
|
|
48
|
+
await transaction(pool, (c) => migrate(c));
|
|
49
|
+
console.log("migrated");
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
if (!owner)
|
|
53
|
+
return usage();
|
|
54
|
+
if (cmd === "rebuild") {
|
|
55
|
+
const state = await new PgStore(pool, owner).rebuild();
|
|
56
|
+
console.log(`rebuilt ${owner}: ${state.records.size} records`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (cmd === "verify") {
|
|
60
|
+
const keysFile = flag("--keys")[0];
|
|
61
|
+
const keys = keysFile ? (JSON.parse(readFileSync(keysFile, "utf8")).keys ?? JSON.parse(readFileSync(keysFile, "utf8"))) : undefined;
|
|
62
|
+
const v = await new PgStore(pool, owner).verify(keys);
|
|
63
|
+
console.log(JSON.stringify(v));
|
|
64
|
+
return v.valid ? 0 : 1;
|
|
65
|
+
}
|
|
66
|
+
if (cmd === "export") {
|
|
67
|
+
console.log(JSON.stringify(await new PgStore(pool, owner).entries(), null, 2));
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
if (cmd === "redact" && rest[0]) {
|
|
71
|
+
await new PgStore(pool, owner).redact(rest[0], rest[1] !== undefined && !rest[1].startsWith("--") ? Number(rest[1]) : undefined);
|
|
72
|
+
console.log(`redacted ${rest[0]}${rest[1] ? ` write ${rest[1]}` : ""}`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (cmd === "drain") {
|
|
76
|
+
const r = await new PgStore(pool, owner).drain(Number(flag("--rate")[0] ?? 100));
|
|
77
|
+
console.log(`drained ${owner}: ${r.applied.length} records, ${r.remaining} still waiting`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
if (cmd === "dry-run" && rest[0]) {
|
|
81
|
+
const bundle = JSON.parse(readFileSync(rest[0], "utf8"));
|
|
82
|
+
console.log(JSON.stringify(await new PgStore(pool, owner).dryRun(bundle), null, 2));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (cmd === "erase" && rest[0]) {
|
|
86
|
+
const r = await new PgStore(pool, owner).erase(rest[0]);
|
|
87
|
+
console.log(`erased ${rest[0]}: ${r.redacted} writes redacted${r.witnessed.length ? `; witnessed entries left as they are: ${r.witnessed.join(", ")}` : ""}`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
if (cmd === "project") {
|
|
91
|
+
const d = definitions();
|
|
92
|
+
const entries = await new PgStore(pool, owner, { projecting: { dict: d.dict, views: d.views, perms: d.perms } }).project();
|
|
93
|
+
console.log(`published ${entries.length} entries`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
if (cmd === "tick" && rest[0]) {
|
|
97
|
+
const d = definitions();
|
|
98
|
+
const now = flag("--now")[0] ?? new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
99
|
+
const store = new PgStore(pool, owner, { projecting: { dict: d.dict, views: d.views, perms: d.perms } });
|
|
100
|
+
const r = await new PgBorder(store, { dict: d.dict, contracts: d.contracts, perms: d.perms }).tick(rest[0], now);
|
|
101
|
+
console.log(JSON.stringify(r, null, 2));
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
return usage();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await pool.end();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function usage() {
|
|
111
|
+
console.log("usage: crubal-pg migrate | rebuild <owner> | verify <owner> [--keys <file>] | export <owner> | redact <owner> <log> [<write>] | erase <owner> <record> | dry-run <owner> <bundle.json> | drain <owner> [--rate N] | project <owner> --defs <file>... | tick <owner> <contract> --now <moment> --defs <file>...");
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
process.exit(await main());
|
|
115
|
+
//# sourceMappingURL=crubal-pg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crubal-pg.js","sourceRoot":"","sources":["../../bin/crubal-pg.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type Permissions } from "@cruba/crubal";
|
|
2
|
+
import type { Dictionary } from "@cruba/crubal";
|
|
3
|
+
import { Exchange, type Role, type RunResult } from "@cruba/crubal";
|
|
4
|
+
import type { JsonObject, Record } from "@cruba/crubal";
|
|
5
|
+
import type { Rule } from "@cruba/crubal";
|
|
6
|
+
import { type Contract } from "@cruba/crubal";
|
|
7
|
+
import type { Offer } from "@cruba/crubal";
|
|
8
|
+
import type { PgStore } from "./store.ts";
|
|
9
|
+
/** What performs a run's act (7.4 step 4): the host's code for a system; never a package's. */
|
|
10
|
+
export type Adapter = (contract: Contract, payload: JsonObject, run: string) => Promise<{
|
|
11
|
+
acted: true;
|
|
12
|
+
body?: string;
|
|
13
|
+
} | {
|
|
14
|
+
failed: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type BorderOptions = {
|
|
17
|
+
dict: Dictionary;
|
|
18
|
+
contracts: Contract[];
|
|
19
|
+
roles?: Role[];
|
|
20
|
+
rules?: Rule[];
|
|
21
|
+
perms?: Permissions;
|
|
22
|
+
adapters?: {
|
|
23
|
+
[contract: string]: Adapter;
|
|
24
|
+
};
|
|
25
|
+
versions?: JsonObject;
|
|
26
|
+
thresholds?: {
|
|
27
|
+
confirm?: number;
|
|
28
|
+
split?: number;
|
|
29
|
+
}; /** The id prefix an arrival takes per level (`organization: "Z"`); the level's first letter otherwise. */
|
|
30
|
+
idPrefixes?: {
|
|
31
|
+
[level: string]: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/** The border's options from the store's own definitions (docs/design/publishing.md). */
|
|
35
|
+
export declare function borderOptions(store: PgStore): Promise<BorderOptions>;
|
|
36
|
+
/**
|
|
37
|
+
* A system acting for a person (14.9): the run's caller is the person, whose access is capped to the items
|
|
38
|
+
* they chose and gated until they lifted it; the entry names the system as `caller` and the person as
|
|
39
|
+
* `for`; a write that grants onward, or runs under an emergency edge, is refused.
|
|
40
|
+
*/
|
|
41
|
+
export type Acting = {
|
|
42
|
+
system: string;
|
|
43
|
+
person: string;
|
|
44
|
+
items?: string[];
|
|
45
|
+
gated?: boolean;
|
|
46
|
+
};
|
|
47
|
+
export declare class PgBorder {
|
|
48
|
+
readonly store: PgStore;
|
|
49
|
+
readonly o: BorderOptions;
|
|
50
|
+
readonly contracts: Map<string, Contract>;
|
|
51
|
+
/** Set for one request when a system acts for a person (14.9); every run of this border then runs as the person. */
|
|
52
|
+
acting?: Acting;
|
|
53
|
+
constructor(store: PgStore, o: BorderOptions);
|
|
54
|
+
/** The person's access, narrowed as they chose (14.9), laid over the owner's access for this run. */
|
|
55
|
+
private cap;
|
|
56
|
+
/** Refuse what a system acting for a person may never do (14.9, 14.12 items 8 and 9). */
|
|
57
|
+
private refuseActing;
|
|
58
|
+
/** A border over a store's own definitions. */
|
|
59
|
+
static over(store: PgStore): Promise<PgBorder>;
|
|
60
|
+
private get owner();
|
|
61
|
+
/** Runs so far and the keys acted on, from the runs table, so a fresh Exchange continues where the last left off. */
|
|
62
|
+
private past;
|
|
63
|
+
/**
|
|
64
|
+
* One border run as one entry: the Exchange is built over the draft, with the store's edges, runs and
|
|
65
|
+
* keys; `fn` runs it; its changes and its run write become the entry. The function MUST be pure over the
|
|
66
|
+
* draft (it may run more than once while the working set loads).
|
|
67
|
+
*/
|
|
68
|
+
/** The highest number behind each id prefix the owner has (`P-` → 3050), so an arrival mints the next (9.2a). */
|
|
69
|
+
private idCounts;
|
|
70
|
+
run<T extends RunResult>(fn: (ex: Exchange) => T, at?: string, seed?: {
|
|
71
|
+
observations?: JsonObject[];
|
|
72
|
+
review?: JsonObject[];
|
|
73
|
+
}): Promise<T>;
|
|
74
|
+
/** Keep an intake record with its bytes (9.1), beside the log. */
|
|
75
|
+
private keepIntake;
|
|
76
|
+
/** A receive contract's run on a record (7.4 steps 5–6), its intake kept; with `forRun`, a late answer closing that run. */
|
|
77
|
+
receive(name: string, recordId: string, body: string, callerId?: string, at?: string, forRun?: string): Promise<RunResult>;
|
|
78
|
+
/** An arrival (9.2a): the payload finds or makes its record; its intake kept. */
|
|
79
|
+
arrive(name: string, body: string, level: string, callerId?: string, of?: string, at?: string, within?: string): Promise<RunResult>;
|
|
80
|
+
/**
|
|
81
|
+
* A send (7.4, docs/design/runs.md): `started` in one entry with the payload kept in outtake, the adapter
|
|
82
|
+
* outside any transaction, then `acted` or `failed` in another; an answer the adapter brings back closes
|
|
83
|
+
* the run as a receive. Without an adapter the act is nothing and the run is acted.
|
|
84
|
+
*/
|
|
85
|
+
send(name: string, recordId: string, callerId?: string, at?: string): Promise<RunResult>;
|
|
86
|
+
/** The act and what follows it: acted or failed, and the answer landed against the run. */
|
|
87
|
+
private act;
|
|
88
|
+
/** Try a failed run again from its kept payload (7.5 `retry`), never re-mapping it. */
|
|
89
|
+
retry(run: string, callerId?: string, at?: string): Promise<RunResult>;
|
|
90
|
+
/** What an observation run reads (9.7.3): the observations supporting the record or its parent, and the open split items on them. */
|
|
91
|
+
private evidenceFor;
|
|
92
|
+
/** The owner's offer (7.8), with the signed log entries its values came from, for another owner's take. */
|
|
93
|
+
offer(name: string, only?: string[]): Promise<Offer>;
|
|
94
|
+
/** A take (7.8) of a presenter's record into one of ours, by the offer's mode; one entry naming the presenter's. */
|
|
95
|
+
take(takeName: string, offer: Offer, recordId: string, presenter: Record, callerId?: string, at?: string): Promise<RunResult>;
|
|
96
|
+
/** Taken values past their terms (7.8): removed, one entry per record. */
|
|
97
|
+
expire(now: string, at?: string): Promise<string[]>;
|
|
98
|
+
/** A decision on a dead run (runs.md): retry (a new run, the key released) or drop; then the act for a retry. */
|
|
99
|
+
decideRun(run: string, decision: "retry" | "drop", callerId?: string, at?: string): Promise<RunResult>;
|
|
100
|
+
/** Run the contract that reverses a run (7.5 `undo`), naming its cause. */
|
|
101
|
+
undo(run: string, callerId?: string, at?: string): Promise<RunResult>;
|
|
102
|
+
/** The stored cursor of a stream contract for a view, when it is for the view's current version. */
|
|
103
|
+
private cursorOf;
|
|
104
|
+
/**
|
|
105
|
+
* A stream contract's run (12.6.2): the entries of each view since the cursor, pushed (the stored
|
|
106
|
+
* cursor) or pulled (the caller's). A view above public goes only to a party holding `run` for the
|
|
107
|
+
* contract. A batch with entries is logged as a run keyed by the contract, the views and the ranges.
|
|
108
|
+
*/
|
|
109
|
+
streamRun(name: string, callerId?: string, cursor?: {
|
|
110
|
+
[view: string]: number;
|
|
111
|
+
}, at?: string): Promise<RunResult>;
|
|
112
|
+
/** An index acknowledges the last `seq` it holds per view (12.6.2); the store resumes from the next. */
|
|
113
|
+
ack(name: string, cursor: {
|
|
114
|
+
[view: string]: number;
|
|
115
|
+
}): Promise<RunResult>;
|
|
116
|
+
/**
|
|
117
|
+
* A scheduled contract's tick (7.11): every tick due since the last one runs (or the latest, or none,
|
|
118
|
+
* by `missed`), each keyed by the contract and the tick so a restarted host never runs one twice. The
|
|
119
|
+
* schedule begins at the first clock reading.
|
|
120
|
+
*/
|
|
121
|
+
tick(name: string, now: string, callerId?: string): Promise<RunResult>;
|
|
122
|
+
}
|