nervur 0.22.2-3 → 0.22.2-4
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 +74 -1
- package/README.md +36 -0
- package/dist/browser/browser-ground.js +13 -3
- package/dist/browser/locked-custody.d.ts +2 -0
- package/dist/browser/locked-custody.js +4 -0
- package/package.json +2 -2
package/AUTHORING.md
CHANGED
|
@@ -144,6 +144,12 @@ and TypeScript reads from it the types of her cells and her needs.
|
|
|
144
144
|
| `roles` | named tests over the asker and her cells |
|
|
145
145
|
| `state` | a function of the being that names her current state |
|
|
146
146
|
| `asks` | one entry per method she answers |
|
|
147
|
+
| `view` | markup as text over her asks, which a screen renders |
|
|
148
|
+
|
|
149
|
+
A view is data, at most 64 KiB, carried in her describe to every asker.
|
|
150
|
+
It holds no script, and a renderer loads nothing it names, so a view
|
|
151
|
+
neither acts nor tracks. The library defines no markup: a renderer
|
|
152
|
+
speaks its own.
|
|
147
153
|
|
|
148
154
|
Every field is optional but `kind` and `asks`. A class with no `state`
|
|
149
155
|
has one state, named `ready`. The kind is how the house finds her code
|
|
@@ -195,7 +201,7 @@ nothing lands.
|
|
|
195
201
|
| `this.house.alarm({ at, ask, args, key })` | one of her asks, at that time |
|
|
196
202
|
| `this.house.cancelAlarm({ key })` | an alarm removed |
|
|
197
203
|
| `this.<need>.<method>({…}, { reply? })` | an awaited call, or an effect |
|
|
198
|
-
| `this.held(id, Need).<ask>({…}, { reply? })` | the same, on a standing |
|
|
204
|
+
| `this.held(id, Need).<ask>({…}, { reply?, after? })` | the same, on a standing, and a watch where `after` is given |
|
|
199
205
|
| `this.standings` | `list()`, `note(id, notes)` and `drop(id)` |
|
|
200
206
|
| `this.handle(ask, { bind?, notes?, once?, expires? })` | a handle to one of her asks |
|
|
201
207
|
| `this.invite(id, { notes?, expires? })` | a new occupant, as a handle |
|
|
@@ -239,6 +245,29 @@ one receiver leave one at a time, in the order she called them.
|
|
|
239
245
|
Asks to one being run one at a time, in the order they arrive. A
|
|
240
246
|
`readOnly` ask runs beside that queue, on the cells last landed.
|
|
241
247
|
|
|
248
|
+
### Watching an answer
|
|
249
|
+
|
|
250
|
+
A watch is a `readOnly` ask asked with `after`, the answer you already
|
|
251
|
+
hold. The house answers at once where the answer differs. Where it is
|
|
252
|
+
the same, the house holds the ask and runs it again each time an ask of
|
|
253
|
+
that being lands. It answers the first answer that differs, or the same
|
|
254
|
+
one when the wait runs out. So a chat, an order's status or a dashboard
|
|
255
|
+
is a loop of watches, and nothing polls.
|
|
256
|
+
|
|
257
|
+
Through the hand, pass `after` beside the method, as the answer you
|
|
258
|
+
hold: `{ result: [...] }`. From a being, pass the result she holds:
|
|
259
|
+
`this.held(room, Messages).messages({}, { after: seen })`, inside a
|
|
260
|
+
`readOnly` ask of her own, so she stays free while she waits. Only a
|
|
261
|
+
`readOnly` ask is watched, and a watch on anything else is refused where
|
|
262
|
+
she makes it.
|
|
263
|
+
|
|
264
|
+
A watch moves only on what its asker could read, since it runs as that
|
|
265
|
+
asker. One asker holds one watch on one ask with the same args, and a
|
|
266
|
+
second answers the first at once. A watch across a door holds its
|
|
267
|
+
relation until it answers, since Quo moves a relation one ask at a time.
|
|
268
|
+
So watch a far being through a handle to the watched ask alone, a
|
|
269
|
+
relation of its own, and ask everything else on your other standing.
|
|
270
|
+
|
|
242
271
|
### Relations
|
|
243
272
|
|
|
244
273
|
An **occupant** is someone who may ask her. She mints one with
|
|
@@ -719,6 +748,50 @@ A user unit stops when its user logs out, unless lingering is enabled
|
|
|
719
748
|
with `loginctl enable-linger`. On macOS, the agent goes to
|
|
720
749
|
`~/Library/LaunchAgents/` and is started with `launchctl bootstrap`.
|
|
721
750
|
|
|
751
|
+
### In a page
|
|
752
|
+
|
|
753
|
+
`BrowserGround.open({ recipe })` from `nervur/browser` runs the same
|
|
754
|
+
houses in a page. Every tab and the service worker of one origin share
|
|
755
|
+
one ground: the one holding the Web Lock runs it, and the others reach
|
|
756
|
+
its hand over a `BroadcastChannel`. When it closes, the next opens the
|
|
757
|
+
ground from the same storage. `hand` answers the same three requests
|
|
758
|
+
the command sends: `describe`, a faculty's method, and an ask of a being
|
|
759
|
+
in a named house.
|
|
760
|
+
|
|
761
|
+
Its bodies are the browser's. Seeds are sealed under an AES key that
|
|
762
|
+
IndexedDB holds unextractable, and memory is IndexedDB, named
|
|
763
|
+
`indexeddb` in an entry. Classes load from the page's own origin through
|
|
764
|
+
the `origin` body, `{ body: 'origin', at: '/house/index.js' }`, and a
|
|
765
|
+
path off the origin is refused. The recipe is the object you pass: its
|
|
766
|
+
faculties, made by a function, and any custom body.
|
|
767
|
+
|
|
768
|
+
Any script on the origin can use the ground's keys, so the ground is
|
|
769
|
+
whoever serves the origin's script. Give it an origin of its own, serve
|
|
770
|
+
nothing a stranger wrote there, and set a strict content security
|
|
771
|
+
policy. The page and the house's module must share one copy of
|
|
772
|
+
`nervur/being`, since a house knows a class by a mark that module gives.
|
|
773
|
+
A bundler's shared chunk or an import map gives the one copy.
|
|
774
|
+
|
|
775
|
+
A service worker opens the ground the same way, on a push, where no tab
|
|
776
|
+
runs it. It may not `import()`, so hand it the modules it imported
|
|
777
|
+
itself: `platform: { load: (href) => modules[new URL(href).pathname] }`.
|
|
778
|
+
|
|
779
|
+
### In an app
|
|
780
|
+
|
|
781
|
+
`AppGround.open({ shell })` from `nervur/app` is a BrowserGround in the
|
|
782
|
+
app's web view, on two interfaces the shell fills in native code:
|
|
783
|
+
|
|
784
|
+
- `NativeSecrets`: `get(name)` and `set(name, value)`, text kept by the
|
|
785
|
+
iOS Keychain or the Android Keystore on this device alone.
|
|
786
|
+
- `NativeStore`: `get(key)`, `keys(prefix)`, and `swap(writes, expect)`,
|
|
787
|
+
which lands every write only where each key in `expect` still holds
|
|
788
|
+
what it names, and answers whether it landed.
|
|
789
|
+
|
|
790
|
+
The library holds the rest. Each house's seed goes into the secret
|
|
791
|
+
store, and each memory into the native store, which the system never
|
|
792
|
+
evicts as it may a web view's storage. A push token reaches the ground
|
|
793
|
+
through a faculty your recipe makes.
|
|
794
|
+
|
|
722
795
|
## What the house guarantees
|
|
723
796
|
|
|
724
797
|
1. **Every need is covered, or she is absent.** An absent being answers
|
package/README.md
CHANGED
|
@@ -107,6 +107,39 @@ the house's seed, its ledger and its record in `state/`, readable by
|
|
|
107
107
|
you alone. Keep that folder secret: a house opens on no other seed.
|
|
108
108
|
`npx nervur help` lists everything the ground offers.
|
|
109
109
|
|
|
110
|
+
## A ground in a page
|
|
111
|
+
|
|
112
|
+
The same house runs in a browser. Bundle the house's module and serve it
|
|
113
|
+
on your origin, beside a page that opens the ground.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// page.ts
|
|
117
|
+
import { BrowserGround } from 'nervur/browser';
|
|
118
|
+
|
|
119
|
+
// Every tab of this origin joins one ground, and the tab holding its lock runs it.
|
|
120
|
+
const ground = await BrowserGround.open();
|
|
121
|
+
|
|
122
|
+
// The house's code is a module on this origin, and its rows live in IndexedDB.
|
|
123
|
+
await ground.hand({
|
|
124
|
+
faculty: 'houses',
|
|
125
|
+
method: 'add',
|
|
126
|
+
args: { name: 'main', memory: { body: 'indexeddb' }, classes: { body: 'origin', at: '/house/index.js' } },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
console.log(await ground.hand({ house: 'main', method: 'hello', args: { name: 'Ada' } }));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Every tab of the origin reaches the one ground, and when the tab running
|
|
133
|
+
it closes, the next opens it from the same storage. Its seeds are sealed
|
|
134
|
+
under a key the browser never hands out. Any script on the origin can
|
|
135
|
+
use them, so give the ground an origin of its own, with a strict content
|
|
136
|
+
security policy. The page and the house's module must share one copy of
|
|
137
|
+
`nervur/being`, as a bundler's shared chunk gives.
|
|
138
|
+
|
|
139
|
+
An app on a phone opens the same ground with `AppGround` from
|
|
140
|
+
`nervur/app`, on the Keychain or the Keystore and a store the system
|
|
141
|
+
keeps. `AUTHORING.md` shows both.
|
|
142
|
+
|
|
110
143
|
## Entries
|
|
111
144
|
|
|
112
145
|
| Entry | For |
|
|
@@ -114,6 +147,9 @@ you alone. Keep that folder secret: a house opens on no other seed.
|
|
|
114
147
|
| `nervur/being` | writing a being: `Being`, `s`, `need`, `Args`, `Result` |
|
|
115
148
|
| `nervur` | any engine: `Ground`, `House` and the bodies they take |
|
|
116
149
|
| `nervur/node` | a ground on Node: `NodeGround`, the `nervur` command, and its bodies |
|
|
150
|
+
| `nervur/browser` | a ground in a page or its service worker: `BrowserGround` and its bodies |
|
|
151
|
+
| `nervur/app` | a ground in a phone's app: `AppGround`, and the two interfaces its shell fills |
|
|
152
|
+
| `nervur/serve` | a faculty's program in JavaScript: `serve` |
|
|
117
153
|
| `nervur/bench` | tests: `Bench`, `BenchGround`, `FakeNetwork` and the fakes of every body |
|
|
118
154
|
|
|
119
155
|
## License
|
|
@@ -40,6 +40,8 @@ export class BrowserGround {
|
|
|
40
40
|
// Asks sent to another page's ground, by id, with the page they went to.
|
|
41
41
|
#pending = new Map();
|
|
42
42
|
#ground;
|
|
43
|
+
// The custody and the memories its boot opened, each closed when it closes.
|
|
44
|
+
#opened = [];
|
|
43
45
|
#persisted = false;
|
|
44
46
|
#leader;
|
|
45
47
|
#waiting = [];
|
|
@@ -178,8 +180,13 @@ export class BrowserGround {
|
|
|
178
180
|
const name = this.#name;
|
|
179
181
|
const platform = this.#platform;
|
|
180
182
|
const recipe = this.#options.recipe ?? {};
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
+
// Every store it opens is kept, so its close lets each go before the lock passes.
|
|
184
|
+
const kept = (opened) => {
|
|
185
|
+
this.#opened.push(opened);
|
|
186
|
+
return opened;
|
|
187
|
+
};
|
|
188
|
+
const custody = kept(await platform.custody(`${name}-custody`));
|
|
189
|
+
const memory = kept(await platform.memory(`${name}-ground`));
|
|
183
190
|
this.#persisted = await platform.persist().catch(() => false);
|
|
184
191
|
// It only dials, so it writes no address into an invitation.
|
|
185
192
|
const web = new WebCarry({ allowPrivate: this.#options.allowPrivate === true });
|
|
@@ -188,7 +195,7 @@ export class BrowserGround {
|
|
|
188
195
|
for (const [faculty, made] of Object.entries(recipe.faculties?.() ?? {}))
|
|
189
196
|
faculties[faculty] = await made;
|
|
190
197
|
const bodies = {
|
|
191
|
-
memory: joined({ indexeddb: ({ house }) => platform.memory(`${name}-house-${house}`) }, recipe.bodies?.memory),
|
|
198
|
+
memory: joined({ indexeddb: async ({ house }) => kept(await platform.memory(`${name}-house-${house}`)) }, recipe.bodies?.memory),
|
|
192
199
|
classes: joined({
|
|
193
200
|
origin: ({ args }) => {
|
|
194
201
|
if (typeof args.at !== 'string')
|
|
@@ -209,6 +216,9 @@ export class BrowserGround {
|
|
|
209
216
|
const ground = this.#ground;
|
|
210
217
|
this.#ground = undefined;
|
|
211
218
|
await ground?.close();
|
|
219
|
+
// Its IndexedDB connections go with it, so the next ground in this page opens on none of its.
|
|
220
|
+
for (const opened of this.#opened.splice(0))
|
|
221
|
+
opened.close?.();
|
|
212
222
|
this.#release?.();
|
|
213
223
|
this.#orphan(() => true);
|
|
214
224
|
for (const resolve of this.#waiting)
|
|
@@ -19,6 +19,8 @@ export declare class LockedCustody implements Custody {
|
|
|
19
19
|
constructor(shelf: Shelf, subtle?: SubtleCrypto);
|
|
20
20
|
/** The custody of the origin, on a shelf in IndexedDB named `name`. */
|
|
21
21
|
static open(name?: string, factory?: IDBFactory): Promise<LockedCustody>;
|
|
22
|
+
/** Its shelf let go, where the shelf holds a connection. */
|
|
23
|
+
close(): void;
|
|
22
24
|
keys({ house }: {
|
|
23
25
|
house: string;
|
|
24
26
|
}): Promise<Keys>;
|
|
@@ -54,6 +54,10 @@ export class LockedCustody {
|
|
|
54
54
|
static async open(name = 'nervur-custody', factory = indexedDB) {
|
|
55
55
|
return new LockedCustody(await IndexedDbShelf.open(name, factory));
|
|
56
56
|
}
|
|
57
|
+
/** Its shelf let go, where the shelf holds a connection. */
|
|
58
|
+
close() {
|
|
59
|
+
this.#shelf.close?.();
|
|
60
|
+
}
|
|
57
61
|
keys({ house }) {
|
|
58
62
|
if (!NAME.test(house))
|
|
59
63
|
return Promise.reject(new TypeError(`no house is named ${house}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.22.2-
|
|
3
|
+
"version": "0.22.2-4",
|
|
4
4
|
"description": "Nervur's kit of Quo: write beings and faculties, and open the house that holds them on any ground.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
70
70
|
"prepack": "npm run build",
|
|
71
71
|
"check": "tsc -p tsconfig.json && tsc -p tsconfig.pure.json && node test.mjs \"test/**/*.test.ts\"",
|
|
72
|
-
"deep": "tsc -p
|
|
72
|
+
"deep": "tsc -p deep && NERVUR_TEST_CAP=900000 node test.mjs \"deep/**/*.test.ts\"",
|
|
73
73
|
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
74
74
|
},
|
|
75
75
|
"engines": {
|