nervur 0.19.1 → 0.19.2

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.
@@ -53,7 +53,16 @@ export class Lent extends Being {
53
53
  return bp;
54
54
  return { ...bp, asks: [...bp.asks, LENT_ASKS.offer, LENT_ASKS.retract] };
55
55
  }
56
+ // An offer is open until it is taken or retracted. The taker's knock is the
57
+ // first word she hears from the occupant she minted for it, so hearing
58
+ // from that id closes the offer, and `open` holds only offers still
59
+ // waiting and not every lend she ever made.
56
60
  async answer(asker, method, args = {}) {
61
+ const open = this.cells.open;
62
+ if (open && asker.id !== undefined && asker.id !== BOX)
63
+ for (const [heir, id] of Object.entries(open))
64
+ if (id === asker.id)
65
+ delete open[heir];
57
66
  if (asker.id === BOX && method === 'offer')
58
67
  return this.offer();
59
68
  if (asker.id === BOX && method === 'retract')
@@ -22,7 +22,6 @@ export function conformStore(label, make, { test }) {
22
22
  assert.deepEqual(await s.list(), []);
23
23
  assert.equal(await s.load('main'), undefined);
24
24
  assert.equal(await s.take('main'), undefined);
25
- assert.deepEqual(await s.hints(), {});
26
25
  });
27
26
  t('put, then load: seed, partition and record come back as they went in', async () => {
28
27
  const s = await make();
@@ -111,13 +110,4 @@ export function conformStore(label, make, { test }) {
111
110
  await s.put('main', kept(9));
112
111
  assert.deepEqual((await s.load('main')).seed, seed(9));
113
112
  });
114
- t('hints are kept by pk and the last one wins', async () => {
115
- const s = await make();
116
- const pk = 'cd'.repeat(64);
117
- await s.hint(pk, 'https://quo.acme.com/quo');
118
- await s.hint('ef'.repeat(64), 'http://127.0.0.1:1/quo');
119
- assert.deepEqual(await s.hints(), { [pk]: 'https://quo.acme.com/quo', ['ef'.repeat(64)]: 'http://127.0.0.1:1/quo' });
120
- await s.hint(pk, 'https://quo.other.com/quo');
121
- assert.equal((await s.hints())[pk], 'https://quo.other.com/quo');
122
- });
123
113
  }
@@ -39,10 +39,12 @@ export declare class Harbor {
39
39
  protected boxFor(name: string, _record: WardRecord): Promise<Invitation | undefined>;
40
40
  boot(): Promise<void>;
41
41
  partitionOf(name: string): Record<string, unknown> | undefined;
42
+ quiet(): Promise<void>;
42
43
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
43
44
  deliver(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
44
45
  hint(pk: string, url: string): void;
45
- remember(pk: string, url: string): Promise<void>;
46
+ learn(pk: string, url: string): Promise<boolean>;
47
+ forget(pk: string): void;
46
48
  prove(pk: string, reach: Reach): Promise<boolean>;
47
49
  bind(pks: string[], reach: Reach, held: boolean): Promise<void>;
48
50
  unbind(reach: Reach): void;
@@ -12,8 +12,10 @@
12
12
  // doors; a socket a dialer holds to it, bound once the door behind the
13
13
  // dialer's claim has proved it holds the key, and unbound at close; the
14
14
  // claims of a listener this harbor dialed, proved the same way and reached
15
- // through that line; and a hint, a pk at a URL, kept in the store. A hint
16
- // never displaces a reach proved at a door. And one fallback: a dialer with nothing in its
15
+ // through that line; and a hint, a pk at a URL. A hint never displaces a
16
+ // reach proved at a door. The directory lives in memory and is kept by no
17
+ // store: whoever learns a hint keeps it where it keeps everything, in a
18
+ // being's cells, and hands it back at every boot. And one fallback: a dialer with nothing in its
17
19
  // directory for a pk sends down the socket it holds, because the listener
18
20
  // it dialed is the rendezvous and may hold that pk on another socket. Bytes
19
21
  // that arrive from the wire go to an own door or a held socket and never
@@ -40,6 +42,9 @@ const PROVING = 8;
40
42
  // How many times each reach has announced: a proof from an older announce
41
43
  // binds nothing when it lands.
42
44
  const announces = new WeakMap();
45
+ // The reaches that are hints, so a hint can be replaced and put down without
46
+ // touching a reach a door proved.
47
+ const hinted = new WeakSet();
43
48
  export class Harbor {
44
49
  store;
45
50
  loader;
@@ -92,9 +97,9 @@ export class Harbor {
92
97
  boxFor(name, _record) {
93
98
  return this.offering?.(name) ?? Promise.resolve(undefined);
94
99
  }
95
- // Boot every ward the store keeps, and learn the hints. One ward that will
96
- // not host, a partition this kit cannot read or a loader that throws, is
97
- // named in `unbooted` and takes nobody else down with it.
100
+ // Boot every ward the store keeps. One ward that will not host, a
101
+ // partition this kit cannot read or a loader that throws, is named in
102
+ // `unbooted` and takes nobody else down with it.
98
103
  async boot() {
99
104
  for (const name of await this.store.list()) {
100
105
  // A ward this harbor already hosts is not booted twice. A terrain that
@@ -111,8 +116,6 @@ export class Harbor {
111
116
  this.unbooted.push(name);
112
117
  }
113
118
  }
114
- for (const [pk, url] of Object.entries(await this.store.hints()))
115
- this.hint(pk, url);
116
119
  }
117
120
  // The object a ward writes into, by the name it is kept under: the
118
121
  // partition the harbor was handed at boot and keeps so it can save it.
@@ -126,6 +129,19 @@ export class Harbor {
126
129
  return this.#saving.get(name)?.memory;
127
130
  }
128
131
  // ---- saving
132
+ // Every ward's writes kept and none waiting: no row named, no save armed
133
+ // and none in flight. A write can name a row while another ward is being
134
+ // saved, so this goes round until a pass finds nothing. A process about to
135
+ // exit waits here, since its exit is not a turn the timer is given.
136
+ async quiet() {
137
+ for (;;) {
138
+ const busy = [...this.#saving].filter(([, s]) => !s.gone && (s.dirty.size > 0 || s.timer !== undefined || s.running !== null));
139
+ if (busy.length === 0)
140
+ return;
141
+ for (const [name, s] of busy)
142
+ await (s.running ?? this.#flush(name));
143
+ }
144
+ }
129
145
  // The ward wrote one row. Its partition is saved once the line is free, and
130
146
  // once for every burst of writes, with every row named in that burst.
131
147
  #wrote(name, row) {
@@ -362,14 +378,38 @@ export class Harbor {
362
378
  const bound = this.reaches.get(pk);
363
379
  return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
364
380
  }
381
+ // A hint, taken on the word of whoever gives it. A hint already standing
382
+ // for the pk is replaced, since a URL is the one part of a ward that moves;
383
+ // a reach proven at a door is never displaced by one.
365
384
  hint(pk, url) {
366
- if (this.reaches.has(pk))
385
+ const standing = this.reaches.get(pk);
386
+ if (standing && !hinted.has(standing.reach))
367
387
  return; // a reach proven at a door beats a hint, held or reached through a dialed line
368
- this.reaches.set(pk, { reach: request(url), held: false });
388
+ const reach = request(url);
389
+ hinted.add(reach);
390
+ this.reaches.set(pk, { reach, held: false });
369
391
  }
370
- async remember(pk, url) {
392
+ // A hint kept only once the door behind it has proven it holds the pk's
393
+ // key, the same probe an announce is proven by. A URL that answers for
394
+ // another ward, or for nobody, is refused and nothing is kept: whoever
395
+ // hands a harbor a hint could otherwise point one ward's asks at a place
396
+ // that drops them. True is kept.
397
+ async learn(pk, url) {
398
+ const standing = this.reaches.get(pk);
399
+ if (standing && !hinted.has(standing.reach))
400
+ return true; // already reached, and proven at a door
401
+ const reach = request(url);
402
+ if (!(await this.prove(pk, reach)))
403
+ return false;
371
404
  this.hint(pk, url);
372
- await this.store.hint(pk, url);
405
+ return true;
406
+ }
407
+ // A hint put down. A reach proven at a door is the line's, and goes when
408
+ // the line does, never by a word from here.
409
+ forget(pk) {
410
+ const standing = this.reaches.get(pk);
411
+ if (standing && hinted.has(standing.reach))
412
+ this.reaches.delete(pk);
373
413
  }
374
414
  // An announce is a claim, and a claim binds nothing until proven: anyone
375
415
  // who can reach a listener could otherwise name a pk that is not theirs and
@@ -15,8 +15,6 @@ export type Store = {
15
15
  save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
16
16
  record(name: string, record: WardRecord): Promise<void>;
17
17
  take(name: string): Promise<Kept | undefined>;
18
- hints(): Promise<Record<string, string>>;
19
- hint(pk: string, url: string): Promise<void>;
20
18
  };
21
19
  export declare const values: (p: Record<string, unknown>) => Record<string, unknown>;
22
20
  export declare const rowsOf: (partition: Record<string, unknown>, rows: readonly string[]) => [string, Record<string, unknown> | undefined][];
@@ -27,14 +25,11 @@ type Row = {
27
25
  };
28
26
  export declare class MemoryStore implements Store {
29
27
  readonly rows: Map<string, Row>;
30
- readonly reach: Map<string, string>;
31
28
  list(): Promise<string[]>;
32
29
  load(name: string): Promise<Kept | undefined>;
33
30
  put(name: string, kept: Kept): Promise<void>;
34
31
  save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
35
32
  record(name: string, record: WardRecord): Promise<void>;
36
33
  take(name: string): Promise<Kept | undefined>;
37
- hints(): Promise<Record<string, string>>;
38
- hint(pk: string, url: string): Promise<void>;
39
34
  }
40
35
  export {};
@@ -2,8 +2,7 @@
2
2
  // The store: keeping wards by name. A ward is three things and the store
3
3
  // keeps all three under one name: the seed, the partition and the ward
4
4
  // record, which says where the class bodies come from and which being is
5
- // the user's. Plus the directory's hints, which are the harbor's and
6
- // survive a restart. The store reads nothing it keeps; the partition is
5
+ // the user's. The store reads nothing it keeps; the partition is
7
6
  // values the ward wrote, and the seed is bytes only the ward derives from.
8
7
  // Every terrain has one: files on a disk, IndexedDB in a tab, an object's
9
8
  // storage on the edge; the memory store below is the library's own, for a
@@ -31,7 +30,6 @@ const keep = (row, partition, rows) => {
31
30
  };
32
31
  export class MemoryStore {
33
32
  rows = new Map();
34
- reach = new Map();
35
33
  async list() {
36
34
  return [...this.rows.keys()];
37
35
  }
@@ -61,10 +59,4 @@ export class MemoryStore {
61
59
  this.rows.delete(name);
62
60
  return kept;
63
61
  }
64
- async hints() {
65
- return Object.fromEntries(this.reach);
66
- }
67
- async hint(pk, url) {
68
- this.reach.set(pk, url);
69
- }
70
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
5
5
  "keywords": [
6
6
  "quo",
package/src/being/lent.ts CHANGED
@@ -58,7 +58,13 @@ export class Lent extends Being {
58
58
  return { ...bp, asks: [...bp.asks, LENT_ASKS.offer, LENT_ASKS.retract] };
59
59
  }
60
60
 
61
+ // An offer is open until it is taken or retracted. The taker's knock is the
62
+ // first word she hears from the occupant she minted for it, so hearing
63
+ // from that id closes the offer, and `open` holds only offers still
64
+ // waiting and not every lend she ever made.
61
65
  override async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
66
+ const open = this.cells.open as Record<string, string> | undefined;
67
+ if (open && asker.id !== undefined && asker.id !== BOX) for (const [heir, id] of Object.entries(open)) if (id === asker.id) delete open[heir];
62
68
  if (asker.id === BOX && method === 'offer') return this.offer();
63
69
  if (asker.id === BOX && method === 'retract') return this.retract(args);
64
70
  return super.answer(asker, method, args);
@@ -27,7 +27,6 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
27
27
  assert.deepEqual(await s.list(), []);
28
28
  assert.equal(await s.load('main'), undefined);
29
29
  assert.equal(await s.take('main'), undefined);
30
- assert.deepEqual(await s.hints(), {});
31
30
  });
32
31
 
33
32
  t('put, then load: seed, partition and record come back as they went in', async () => {
@@ -123,14 +122,4 @@ export function conformStore(label: string, make: () => Promise<Store>, { test }
123
122
  await s.put('main', kept(9));
124
123
  assert.deepEqual((await s.load('main'))!.seed, seed(9));
125
124
  });
126
-
127
- t('hints are kept by pk and the last one wins', async () => {
128
- const s = await make();
129
- const pk = 'cd'.repeat(64);
130
- await s.hint(pk, 'https://quo.acme.com/quo');
131
- await s.hint('ef'.repeat(64), 'http://127.0.0.1:1/quo');
132
- assert.deepEqual(await s.hints(), { [pk]: 'https://quo.acme.com/quo', ['ef'.repeat(64)]: 'http://127.0.0.1:1/quo' });
133
- await s.hint(pk, 'https://quo.other.com/quo');
134
- assert.equal((await s.hints())[pk], 'https://quo.other.com/quo');
135
- });
136
125
  }
@@ -12,8 +12,10 @@
12
12
  // doors; a socket a dialer holds to it, bound once the door behind the
13
13
  // dialer's claim has proved it holds the key, and unbound at close; the
14
14
  // claims of a listener this harbor dialed, proved the same way and reached
15
- // through that line; and a hint, a pk at a URL, kept in the store. A hint
16
- // never displaces a reach proved at a door. And one fallback: a dialer with nothing in its
15
+ // through that line; and a hint, a pk at a URL. A hint never displaces a
16
+ // reach proved at a door. The directory lives in memory and is kept by no
17
+ // store: whoever learns a hint keeps it where it keeps everything, in a
18
+ // being's cells, and hands it back at every boot. And one fallback: a dialer with nothing in its
17
19
  // directory for a pk sends down the socket it holds, because the listener
18
20
  // it dialed is the rendezvous and may hold that pk on another socket. Bytes
19
21
  // that arrive from the wire go to an own door or a held socket and never
@@ -110,6 +112,9 @@ const PROVING = 8;
110
112
  // How many times each reach has announced: a proof from an older announce
111
113
  // binds nothing when it lands.
112
114
  const announces = new WeakMap<Reach, number>();
115
+ // The reaches that are hints, so a hint can be replaced and put down without
116
+ // touching a reach a door proved.
117
+ const hinted = new WeakSet<Reach>();
113
118
 
114
119
  export class Harbor {
115
120
  readonly store: Store;
@@ -166,9 +171,9 @@ export class Harbor {
166
171
  return this.offering?.(name) ?? Promise.resolve(undefined);
167
172
  }
168
173
 
169
- // Boot every ward the store keeps, and learn the hints. One ward that will
170
- // not host, a partition this kit cannot read or a loader that throws, is
171
- // named in `unbooted` and takes nobody else down with it.
174
+ // Boot every ward the store keeps. One ward that will not host, a
175
+ // partition this kit cannot read or a loader that throws, is named in
176
+ // `unbooted` and takes nobody else down with it.
172
177
  async boot(): Promise<void> {
173
178
  for (const name of await this.store.list()) {
174
179
  // A ward this harbor already hosts is not booted twice. A terrain that
@@ -182,7 +187,6 @@ export class Harbor {
182
187
  this.unbooted.push(name);
183
188
  }
184
189
  }
185
- for (const [pk, url] of Object.entries(await this.store.hints())) this.hint(pk, url);
186
190
  }
187
191
 
188
192
  // The object a ward writes into, by the name it is kept under: the
@@ -199,6 +203,18 @@ export class Harbor {
199
203
 
200
204
  // ---- saving
201
205
 
206
+ // Every ward's writes kept and none waiting: no row named, no save armed
207
+ // and none in flight. A write can name a row while another ward is being
208
+ // saved, so this goes round until a pass finds nothing. A process about to
209
+ // exit waits here, since its exit is not a turn the timer is given.
210
+ async quiet(): Promise<void> {
211
+ for (;;) {
212
+ const busy = [...this.#saving].filter(([, s]) => !s.gone && (s.dirty.size > 0 || s.timer !== undefined || s.running !== null));
213
+ if (busy.length === 0) return;
214
+ for (const [name, s] of busy) await (s.running ?? this.#flush(name));
215
+ }
216
+ }
217
+
202
218
  // The ward wrote one row. Its partition is saved once the line is free, and
203
219
  // once for every burst of writes, with every row named in that burst.
204
220
  #wrote(name: string, row: string): void {
@@ -418,14 +434,36 @@ export class Harbor {
418
434
  return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
419
435
  }
420
436
 
437
+ // A hint, taken on the word of whoever gives it. A hint already standing
438
+ // for the pk is replaced, since a URL is the one part of a ward that moves;
439
+ // a reach proven at a door is never displaced by one.
421
440
  hint(pk: string, url: string): void {
422
- if (this.reaches.has(pk)) return; // a reach proven at a door beats a hint, held or reached through a dialed line
423
- this.reaches.set(pk, { reach: request(url), held: false });
441
+ const standing = this.reaches.get(pk);
442
+ if (standing && !hinted.has(standing.reach)) return; // a reach proven at a door beats a hint, held or reached through a dialed line
443
+ const reach = request(url);
444
+ hinted.add(reach);
445
+ this.reaches.set(pk, { reach, held: false });
424
446
  }
425
447
 
426
- async remember(pk: string, url: string): Promise<void> {
448
+ // A hint kept only once the door behind it has proven it holds the pk's
449
+ // key, the same probe an announce is proven by. A URL that answers for
450
+ // another ward, or for nobody, is refused and nothing is kept: whoever
451
+ // hands a harbor a hint could otherwise point one ward's asks at a place
452
+ // that drops them. True is kept.
453
+ async learn(pk: string, url: string): Promise<boolean> {
454
+ const standing = this.reaches.get(pk);
455
+ if (standing && !hinted.has(standing.reach)) return true; // already reached, and proven at a door
456
+ const reach = request(url);
457
+ if (!(await this.prove(pk, reach))) return false;
427
458
  this.hint(pk, url);
428
- await this.store.hint(pk, url);
459
+ return true;
460
+ }
461
+
462
+ // A hint put down. A reach proven at a door is the line's, and goes when
463
+ // the line does, never by a word from here.
464
+ forget(pk: string): void {
465
+ const standing = this.reaches.get(pk);
466
+ if (standing && hinted.has(standing.reach)) this.reaches.delete(pk);
429
467
  }
430
468
 
431
469
  // An announce is a claim, and a claim binds nothing until proven: anyone
@@ -2,8 +2,7 @@
2
2
  // The store: keeping wards by name. A ward is three things and the store
3
3
  // keeps all three under one name: the seed, the partition and the ward
4
4
  // record, which says where the class bodies come from and which being is
5
- // the user's. Plus the directory's hints, which are the harbor's and
6
- // survive a restart. The store reads nothing it keeps; the partition is
5
+ // the user's. The store reads nothing it keeps; the partition is
7
6
  // values the ward wrote, and the seed is bytes only the ward derives from.
8
7
  // Every terrain has one: files on a disk, IndexedDB in a tab, an object's
9
8
  // storage on the edge; the memory store below is the library's own, for a
@@ -32,8 +31,6 @@ export type Store = {
32
31
  record(name: string, record: WardRecord): Promise<void>;
33
32
  // Take a ward out: the first move of a migration. What was kept comes back.
34
33
  take(name: string): Promise<Kept | undefined>;
35
- hints(): Promise<Record<string, string>>;
36
- hint(pk: string, url: string): Promise<void>;
37
34
  };
38
35
 
39
36
  // The partition is values only, and the ward hands it out through a guard,
@@ -64,7 +61,6 @@ type Row = { seed: Uint8Array; parts: Map<string, Record<string, unknown>>; reco
64
61
 
65
62
  export class MemoryStore implements Store {
66
63
  readonly rows = new Map<string, Row>();
67
- readonly reach = new Map<string, string>();
68
64
 
69
65
  async list(): Promise<string[]> {
70
66
  return [...this.rows.keys()];
@@ -92,10 +88,4 @@ export class MemoryStore implements Store {
92
88
  this.rows.delete(name);
93
89
  return kept;
94
90
  }
95
- async hints(): Promise<Record<string, string>> {
96
- return Object.fromEntries(this.reach);
97
- }
98
- async hint(pk: string, url: string): Promise<void> {
99
- this.reach.set(pk, url);
100
- }
101
91
  }