@nervur-org/dock 0.5.0 → 0.6.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 +7 -4
- package/api/route.ts +4 -72
- package/beings/avatar.ts +11 -7
- package/beings/carry.ts +129 -116
- package/beings/clock.ts +11 -7
- package/beings/desk.ts +7 -6
- package/beings/doorbell.ts +36 -7
- package/beings/index.ts +17 -4
- package/beings/join.ts +7 -4
- package/beings/link.ts +1 -1
- package/beings/porter.ts +5 -1
- package/beings/presence.ts +15 -5
- package/beings/setup.ts +27 -21
- package/beings/user.ts +26 -17
- package/beings/way.ts +1 -1
- package/{dock → box}/agents.ts +37 -44
- package/{dock → box}/apns.ts +4 -4
- package/box/box.ts +31 -0
- package/box/contract.ts +41 -0
- package/box/dialers.ts +88 -0
- package/box/dock.ts +148 -0
- package/box/envoy.ts +96 -0
- package/box/faculty.ts +111 -0
- package/{dock → box}/fcm.ts +1 -1
- package/box/grants.ts +90 -0
- package/box/http.ts +93 -0
- package/{dock → box}/http2.ts +7 -7
- package/box/index.ts +201 -0
- package/box/join.ts +78 -0
- package/box/local.ts +37 -0
- package/{dock → box}/location.ts +15 -3
- package/{dock → box}/notifications.ts +17 -5
- package/box/push.ts +85 -0
- package/box/ring.ts +85 -0
- package/{dock → box}/service.ts +2 -2
- package/box/timer.ts +150 -0
- package/box/tree.ts +22 -0
- package/{dock → box}/webpush.ts +2 -2
- package/cli/client.ts +1 -1
- package/cli/daemon.ts +101 -103
- package/cli/estate.ts +2 -3
- package/cli/nervur.ts +44 -25
- package/cli/stand.ts +1 -1
- package/dist/api/route.d.ts +1 -2
- package/dist/api/route.js +4 -76
- package/dist/beings/avatar.d.ts +4 -3
- package/dist/beings/avatar.js +9 -6
- package/dist/beings/carry.d.ts +10 -12
- package/dist/beings/carry.js +119 -127
- package/dist/beings/clock.js +10 -7
- package/dist/beings/desk.js +8 -7
- package/dist/beings/doorbell.d.ts +3 -1
- package/dist/beings/doorbell.js +37 -4
- package/dist/beings/index.d.ts +24 -4
- package/dist/beings/index.js +15 -4
- package/dist/beings/join.d.ts +1 -0
- package/dist/beings/join.js +6 -4
- package/dist/beings/link.d.ts +0 -1
- package/dist/beings/link.js +1 -1
- package/dist/beings/porter.js +5 -1
- package/dist/beings/presence.d.ts +1 -0
- package/dist/beings/presence.js +12 -3
- package/dist/beings/setup.d.ts +5 -2
- package/dist/beings/setup.js +16 -15
- package/dist/beings/user.d.ts +4 -3
- package/dist/beings/user.js +18 -11
- package/dist/{dock → box}/agents.d.ts +7 -4
- package/dist/{dock → box}/agents.js +30 -37
- package/dist/{dock → box}/apns.d.ts +1 -1
- package/dist/{dock → box}/apns.js +3 -3
- package/dist/box/box.d.ts +9 -0
- package/dist/box/box.js +18 -0
- package/dist/box/contract.d.ts +6 -0
- package/dist/box/contract.js +35 -0
- package/dist/{dock → box}/dialers.d.ts +7 -4
- package/dist/box/dialers.js +76 -0
- package/dist/box/dock.d.ts +182 -0
- package/dist/box/dock.js +128 -0
- package/dist/box/envoy.d.ts +44 -0
- package/dist/box/envoy.js +75 -0
- package/dist/box/faculty.d.ts +68 -0
- package/dist/box/faculty.js +97 -0
- package/dist/{dock → box}/fcm.d.ts +1 -1
- package/dist/{dock → box}/grants.d.ts +4 -3
- package/dist/box/grants.js +78 -0
- package/dist/box/http.d.ts +26 -0
- package/dist/box/http.js +89 -0
- package/dist/{dock → box}/http2.d.ts +1 -1
- package/dist/{dock → box}/http2.js +6 -6
- package/dist/box/index.d.ts +46 -0
- package/dist/box/index.js +179 -0
- package/dist/box/join.d.ts +19 -0
- package/dist/box/join.js +67 -0
- package/dist/{dock → box}/local.d.ts +3 -2
- package/dist/box/local.js +27 -0
- package/dist/{dock → box}/location.d.ts +3 -0
- package/dist/box/location.js +21 -0
- package/dist/{dock → box}/notifications.d.ts +3 -0
- package/dist/{dock → box}/notifications.js +14 -4
- package/dist/{dock → box}/push.d.ts +12 -18
- package/dist/box/push.js +83 -0
- package/dist/box/ring.d.ts +18 -0
- package/dist/box/ring.js +55 -0
- package/dist/{dock → box}/service.d.ts +1 -1
- package/dist/{dock → box}/service.js +1 -1
- package/dist/{dock → box}/timer.d.ts +2 -0
- package/dist/box/timer.js +127 -0
- package/dist/box/tree.d.ts +5 -0
- package/dist/box/tree.js +14 -0
- package/dist/{dock → box}/webpush.d.ts +1 -1
- package/dist/cli/client.d.ts +1 -0
- package/dist/cli/daemon.d.ts +2 -8
- package/dist/cli/daemon.js +102 -107
- package/dist/cli/estate.js +2 -3
- package/dist/cli/nervur.js +44 -24
- package/dist/cli/stand.d.ts +0 -14
- package/dist/cli/stand.js +1 -1
- package/dist/harbor/browser.d.ts +8 -5
- package/dist/harbor/browser.js +20 -21
- package/dist/harbor/capacitor.d.ts +3 -3
- package/dist/harbor/capacitor.js +9 -8
- package/dist/harbor/disk.d.ts +9 -9
- package/dist/harbor/disk.js +56 -51
- package/dist/harbor/edge/alarm.d.ts +4 -2
- package/dist/harbor/edge/alarm.js +9 -19
- package/dist/harbor/edge/edge.d.ts +7 -6
- package/dist/harbor/edge/edge.js +58 -53
- package/dist/harbor/edge/exercise.js +4 -4
- package/dist/harbor/edge/given.d.ts +0 -1
- package/dist/harbor/edge/given.js +0 -29
- package/dist/harbor/edge/storage.d.ts +0 -2
- package/dist/harbor/edge/storage.js +2 -2
- package/dist/harbor/http.js +4 -2
- package/dist/harbor/sealed.d.ts +0 -5
- package/dist/harbor/sealed.js +0 -39
- package/dist/harbor/tauri.d.ts +2 -2
- package/dist/harbor/tauri.js +13 -10
- package/dist/human/app.d.ts +1 -1
- package/dist/human/app.js +10 -12
- package/dist/human/door.d.ts +0 -1
- package/dist/human/door.js +1 -1
- package/dist/human/html.js +1 -1
- package/dist/human/move.js +1 -1
- package/dist/human/tab.bundle.txt +3629 -2824
- package/dist/human/wake.d.ts +5 -2
- package/dist/human/wake.js +23 -14
- package/dist/human/web.d.ts +0 -1
- package/dist/human/web.js +3 -3
- package/dist/human/worker.bundle.txt +2945 -2118
- package/dist/human/worlds.js +3 -3
- package/dist/mcp/agent.d.ts +0 -1
- package/dist/mcp/agent.js +1 -1
- package/dist/mcp/allow.js +3 -2
- package/dist/mcp/http.d.ts +0 -1
- package/dist/mcp/http.js +1 -1
- package/dist/mcp/oauth.d.ts +3 -4
- package/dist/mcp/oauth.js +4 -4
- package/dist/mcp/route.d.ts +1 -1
- package/dist/mcp/runner.d.ts +0 -1
- package/dist/mcp/runner.js +1 -1
- package/dist/mcp/server.d.ts +0 -1
- package/dist/mcp/server.js +1 -1
- package/harbor/browser.ts +22 -24
- package/harbor/capacitor.ts +9 -8
- package/harbor/disk.ts +53 -48
- package/harbor/edge/alarm.ts +15 -12
- package/harbor/edge/edge.ts +61 -55
- package/harbor/edge/exercise.ts +4 -4
- package/harbor/edge/given.ts +3 -33
- package/harbor/edge/storage.ts +2 -2
- package/harbor/http.ts +5 -3
- package/harbor/sealed.ts +0 -35
- package/harbor/tauri.ts +13 -10
- package/human/app.ts +12 -13
- package/human/door.ts +1 -1
- package/human/html.ts +1 -1
- package/human/local.ts +1 -1
- package/human/move.ts +1 -1
- package/human/tab.bundle.txt +3629 -2824
- package/human/wake.ts +36 -16
- package/human/web.ts +4 -4
- package/human/worker.bundle.txt +2945 -2118
- package/human/worlds.ts +3 -3
- package/mcp/agent.ts +1 -1
- package/mcp/allow.ts +2 -1
- package/mcp/http.ts +1 -1
- package/mcp/oauth.ts +4 -4
- package/mcp/route.ts +1 -1
- package/mcp/runner.ts +1 -1
- package/mcp/server.ts +1 -1
- package/package.json +9 -8
- package/beings/envoy.ts +0 -218
- package/dist/beings/envoy.d.ts +0 -54
- package/dist/beings/envoy.js +0 -211
- package/dist/dock/console.d.ts +0 -6
- package/dist/dock/console.js +0 -20
- package/dist/dock/dialers.js +0 -75
- package/dist/dock/faculty.d.ts +0 -24
- package/dist/dock/faculty.js +0 -142
- package/dist/dock/grants.js +0 -99
- package/dist/dock/index.d.ts +0 -41
- package/dist/dock/index.js +0 -113
- package/dist/dock/local.js +0 -44
- package/dist/dock/location.js +0 -11
- package/dist/dock/push.js +0 -132
- package/dist/dock/timer.js +0 -149
- package/dock/console.ts +0 -23
- package/dock/dialers.ts +0 -85
- package/dock/faculty.ts +0 -148
- package/dock/grants.ts +0 -111
- package/dock/index.ts +0 -133
- package/dock/local.ts +0 -48
- package/dock/push.ts +0 -161
- package/dock/timer.ts +0 -174
- /package/dist/{dock → box}/fcm.js +0 -0
- /package/dist/{dock → box}/webpush.js +0 -0
package/beings/index.ts
CHANGED
|
@@ -4,13 +4,26 @@ export { User, DESK, HOME, BELL } from './user.ts';
|
|
|
4
4
|
export { Desk } from './desk.ts';
|
|
5
5
|
export { WAY, kindOf, verified, VERIFY, SEND, type Proof, type Verified } from './way.ts';
|
|
6
6
|
export { Avatar, USER, PUSHER } from './avatar.ts';
|
|
7
|
-
export {
|
|
7
|
+
export { carry, blueprint, CARRIES, type Carrying } from './carry.ts';
|
|
8
8
|
export { Presence } from './presence.ts';
|
|
9
9
|
export { Courier, MOVER, isMoving, type Moving } from './courier.ts';
|
|
10
10
|
export { Porter } from './porter.ts';
|
|
11
11
|
export { Doorbell } from './doorbell.ts';
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export { webPush, mintVapid, publicOf, apnsPush, pkcs8, HOSTS, fcmPush, isAccount, DOORS, handedTo, ringing, routed, type Vapid, type Apns, type Account, type Doors } from '../
|
|
12
|
+
export { BOX, BOX_SEED, Dock, LOCAL, LOCATION, NOTIFICATIONS, PUSH, TIMER, Faculty, Local, Location, Notifications, Push, Timer, contract, covers, nameOf, titleOf, keyed, join, settled, stand, standHarbor, classes, lent, type Contract, type Tree, type Where, type Watched, type Rooted } from '../box/index.ts';
|
|
13
|
+
export { isToken, type Ring, type Token } from '../box/index.ts';
|
|
14
|
+
export { webPush, mintVapid, publicOf, apnsPush, pkcs8, HOSTS, fcmPush, isAccount, DOORS, handedTo, ringing, routed, type Vapid, type Apns, type Account, type Doors } from '../box/index.ts';
|
|
15
15
|
export { Clock, CLOCK, setupClock, placeClock, readSchedule, parseCron, nextFire, civil, fromCivil, type Schedule, type Fire, type Cron, type Civil } from './clock.ts';
|
|
16
16
|
export { sanitise, hint, hintFor, groups, type Look, type Hint } from './look.ts';
|
|
17
|
+
|
|
18
|
+
import { User } from './user.ts';
|
|
19
|
+
import { Desk } from './desk.ts';
|
|
20
|
+
import { Avatar } from './avatar.ts';
|
|
21
|
+
import { Presence } from './presence.ts';
|
|
22
|
+
import { Clock } from './clock.ts';
|
|
23
|
+
import { Courier } from './courier.ts';
|
|
24
|
+
import { Porter } from './porter.ts';
|
|
25
|
+
import { Doorbell } from './doorbell.ts';
|
|
26
|
+
|
|
27
|
+
// The beings this folder brings to every harbor, as a tree: a ward names
|
|
28
|
+
// one by where it lives, `beings/User`, as it names a faculty.
|
|
29
|
+
export const BEINGS = { beings: { User, Desk, Avatar, Presence, Clock, Courier, Porter, Doorbell } };
|
package/beings/join.ts
CHANGED
|
@@ -11,14 +11,17 @@ import type { Hosted } from 'nervur/harbor';
|
|
|
11
11
|
import type { Invitation } from 'nervur';
|
|
12
12
|
import type { Avatar } from './avatar.ts';
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// An identity is a word, and never the key of a being the ward holds: the
|
|
15
|
+
// ward is asked, so no route names who its beings are.
|
|
16
|
+
export const isIdentity = (hosted: Hosted, identity: unknown): identity is string => typeof identity === 'string' && /^[\w.-]+$/.test(identity) && !hosted.keys().includes(identity);
|
|
17
|
+
|
|
18
|
+
// Her avatar in a ward, found or booted under the identity's key.
|
|
16
19
|
export async function avatarOf(hosted: Hosted, identity: string): Promise<{ avatar?: Avatar; error?: string }> {
|
|
17
|
-
if (
|
|
20
|
+
if (!isIdentity(hosted, identity)) return { error: 'an identity is a word, and not a being of the ward' };
|
|
18
21
|
const key = `avatar:${identity}`;
|
|
19
22
|
let avatar = hosted.being(key) as Avatar | undefined;
|
|
20
23
|
if (!avatar) {
|
|
21
|
-
const out = (await hosted.ask('boot', { key, class: 'Avatar' })) as { error?: string };
|
|
24
|
+
const out = (await hosted.ask('boot', { key, class: 'beings/Avatar' })) as { error?: string };
|
|
22
25
|
if (out.error) return { error: out.error };
|
|
23
26
|
avatar = hosted.being(key) as Avatar;
|
|
24
27
|
}
|
package/beings/link.ts
CHANGED
|
@@ -17,7 +17,7 @@ export const KEY = 'nervur';
|
|
|
17
17
|
// a query string is in every log and every referer, and one read of it is
|
|
18
18
|
// one way in taken. `<kind>.<token>`, the kind the desk knows it by and the
|
|
19
19
|
// token that door minted.
|
|
20
|
-
|
|
20
|
+
const PROOF = 'proof';
|
|
21
21
|
const HEX = (n: number) => `[0-9a-f]{${n}}`;
|
|
22
22
|
const FULL = new RegExp(`^(${HEX(128)})\\.(${HEX(64)})\\.(${HEX(64)})$`);
|
|
23
23
|
const PUBLIC = new RegExp(`^${HEX(128)}$`);
|
package/beings/porter.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// that a loop in this session stops somewhere a human can see.
|
|
16
16
|
import { Being, isSilence, isWord, wordOf } from 'nervur';
|
|
17
17
|
import type { JsonObject } from 'nervur';
|
|
18
|
+
import { Doorbell } from './doorbell.ts';
|
|
18
19
|
|
|
19
20
|
const ROOM = 64;
|
|
20
21
|
|
|
@@ -28,8 +29,11 @@ export class Porter extends Being {
|
|
|
28
29
|
const minted = typeof this.cells.minted === 'number' ? this.cells.minted : 0;
|
|
29
30
|
if (minted >= ROOM) return { error: 'this bell holds no more doorbells' };
|
|
30
31
|
|
|
32
|
+
// The class says where it lives and the porter reads it off her. A path
|
|
33
|
+
// spelled here would be the doorbell named by whoever boots her, which
|
|
34
|
+
// is a class described by someone who is not it.
|
|
31
35
|
const key = `doorbell:${minted + 1}`;
|
|
32
|
-
const held = await this.boot(
|
|
36
|
+
const held = await this.boot(Doorbell.at, key, key);
|
|
33
37
|
if (held === null) return { error: 'this bell could not make a doorbell' };
|
|
34
38
|
this.cells.minted = minted + 1;
|
|
35
39
|
|
package/beings/presence.ts
CHANGED
|
@@ -15,25 +15,35 @@
|
|
|
15
15
|
// `settled` before it says the box is up.
|
|
16
16
|
import type { Asker, OccupantRecord, Stance } from 'nervur';
|
|
17
17
|
import { Avatar, PUSHER, USER } from './avatar.ts';
|
|
18
|
-
import {
|
|
18
|
+
import { LOCATION } from '../box/location.ts';
|
|
19
|
+
import { NOTIFICATIONS } from '../box/notifications.ts';
|
|
20
|
+
import { PUSH } from '../box/push.ts';
|
|
21
|
+
import { TIMER } from '../box/timer.ts';
|
|
22
|
+
import { join } from '../box/join.ts';
|
|
23
|
+
import { titleOf } from '../box/contract.ts';
|
|
19
24
|
|
|
20
25
|
export class Presence extends Avatar {
|
|
21
26
|
// Her world's user being sees the box, and nobody else: the way back she
|
|
22
27
|
// handed over at the join is the one occupant who may ask through her.
|
|
23
|
-
static
|
|
28
|
+
static carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean {
|
|
24
29
|
return occupant?.id === PUSHER;
|
|
25
30
|
}
|
|
26
31
|
// Her standing at the user being is hers to speak through and never to
|
|
27
32
|
// carry: the user being carries her, so carrying it back would be a
|
|
28
33
|
// loop, and it is the one standing that is not a faculty.
|
|
29
|
-
static
|
|
34
|
+
static skips(id: string): boolean {
|
|
30
35
|
return id === USER;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
//
|
|
38
|
+
// What a device can do that a world may want: the contracts she asks her
|
|
39
|
+
// box for, each held under the word the contract calls itself.
|
|
40
|
+
static needs = [LOCATION, NOTIFICATIONS, PUSH, TIMER];
|
|
41
|
+
|
|
42
|
+
// Which of them the box lent her, by that word, settled after birth.
|
|
34
43
|
readonly lent: Promise<Record<string, string | null>>;
|
|
35
44
|
constructor(stance: Stance) {
|
|
36
45
|
super(stance);
|
|
37
|
-
|
|
46
|
+
const C = this.constructor as typeof Presence;
|
|
47
|
+
this.lent = Promise.all(C.needs.map(async (c) => [titleOf(c), await join(this, c, titleOf(c))] as const)).then((all) => Object.fromEntries(all));
|
|
38
48
|
}
|
|
39
49
|
}
|
package/beings/setup.ts
CHANGED
|
@@ -1,45 +1,51 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The root's setup of a fresh ward, as every terrain does it: the front
|
|
3
|
-
// booted public
|
|
4
|
-
// `user:<name>`, placed by the root's knock, and she holding her
|
|
5
|
-
// door as an owner under `home`. She names the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
2
|
+
// The root's setup of a fresh ward, as every terrain does it: the front
|
|
3
|
+
// booted public under `desk`, one home being, the front holding a standing
|
|
4
|
+
// to her under `user:<name>`, placed by the root's knock, and she holding her
|
|
5
|
+
// own ward's door as an owner under `home`. She names the front `desk`, and
|
|
6
|
+
// that id is the only thing that makes it the front. A disk harbor does this
|
|
7
|
+
// at `nervur init`, the edge at its root route's init; the memory harbor's
|
|
8
|
+
// test world does the same by hand.
|
|
9
9
|
import type { Invitation } from 'nervur';
|
|
10
10
|
import type { Hosted } from 'nervur/harbor';
|
|
11
11
|
import { DESK, HOME } from './user.ts';
|
|
12
12
|
|
|
13
|
+
// The classes setup boots, named where they live. A terrain hands its own:
|
|
14
|
+
// an organisation's ward has an org's home being of the estate's class, and
|
|
15
|
+
// a ward may have an estate's front in place of the dock's.
|
|
16
|
+
export type Setup = { front: string; home: string };
|
|
17
|
+
const SETUP: Setup ={ front: 'beings/Desk', home: 'beings/User' };
|
|
18
|
+
|
|
13
19
|
// The root boots and steps back: it invites an owner on the ward's own pk
|
|
14
|
-
// and the
|
|
20
|
+
// and the home being knocks for it, so from then on placing a being,
|
|
15
21
|
// minting a way in or removing one is an ask on her, shown by her gate to a
|
|
16
22
|
// device that may mint, and the box is not logged into for it. The same
|
|
17
23
|
// standing a user being of another ward takes to own this one; here it is
|
|
18
24
|
// her own.
|
|
19
|
-
|
|
25
|
+
async function setupHome(hosted: Hosted, user: string): Promise<void> {
|
|
20
26
|
const inv = (await hosted.ask('invite', { being: hosted.pk, id: HOME })) as Invitation | { error?: string };
|
|
21
27
|
if ('error' in inv) throw new Error(`home: ${inv.error}`);
|
|
22
28
|
const placed = (await hosted.ask('knock', { being: user, id: HOME, invitation: inv })) as { taken?: string; error?: string };
|
|
23
29
|
if (placed.taken !== HOME) throw new Error(`home: ${placed.error ?? 'she could not take her own door'}`);
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!/^[\w.-]+$/.test(user)
|
|
32
|
+
// Everything setup places is placed the same way whatever the classes are:
|
|
33
|
+
// a home key the front already holds is refused by the ward, as any key
|
|
34
|
+
// taken is.
|
|
35
|
+
export async function setup(hosted: Hosted, user: string, classes: Partial<Setup> = {}): Promise<void> {
|
|
36
|
+
const { front, home } = { ...SETUP, ...classes };
|
|
37
|
+
if (!/^[\w.-]+$/.test(user)) throw new Error(`a user is a word: ${user}`);
|
|
32
38
|
const boot = async (args: Record<string, unknown>) => {
|
|
33
39
|
const out = (await hosted.ask('boot', args)) as { booted?: string; error?: string };
|
|
34
40
|
if (out.error) throw new Error(`init: ${out.error}`);
|
|
35
41
|
};
|
|
36
|
-
await boot({ key:
|
|
37
|
-
const pub = (await hosted.ask('public', { key:
|
|
42
|
+
await boot({ key: DESK, class: front });
|
|
43
|
+
const pub = (await hosted.ask('public', { key: DESK })) as { error?: string };
|
|
38
44
|
if (pub.error) throw new Error(`init: ${pub.error}`);
|
|
39
|
-
await boot({ key: user, class:
|
|
45
|
+
await boot({ key: user, class: home });
|
|
40
46
|
const inv = (await hosted.ask('invite', { being: user, id: DESK })) as Invitation;
|
|
41
|
-
// the
|
|
42
|
-
const placed = (await hosted.ask('knock', { being:
|
|
43
|
-
if (placed.taken !== `user:${user}`) throw new Error('init: the
|
|
47
|
+
// the front's first hello names her, since a being does not know her own key
|
|
48
|
+
const placed = (await hosted.ask('knock', { being: DESK, id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } })) as { taken?: string };
|
|
49
|
+
if (placed.taken !== `user:${user}`) throw new Error('init: the front could not reach the home being');
|
|
44
50
|
await setupHome(hosted, user);
|
|
45
51
|
}
|
package/beings/user.ts
CHANGED
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
// client identity the desk established. A device she let mint brings the
|
|
8
8
|
// next one in, so a human adds a device from a device already in and only
|
|
9
9
|
// the first one has to come from the root, on the box.
|
|
10
|
-
import { isInvitation, isSilence, isWord, wordOf } from 'nervur';
|
|
11
|
-
import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Stance } from 'nervur';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
10
|
+
import { Being, isInvitation, isSilence, isWord, wordOf } from 'nervur';
|
|
11
|
+
import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from 'nervur';
|
|
12
|
+
import { CARRIES, carry } from './carry.ts';
|
|
13
|
+
import { banner, isToken, sealBanner } from '../box/index.ts';
|
|
14
|
+
import { PUSH as PUSHING } from '../box/push.ts';
|
|
15
|
+
import { join } from '../box/join.ts';
|
|
16
|
+
|
|
17
|
+
// Her standing at the box's push faculty, under an id of her own.
|
|
18
|
+
const PUSH = 'push';
|
|
14
19
|
|
|
15
20
|
export const DESK = 'desk';
|
|
16
21
|
// The prefix her doorbell standings are filed under, one per device: the
|
|
@@ -68,13 +73,13 @@ const mayMint = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(oc
|
|
|
68
73
|
// is a choice an org makes rather than one the dock makes for it.
|
|
69
74
|
const mayRevoke = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(occ) && held(occ).revoke);
|
|
70
75
|
|
|
71
|
-
// She carries her standings,
|
|
72
|
-
// human allowed to reach them at the exchange: the note says so. A device
|
|
73
|
-
// without the note sees her own asks alone, and a model sees
|
|
76
|
+
// She carries her standings, her org, the calendar, the house, for a device
|
|
77
|
+
// the human allowed to reach them at the exchange: the note says so. A device
|
|
78
|
+
// without the note sees her own asks alone, and a model sees the org only
|
|
74
79
|
// because the human said it may. A device the root minted reaches: the root
|
|
75
80
|
// is the owner of everything she holds and chose to mint it.
|
|
76
|
-
export class User extends
|
|
77
|
-
static
|
|
81
|
+
export class User extends Being {
|
|
82
|
+
static carries(occ: OccupantRecord | undefined): boolean {
|
|
78
83
|
return isDevice(occ) && held(occ).reach;
|
|
79
84
|
}
|
|
80
85
|
// `ways` holds a device's way back that could not be taken when it was
|
|
@@ -82,20 +87,24 @@ export class User extends Carrier {
|
|
|
82
87
|
// not bound yet, a phone in a tunnel. Nothing delivered spends nothing,
|
|
83
88
|
// so the invitation is tried again at the next push.
|
|
84
89
|
// The bell standing is plumbing and carries nothing: a device that may
|
|
85
|
-
// reach what she holds sees
|
|
90
|
+
// reach what she holds sees the org and the house, and never the envoy that
|
|
86
91
|
// wakes it. Her ways back are plumbing too, and a way back to a presence
|
|
87
92
|
// being carries that device's faculties, `to:phone-location-where`, which
|
|
88
93
|
// is how a model at home learns where the human is.
|
|
89
|
-
static
|
|
90
|
-
return
|
|
94
|
+
static hidden(id: string): boolean {
|
|
95
|
+
return CARRIES.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
|
|
91
96
|
}
|
|
92
97
|
// Her own door is carried to a device that may mint, and to nobody else:
|
|
93
98
|
// `mint` is the word for a device trusted to change who is in her world,
|
|
94
99
|
// and owning the world from that device is that trust said in full. A
|
|
95
|
-
// device that reaches sees
|
|
96
|
-
static
|
|
100
|
+
// device that reaches sees the org and the house, and no `home-boot`.
|
|
101
|
+
static shows(id: string, occ: OccupantRecord | undefined): boolean {
|
|
97
102
|
return id !== HOME || held(occ).mint;
|
|
98
103
|
}
|
|
104
|
+
|
|
105
|
+
override answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
|
|
106
|
+
return carry(this, asker, method, args, (a, m, g) => super.answer(a, m, g));
|
|
107
|
+
}
|
|
99
108
|
static override cells = { name: '', reports: [] as JsonObject[], ways: {} as Record<string, Invitation>, outbox: {} as Record<string, JsonObject[]> };
|
|
100
109
|
|
|
101
110
|
// Her standing at the box's push faculty, taken again at every birth: a
|
|
@@ -111,7 +120,7 @@ export class User extends Carrier {
|
|
|
111
120
|
readonly #push: Promise<string | null>;
|
|
112
121
|
constructor(stance: Stance) {
|
|
113
122
|
super(stance);
|
|
114
|
-
this.#push = join(this, PUSH);
|
|
123
|
+
this.#push = join(this, PUSHING, PUSH);
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
// A world whose own being she is, is a home: a device reaching her holds
|
|
@@ -373,9 +382,9 @@ export class User extends Carrier {
|
|
|
373
382
|
return Object.entries(this.cells.occupants)
|
|
374
383
|
.filter(([id]) => id !== DESK)
|
|
375
384
|
.map(([id, rec]) => {
|
|
376
|
-
const notes =
|
|
385
|
+
const notes = rec.notes;
|
|
377
386
|
const name = typeof notes.client === 'string' ? notes.client : id;
|
|
378
|
-
const has = MINTINGS.filter((w) => held(rec
|
|
387
|
+
const has = MINTINGS.filter((w) => held(rec)[w]);
|
|
379
388
|
const line = [...(name === me ? ['this one'] : []), ...(has.length ? [`may ${has.join(', ')}`] : [])].join(' · ');
|
|
380
389
|
return { name, line, client: name, ...(name === me ? { you: true } : {}), ...(has.length ? { may: has } : {}) };
|
|
381
390
|
});
|
package/beings/way.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Two kinds of holder, and both are ordinary beings:
|
|
9
9
|
//
|
|
10
|
-
// - the box's, lent by the
|
|
10
|
+
// - the box's, lent by the box ward, as `local` is: the daemon vouches for
|
|
11
11
|
// a process it saw on its own socket, and what it knows lives in that
|
|
12
12
|
// being, on the box, dying with it.
|
|
13
13
|
// - the world's own, in the world's ward, as an estate's post is: she holds
|
package/{dock → box}/agents.ts
RENAMED
|
@@ -1,47 +1,30 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The box's agents:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// The box's agents: on an event, run something for an identity, as the timer
|
|
3
|
+
// does on a moment. What she holds is the table of agents in her cells,
|
|
4
|
+
// placed by the root with `keep`. A row names a client identity, the ward
|
|
5
|
+
// she is admitted into, and what runs for her: a process, a command with its
|
|
6
|
+
// args in its folder, or a model at a url.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// on a daemon she admits each identity into its ward as an avatar, through
|
|
13
|
-
// the box's own way in, and stands the side her row says over that avatar.
|
|
14
|
-
// A push from the user being is the event either way: the agent side runs
|
|
15
|
-
// the command with it and reports what it printed; the runner side turns it
|
|
16
|
-
// into a turn of the model's conversation with her describe as tools. The
|
|
17
|
-
// queue is in the avatar's cells, so it survives a restart; the row is in
|
|
18
|
-
// hers, so the agent comes back with the box.
|
|
19
|
-
//
|
|
20
|
-
// The daemon hands her how to run a row, as it hands the grants faculty how
|
|
21
|
-
// to persist: running an agent is admitting through a socket only a daemon
|
|
22
|
-
// has, and this file is not in the dock's index, since a tab has no process
|
|
23
|
-
// to spawn. A box with no runner keeps the table and runs nobody.
|
|
8
|
+
// How a row runs is the terrain's, since admitting through a socket and
|
|
9
|
+
// spawning a process are things only a daemon has, so it is handed to her
|
|
10
|
+
// at birth in the class body the terrain brings; this body runs nobody and
|
|
11
|
+
// stands nowhere. Every row runs once the box is up.
|
|
24
12
|
import { OWNER } from 'nervur';
|
|
25
|
-
import
|
|
13
|
+
import { contract } from './contract.ts';
|
|
14
|
+
import type { Asker, JsonObject, OccupantRecord, Stance } from 'nervur';
|
|
15
|
+
import type { Harborage } from './box.ts';
|
|
26
16
|
import { Faculty } from './faculty.ts';
|
|
27
17
|
|
|
28
|
-
// The name the dock ward holds her under.
|
|
29
|
-
export const AGENTS = 'agents';
|
|
30
|
-
|
|
31
|
-
// One agent. `id` is the client identity the avatar is known by in her ward.
|
|
32
|
-
// A process row and a model row, told apart by what they name.
|
|
33
18
|
export type Process = { command: string; args: string[]; dir: string };
|
|
34
19
|
export type Model = { url: string; model: string; key?: string; turns?: number };
|
|
35
20
|
export type Row = { id: string; ward: string } & (Process | Model);
|
|
36
21
|
export const isProcess = (r: Row): r is { id: string; ward: string } & Process => 'command' in r;
|
|
37
22
|
|
|
38
|
-
// What a
|
|
23
|
+
// What a terrain stands for a row: a side over her avatar, closed when the
|
|
39
24
|
// row is dropped or the box goes down.
|
|
40
25
|
export type Side = { close(): Promise<void> };
|
|
41
26
|
export type Runner = (row: Row) => Promise<Side>;
|
|
42
27
|
|
|
43
|
-
// The root and nobody else, as the clock gates `keep`: an ask gated this way
|
|
44
|
-
// is the device's own and cannot be reached through a door.
|
|
45
28
|
const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
|
|
46
29
|
|
|
47
30
|
const ROW = {
|
|
@@ -50,26 +33,36 @@ const ROW = {
|
|
|
50
33
|
required: ['id', 'ward'],
|
|
51
34
|
};
|
|
52
35
|
|
|
36
|
+
const ASKS = {
|
|
37
|
+
keep: { description: 'add an agent, or replace the one of that id: an identity, her ward, and what runs for her, a command with args in a folder, or a model at a url', input: ROW, for: root },
|
|
38
|
+
drop: { description: 'stop an agent and forget her row; the avatar in her ward stays until the root removes her', input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, for: root },
|
|
39
|
+
list: { description: 'every agent she holds', input: { type: 'object' }, for: root },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// What the box's own code needs from the faculty that runs its agents.
|
|
43
|
+
export const AGENTS = contract('agents', ASKS);
|
|
44
|
+
|
|
53
45
|
export class Agents extends Faculty {
|
|
54
46
|
static override cells: JsonObject = { watched: {}, minted: 0, agents: {} };
|
|
55
|
-
static override asks =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
47
|
+
static override asks = ASKS;
|
|
48
|
+
static override fulfils = [AGENTS];
|
|
49
|
+
static override stands(): boolean {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
60
52
|
|
|
61
|
-
runner: Runner | undefined;
|
|
53
|
+
readonly runner: Runner | undefined;
|
|
62
54
|
readonly running = new Map<string, Side>();
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
constructor(stance: Stance, box?: Harborage, runner?: Runner) {
|
|
57
|
+
super(stance, box);
|
|
58
|
+
this.runner = runner;
|
|
59
|
+
this.box.up(async () => {
|
|
60
|
+
for (const row of Object.values(this.rows)) await this.#run(row);
|
|
61
|
+
});
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
async start(runner: Runner): Promise<void> {
|
|
71
|
-
this.runner = runner;
|
|
72
|
-
for (const row of Object.values(this.rows)) await this.#run(row);
|
|
64
|
+
private get rows(): Record<string, Row> {
|
|
65
|
+
return this.cells.agents as unknown as Record<string, Row>;
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
async #run(row: Row): Promise<void> {
|
|
@@ -110,7 +103,7 @@ export class Agents extends Faculty {
|
|
|
110
103
|
}
|
|
111
104
|
|
|
112
105
|
list(): JsonObject {
|
|
113
|
-
return { agents: Object.values(this.rows)
|
|
106
|
+
return { agents: Object.values(this.rows) };
|
|
114
107
|
}
|
|
115
108
|
|
|
116
109
|
// The device puts the box down: every side closes, the rows stay.
|
package/{dock → box}/apns.ts
RENAMED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// `quo-harbor.md`'s to say and not this file's. A stand-in in a test is an
|
|
28
28
|
// ordinary HTTP/1.1 server, so what a test proves is the shape and the
|
|
29
29
|
// signing rather than the transport, as the tab's push service did.
|
|
30
|
-
import type { Ring, Token } from './
|
|
30
|
+
import type { Ring, Token } from './ring.ts';
|
|
31
31
|
|
|
32
32
|
const subtle = () => globalThis.crypto.subtle;
|
|
33
33
|
const ES256 = { name: 'ECDSA', namedCurve: 'P-256' } as const;
|
|
@@ -83,9 +83,9 @@ export async function provider(a: Apns, made: { at: number; jwt: string } | null
|
|
|
83
83
|
// The device a token names, or nothing a ring may send. It is checked
|
|
84
84
|
// where a human can read why rather than by Apple two hops later.
|
|
85
85
|
export function device(token: Token): string {
|
|
86
|
-
const
|
|
87
|
-
if (typeof
|
|
88
|
-
return
|
|
86
|
+
const named = token.device;
|
|
87
|
+
if (typeof named !== 'string' || !/^[0-9a-fA-F]{8,200}$/.test(named)) throw new Error('an apns token is a device token');
|
|
88
|
+
return named;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// One ring, as Apple reads it: the path, the headers and the body. A ring
|
package/box/box.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// What the harbor is to a faculty: the three things only a box has, handed
|
|
3
|
+
// to every faculty at birth, so nobody writes into a faculty afterwards.
|
|
4
|
+
//
|
|
5
|
+
// `save` keeps her ward, for a faculty whose cells change outside any ask, a
|
|
6
|
+
// token issued by a route. `now` is the box's clock. `up` is work to do once
|
|
7
|
+
// the box is up, every ward hosted and every being settled, for a faculty
|
|
8
|
+
// whose work reaches the rest of the box, a line dialed or an agent run; the
|
|
9
|
+
// terrain that says the box is up waits for that work, so a box is up when
|
|
10
|
+
// it has landed, and work handed after that runs at once.
|
|
11
|
+
export type Harborage = { save(): Promise<void>; now(): number; up(work: () => Promise<void>): void };
|
|
12
|
+
|
|
13
|
+
// The harborage a faculty holds when none was handed: a suite's ward, which
|
|
14
|
+
// keeps nothing, reads the runtime's clock, and does its work at once.
|
|
15
|
+
export const ALONE: Harborage = { save: () => Promise.resolve(), now: () => Date.now(), up: (work) => void work().catch(() => {}) };
|
|
16
|
+
|
|
17
|
+
// A harborage over a ward's save, raised by the terrain once the box is up.
|
|
18
|
+
export function harborageOver(save: () => Promise<void>, now: () => number = () => Date.now()): Harborage & { raise(): Promise<void> } {
|
|
19
|
+
let waiting: (() => Promise<void>)[] | null = [];
|
|
20
|
+
const run = (work: () => Promise<void>) => work().catch(() => {});
|
|
21
|
+
return {
|
|
22
|
+
save,
|
|
23
|
+
now,
|
|
24
|
+
up: (work) => (waiting ? void waiting.push(work) : void run(work)),
|
|
25
|
+
raise: async () => {
|
|
26
|
+
const held = waiting ?? [];
|
|
27
|
+
waiting = null;
|
|
28
|
+
await Promise.all(held.map(run));
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
package/box/contract.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A contract: what a caller needs from a faculty, said as a blueprint and
|
|
3
|
+
// nothing else, so it is written, read and hashed exactly as a describe is.
|
|
4
|
+
// It is known by its digest, and that digest is the name a box lends under.
|
|
5
|
+
// Nothing enforces it, as Quo enforces no blueprint: the caller reads the
|
|
6
|
+
// describe she was handed and decides with `covers`.
|
|
7
|
+
import { canonical, digest } from 'nervur';
|
|
8
|
+
import type { Ask, AskSpec, Blueprint } from 'nervur';
|
|
9
|
+
|
|
10
|
+
export type Contract = Blueprint;
|
|
11
|
+
|
|
12
|
+
// A contract out of the asks a class declares. Descriptions stay behind,
|
|
13
|
+
// since they are words for a reader and a reworded sentence must not move a
|
|
14
|
+
// name. The title is the contract's own word for itself.
|
|
15
|
+
export function contract(title: string, asks: Record<string, AskSpec>): Contract {
|
|
16
|
+
return {
|
|
17
|
+
asks: Object.entries(asks).map(([name, spec]) => ({ name, input: spec.input ?? { type: 'object' }, ...(spec.output !== undefined ? { output: spec.output } : {}) })),
|
|
18
|
+
notes: { title },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The name a contract is lent under.
|
|
23
|
+
export const nameOf = (c: Contract): Promise<string> => digest(c);
|
|
24
|
+
|
|
25
|
+
// The word a contract calls itself.
|
|
26
|
+
export const titleOf = (c: Contract): string => {
|
|
27
|
+
const t = (c.notes as { title?: unknown } | null)?.title;
|
|
28
|
+
return typeof t === 'string' ? t : '';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Whether a blueprint covers a contract, ask by ask: every ask the contract
|
|
32
|
+
// names stands in it with the same input, and the same output where the
|
|
33
|
+
// contract says one, in the canonical form. It may carry asks of its own.
|
|
34
|
+
export function covers(held: Blueprint, c: Contract): boolean {
|
|
35
|
+
const byName = new Map<string, Ask>(held.asks.map((a) => [a.name, a]));
|
|
36
|
+
return c.asks.every((want) => {
|
|
37
|
+
const got = byName.get(want.name);
|
|
38
|
+
if (!got || canonical(got.input) !== canonical(want.input)) return false;
|
|
39
|
+
return want.output === undefined || (got.output !== undefined && canonical(got.output) === canonical(want.output));
|
|
40
|
+
});
|
|
41
|
+
}
|
package/box/dialers.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The box's lines: a socket held to another box's quo. route, one per row,
|
|
3
|
+
// announcing this harbor's wards on open and reconnecting when a line drops.
|
|
4
|
+
// The rows are her cells, placed by the root with `keep`. The harbor she
|
|
5
|
+
// dials for is the terrain's, handed at birth in the class body it brings;
|
|
6
|
+
// this body dials nothing and stands nowhere. Every row is dialed once the
|
|
7
|
+
// box is up, and a row kept after that is dialed at once.
|
|
8
|
+
import { OWNER } from 'nervur';
|
|
9
|
+
import type { Asker, JsonObject, OccupantRecord, Stance } from 'nervur';
|
|
10
|
+
import { dial, type Dialer, type Harbor } from 'nervur/harbor';
|
|
11
|
+
import type { Harborage } from './box.ts';
|
|
12
|
+
import { contract } from './contract.ts';
|
|
13
|
+
import { Faculty } from './faculty.ts';
|
|
14
|
+
|
|
15
|
+
export type Row = { url: string };
|
|
16
|
+
|
|
17
|
+
const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
|
|
18
|
+
|
|
19
|
+
const URL_ = { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] };
|
|
20
|
+
|
|
21
|
+
const ASKS = {
|
|
22
|
+
keep: { description: 'hold a socket to that quo. route, from now and from every boot, until it is dropped', input: URL_, for: root },
|
|
23
|
+
drop: { description: 'close the socket to that route and forget it', input: URL_, for: root },
|
|
24
|
+
list: { description: 'every route she dials, and whether the line is open now', input: { type: 'object' }, for: root },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// What the box's own code needs from the faculty that holds its lines.
|
|
28
|
+
export const DIALERS = contract('dialers', ASKS);
|
|
29
|
+
|
|
30
|
+
export class Dialers extends Faculty {
|
|
31
|
+
static override cells: JsonObject = { watched: {}, minted: 0, dialers: {} };
|
|
32
|
+
static override asks = ASKS;
|
|
33
|
+
static override fulfils = [DIALERS];
|
|
34
|
+
static override stands(): boolean {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
readonly harbor: Harbor | undefined;
|
|
39
|
+
readonly open = new Map<string, Dialer>();
|
|
40
|
+
#up = false;
|
|
41
|
+
|
|
42
|
+
constructor(stance: Stance, box?: Harborage, harbor?: Harbor) {
|
|
43
|
+
super(stance, box);
|
|
44
|
+
this.harbor = harbor;
|
|
45
|
+
this.box.up(() => {
|
|
46
|
+
this.#up = true;
|
|
47
|
+
for (const row of Object.values(this.rows)) this.#dial(row.url);
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private get rows(): Record<string, Row> {
|
|
53
|
+
return this.cells.dialers as unknown as Record<string, Row>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#dial(url: string): void {
|
|
57
|
+
if (!this.harbor || !this.#up || this.open.has(url)) return;
|
|
58
|
+
this.open.set(url, dial(this.harbor, url));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
keep(args: JsonObject): JsonObject {
|
|
62
|
+
const url = args.url;
|
|
63
|
+
if (typeof url !== 'string' || !/^https?:\/\/\S+$/.test(url)) return { error: 'url is the quo. route of the harbor dialed, over http or https' };
|
|
64
|
+
const clean = url.replace(/\/$/, '');
|
|
65
|
+
this.rows[clean] = { url: clean };
|
|
66
|
+
this.#dial(clean);
|
|
67
|
+
return { kept: clean };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
drop(args: JsonObject): JsonObject {
|
|
71
|
+
const url = typeof args.url === 'string' ? args.url.replace(/\/$/, '') : '';
|
|
72
|
+
if (!this.rows[url]) return { error: 'no such route' };
|
|
73
|
+
this.open.get(url)?.close();
|
|
74
|
+
this.open.delete(url);
|
|
75
|
+
delete this.rows[url];
|
|
76
|
+
return { dropped: url };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
list(): JsonObject {
|
|
80
|
+
return { dialers: Object.keys(this.rows).map((url) => ({ url, open: this.open.get(url)?.socket !== null && this.open.has(url) })) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The device puts the box down: every line closes, the rows stay.
|
|
84
|
+
close(): void {
|
|
85
|
+
for (const d of this.open.values()) d.close();
|
|
86
|
+
this.open.clear();
|
|
87
|
+
}
|
|
88
|
+
}
|