nervur 0.22.1 → 0.22.2-3
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/AUTHORING.md +741 -0
- package/KIT-SPEC.md +286 -0
- package/README.md +97 -436
- package/dist/app/app-ground.d.ts +18 -0
- package/dist/app/app-ground.js +25 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +5 -0
- package/dist/app/native.d.ts +46 -0
- package/dist/app/native.js +101 -0
- package/dist/being/being.d.ts +229 -0
- package/dist/being/being.js +20 -0
- package/dist/being/covers.d.ts +9 -0
- package/dist/being/covers.js +21 -0
- package/dist/being/index.d.ts +3 -0
- package/dist/being/index.js +5 -0
- package/dist/being/need.d.ts +49 -0
- package/dist/being/need.js +33 -0
- package/dist/being/schema.d.ts +100 -0
- package/dist/being/schema.js +132 -0
- package/dist/being/table.d.ts +42 -0
- package/dist/being/table.js +288 -0
- package/dist/bench/bench-ground.d.ts +55 -0
- package/dist/bench/bench-ground.js +101 -0
- package/dist/bench/bench.d.ts +87 -0
- package/dist/bench/bench.js +260 -0
- package/dist/bench/fake-carry.d.ts +32 -0
- package/dist/bench/fake-carry.js +56 -0
- package/dist/bench/fake-clock.d.ts +17 -0
- package/dist/bench/fake-clock.js +34 -0
- package/dist/bench/fake-custody.d.ts +9 -0
- package/dist/bench/fake-custody.js +10 -0
- package/dist/bench/fake-faculty.d.ts +25 -0
- package/dist/bench/fake-faculty.js +48 -0
- package/dist/bench/fake-keys.d.ts +5 -0
- package/dist/bench/fake-keys.js +9 -0
- package/dist/bench/fake-memory.d.ts +14 -0
- package/dist/bench/fake-memory.js +46 -0
- package/dist/bench/fake-network.d.ts +57 -0
- package/dist/bench/fake-network.js +169 -0
- package/dist/bench/index.d.ts +11 -0
- package/dist/bench/index.js +13 -0
- package/dist/bench/seeded.d.ts +11 -0
- package/dist/bench/seeded.js +37 -0
- package/dist/bench/settle.d.ts +2 -0
- package/dist/bench/settle.js +49 -0
- package/dist/bench/stewards.d.ts +17 -0
- package/dist/bench/stewards.js +20 -0
- package/dist/bodies/class-list.d.ts +14 -0
- package/dist/bodies/class-list.js +33 -0
- package/dist/bodies/joined-carry.d.ts +23 -0
- package/dist/bodies/joined-carry.js +42 -0
- package/dist/bodies/noble-crypto.d.ts +22 -0
- package/dist/bodies/noble-crypto.js +88 -0
- package/dist/bodies/seed-keys.d.ts +10 -0
- package/dist/bodies/seed-keys.js +42 -0
- package/dist/bodies/strict-tools.d.ts +10 -0
- package/dist/bodies/strict-tools.js +306 -0
- package/dist/bodies/web-carry.d.ts +65 -0
- package/dist/bodies/web-carry.js +224 -0
- package/dist/bodies/web-clock.d.ts +12 -0
- package/dist/bodies/web-clock.js +33 -0
- package/dist/browser/browser-ground.d.ts +69 -0
- package/dist/browser/browser-ground.js +219 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/indexeddb-memory.d.ts +17 -0
- package/dist/browser/indexeddb-memory.js +97 -0
- package/dist/browser/locked-custody.d.ts +25 -0
- package/dist/browser/locked-custody.js +89 -0
- package/dist/browser/origin-classes.d.ts +7 -0
- package/dist/browser/origin-classes.js +31 -0
- package/dist/foundation.d.ts +150 -0
- package/dist/foundation.js +4 -0
- package/dist/ground/ground.d.ts +177 -0
- package/dist/ground/ground.js +363 -0
- package/dist/house/crossing.d.ts +37 -0
- package/dist/house/crossing.js +131 -0
- package/dist/house/house.d.ts +80 -0
- package/dist/house/house.js +1753 -0
- package/dist/house/rows-shape.d.ts +103 -0
- package/dist/house/rows-shape.js +12 -0
- package/dist/house/rows.d.ts +27 -0
- package/dist/house/rows.js +168 -0
- package/dist/index.d.ts +29 -1
- package/dist/index.js +25 -14
- package/dist/node/bridge.d.ts +17 -0
- package/dist/node/bridge.js +188 -0
- package/dist/node/cli.js +170 -0
- package/dist/node/custody.d.ts +20 -0
- package/dist/node/custody.js +34 -0
- package/dist/node/file-keys.d.ts +7 -0
- package/dist/node/file-keys.js +30 -0
- package/dist/node/file-memory.d.ts +13 -0
- package/dist/node/file-memory.js +91 -0
- package/dist/node/folder-classes.d.ts +5 -0
- package/dist/node/folder-classes.js +40 -0
- package/dist/node/gone.d.ts +2 -0
- package/dist/node/gone.js +7 -0
- package/dist/node/hand.d.ts +29 -0
- package/dist/node/hand.js +85 -0
- package/dist/node/held-keys.d.ts +14 -0
- package/dist/node/held-keys.js +36 -0
- package/dist/node/http.d.ts +11 -0
- package/dist/node/http.js +90 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.js +15 -0
- package/dist/node/keychain-keys.d.ts +10 -0
- package/dist/node/keychain-keys.js +51 -0
- package/dist/node/ledger-memory.d.ts +17 -0
- package/dist/node/ledger-memory.js +151 -0
- package/dist/node/lock.d.ts +10 -0
- package/dist/node/lock.js +78 -0
- package/dist/node/node-ground.d.ts +33 -0
- package/dist/node/node-ground.js +147 -0
- package/dist/node/notify.d.ts +1 -0
- package/dist/node/notify.js +18 -0
- package/dist/node/tcp-carry.d.ts +39 -0
- package/dist/node/tcp-carry.js +216 -0
- package/dist/node/websocket.d.ts +6 -0
- package/dist/node/websocket.js +118 -0
- package/dist/quo/frame.d.ts +20 -0
- package/dist/quo/frame.js +48 -0
- package/dist/quo/room.d.ts +154 -0
- package/dist/quo/room.js +362 -0
- package/dist/serve/index.d.ts +27 -0
- package/dist/serve/index.js +55 -0
- package/package.json +40 -56
- package/dist/browser.d.ts +0 -1
- package/dist/browser.js +0 -8
- package/dist/cli.js +0 -16
- package/dist/core/being/being.d.ts +0 -17
- package/dist/core/being/being.js +0 -72
- package/dist/core/being/digest.d.ts +0 -3
- package/dist/core/being/digest.js +0 -17
- package/dist/core/being/faculty.d.ts +0 -10
- package/dist/core/being/faculty.js +0 -77
- package/dist/core/being/index.d.ts +0 -6
- package/dist/core/being/index.js +0 -8
- package/dist/core/being/kind.d.ts +0 -6
- package/dist/core/being/kind.js +0 -34
- package/dist/core/being/types.d.ts +0 -61
- package/dist/core/being/types.js +0 -4
- package/dist/core/being/words.d.ts +0 -13
- package/dist/core/being/words.js +0 -14
- package/dist/core/browser/index.d.ts +0 -37
- package/dist/core/browser/index.js +0 -196
- package/dist/core/browser/worker.d.ts +0 -11
- package/dist/core/browser/worker.js +0 -37
- package/dist/core/cli/command.d.ts +0 -13
- package/dist/core/cli/command.js +0 -330
- package/dist/core/cli/edge.d.ts +0 -7
- package/dist/core/cli/edge.js +0 -79
- package/dist/core/cli/harbor.d.ts +0 -25
- package/dist/core/cli/harbor.js +0 -115
- package/dist/core/contract/index.d.ts +0 -42
- package/dist/core/contract/index.js +0 -34
- package/dist/core/contract/link.d.ts +0 -4
- package/dist/core/contract/link.js +0 -45
- package/dist/core/crypto/aes.d.ts +0 -3
- package/dist/core/crypto/aes.js +0 -24
- package/dist/core/crypto/bytes.d.ts +0 -6
- package/dist/core/crypto/bytes.js +0 -33
- package/dist/core/crypto/ed25519.d.ts +0 -3
- package/dist/core/crypto/ed25519.js +0 -93
- package/dist/core/crypto/hash.d.ts +0 -2
- package/dist/core/crypto/hash.js +0 -11
- package/dist/core/crypto/index.d.ts +0 -7
- package/dist/core/crypto/index.js +0 -10
- package/dist/core/crypto/json.d.ts +0 -8
- package/dist/core/crypto/json.js +0 -250
- package/dist/core/crypto/mlkem.d.ts +0 -12
- package/dist/core/crypto/mlkem.js +0 -36
- package/dist/core/crypto/subtle.d.ts +0 -3
- package/dist/core/crypto/subtle.js +0 -11
- package/dist/core/crypto/x25519.d.ts +0 -2
- package/dist/core/crypto/x25519.js +0 -24
- package/dist/core/edge/index.d.ts +0 -164
- package/dist/core/edge/index.js +0 -739
- package/dist/core/edge/kit.d.ts +0 -2
- package/dist/core/edge/kit.js +0 -2
- package/dist/core/folder/index.d.ts +0 -18
- package/dist/core/folder/index.js +0 -130
- package/dist/core/git/http.d.ts +0 -6
- package/dist/core/git/http.js +0 -102
- package/dist/core/git/index.d.ts +0 -5
- package/dist/core/git/index.js +0 -10
- package/dist/core/git/object.d.ts +0 -40
- package/dist/core/git/object.js +0 -103
- package/dist/core/git/pack.d.ts +0 -9
- package/dist/core/git/pack.js +0 -171
- package/dist/core/git/store.d.ts +0 -23
- package/dist/core/git/store.js +0 -120
- package/dist/core/git/zlib.d.ts +0 -6
- package/dist/core/git/zlib.js +0 -210
- package/dist/core/harbor/carrying.d.ts +0 -50
- package/dist/core/harbor/carrying.js +0 -66
- package/dist/core/harbor/catalogue.d.ts +0 -33
- package/dist/core/harbor/catalogue.js +0 -365
- package/dist/core/harbor/dna.d.ts +0 -13
- package/dist/core/harbor/dna.js +0 -120
- package/dist/core/harbor/dock.d.ts +0 -62
- package/dist/core/harbor/dock.js +0 -381
- package/dist/core/harbor/harbor.d.ts +0 -16
- package/dist/core/harbor/harbor.js +0 -448
- package/dist/core/harbor/index.d.ts +0 -11
- package/dist/core/harbor/index.js +0 -14
- package/dist/core/harbor/memory.d.ts +0 -10
- package/dist/core/harbor/memory.js +0 -25
- package/dist/core/harbor/package.d.ts +0 -20
- package/dist/core/harbor/package.js +0 -172
- package/dist/core/harbor/probe.d.ts +0 -22
- package/dist/core/harbor/probe.js +0 -15
- package/dist/core/harbor/registry.d.ts +0 -15
- package/dist/core/harbor/registry.js +0 -110
- package/dist/core/harbor/root-line.d.ts +0 -7
- package/dist/core/harbor/root-line.js +0 -9
- package/dist/core/harbor/terrain.d.ts +0 -25
- package/dist/core/harbor/terrain.js +0 -28
- package/dist/core/http/index.d.ts +0 -2
- package/dist/core/http/index.js +0 -154
- package/dist/core/http/websocket.d.ts +0 -29
- package/dist/core/http/websocket.js +0 -133
- package/dist/core/line/answer.d.ts +0 -8
- package/dist/core/line/answer.js +0 -60
- package/dist/core/line/frame.d.ts +0 -29
- package/dist/core/line/frame.js +0 -77
- package/dist/core/line/ground.d.ts +0 -3
- package/dist/core/line/ground.js +0 -7
- package/dist/core/line/index.d.ts +0 -4
- package/dist/core/line/index.js +0 -8
- package/dist/core/line/web.d.ts +0 -12
- package/dist/core/line/web.js +0 -117
- package/dist/core/node/index.d.ts +0 -23
- package/dist/core/node/index.js +0 -124
- package/dist/core/pointer/bodies.d.ts +0 -31
- package/dist/core/pointer/bodies.js +0 -118
- package/dist/core/pointer/index.d.ts +0 -2
- package/dist/core/pointer/index.js +0 -4
- package/dist/core/pointer/world.d.ts +0 -45
- package/dist/core/pointer/world.js +0 -106
- package/dist/core/proof/contracts.d.ts +0 -19
- package/dist/core/proof/contracts.js +0 -116
- package/dist/core/proof/expect.d.ts +0 -6
- package/dist/core/proof/expect.js +0 -18
- package/dist/core/proof/index.d.ts +0 -3
- package/dist/core/proof/index.js +0 -14
- package/dist/core/proof/law.d.ts +0 -25
- package/dist/core/proof/law.js +0 -715
- package/dist/core/quo/address.d.ts +0 -19
- package/dist/core/quo/address.js +0 -84
- package/dist/core/quo/door.d.ts +0 -34
- package/dist/core/quo/door.js +0 -174
- package/dist/core/quo/index.d.ts +0 -9
- package/dist/core/quo/index.js +0 -12
- package/dist/core/quo/invitation.d.ts +0 -8
- package/dist/core/quo/invitation.js +0 -19
- package/dist/core/quo/keys.d.ts +0 -40
- package/dist/core/quo/keys.js +0 -79
- package/dist/core/quo/payload.d.ts +0 -15
- package/dist/core/quo/payload.js +0 -55
- package/dist/core/quo/relations.d.ts +0 -40
- package/dist/core/quo/relations.js +0 -33
- package/dist/core/quo/reply.d.ts +0 -13
- package/dist/core/quo/reply.js +0 -35
- package/dist/core/quo/seal.d.ts +0 -42
- package/dist/core/quo/seal.js +0 -78
- package/dist/core/quo/standing.d.ts +0 -39
- package/dist/core/quo/standing.js +0 -90
- package/dist/core/tcp/index.d.ts +0 -27
- package/dist/core/tcp/index.js +0 -209
- package/dist/core/ward/allowance.d.ts +0 -14
- package/dist/core/ward/allowance.js +0 -32
- package/dist/core/ward/cells.d.ts +0 -7
- package/dist/core/ward/cells.js +0 -74
- package/dist/core/ward/house.d.ts +0 -70
- package/dist/core/ward/house.js +0 -499
- package/dist/core/ward/index.d.ts +0 -5
- package/dist/core/ward/index.js +0 -7
- package/dist/core/ward/partition.d.ts +0 -51
- package/dist/core/ward/partition.js +0 -66
- package/dist/core/ward/stance.d.ts +0 -31
- package/dist/core/ward/stance.js +0 -193
- package/dist/core/ward/ward.d.ts +0 -44
- package/dist/core/ward/ward.js +0 -140
- package/dist/core.d.ts +0 -10
- package/dist/core.js +0 -19
- package/dist/defaults/dock/index.d.ts +0 -4
- package/dist/defaults/dock/index.js +0 -11
- package/dist/defaults/index.d.ts +0 -5
- package/dist/defaults/index.js +0 -10
- package/dist/defaults/memory/index.d.ts +0 -12
- package/dist/defaults/memory/index.js +0 -39
- package/dist/defaults/tcp/index.d.ts +0 -16
- package/dist/defaults/tcp/index.js +0 -82
- package/dist/defaults/ward/index.d.ts +0 -4
- package/dist/defaults/ward/index.js +0 -11
- package/dist/defaults/web/index.d.ts +0 -15
- package/dist/defaults/web/index.js +0 -90
- package/dist/edge.d.ts +0 -1
- package/dist/edge.js +0 -9
- package/dist/kit.d.ts +0 -4
- package/dist/kit.js +0 -12
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -8
- package/src/browser.ts +0 -10
- package/src/cli.ts +0 -15
- package/src/core/being/being.ts +0 -84
- package/src/core/being/digest.ts +0 -18
- package/src/core/being/faculty.ts +0 -76
- package/src/core/being/index.ts +0 -8
- package/src/core/being/kind.ts +0 -37
- package/src/core/being/types.ts +0 -74
- package/src/core/being/words.ts +0 -31
- package/src/core/browser/index.ts +0 -219
- package/src/core/browser/worker.ts +0 -63
- package/src/core/cli/command.ts +0 -311
- package/src/core/cli/edge.ts +0 -86
- package/src/core/cli/harbor.ts +0 -120
- package/src/core/contract/index.ts +0 -82
- package/src/core/contract/link.ts +0 -58
- package/src/core/crypto/aes.ts +0 -26
- package/src/core/crypto/bytes.ts +0 -37
- package/src/core/crypto/ed25519.ts +0 -92
- package/src/core/crypto/hash.ts +0 -14
- package/src/core/crypto/index.ts +0 -10
- package/src/core/crypto/json.ts +0 -241
- package/src/core/crypto/mlkem.ts +0 -38
- package/src/core/crypto/subtle.ts +0 -13
- package/src/core/crypto/x25519.ts +0 -23
- package/src/core/edge/index.ts +0 -865
- package/src/core/folder/index.ts +0 -137
- package/src/core/git/http.ts +0 -103
- package/src/core/git/index.ts +0 -10
- package/src/core/git/object.ts +0 -116
- package/src/core/git/pack.ts +0 -147
- package/src/core/git/store.ts +0 -122
- package/src/core/git/zlib.ts +0 -197
- package/src/core/harbor/carrying.ts +0 -121
- package/src/core/harbor/catalogue.ts +0 -386
- package/src/core/harbor/dna.ts +0 -116
- package/src/core/harbor/dock.ts +0 -420
- package/src/core/harbor/harbor.ts +0 -441
- package/src/core/harbor/index.ts +0 -14
- package/src/core/harbor/memory.ts +0 -31
- package/src/core/harbor/package.ts +0 -195
- package/src/core/harbor/probe.ts +0 -39
- package/src/core/harbor/registry.ts +0 -107
- package/src/core/harbor/root-line.ts +0 -19
- package/src/core/harbor/terrain.ts +0 -54
- package/src/core/http/index.ts +0 -147
- package/src/core/http/websocket.ts +0 -124
- package/src/core/line/answer.ts +0 -60
- package/src/core/line/frame.ts +0 -83
- package/src/core/line/ground.ts +0 -18
- package/src/core/line/index.ts +0 -8
- package/src/core/line/web.ts +0 -123
- package/src/core/node/index.ts +0 -139
- package/src/core/pointer/bodies.ts +0 -122
- package/src/core/pointer/index.ts +0 -4
- package/src/core/pointer/world.ts +0 -134
- package/src/core/proof/contracts.ts +0 -134
- package/src/core/proof/expect.ts +0 -25
- package/src/core/proof/index.ts +0 -14
- package/src/core/proof/law.ts +0 -742
- package/src/core/quo/address.ts +0 -87
- package/src/core/quo/door.ts +0 -180
- package/src/core/quo/index.ts +0 -12
- package/src/core/quo/invitation.ts +0 -19
- package/src/core/quo/keys.ts +0 -91
- package/src/core/quo/payload.ts +0 -61
- package/src/core/quo/relations.ts +0 -62
- package/src/core/quo/reply.ts +0 -38
- package/src/core/quo/seal.ts +0 -101
- package/src/core/quo/standing.ts +0 -111
- package/src/core/stand/main.ts +0 -20
- package/src/core/stand/stand.ts +0 -259
- package/src/core/tcp/index.ts +0 -227
- package/src/core/ward/allowance.ts +0 -47
- package/src/core/ward/cells.ts +0 -77
- package/src/core/ward/house.ts +0 -550
- package/src/core/ward/index.ts +0 -7
- package/src/core/ward/partition.ts +0 -120
- package/src/core/ward/stance.ts +0 -215
- package/src/core/ward/ward.ts +0 -166
- package/src/core.ts +0 -55
- package/src/defaults/dock/index.ts +0 -12
- package/src/defaults/index.ts +0 -10
- package/src/defaults/memory/index.ts +0 -50
- package/src/defaults/tcp/index.ts +0 -89
- package/src/defaults/ward/index.ts +0 -12
- package/src/defaults/web/index.ts +0 -97
- package/src/edge.ts +0 -11
- package/src/index.ts +0 -18
- package/src/kit.ts +0 -15
- package/src/node.ts +0 -10
- /package/dist/{cli.d.ts → node/cli.d.ts} +0 -0
package/AUTHORING.md
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
# Writing for nervur
|
|
2
|
+
|
|
3
|
+
This guide teaches the two things you write with `nervur`. A **being**
|
|
4
|
+
holds logic and state. A **faculty** reaches the world outside. A
|
|
5
|
+
**ground** runs them on a machine, and the library ships it. The
|
|
6
|
+
examples build one small shop, and every file here is a file the
|
|
7
|
+
package's own tests run.
|
|
8
|
+
|
|
9
|
+
The shop has three classes and one faculty.
|
|
10
|
+
|
|
11
|
+
- `Order` is one order, from its first item to its shipping.
|
|
12
|
+
- `Shop` is the steward, the being that runs the house.
|
|
13
|
+
- `Lobby` is the public being, which strangers may ask.
|
|
14
|
+
- `Payments` is a faculty that charges money, which the ground's
|
|
15
|
+
`recipe.ts` makes.
|
|
16
|
+
|
|
17
|
+
## Words
|
|
18
|
+
|
|
19
|
+
| Word | What it is |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| being | an instance of a class, born for one ask and dropped after it |
|
|
22
|
+
| cells | her state, JSON values, kept when an ask lands |
|
|
23
|
+
| ask | a method others may call on her, with its entry |
|
|
24
|
+
| asker | who calls her now: an id and notes |
|
|
25
|
+
| occupant | someone who may ask her, by id |
|
|
26
|
+
| standing | someone she may ask, by id |
|
|
27
|
+
| blueprint | the shape of what can be called: a name and its methods |
|
|
28
|
+
| need | the blueprint she calls, at its minimum, under a member of her own |
|
|
29
|
+
| faculty | anything outside the house that answers a blueprint |
|
|
30
|
+
| offer | a faculty the ground hands the house: its blueprint and its object |
|
|
31
|
+
| role | a named test over the asker and her cells |
|
|
32
|
+
| state | a name read from her cells that decides which asks exist |
|
|
33
|
+
| house | what holds beings, keeps their cells, and seals every ask |
|
|
34
|
+
| ground | the process houses run in, holding their seeds, faculties and hands |
|
|
35
|
+
|
|
36
|
+
## A being
|
|
37
|
+
|
|
38
|
+
A being is pure logic over her cells. She never knows where she runs,
|
|
39
|
+
who carries her asks, or how her cells are kept. The house does all of
|
|
40
|
+
it, and she trusts it.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// classes/order.ts
|
|
44
|
+
import { Being, s, need, type Args } from 'nervur/being';
|
|
45
|
+
|
|
46
|
+
export const Payments = need('payments', {
|
|
47
|
+
charge: {
|
|
48
|
+
args: s.object({ order: s.string(), amount: s.number(), notify: s.handle() }),
|
|
49
|
+
result: s.object({ pending: s.boolean() }),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const Courier = need('courier', {
|
|
54
|
+
pickup: { args: s.object({ items: s.array(s.string()) }) },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export class Order extends Being.of({
|
|
58
|
+
kind: 'com.acme.order',
|
|
59
|
+
description: 'One order, from its first item to its shipping.',
|
|
60
|
+
needs: { pay: Payments },
|
|
61
|
+
cells: {
|
|
62
|
+
items: [] as string[],
|
|
63
|
+
total: 0,
|
|
64
|
+
state: 'open',
|
|
65
|
+
paidAt: 0,
|
|
66
|
+
courier: '',
|
|
67
|
+
},
|
|
68
|
+
roles: { owner: (asker) => asker.steward.owner === true },
|
|
69
|
+
state: (me) => me.cells.state,
|
|
70
|
+
asks: {
|
|
71
|
+
hire: {
|
|
72
|
+
for: 'steward',
|
|
73
|
+
hints: { idempotent: true },
|
|
74
|
+
args: s.object({ courier: s.handle() }),
|
|
75
|
+
result: s.string(),
|
|
76
|
+
},
|
|
77
|
+
add: {
|
|
78
|
+
in: 'open',
|
|
79
|
+
for: 'owner',
|
|
80
|
+
to: 'open',
|
|
81
|
+
args: s.object({ sku: s.string(), price: s.number() }),
|
|
82
|
+
result: s.object({ total: s.number() }),
|
|
83
|
+
},
|
|
84
|
+
checkout: { in: 'open', for: 'owner', to: 'paying' },
|
|
85
|
+
charged: { in: 'paying', args: s.reply(Payments.charge), to: ['paying', 'open'] },
|
|
86
|
+
settled: { in: 'paying', for: 'handle', to: 'paid' },
|
|
87
|
+
ship: { in: 'paid', for: 'owner', to: 'shipped' },
|
|
88
|
+
},
|
|
89
|
+
}) {
|
|
90
|
+
// The courier's invitation arrives as her standing. The same one twice is the same standing.
|
|
91
|
+
hire({ courier }: Args<Order, 'hire'>) {
|
|
92
|
+
this.cells.courier = courier;
|
|
93
|
+
return courier;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
add({ sku, price }: Args<Order, 'add'>) {
|
|
97
|
+
this.cells.items = [...this.cells.items, sku];
|
|
98
|
+
this.cells.total += price;
|
|
99
|
+
return { total: this.cells.total };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
checkout() {
|
|
103
|
+
if (this.cells.items.length === 0) this.fail('Add an item first.');
|
|
104
|
+
const notify = this.handle('settled', { once: true });
|
|
105
|
+
this.pay.charge({ order: this.id, amount: this.cells.total, notify }, { reply: 'charged' });
|
|
106
|
+
this.cells.state = 'paying';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
charged({ error }: Args<Order, 'charged'>) {
|
|
110
|
+
if (error) this.cells.state = 'open';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
settled() {
|
|
114
|
+
this.cells.state = 'paid';
|
|
115
|
+
this.cells.paidAt = this.house.now();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ship() {
|
|
119
|
+
if (this.cells.courier === '') this.fail('Hire a courier first.');
|
|
120
|
+
this.held(this.cells.courier, Courier).pickup({ items: this.cells.items });
|
|
121
|
+
this.cells.state = 'shipped';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The order wrote no retry, no catch, no key and no address. `charge` is
|
|
127
|
+
an effect, so it leaves only once `checkout` has landed. Its answer comes
|
|
128
|
+
back to `charged`, and a refused charge opens the order again. The
|
|
129
|
+
provider calls `settled` through the handle once the money arrives.
|
|
130
|
+
`hire` takes a courier's invitation as her standing, and `ship` asks the
|
|
131
|
+
courier through it. `shipped` is a terminal state, and nothing leaves it.
|
|
132
|
+
|
|
133
|
+
### The declaration
|
|
134
|
+
|
|
135
|
+
A class extends `Being.of({ … })`. That one object declares the class,
|
|
136
|
+
and TypeScript reads from it the types of her cells and her needs.
|
|
137
|
+
|
|
138
|
+
| Field | What it is |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `kind` | the code's name: a reversed domain you own, then a name |
|
|
141
|
+
| `description` | one line for readers and agents |
|
|
142
|
+
| `cells` | the state and its defaults, written where a key is missing |
|
|
143
|
+
| `needs` | blueprints she calls, each under a member name she chooses |
|
|
144
|
+
| `roles` | named tests over the asker and her cells |
|
|
145
|
+
| `state` | a function of the being that names her current state |
|
|
146
|
+
| `asks` | one entry per method she answers |
|
|
147
|
+
|
|
148
|
+
Every field is optional but `kind` and `asks`. A class with no `state`
|
|
149
|
+
has one state, named `ready`. The kind is how the house finds her code
|
|
150
|
+
again, so a bundler that renames classes changes nothing.
|
|
151
|
+
|
|
152
|
+
A method in TypeScript names its args with `Args<Class, 'method'>`,
|
|
153
|
+
since a subclass's method takes no type from its base. A method in
|
|
154
|
+
JavaScript writes nothing.
|
|
155
|
+
|
|
156
|
+
### An ask's entry
|
|
157
|
+
|
|
158
|
+
| Field | What it says |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `in` | the states where the ask exists; omitted, every state |
|
|
161
|
+
| `for` | the roles that may call it; omitted, every occupant but handles |
|
|
162
|
+
| `to` | the states it may land in; omitted, the state it began in |
|
|
163
|
+
| `args` | the schema of what it takes; omitted, the empty object alone |
|
|
164
|
+
| `result` | the schema of what it answers; omitted, nothing |
|
|
165
|
+
| `hints` | `readOnly`, `idempotent`, `destructive` |
|
|
166
|
+
| `examples` | cells, a role, args, fakes, and what it gives |
|
|
167
|
+
| `description` | one line for readers and agents |
|
|
168
|
+
| `wait` | milliseconds she may run; omitted, thirty seconds |
|
|
169
|
+
|
|
170
|
+
`in`, `for` and `to` each take one name or a list of names. A method
|
|
171
|
+
with no entry is never reached. An entry with no method is refused when
|
|
172
|
+
the house first loads the class.
|
|
173
|
+
|
|
174
|
+
Five roles are the house's. `handle` is whoever holds a handle to this
|
|
175
|
+
ask. `stranger` is the asker of a public being. `steward` is her
|
|
176
|
+
steward. `being` is another being her steward introduced to her. `root`
|
|
177
|
+
is the owner, through the house's hand. Every other role is yours, a
|
|
178
|
+
function of `(asker, me)`.
|
|
179
|
+
|
|
180
|
+
The house checks the table when it first loads the class. It refuses a
|
|
181
|
+
state no ask reaches, an ask no role reaches, and a role no ask names.
|
|
182
|
+
After a method runs, a state outside the entry's `to` fails the ask, and
|
|
183
|
+
nothing lands.
|
|
184
|
+
|
|
185
|
+
### What she reaches
|
|
186
|
+
|
|
187
|
+
| Member | What it is |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `this.id` | her own id |
|
|
190
|
+
| `this.position` | `steward`, `public` or `normal` |
|
|
191
|
+
| `this.asker` | `{ id, notes, steward }` of who asks now; `signer` for a stranger |
|
|
192
|
+
| `this.cells` | her values |
|
|
193
|
+
| `this.house.now()` | the time, in milliseconds since the epoch |
|
|
194
|
+
| `this.house.random({ length })` | random bytes |
|
|
195
|
+
| `this.house.alarm({ at, ask, args, key })` | one of her asks, at that time |
|
|
196
|
+
| `this.house.cancelAlarm({ key })` | an alarm removed |
|
|
197
|
+
| `this.<need>.<method>({…}, { reply? })` | an awaited call, or an effect |
|
|
198
|
+
| `this.held(id, Need).<ask>({…}, { reply? })` | the same, on a standing |
|
|
199
|
+
| `this.standings` | `list()`, `note(id, notes)` and `drop(id)` |
|
|
200
|
+
| `this.handle(ask, { bind?, notes?, once?, expires? })` | a handle to one of her asks |
|
|
201
|
+
| `this.invite(id, { notes?, expires? })` | a new occupant, as a handle |
|
|
202
|
+
| `this.occupants` | `list()`, `note(id, notes)` and `dismiss(id)` |
|
|
203
|
+
| `this.steward` | her steward, where she is not one |
|
|
204
|
+
| `this.powers` | the house's powers, where she is the steward |
|
|
205
|
+
| `this.fail(message)` | an error the asker can act on |
|
|
206
|
+
|
|
207
|
+
An alarm lives in the house's rows and survives every restart. A second
|
|
208
|
+
alarm under one key replaces the first. When its time comes, the house
|
|
209
|
+
asks her named ask as her steward would.
|
|
210
|
+
|
|
211
|
+
### Awaited calls and effects
|
|
212
|
+
|
|
213
|
+
The callee decides how it is called, with one flag. A method or ask
|
|
214
|
+
marked `idempotent` is safe to repeat, so the caller awaits it during
|
|
215
|
+
her ask. Everything else is an effect. `readOnly` is stricter: it writes
|
|
216
|
+
nothing, it implies `idempotent`, and the house holds it.
|
|
217
|
+
|
|
218
|
+
An awaited call answers during her ask. `await this.fx.rate({…})`
|
|
219
|
+
returns the answer, and a failure throws an error she may catch. It
|
|
220
|
+
waits thirty seconds unless its entry says otherwise, and never more
|
|
221
|
+
than five minutes.
|
|
222
|
+
|
|
223
|
+
An awaited call never comes back to a being in its own chain. While she
|
|
224
|
+
awaits, she holds her queue. A call back to her, other than a
|
|
225
|
+
`readOnly` one, waits behind the ask that waits for it, until the wait
|
|
226
|
+
runs out and the call fails as `answered nothing`. Call back with an
|
|
227
|
+
effect instead.
|
|
228
|
+
|
|
229
|
+
An effect leaves after her ask lands. She calls it and it returns
|
|
230
|
+
nothing. The house writes it in the same write as her cells, then sends
|
|
231
|
+
it with one call id until it is answered. So an effect acts at most
|
|
232
|
+
once. Its answer comes back to the ask `reply` names, as `{ result }` or
|
|
233
|
+
`{ error: { message } }`.
|
|
234
|
+
|
|
235
|
+
An effect gives up at its deadline, seven days for a standing and the
|
|
236
|
+
offer's window for a faculty. The reply then hears an error. Effects to
|
|
237
|
+
one receiver leave one at a time, in the order she called them.
|
|
238
|
+
|
|
239
|
+
Asks to one being run one at a time, in the order they arrive. A
|
|
240
|
+
`readOnly` ask runs beside that queue, on the cells last landed.
|
|
241
|
+
|
|
242
|
+
### Relations
|
|
243
|
+
|
|
244
|
+
An **occupant** is someone who may ask her. She mints one with
|
|
245
|
+
`this.invite(id, { notes })`, which answers a handle, and lets one go with
|
|
246
|
+
`dismiss`.
|
|
247
|
+
|
|
248
|
+
A **standing** is someone she may ask. She receives one where an ask's
|
|
249
|
+
args carry an invitation under `s.handle`, or where her steward
|
|
250
|
+
introduces one. She asks through it with `this.held(id, Need)`, which
|
|
251
|
+
checks the standing's describe covers the need.
|
|
252
|
+
|
|
253
|
+
Every relation carries two sets of notes. `notes` are hers alone.
|
|
254
|
+
`steward` are her steward's, written when the steward made the relation,
|
|
255
|
+
and she reads them and never writes them. The order's `owner` role reads
|
|
256
|
+
the steward's notes, so only the steward decides who owns an order.
|
|
257
|
+
|
|
258
|
+
A **handle** is the only way a relation leaves her. `this.handle(ask)`
|
|
259
|
+
admits its holder to that one ask. `once` dismisses it after its first
|
|
260
|
+
ask lands, and `bind` fixes args the holder cannot change. The house
|
|
261
|
+
turns a handle into an invitation for a far house, or a token for a
|
|
262
|
+
faculty. A handle never enters her cells.
|
|
263
|
+
|
|
264
|
+
`expires`, on `this.handle`, `this.invite` and the steward's `invite`,
|
|
265
|
+
gives each invitation minted from the handle that many milliseconds to
|
|
266
|
+
be taken. Its first knock lands within that time, or it hears silence.
|
|
267
|
+
One taken in time never expires, and one without `expires` waits for
|
|
268
|
+
ever. Give it to every invitation a being mints on each answer, so the
|
|
269
|
+
unspent ones go.
|
|
270
|
+
|
|
271
|
+
### The steward
|
|
272
|
+
|
|
273
|
+
Every house has one steward, with the id `steward`. The holder of the
|
|
274
|
+
house's hand asks her as the occupant `root`, which no sealed box can
|
|
275
|
+
forge. She alone holds `this.powers`.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// classes/shop.ts
|
|
279
|
+
import { Being, s, type Args } from 'nervur/being';
|
|
280
|
+
|
|
281
|
+
export class Shop extends Being.of({
|
|
282
|
+
kind: 'com.acme.shop',
|
|
283
|
+
description: 'The shop’s steward: opens orders, and enrols whoever signs up.',
|
|
284
|
+
cells: { opened: 0 },
|
|
285
|
+
roles: { pilot: (asker) => asker.id === 'root' },
|
|
286
|
+
asks: {
|
|
287
|
+
open: {
|
|
288
|
+
for: 'pilot',
|
|
289
|
+
description: 'Opens an order, and hands back an invitation for its owner.',
|
|
290
|
+
args: s.object({ id: s.string() }),
|
|
291
|
+
result: s.object({ owner: s.invitation() }),
|
|
292
|
+
},
|
|
293
|
+
orders: { for: 'pilot', hints: { readOnly: true }, result: s.array(s.string()) },
|
|
294
|
+
hire: {
|
|
295
|
+
for: 'pilot',
|
|
296
|
+
description: 'Hands a courier’s invitation to an order, which takes it.',
|
|
297
|
+
hints: { idempotent: true },
|
|
298
|
+
args: s.object({ order: s.string(), courier: s.invitation() }),
|
|
299
|
+
result: s.string(),
|
|
300
|
+
},
|
|
301
|
+
enroll: {
|
|
302
|
+
for: 'being',
|
|
303
|
+
hints: { idempotent: true },
|
|
304
|
+
args: s.object({ signer: s.bytes() }),
|
|
305
|
+
result: s.object({ invitation: s.invitation() }),
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
}) {
|
|
309
|
+
open({ id }: Args<Shop, 'open'>) {
|
|
310
|
+
this.powers!.bear({ kind: 'com.acme.order', id });
|
|
311
|
+
this.cells.opened += 1;
|
|
312
|
+
return { owner: this.powers!.invite({ id, occupant: 'owner', notes: { owner: true } }) };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async orders() {
|
|
316
|
+
return (await this.powers!.list()).filter((being) => being.kind === 'com.acme.order').map((being) => being.id);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// She carries the invitation unopened, and the order she names takes it.
|
|
320
|
+
async hire({ order, courier }: Args<Shop, 'hire'>) {
|
|
321
|
+
return (await this.powers!.ask({ id: order, method: 'hire', args: { courier } })) as string;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// One order a signer: asked twice, the same id is borne once.
|
|
325
|
+
enroll({ signer }: Args<Shop, 'enroll'>) {
|
|
326
|
+
const id = `order-${Array.from(signer.subarray(0, 8), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
327
|
+
this.powers!.bear({ kind: 'com.acme.order', id });
|
|
328
|
+
const occupant = `owner-${Array.from(this.house.random({ length: 4 }), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
329
|
+
return { invitation: this.powers!.invite({ id, occupant, notes: { owner: true } }) };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
| Power | What it does |
|
|
335
|
+
| --- | --- |
|
|
336
|
+
| `bear({ kind, id, args })` | places a new being; the same id twice answers the first |
|
|
337
|
+
| `remove({ id })` | removes a being and everything of hers |
|
|
338
|
+
| `list()` | every being, her kind, whether she is absent, and her dead letters |
|
|
339
|
+
| `ask({ id, method, args }, { reply? })` | asks any being as her occupant `steward` |
|
|
340
|
+
| `introduce({ from, to, notes })` | gives `from` a standing on `to`, and `to` an occupant |
|
|
341
|
+
| `invite({ id, occupant, notes, expires? })` | gives a being a new occupant, and the steward its handle |
|
|
342
|
+
|
|
343
|
+
`bear`, `remove`, `introduce` and `invite` land in the steward's own
|
|
344
|
+
write. If her ask fails, none of them happened. A being borne runs her
|
|
345
|
+
`born` ask first, where her class declares one.
|
|
346
|
+
|
|
347
|
+
Every other being is placed as `normal`. She holds the standing
|
|
348
|
+
`steward` and the occupant `steward`, and can drop neither.
|
|
349
|
+
|
|
350
|
+
An invitation from outside lands where the owner says. The courier's
|
|
351
|
+
invitation reaches the owner by any road, a mail or a link. The owner
|
|
352
|
+
hands it to the steward's `hire` with the order it is for. `hire`
|
|
353
|
+
declares it `s.invitation`, so the steward carries it unopened and never
|
|
354
|
+
holds it. The order's `hire` declares it `s.handle`, so the order takes
|
|
355
|
+
it, and the standing is hers.
|
|
356
|
+
|
|
357
|
+
Taking an invitation is safe to repeat. The same invitation taken again
|
|
358
|
+
gives the same standing, so the order's `hire` is `idempotent`, and the
|
|
359
|
+
steward awaits it. The owner hears the standing, or why it was refused.
|
|
360
|
+
A steward that bears a being for an invitation bears her first, then
|
|
361
|
+
hands it to her the same way.
|
|
362
|
+
|
|
363
|
+
### A public being
|
|
364
|
+
|
|
365
|
+
A house may name one public being, with the id `public`. She answers
|
|
366
|
+
strangers: every asker with no relation is the occupant `stranger`, and
|
|
367
|
+
`this.asker.signer` is the key the ask was signed with.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
// classes/lobby.ts
|
|
371
|
+
import { Being, need, s } from 'nervur/being';
|
|
372
|
+
|
|
373
|
+
/** What the lobby asks of her steward. */
|
|
374
|
+
const Signup = need('signup', {
|
|
375
|
+
enroll: {
|
|
376
|
+
hints: { idempotent: true },
|
|
377
|
+
args: s.object({ signer: s.bytes() }),
|
|
378
|
+
result: s.object({ invitation: s.invitation() }),
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
export class Lobby extends Being.of({
|
|
383
|
+
kind: 'com.acme.lobby',
|
|
384
|
+
description: 'The shop’s front door: a stranger signs up and receives an order of their own.',
|
|
385
|
+
asks: {
|
|
386
|
+
signup: { for: 'stranger', result: s.object({ invitation: s.invitation() }) },
|
|
387
|
+
},
|
|
388
|
+
}) {
|
|
389
|
+
signup() {
|
|
390
|
+
return this.held('steward', Signup).enroll({ signer: this.asker.signer! });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
A public being's asks are idempotent by default, since a stranger's box
|
|
396
|
+
may arrive twice. The house answers a replayed stranger's box from a
|
|
397
|
+
cache for ten minutes, and nothing runs twice. An ask marked
|
|
398
|
+
`hints: { idempotent: false }` is refused to strangers.
|
|
399
|
+
|
|
400
|
+
Signup awaits the steward. The lobby asks `enroll`, which is
|
|
401
|
+
`idempotent`, so the lobby awaits its answer and returns the invitation
|
|
402
|
+
unopened. A stranger who signs up twice holds one order.
|
|
403
|
+
|
|
404
|
+
Signup is this shop's choice, not the house's. A house is its owner's,
|
|
405
|
+
and a public being does only what its owner wrote. One may answer who
|
|
406
|
+
the house is and nothing more. A house with no public being answers
|
|
407
|
+
strangers silence.
|
|
408
|
+
|
|
409
|
+
### Schemas
|
|
410
|
+
|
|
411
|
+
One builder gives the schema and the TypeScript type: `s.object`,
|
|
412
|
+
`s.string`, `s.number`, `s.integer`, `s.boolean`, `s.array`, `s.enum`,
|
|
413
|
+
`s.const`, `s.bytes`, `s.optional`, `s.handle`, `s.invitation` and
|
|
414
|
+
`s.reply`. They write JSON Schema 2020-12, in a subset the house checks
|
|
415
|
+
on every ask.
|
|
416
|
+
|
|
417
|
+
- `s.bytes` is a `Uint8Array` in the process and lowercase hex across a
|
|
418
|
+
door.
|
|
419
|
+
- `s.handle` carries a relation. A handle leaves as an invitation, and
|
|
420
|
+
an invitation arrives as a new standing id.
|
|
421
|
+
- `s.invitation` carries an invitation unopened, as the lobby does.
|
|
422
|
+
Passed on under `s.handle`, it is taken by the being that receives
|
|
423
|
+
it. Taking one is safe to repeat: the same invitation gives the same
|
|
424
|
+
standing.
|
|
425
|
+
- `s.reply(method)` is the args of an effect's reply: `{ result }` or
|
|
426
|
+
`{ error: { message } }`.
|
|
427
|
+
|
|
428
|
+
### Needs
|
|
429
|
+
|
|
430
|
+
A need is a blueprint at its minimum: what she will call, never what a
|
|
431
|
+
faculty offers. `need(name, methods)` writes one, and she binds it to a
|
|
432
|
+
member of her own in `needs`. An offer covers a need when four things
|
|
433
|
+
hold.
|
|
434
|
+
|
|
435
|
+
1. The blueprint's name is the same.
|
|
436
|
+
2. Every method the need names is offered. More are allowed.
|
|
437
|
+
3. The offer requires no property the need does not require.
|
|
438
|
+
4. Each method is `idempotent` in both, or in neither.
|
|
439
|
+
|
|
440
|
+
Every need is covered, or she is absent. An absent being answers silence
|
|
441
|
+
and keeps her cells, and answers again once an offer covers her needs.
|
|
442
|
+
|
|
443
|
+
### Testing on the bench
|
|
444
|
+
|
|
445
|
+
The bench opens two houses in one process, on fake memory, keys, clock
|
|
446
|
+
and carry. So every ask crosses a door as it would in production.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
// order.test.ts
|
|
450
|
+
import assert from 'node:assert/strict';
|
|
451
|
+
import { test } from 'node:test';
|
|
452
|
+
import { Bench } from 'nervur/bench';
|
|
453
|
+
import { Order } from './classes/order.ts';
|
|
454
|
+
import { Payments, paymentsOffer } from './payments.ts';
|
|
455
|
+
|
|
456
|
+
test('Order keeps her table: every state and role shows what it owes', async () => {
|
|
457
|
+
await Bench.check(Order);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('An order is paid once the provider calls the handle it was given', async () => {
|
|
461
|
+
const payments = new Payments();
|
|
462
|
+
const bench = await Bench.open({ classes: [Order], offers: [paymentsOffer(payments)] });
|
|
463
|
+
const order = await bench.place(Order, { id: 'first' });
|
|
464
|
+
|
|
465
|
+
assert.deepEqual(await order.ask('checkout'), { error: { message: 'Add an item first.' } });
|
|
466
|
+
assert.deepEqual(await order.ask('add', { sku: 'tea', price: 4 }), { result: { total: 4 } });
|
|
467
|
+
assert.deepEqual(await order.ask('checkout'), { result: null });
|
|
468
|
+
await bench.settle();
|
|
469
|
+
assert.equal((await order.cells())!.state, 'paying', 'the charge left once checkout landed, and answered pending');
|
|
470
|
+
|
|
471
|
+
assert.deepEqual(await payments.settle('first'), { result: null });
|
|
472
|
+
await bench.settle();
|
|
473
|
+
assert.equal((await order.cells())!.state, 'paid');
|
|
474
|
+
assert.deepEqual(await order.ask('add', { sku: 'jam', price: 1 }), { error: { message: 'not in this state' } });
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
`place(Class, { id, cells })` bears a being and starts her from those
|
|
479
|
+
cells. A placed being is asked with `ask(method, args, { role })`,
|
|
480
|
+
described with `describe({ role })`, and read with `cells()`. `settle()`
|
|
481
|
+
lets every effect and reply run, and `advance(ms)` moves the fake clock.
|
|
482
|
+
|
|
483
|
+
The bench plays a role with an occupant named for it, whose own notes and
|
|
484
|
+
steward notes both hold the role as `true`.
|
|
485
|
+
|
|
486
|
+
A steward and a public being are placed where the house places them.
|
|
487
|
+
`Bench.open({ steward: Shop, public: Lobby, classes: [Order] })` opens the
|
|
488
|
+
author's house with them there, and `place(Shop)` and `place(Lobby)` find
|
|
489
|
+
them. A role `root` holds is played through the hand, on any being. On
|
|
490
|
+
the public being, a role a stranger holds is played by a box with no
|
|
491
|
+
relation, signed with a key drawn from the bench's seed.
|
|
492
|
+
`Bench.check(Shop, { position: 'steward' })` and `Bench.check(Lobby, {
|
|
493
|
+
position: 'public', steward: Shop })` check them.
|
|
494
|
+
|
|
495
|
+
An entry's `examples` are tests the bench runs. Each is one ask on a
|
|
496
|
+
fresh bench: `cells` start her, `role` asks, `args` are the ask's,
|
|
497
|
+
`fakes` answer her needs, and `gives` is the answer owed. `Bench.check`
|
|
498
|
+
runs every example twice from one seed and flags a class that answers
|
|
499
|
+
differently. It then describes every state to every role, and names every
|
|
500
|
+
finding that failed.
|
|
501
|
+
|
|
502
|
+
## A faculty
|
|
503
|
+
|
|
504
|
+
A faculty is anything a being may call that is not a being: a payment
|
|
505
|
+
provider, a mail sender, a model, a sensor. The ground hands it to the
|
|
506
|
+
house as an offer, and the house matches it to every need it covers.
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
// payments.ts
|
|
510
|
+
import type { FacultyContext, Offer } from 'nervur';
|
|
511
|
+
import { need, s } from 'nervur/being';
|
|
512
|
+
|
|
513
|
+
/** What the faculty offers. An order's need is covered by it. */
|
|
514
|
+
export const PaymentsBlueprint = need('payments', {
|
|
515
|
+
charge: {
|
|
516
|
+
description: 'Charges an order, and calls notify once the money arrives.',
|
|
517
|
+
args: s.object({ order: s.string(), amount: s.number(), notify: s.handle() }),
|
|
518
|
+
result: s.object({ pending: s.boolean() }),
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
type Answer = { result: { pending: boolean } } | { error: { message: string } };
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* A payment provider in the ground's process. It answers a call id it has
|
|
526
|
+
* seen with the answer it gave, so an effect sent twice charges once. A
|
|
527
|
+
* provider that changes the world keeps these where a restart keeps them.
|
|
528
|
+
*/
|
|
529
|
+
export class Payments {
|
|
530
|
+
readonly #answered = new Map<string, Answer>();
|
|
531
|
+
readonly #waiting = new Map<string, { notify: string; context: FacultyContext }>();
|
|
532
|
+
|
|
533
|
+
async charge({ order, amount, notify }: { order: string; amount: number; notify: string }, context: FacultyContext): Promise<Answer> {
|
|
534
|
+
const seen = this.#answered.get(context.id);
|
|
535
|
+
if (seen !== undefined) return seen;
|
|
536
|
+
const answer: Answer = amount > 0 ? { result: { pending: true } } : { error: { message: 'Nothing to charge.' } };
|
|
537
|
+
if (amount > 0) this.#waiting.set(order, { notify, context });
|
|
538
|
+
this.#answered.set(context.id, answer);
|
|
539
|
+
return answer;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** The money for an order arrived: the provider calls the handle it was given. */
|
|
543
|
+
settle(order: string) {
|
|
544
|
+
const waiting = this.#waiting.get(order);
|
|
545
|
+
if (waiting === undefined) throw new Error(`no charge waits for ${order}`);
|
|
546
|
+
this.#waiting.delete(order);
|
|
547
|
+
return waiting.context.call({ token: waiting.notify, id: `settle:${order}` });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** The offer a ground hands: the blueprint, the object, and a week's memory of call ids. */
|
|
552
|
+
export const paymentsOffer = (payments: Payments): Offer => ({ blueprint: PaymentsBlueprint, object: payments, window: 7 * 86_400_000 });
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
An offer is `{ blueprint, object, kinds?, window?, handler?, stop? }`.
|
|
556
|
+
|
|
557
|
+
- **The object has the blueprint's methods.** Each takes one args object
|
|
558
|
+
and a context, and answers `{ result }` or `{ error: { message } }`. A
|
|
559
|
+
throw is a failure to answer, and the house tries again.
|
|
560
|
+
- **The context holds the call id.** An effect arrives again with the same
|
|
561
|
+
call id when an answer was lost. A faculty that changes the world
|
|
562
|
+
answers a call id it has seen with the answer it gave.
|
|
563
|
+
- **A handle arrives as a token.** The faculty calls it with
|
|
564
|
+
`context.call({ token, args, id })`. Its own `id` makes that call run
|
|
565
|
+
once, however often it is sent.
|
|
566
|
+
- **`kinds` is the ground's grant.** It lists the classes that may hold
|
|
567
|
+
the offer. Without it, every class whose need it covers holds it.
|
|
568
|
+
- **`window` is how long the faculty remembers a call id.** It is seven
|
|
569
|
+
days where omitted, and the house gives up on an effect at it.
|
|
570
|
+
- **`handler` answers HTTP on the ground's one listener.** It takes a
|
|
571
|
+
`Request` and answers a `Response`, or `null` where the request is not
|
|
572
|
+
its own. A site, an API or an MCP server is a faculty with a handler.
|
|
573
|
+
- **`stop` is called when the ground stops**, faculties in the reverse of
|
|
574
|
+
the order they were made.
|
|
575
|
+
|
|
576
|
+
The object arrives living. The ground makes and starts the faculty, and
|
|
577
|
+
the house never starts, stops or restarts it. Every being whose need it
|
|
578
|
+
covers holds the same object.
|
|
579
|
+
|
|
580
|
+
A faculty is the ground's, never a house's. The ground's recipe makes
|
|
581
|
+
each one by name, from the settings the ground hands it. `env` is the
|
|
582
|
+
ground's environment, so a secret stays on its machine and out of the
|
|
583
|
+
code. `dir(name)` answers a folder of the faculty's own.
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
// recipe.ts
|
|
587
|
+
import { Payments, paymentsOffer } from './payments.ts';
|
|
588
|
+
|
|
589
|
+
export const faculties = () => ({
|
|
590
|
+
payments: paymentsOffer(new Payments()),
|
|
591
|
+
});
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
Policy over a faculty is written as a being. Limits, approvals and
|
|
595
|
+
quotas are not the faculty's. One being holds the raw faculty through
|
|
596
|
+
`kinds`, and every other being reaches her through a standing.
|
|
597
|
+
|
|
598
|
+
## A ground
|
|
599
|
+
|
|
600
|
+
A ground is the process houses run in, and you write none. `nervur up`
|
|
601
|
+
runs one on a folder: the recipe of its faculties, and a folder of code
|
|
602
|
+
for each house. It keeps a record of its houses, and opens each on the
|
|
603
|
+
bodies the record names.
|
|
604
|
+
|
|
605
|
+
### The shop's folder
|
|
606
|
+
|
|
607
|
+
```text
|
|
608
|
+
nervur-ground/
|
|
609
|
+
recipe.ts
|
|
610
|
+
payments.ts
|
|
611
|
+
classes/index.ts, order.ts, shop.ts, lobby.ts
|
|
612
|
+
state/ made by the ground, its owner's alone
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
A house's folder names what the house holds.
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
// classes/index.ts
|
|
619
|
+
export { Shop as steward } from './shop.ts';
|
|
620
|
+
export { Lobby as public } from './lobby.ts';
|
|
621
|
+
import { Order } from './order.ts';
|
|
622
|
+
|
|
623
|
+
export const beings = [Order];
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Running it
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
npx nervur up .
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The ground boots in one order, and a stop is that order reversed, on an
|
|
633
|
+
interrupt and on `SIGTERM`.
|
|
634
|
+
|
|
635
|
+
1. **Lock.** One ground to its state.
|
|
636
|
+
2. **Its own.** Its seeds and its record open.
|
|
637
|
+
3. **Faculties.** The recipe makes each, in its order.
|
|
638
|
+
4. **Houses.** Each house of the record opens on its bodies.
|
|
639
|
+
5. **Hook.** The listeners, then the hand on its socket.
|
|
640
|
+
6. **Ready.** It tells systemd it is up, where systemd waits.
|
|
641
|
+
|
|
642
|
+
It is set by its environment.
|
|
643
|
+
|
|
644
|
+
| Setting | What it sets |
|
|
645
|
+
| --- | --- |
|
|
646
|
+
| `NERVUR_TCP_PORT` | its TCP port, 7300 where unset |
|
|
647
|
+
| `NERVUR_HTTP_PORT` | its HTTP port, for Quo over the web and every handler; no HTTP where unset |
|
|
648
|
+
| `NERVUR_ADDRESSES` | the public addresses it writes into invitations, by commas |
|
|
649
|
+
| `NERVUR_BIND` | the address it listens on, `0.0.0.0` where unset |
|
|
650
|
+
| `NERVUR_STATE` | its state, `state/` in its folder where unset |
|
|
651
|
+
| `NERVUR_KEYCHAIN` | a keychain service holding its seeds on macOS, in place of files |
|
|
652
|
+
| `NERVUR_WAIT` | its bound on every ask, in milliseconds |
|
|
653
|
+
|
|
654
|
+
The state holds each house's seed in a file its owner alone reads, and
|
|
655
|
+
each house's ledger. A ledger appends every write and never rewrites
|
|
656
|
+
one, and one behind its witness is refused, so a restored backup cannot
|
|
657
|
+
replay what a house already answered. A lost seed is a lost house.
|
|
658
|
+
|
|
659
|
+
A house is added once, and the ground opens it again at every start.
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
npx nervur houses add name=shop memory='{"body":"ledger"}' classes='{"body":"folder","at":"classes"}' faculties='["payments"]'
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
`memory` and `classes` name the bodies the house is handed. `ledger` and
|
|
666
|
+
`folder` are the ground's own, and a recipe may add more, a git registry
|
|
667
|
+
or another store among them. `faculties` names what the house receives,
|
|
668
|
+
and within it each offer's `kinds` names the classes that hold it.
|
|
669
|
+
|
|
670
|
+
### The hand
|
|
671
|
+
|
|
672
|
+
The command is a face on the ground's hand. It holds no word of its own
|
|
673
|
+
beyond `up` and `service`, and reads every other from what the ground
|
|
674
|
+
describes.
|
|
675
|
+
|
|
676
|
+
```bash
|
|
677
|
+
npx nervur help
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
A faculty's method is called as its owner calls it, and a faculty named
|
|
681
|
+
alone shows its methods. Args are one JSON object, or words `key=value`.
|
|
682
|
+
|
|
683
|
+
```bash
|
|
684
|
+
npx nervur houses list
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
`ask` asks a being in a house as `root`: the steward, or the being
|
|
688
|
+
`--id` names. The owner lands a paper in a being with one ask, and her
|
|
689
|
+
`accept` names `root` in its `for`, or names no `for`.
|
|
690
|
+
|
|
691
|
+
```bash
|
|
692
|
+
npx nervur ask shop open id=first
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
npx nervur ask shop --id alice accept invitation=7b22…
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Each prints one JSON value. It exits 0 on a result and 1 on an error
|
|
700
|
+
answered. It exits 2 where nothing was asked, so a script tells a
|
|
701
|
+
refusal from a ground that is down. The hand is `state/hand` in the
|
|
702
|
+
folder where the command runs, or the socket `--at` or `NERVUR_HAND`
|
|
703
|
+
names.
|
|
704
|
+
|
|
705
|
+
### Running it as a service
|
|
706
|
+
|
|
707
|
+
`nervur service` writes the unit that runs a folder: a systemd user unit
|
|
708
|
+
on Linux, started again after any exit, and a launchd agent on macOS.
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
npx nervur service . > ~/.config/systemd/user/nervur-ground.service
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
systemctl --user enable --now nervur-ground
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
A user unit stops when its user logs out, unless lingering is enabled
|
|
719
|
+
with `loginctl enable-linger`. On macOS, the agent goes to
|
|
720
|
+
`~/Library/LaunchAgents/` and is started with `launchctl bootstrap`.
|
|
721
|
+
|
|
722
|
+
## What the house guarantees
|
|
723
|
+
|
|
724
|
+
1. **Every need is covered, or she is absent.** An absent being answers
|
|
725
|
+
silence and keeps her cells.
|
|
726
|
+
2. **The asker's id is true.** Wherever the asker lives, the id she reads
|
|
727
|
+
is who asked.
|
|
728
|
+
3. **Her notes are hers.** No one else writes them, and she never writes
|
|
729
|
+
her steward's.
|
|
730
|
+
4. **She never holds an invitation's bytes.** Handles leave, standing ids
|
|
731
|
+
arrive, and `s.invitation` passes through unopened.
|
|
732
|
+
5. **One ask at a time.** Asks to one being run in order, and `readOnly`
|
|
733
|
+
asks run beside them.
|
|
734
|
+
6. **An ask lands whole or not at all.** Her cells, her relations, her
|
|
735
|
+
effects and her call ids land in one write.
|
|
736
|
+
7. **An effect acts at most once, and its outcome is known.** Its answer,
|
|
737
|
+
or its giving up, reaches her through `reply`.
|
|
738
|
+
8. **Her class is checked before her first ask.** A class that fails
|
|
739
|
+
leaves her absent, and the refusal says why.
|
|
740
|
+
|
|
741
|
+
A failed ask changed nothing she owns, so asking again is safe.
|