@nervur-org/dock 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +128 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +37 -0
- package/api/quo-api.md +196 -0
- package/api/route.ts +227 -0
- package/beings/GLOSSARY.md +118 -0
- package/beings/WORLDS.md +567 -0
- package/beings/avatar.ts +101 -0
- package/beings/carry.ts +159 -0
- package/beings/clock.ts +552 -0
- package/beings/courier.ts +107 -0
- package/beings/desk.ts +128 -0
- package/beings/doorbell.ts +129 -0
- package/beings/envoy.ts +218 -0
- package/beings/index.ts +16 -0
- package/beings/join.ts +51 -0
- package/beings/link.ts +104 -0
- package/beings/look.ts +139 -0
- package/beings/porter.ts +43 -0
- package/beings/presence.ts +39 -0
- package/beings/quo-app-bells.md +280 -0
- package/beings/quo-dock.md +1942 -0
- package/beings/setup.ts +45 -0
- package/beings/side.ts +60 -0
- package/beings/user.ts +459 -0
- package/beings/way.ts +65 -0
- package/cli/bundles.ts +20 -0
- package/cli/client.ts +73 -0
- package/cli/daemon.ts +581 -0
- package/cli/estate/Caddyfile +33 -0
- package/cli/estate/quo.service +35 -0
- package/cli/estate/systems.quo.plist +44 -0
- package/cli/estate.ts +48 -0
- package/cli/folder.ts +27 -0
- package/cli/oauth.ts +32 -0
- package/cli/pilot.ts +38 -0
- package/cli/quo-dock.md +1942 -0
- package/cli/quo.ts +203 -0
- package/cli/stand.ts +160 -0
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.js +193 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +100 -0
- package/dist/beings/carry.d.ts +12 -0
- package/dist/beings/carry.js +174 -0
- package/dist/beings/clock.d.ts +161 -0
- package/dist/beings/clock.js +528 -0
- package/dist/beings/courier.d.ts +50 -0
- package/dist/beings/courier.js +96 -0
- package/dist/beings/desk.d.ts +53 -0
- package/dist/beings/desk.js +135 -0
- package/dist/beings/doorbell.d.ts +78 -0
- package/dist/beings/doorbell.js +133 -0
- package/dist/beings/envoy.d.ts +54 -0
- package/dist/beings/envoy.js +211 -0
- package/dist/beings/index.d.ts +14 -0
- package/dist/beings/index.js +16 -0
- package/dist/beings/join.d.ts +15 -0
- package/dist/beings/join.js +42 -0
- package/dist/beings/link.d.ts +14 -0
- package/dist/beings/link.js +91 -0
- package/dist/beings/look.d.ts +30 -0
- package/dist/beings/look.js +104 -0
- package/dist/beings/porter.d.ts +14 -0
- package/dist/beings/porter.js +41 -0
- package/dist/beings/presence.d.ts +8 -0
- package/dist/beings/presence.js +21 -0
- package/dist/beings/setup.d.ts +3 -0
- package/dist/beings/setup.js +39 -0
- package/dist/beings/side.d.ts +32 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +315 -0
- package/dist/beings/user.js +462 -0
- package/dist/beings/way.d.ts +19 -0
- package/dist/beings/way.js +28 -0
- package/dist/cli/bundles.d.ts +5 -0
- package/dist/cli/bundles.js +17 -0
- package/dist/cli/client.d.ts +15 -0
- package/dist/cli/client.js +58 -0
- package/dist/cli/daemon.d.ts +50 -0
- package/dist/cli/daemon.js +584 -0
- package/dist/cli/estate/Caddyfile +33 -0
- package/dist/cli/estate/quo.service +35 -0
- package/dist/cli/estate/systems.quo.plist +44 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +50 -0
- package/dist/cli/folder.d.ts +2 -0
- package/dist/cli/folder.js +25 -0
- package/dist/cli/oauth.d.ts +2 -0
- package/dist/cli/oauth.js +32 -0
- package/dist/cli/pilot.d.ts +5 -0
- package/dist/cli/pilot.js +22 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +201 -0
- package/dist/cli/stand.d.ts +37 -0
- package/dist/cli/stand.js +110 -0
- package/dist/dock/agents.d.ts +102 -0
- package/dist/dock/agents.js +105 -0
- package/dist/dock/apns.d.ts +27 -0
- package/dist/dock/apns.js +98 -0
- package/dist/dock/console.d.ts +6 -0
- package/dist/dock/console.js +20 -0
- package/dist/dock/dialers.d.ts +56 -0
- package/dist/dock/dialers.js +75 -0
- package/dist/dock/faculty.d.ts +24 -0
- package/dist/dock/faculty.js +142 -0
- package/dist/dock/fcm.d.ts +13 -0
- package/dist/dock/fcm.js +77 -0
- package/dist/dock/grants.d.ts +41 -0
- package/dist/dock/grants.js +99 -0
- package/dist/dock/http2.d.ts +4 -0
- package/dist/dock/http2.js +68 -0
- package/dist/dock/index.d.ts +41 -0
- package/dist/dock/index.js +113 -0
- package/dist/dock/local.d.ts +13 -0
- package/dist/dock/local.js +44 -0
- package/dist/dock/location.d.ts +19 -0
- package/dist/dock/location.js +11 -0
- package/dist/dock/notifications.d.ts +38 -0
- package/dist/dock/notifications.js +60 -0
- package/dist/dock/push.d.ts +79 -0
- package/dist/dock/push.js +132 -0
- package/dist/dock/service.d.ts +4 -0
- package/dist/dock/service.js +107 -0
- package/dist/dock/timer.d.ts +52 -0
- package/dist/dock/timer.js +149 -0
- package/dist/dock/webpush.d.ts +12 -0
- package/dist/dock/webpush.js +144 -0
- package/dist/harbor/browser.d.ts +15 -0
- package/dist/harbor/browser.js +82 -0
- package/dist/harbor/capacitor.d.ts +28 -0
- package/dist/harbor/capacitor.js +218 -0
- package/dist/harbor/disk.d.ts +27 -0
- package/dist/harbor/disk.js +249 -0
- package/dist/harbor/edge/alarm.d.ts +10 -0
- package/dist/harbor/edge/alarm.js +38 -0
- package/dist/harbor/edge/edge.d.ts +54 -0
- package/dist/harbor/edge/edge.js +370 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +232 -0
- package/dist/harbor/edge/given.d.ts +26 -0
- package/dist/harbor/edge/given.js +42 -0
- package/dist/harbor/edge/platform.d.ts +39 -0
- package/dist/harbor/edge/storage.d.ts +30 -0
- package/dist/harbor/edge/storage.js +180 -0
- package/dist/harbor/edge/worker.d.ts +17 -0
- package/dist/harbor/edge/worker.js +67 -0
- package/dist/harbor/files.d.ts +15 -0
- package/dist/harbor/files.js +220 -0
- package/dist/harbor/http.d.ts +19 -0
- package/dist/harbor/http.js +177 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +0 -0
- package/dist/harbor/quo.d.ts +10 -0
- package/dist/harbor/quo.js +42 -0
- package/dist/harbor/resolve.d.ts +16 -0
- package/dist/harbor/resolve.js +17 -0
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/harbor/sealed.d.ts +35 -0
- package/dist/harbor/sealed.js +162 -0
- package/dist/harbor/tauri.d.ts +16 -0
- package/dist/harbor/tauri.js +59 -0
- package/dist/harbor/ward.d.ts +9 -0
- package/dist/harbor/ward.js +15 -0
- package/dist/human/app.d.ts +32 -0
- package/dist/human/app.js +527 -0
- package/dist/human/apps.d.ts +10 -0
- package/dist/human/apps.js +29 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/door.d.ts +19 -0
- package/dist/human/door.js +92 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +33 -0
- package/dist/human/html.d.ts +55 -0
- package/dist/human/html.js +451 -0
- package/dist/human/local.d.ts +7 -0
- package/dist/human/local.js +10 -0
- package/dist/human/mark.d.ts +8 -0
- package/dist/human/mark.js +47 -0
- package/dist/human/move.d.ts +17 -0
- package/dist/human/move.js +90 -0
- package/dist/human/screen.d.ts +19 -0
- package/dist/human/screen.js +119 -0
- package/dist/human/style.d.ts +2 -0
- package/dist/human/style.js +60 -0
- package/dist/human/tab.bundle.txt +4775 -0
- package/dist/human/tab.d.ts +15 -0
- package/dist/human/tab.js +409 -0
- package/dist/human/tree.d.ts +50 -0
- package/dist/human/tree.js +157 -0
- package/dist/human/wake.d.ts +34 -0
- package/dist/human/wake.js +179 -0
- package/dist/human/web.d.ts +33 -0
- package/dist/human/web.js +184 -0
- package/dist/human/worker.bundle.txt +3868 -0
- package/dist/human/worker.d.ts +1 -0
- package/dist/human/worker.js +81 -0
- package/dist/human/worlds.d.ts +24 -0
- package/dist/human/worlds.js +99 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/allow.d.ts +20 -0
- package/dist/mcp/allow.js +88 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +92 -0
- package/dist/mcp/oauth.d.ts +91 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/route.d.ts +15 -0
- package/dist/mcp/route.js +42 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +99 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +102 -0
- package/dock/agents.ts +121 -0
- package/dock/apns.ts +138 -0
- package/dock/console.ts +23 -0
- package/dock/dialers.ts +85 -0
- package/dock/faculty.ts +148 -0
- package/dock/fcm.ts +103 -0
- package/dock/grants.ts +111 -0
- package/dock/http2.ts +70 -0
- package/dock/index.ts +133 -0
- package/dock/local.ts +48 -0
- package/dock/location.ts +22 -0
- package/dock/notifications.ts +67 -0
- package/dock/push.ts +161 -0
- package/dock/quo-app-bells.md +280 -0
- package/dock/quo-dock.md +1942 -0
- package/dock/service.ts +100 -0
- package/dock/timer.ts +174 -0
- package/dock/webpush.ts +182 -0
- package/harbor/browser.ts +97 -0
- package/harbor/capacitor.ts +223 -0
- package/harbor/disk.ts +237 -0
- package/harbor/edge/alarm.ts +48 -0
- package/harbor/edge/edge.ts +407 -0
- package/harbor/edge/exercise.ts +241 -0
- package/harbor/edge/given.ts +106 -0
- package/harbor/edge/platform.d.ts +39 -0
- package/harbor/edge/storage.ts +179 -0
- package/harbor/edge/text.d.ts +15 -0
- package/harbor/edge/worker.ts +70 -0
- package/harbor/edge/wrangler.toml +30 -0
- package/harbor/files.ts +221 -0
- package/harbor/http.ts +188 -0
- package/harbor/idb.ts +0 -0
- package/harbor/quo-harbor.md +850 -0
- package/harbor/quo.ts +44 -0
- package/harbor/resolve.ts +31 -0
- package/harbor/seal.ts +26 -0
- package/harbor/sealed.ts +183 -0
- package/harbor/tauri.ts +79 -0
- package/harbor/ward.ts +28 -0
- package/human/app.ts +580 -0
- package/human/apps.ts +47 -0
- package/human/dom.ts +21 -0
- package/human/door.ts +113 -0
- package/human/guest.ts +34 -0
- package/human/html.ts +490 -0
- package/human/local.ts +30 -0
- package/human/mark.ts +53 -0
- package/human/move.ts +122 -0
- package/human/quo-human.md +1042 -0
- package/human/screen.ts +136 -0
- package/human/style.ts +63 -0
- package/human/tab.bundle.txt +4775 -0
- package/human/tab.ts +446 -0
- package/human/tree.ts +217 -0
- package/human/wake.ts +191 -0
- package/human/web.ts +259 -0
- package/human/worker.bundle.txt +3868 -0
- package/human/worker.ts +124 -0
- package/human/worlds.ts +127 -0
- package/mcp/agent.ts +94 -0
- package/mcp/allow.ts +131 -0
- package/mcp/http.ts +92 -0
- package/mcp/oauth.ts +270 -0
- package/mcp/quo-mcp.md +343 -0
- package/mcp/route.ts +45 -0
- package/mcp/runner.ts +136 -0
- package/mcp/server.ts +101 -0
- package/package.json +121 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Asker, JsonObject, OccupantRecord } from '@nervur-org/nervur';
|
|
2
|
+
import { Faculty } from './faculty.ts';
|
|
3
|
+
export declare const AGENTS = "agents";
|
|
4
|
+
export type Process = {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
dir: string;
|
|
8
|
+
};
|
|
9
|
+
export type Model = {
|
|
10
|
+
url: string;
|
|
11
|
+
model: string;
|
|
12
|
+
key?: string;
|
|
13
|
+
turns?: number;
|
|
14
|
+
};
|
|
15
|
+
export type Row = {
|
|
16
|
+
id: string;
|
|
17
|
+
ward: string;
|
|
18
|
+
} & (Process | Model);
|
|
19
|
+
export declare const isProcess: (r: Row) => r is {
|
|
20
|
+
id: string;
|
|
21
|
+
ward: string;
|
|
22
|
+
} & Process;
|
|
23
|
+
export type Side = {
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
export type Runner = (row: Row) => Promise<Side>;
|
|
27
|
+
declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
28
|
+
export declare class Agents extends Faculty {
|
|
29
|
+
#private;
|
|
30
|
+
static cells: JsonObject;
|
|
31
|
+
static asks: {
|
|
32
|
+
keep: {
|
|
33
|
+
description: string;
|
|
34
|
+
input: {
|
|
35
|
+
type: string;
|
|
36
|
+
properties: {
|
|
37
|
+
id: {
|
|
38
|
+
type: string;
|
|
39
|
+
};
|
|
40
|
+
ward: {
|
|
41
|
+
type: string;
|
|
42
|
+
};
|
|
43
|
+
command: {
|
|
44
|
+
type: string;
|
|
45
|
+
};
|
|
46
|
+
args: {
|
|
47
|
+
type: string;
|
|
48
|
+
items: {
|
|
49
|
+
type: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
dir: {
|
|
53
|
+
type: string;
|
|
54
|
+
};
|
|
55
|
+
url: {
|
|
56
|
+
type: string;
|
|
57
|
+
};
|
|
58
|
+
model: {
|
|
59
|
+
type: string;
|
|
60
|
+
};
|
|
61
|
+
key: {
|
|
62
|
+
type: string;
|
|
63
|
+
};
|
|
64
|
+
turns: {
|
|
65
|
+
type: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
required: string[];
|
|
69
|
+
};
|
|
70
|
+
for: typeof root;
|
|
71
|
+
};
|
|
72
|
+
drop: {
|
|
73
|
+
description: string;
|
|
74
|
+
input: {
|
|
75
|
+
type: string;
|
|
76
|
+
properties: {
|
|
77
|
+
id: {
|
|
78
|
+
type: string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
required: string[];
|
|
82
|
+
};
|
|
83
|
+
for: typeof root;
|
|
84
|
+
};
|
|
85
|
+
list: {
|
|
86
|
+
description: string;
|
|
87
|
+
input: {
|
|
88
|
+
type: string;
|
|
89
|
+
};
|
|
90
|
+
for: typeof root;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
runner: Runner | undefined;
|
|
94
|
+
readonly running: Map<string, Side>;
|
|
95
|
+
private get rows();
|
|
96
|
+
start(runner: Runner): Promise<void>;
|
|
97
|
+
keep(args: JsonObject): Promise<JsonObject>;
|
|
98
|
+
drop(args: JsonObject): Promise<JsonObject>;
|
|
99
|
+
list(): JsonObject;
|
|
100
|
+
close(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The box's agents: `agents`, one faculty of the dock ward, standing for the
|
|
3
|
+
// one thing a box does on an event as the timer does on a moment: run a
|
|
4
|
+
// process. What she holds is the table of agents, in her cells and in no
|
|
5
|
+
// file, placed by the root with `keep` the way a schedule is placed on the
|
|
6
|
+
// clock, so a box has one place its state is and the daemon reads no recipe.
|
|
7
|
+
//
|
|
8
|
+
// A row names a client identity, the ward she is admitted into, and what
|
|
9
|
+
// runs for her, one of two: a process, a command with its args in its
|
|
10
|
+
// folder, or a model that speaks function calling, a base URL and a model
|
|
11
|
+
// name, OpenRouter, LM Studio, anything with a tools array. At her birth
|
|
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.
|
|
24
|
+
import { OWNER } from '@nervur-org/nervur';
|
|
25
|
+
import { Faculty } from './faculty.js';
|
|
26
|
+
// The name the dock ward holds her under.
|
|
27
|
+
export const AGENTS = 'agents';
|
|
28
|
+
export const isProcess = (r) => 'command' in r;
|
|
29
|
+
// The root and nobody else, as the clock gates `keep`: an ask gated this way
|
|
30
|
+
// is the device's own and cannot be reached through a door.
|
|
31
|
+
const root = (_occ, asker) => asker.id === OWNER;
|
|
32
|
+
const ROW = {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: { id: { type: 'string' }, ward: { type: 'string' }, command: { type: 'string' }, args: { type: 'array', items: { type: 'string' } }, dir: { type: 'string' }, url: { type: 'string' }, model: { type: 'string' }, key: { type: 'string' }, turns: { type: 'number' } },
|
|
35
|
+
required: ['id', 'ward'],
|
|
36
|
+
};
|
|
37
|
+
export class Agents extends Faculty {
|
|
38
|
+
static cells = { watched: {}, minted: 0, agents: {} };
|
|
39
|
+
static asks = {
|
|
40
|
+
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 },
|
|
41
|
+
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 },
|
|
42
|
+
list: { description: 'every agent she holds', input: { type: 'object' }, for: root },
|
|
43
|
+
};
|
|
44
|
+
runner;
|
|
45
|
+
running = new Map();
|
|
46
|
+
get rows() {
|
|
47
|
+
return this.cells.agents;
|
|
48
|
+
}
|
|
49
|
+
// Every row up, once the daemon has handed the runner. Called after the
|
|
50
|
+
// wards are hosted, since an avatar is admitted into one of them.
|
|
51
|
+
async start(runner) {
|
|
52
|
+
this.runner = runner;
|
|
53
|
+
for (const row of Object.values(this.rows))
|
|
54
|
+
await this.#run(row);
|
|
55
|
+
}
|
|
56
|
+
async #run(row) {
|
|
57
|
+
if (!this.runner)
|
|
58
|
+
return;
|
|
59
|
+
await this.running.get(row.id)?.close();
|
|
60
|
+
this.running.set(row.id, await this.runner(row));
|
|
61
|
+
}
|
|
62
|
+
async keep(args) {
|
|
63
|
+
const id = args.id, ward = args.ward;
|
|
64
|
+
if (typeof id !== 'string' || !/^[\w.-]+$/.test(id))
|
|
65
|
+
return { error: 'id is a word' };
|
|
66
|
+
if (typeof ward !== 'string')
|
|
67
|
+
return { error: 'ward names the ward she is admitted into' };
|
|
68
|
+
let row;
|
|
69
|
+
if (typeof args.command === 'string' && typeof args.dir === 'string') {
|
|
70
|
+
row = { id, ward, command: args.command, args: Array.isArray(args.args) ? args.args.filter((a) => typeof a === 'string') : [], dir: args.dir };
|
|
71
|
+
}
|
|
72
|
+
else if (typeof args.url === 'string' && typeof args.model === 'string') {
|
|
73
|
+
row = { id, ward, url: args.url, model: args.model, ...(typeof args.key === 'string' ? { key: args.key } : {}), ...(typeof args.turns === 'number' ? { turns: args.turns } : {}) };
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
return { error: 'a row runs a process, { command, args?, dir }, or a model, { url, model, key?, turns? }' };
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await this.#run(row);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
83
|
+
}
|
|
84
|
+
this.rows[id] = row;
|
|
85
|
+
return { kept: id };
|
|
86
|
+
}
|
|
87
|
+
async drop(args) {
|
|
88
|
+
const id = args.id;
|
|
89
|
+
if (typeof id !== 'string' || !this.rows[id])
|
|
90
|
+
return { error: 'no such agent' };
|
|
91
|
+
await this.running.get(id)?.close();
|
|
92
|
+
this.running.delete(id);
|
|
93
|
+
delete this.rows[id];
|
|
94
|
+
return { dropped: id };
|
|
95
|
+
}
|
|
96
|
+
list() {
|
|
97
|
+
return { agents: Object.values(this.rows) };
|
|
98
|
+
}
|
|
99
|
+
// The device puts the box down: every side closes, the rows stay.
|
|
100
|
+
async close() {
|
|
101
|
+
for (const a of this.running.values())
|
|
102
|
+
await a.close();
|
|
103
|
+
this.running.clear();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Ring, Token } from './push.ts';
|
|
2
|
+
export declare function pkcs8(pem: string): Uint8Array;
|
|
3
|
+
export type Apns = {
|
|
4
|
+
key: string;
|
|
5
|
+
keyId: string;
|
|
6
|
+
teamId: string;
|
|
7
|
+
topic: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const HOSTS: {
|
|
10
|
+
production: string;
|
|
11
|
+
sandbox: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function provider(a: Apns, made: {
|
|
14
|
+
at: number;
|
|
15
|
+
jwt: string;
|
|
16
|
+
} | null): Promise<{
|
|
17
|
+
at: number;
|
|
18
|
+
jwt: string;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function device(token: Token): string;
|
|
21
|
+
export declare function request(a: Apns, jwt: string, token: Token, sealed?: string): {
|
|
22
|
+
path: string;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
body: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function answered(status: number, text: string): void;
|
|
27
|
+
export declare function apnsPush(a: Apns, at?: typeof HOSTS): Ring;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const subtle = () => globalThis.crypto.subtle;
|
|
2
|
+
const ES256 = { name: 'ECDSA', namedCurve: 'P-256' };
|
|
3
|
+
const b64url = (bytes) => {
|
|
4
|
+
let binary = '';
|
|
5
|
+
for (const b of bytes)
|
|
6
|
+
binary += String.fromCharCode(b);
|
|
7
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
|
|
8
|
+
};
|
|
9
|
+
const utf8 = (v) => new TextEncoder().encode(v);
|
|
10
|
+
// A `.p8` from the portal is PKCS#8 in a PEM wrapper. The wrapper is
|
|
11
|
+
// stripped rather than parsed: the base64 between the two lines is the DER.
|
|
12
|
+
export function pkcs8(pem) {
|
|
13
|
+
const body = pem.replace(/-----(BEGIN|END)[^-]*-----/g, '').replace(/\s+/g, '');
|
|
14
|
+
if (!body)
|
|
15
|
+
throw new Error('an APNs key is a PKCS#8 PEM');
|
|
16
|
+
const binary = atob(body);
|
|
17
|
+
const out = new Uint8Array(binary.length);
|
|
18
|
+
for (let at = 0; at < binary.length; at += 1)
|
|
19
|
+
out[at] = binary.charCodeAt(at);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
// Apple's two hosts. A token is minted by one of them and means nothing to
|
|
23
|
+
// the other, and only the device knows which: a build signed for
|
|
24
|
+
// development gets a sandbox token, a build from a store gets a production
|
|
25
|
+
// one, and nothing in the token itself says so. So the device says it, in
|
|
26
|
+
// the token it hands over.
|
|
27
|
+
export const HOSTS = { production: 'https://api.push.apple.com', sandbox: 'https://api.sandbox.push.apple.com' };
|
|
28
|
+
// Apple wants this refreshed no more often than every twenty minutes and no
|
|
29
|
+
// less than every sixty, so it is minted once and kept for forty-five.
|
|
30
|
+
const LIFE = 45 * 60 * 1000;
|
|
31
|
+
// The provider token, and the request one ring makes. Both are here and
|
|
32
|
+
// neither is a transport, because Apple is reached two ways in this kit and
|
|
33
|
+
// what is sent must be the same both times: a `fetch` where a `fetch`
|
|
34
|
+
// speaks HTTP/2, and `node:http2` in `http2.ts` where it does not.
|
|
35
|
+
export async function provider(a, made) {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
if (made && now - made.at < LIFE)
|
|
38
|
+
return made;
|
|
39
|
+
const key = await subtle().importKey('pkcs8', pkcs8(a.key), ES256, false, ['sign']);
|
|
40
|
+
const head = b64url(utf8(JSON.stringify({ alg: 'ES256', kid: a.keyId })));
|
|
41
|
+
const claims = b64url(utf8(JSON.stringify({ iss: a.teamId, iat: Math.floor(now / 1000) })));
|
|
42
|
+
const signed = await subtle().sign({ name: 'ECDSA', hash: 'SHA-256' }, key, utf8(`${head}.${claims}`));
|
|
43
|
+
return { at: now, jwt: `${head}.${claims}.${b64url(new Uint8Array(signed))}` };
|
|
44
|
+
}
|
|
45
|
+
// The device a token names, or nothing a ring may send. It is checked
|
|
46
|
+
// where a human can read why rather than by Apple two hops later.
|
|
47
|
+
export function device(token) {
|
|
48
|
+
const device = token.device;
|
|
49
|
+
if (typeof device !== 'string' || !/^[0-9a-fA-F]{8,200}$/.test(device))
|
|
50
|
+
throw new Error('an apns token is a device token');
|
|
51
|
+
return device;
|
|
52
|
+
}
|
|
53
|
+
// One ring, as Apple reads it: the path, the headers and the body. A ring
|
|
54
|
+
// with a banner is an alert Apple must show, and `mutable-content` is what
|
|
55
|
+
// sends it through the device's own extension first, which is the only
|
|
56
|
+
// process that can open the seal. The title and body Apple carries are the
|
|
57
|
+
// placeholder the extension replaces, and they say nothing: they are what a
|
|
58
|
+
// person sees if her own phone cannot open its own banner. A ring with no
|
|
59
|
+
// banner stays a background wake, where the device drains what it missed
|
|
60
|
+
// over Quo and composes its own line.
|
|
61
|
+
export function request(a, jwt, token, sealed) {
|
|
62
|
+
const said = sealed === undefined
|
|
63
|
+
? { aps: { 'content-available': 1 } }
|
|
64
|
+
: { aps: { alert: { 'loc-key': 'quo.ring' }, 'mutable-content': 1, sound: 'default' }, sealed };
|
|
65
|
+
return {
|
|
66
|
+
path: `/3/device/${device(token)}`,
|
|
67
|
+
headers: {
|
|
68
|
+
authorization: `bearer ${jwt}`,
|
|
69
|
+
'apns-topic': a.topic,
|
|
70
|
+
'apns-push-type': sealed === undefined ? 'background' : 'alert',
|
|
71
|
+
'apns-priority': sealed === undefined ? '5' : '10',
|
|
72
|
+
'apns-expiration': '0',
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(said),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// What Apple said. 410 is Apple saying this device token is gone. Nothing
|
|
78
|
+
// here forgets it, as nothing forgets a spent Web Push endpoint: a device
|
|
79
|
+
// sends a fresh token whenever it comes to the foreground, and a world that
|
|
80
|
+
// dropped one would stop ringing a phone that is merely off.
|
|
81
|
+
export function answered(status, text) {
|
|
82
|
+
if (status < 200 || status > 299)
|
|
83
|
+
throw new Error(`apns answered ${status} ${text.slice(0, 200)}`);
|
|
84
|
+
}
|
|
85
|
+
// The ground. An `apns` token is a device token and which of Apple's two
|
|
86
|
+
// hosts minted it; `at` is what a test points somewhere else.
|
|
87
|
+
export function apnsPush(a, at = HOSTS) {
|
|
88
|
+
let made = null;
|
|
89
|
+
return {
|
|
90
|
+
async ring(token, sealed) {
|
|
91
|
+
made = await provider(a, made);
|
|
92
|
+
const host = token.sandbox === true ? at.sandbox : at.production;
|
|
93
|
+
const req = request(a, made.jwt, token, sealed);
|
|
94
|
+
const res = await fetch(`${host}${req.path}`, { method: 'POST', headers: req.headers, body: req.body });
|
|
95
|
+
answered(res.status, await res.text());
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Carrier } from '../beings/carry.ts';
|
|
2
|
+
import type { Asker, OccupantRecord } from '@nervur-org/nervur';
|
|
3
|
+
export declare const CONSOLE = "console";
|
|
4
|
+
export declare class Console extends Carrier {
|
|
5
|
+
static carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean;
|
|
6
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The box, as one being. A device has no surface of its own, so a box that
|
|
3
|
+
// has joined no world has nothing to draw: the Console is that page and only
|
|
4
|
+
// that. She carries the faculties her harbor stood, showing each one's asks
|
|
5
|
+
// as her own, and the ordinary screen draws her exactly as it draws a
|
|
6
|
+
// world's user being, through one avatar holding one standing.
|
|
7
|
+
//
|
|
8
|
+
// She exists because a fresh install otherwise asks for location and
|
|
9
|
+
// notifications in its manifest and shows nothing that ever asks for them,
|
|
10
|
+
// which is a store reviewer's first question and a human's second.
|
|
11
|
+
//
|
|
12
|
+
// She carries for whoever the harbor's root let in, and for nobody else: the
|
|
13
|
+
// root is the box, and it invites the one avatar its own screen runs.
|
|
14
|
+
import { Carrier } from '../beings/carry.js';
|
|
15
|
+
export const CONSOLE = 'console';
|
|
16
|
+
export class Console extends Carrier {
|
|
17
|
+
static carries(occupant, _asker) {
|
|
18
|
+
return occupant !== undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Asker, JsonObject, OccupantRecord } from '@nervur-org/nervur';
|
|
2
|
+
import { type Dialer, type Harbor } from '@nervur-org/nervur/harbor';
|
|
3
|
+
import { Faculty } from './faculty.ts';
|
|
4
|
+
export declare const DIALERS = "dialers";
|
|
5
|
+
export type Row = {
|
|
6
|
+
url: string;
|
|
7
|
+
};
|
|
8
|
+
declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
|
|
9
|
+
export declare class Dialers extends Faculty {
|
|
10
|
+
#private;
|
|
11
|
+
static cells: JsonObject;
|
|
12
|
+
static asks: {
|
|
13
|
+
keep: {
|
|
14
|
+
description: string;
|
|
15
|
+
input: {
|
|
16
|
+
type: string;
|
|
17
|
+
properties: {
|
|
18
|
+
url: {
|
|
19
|
+
type: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
required: string[];
|
|
23
|
+
};
|
|
24
|
+
for: typeof root;
|
|
25
|
+
};
|
|
26
|
+
drop: {
|
|
27
|
+
description: string;
|
|
28
|
+
input: {
|
|
29
|
+
type: string;
|
|
30
|
+
properties: {
|
|
31
|
+
url: {
|
|
32
|
+
type: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
required: string[];
|
|
36
|
+
};
|
|
37
|
+
for: typeof root;
|
|
38
|
+
};
|
|
39
|
+
list: {
|
|
40
|
+
description: string;
|
|
41
|
+
input: {
|
|
42
|
+
type: string;
|
|
43
|
+
};
|
|
44
|
+
for: typeof root;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
harbor: Harbor | undefined;
|
|
48
|
+
readonly open: Map<string, Dialer>;
|
|
49
|
+
private get rows();
|
|
50
|
+
start(harbor: Harbor): void;
|
|
51
|
+
keep(args: JsonObject): JsonObject;
|
|
52
|
+
drop(args: JsonObject): JsonObject;
|
|
53
|
+
list(): JsonObject;
|
|
54
|
+
close(): void;
|
|
55
|
+
}
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The box's dialers: `dialers`, one faculty of the dock ward, standing for
|
|
3
|
+
// the sockets this box holds open to other harbors' `quo.` routes. A Mac
|
|
4
|
+
// behind NAT, a phone on the mobile network, a droplet dialing home: each
|
|
5
|
+
// is a dialer, and what it dials is a row in her cells, placed once by the
|
|
6
|
+
// root with `keep` the way a schedule is placed on the clock and an agent
|
|
7
|
+
// on the agents faculty, in no file.
|
|
8
|
+
//
|
|
9
|
+
// A row is one URL, the `quo.` route of the harbor dialed. She holds the
|
|
10
|
+
// socket from her birth on, as every faculty holds what the box holds: one
|
|
11
|
+
// line per row, announcing this harbor's wards on open, binding the far
|
|
12
|
+
// side's pks, reconnecting with backoff when the line drops, and closed
|
|
13
|
+
// when the row is dropped or the box goes down. The dialer itself is the
|
|
14
|
+
// library's, written on the standard socket surface, so this faculty holds
|
|
15
|
+
// on every terrain that has a socket; the daemon hands her the harbor to
|
|
16
|
+
// dial for, as it hands the agents faculty how to run a row.
|
|
17
|
+
import { OWNER } from '@nervur-org/nervur';
|
|
18
|
+
import { dial } from '@nervur-org/nervur/harbor';
|
|
19
|
+
import { Faculty } from './faculty.js';
|
|
20
|
+
// The name the dock ward holds her under.
|
|
21
|
+
export const DIALERS = 'dialers';
|
|
22
|
+
// The root and nobody else: a line held open is the device's own to place.
|
|
23
|
+
const root = (_occ, asker) => asker.id === OWNER;
|
|
24
|
+
const URL_ = { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] };
|
|
25
|
+
export class Dialers extends Faculty {
|
|
26
|
+
static cells = { watched: {}, minted: 0, dialers: {} };
|
|
27
|
+
static asks = {
|
|
28
|
+
keep: { description: 'hold a socket to that quo. route, from now and from every boot, until it is dropped', input: URL_, for: root },
|
|
29
|
+
drop: { description: 'close the socket to that route and forget it', input: URL_, for: root },
|
|
30
|
+
list: { description: 'every route she dials, and whether the line is open now', input: { type: 'object' }, for: root },
|
|
31
|
+
};
|
|
32
|
+
harbor;
|
|
33
|
+
open = new Map();
|
|
34
|
+
get rows() {
|
|
35
|
+
return this.cells.dialers;
|
|
36
|
+
}
|
|
37
|
+
// Every row dialed, once the daemon has handed the harbor to dial for.
|
|
38
|
+
start(harbor) {
|
|
39
|
+
this.harbor = harbor;
|
|
40
|
+
for (const row of Object.values(this.rows))
|
|
41
|
+
this.#dial(row.url);
|
|
42
|
+
}
|
|
43
|
+
#dial(url) {
|
|
44
|
+
if (!this.harbor || this.open.has(url))
|
|
45
|
+
return;
|
|
46
|
+
this.open.set(url, dial(this.harbor, url));
|
|
47
|
+
}
|
|
48
|
+
keep(args) {
|
|
49
|
+
const url = args.url;
|
|
50
|
+
if (typeof url !== 'string' || !/^https?:\/\/\S+$/.test(url))
|
|
51
|
+
return { error: 'url is the quo. route of the harbor dialed, over http or https' };
|
|
52
|
+
const clean = url.replace(/\/$/, '');
|
|
53
|
+
this.rows[clean] = { url: clean };
|
|
54
|
+
this.#dial(clean);
|
|
55
|
+
return { kept: clean };
|
|
56
|
+
}
|
|
57
|
+
drop(args) {
|
|
58
|
+
const url = typeof args.url === 'string' ? args.url.replace(/\/$/, '') : '';
|
|
59
|
+
if (!this.rows[url])
|
|
60
|
+
return { error: 'no such route' };
|
|
61
|
+
this.open.get(url)?.close();
|
|
62
|
+
this.open.delete(url);
|
|
63
|
+
delete this.rows[url];
|
|
64
|
+
return { dropped: url };
|
|
65
|
+
}
|
|
66
|
+
list() {
|
|
67
|
+
return { dialers: Object.keys(this.rows).map((url) => ({ url, open: this.open.get(url)?.socket !== null && this.open.has(url) })) };
|
|
68
|
+
}
|
|
69
|
+
// The device puts the box down: every line closes, the rows stay.
|
|
70
|
+
close() {
|
|
71
|
+
for (const d of this.open.values())
|
|
72
|
+
d.close();
|
|
73
|
+
this.open.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Being } from '@nervur-org/nervur';
|
|
2
|
+
import type { Asker, JsonObject } from '@nervur-org/nervur';
|
|
3
|
+
export type Watched = {
|
|
4
|
+
by: string;
|
|
5
|
+
method: string;
|
|
6
|
+
args: JsonObject;
|
|
7
|
+
};
|
|
8
|
+
export type Woke = {
|
|
9
|
+
id: string;
|
|
10
|
+
by: string;
|
|
11
|
+
out: unknown;
|
|
12
|
+
};
|
|
13
|
+
export declare class Faculty extends Being {
|
|
14
|
+
static cells: JsonObject;
|
|
15
|
+
watch(args: JsonObject, asker: Asker): Promise<JsonObject>;
|
|
16
|
+
protected welcomed(_id: string): Promise<void>;
|
|
17
|
+
protected woke(round?: (w: Watched) => JsonObject, only?: string): Promise<Woke[] | null>;
|
|
18
|
+
get watching(): Record<string, Watched>;
|
|
19
|
+
}
|
|
20
|
+
export declare const settled: () => Promise<void>;
|
|
21
|
+
export declare function join(b: Being, name: string, watch?: {
|
|
22
|
+
method: string;
|
|
23
|
+
args?: JsonObject;
|
|
24
|
+
}, id?: string): Promise<string | null>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A faculty: one being of the dock ward, standing for one thing the box can
|
|
3
|
+
// do. She is a being and nothing else, so what is here is not a second kind
|
|
4
|
+
// of object but the two moves every faculty shares.
|
|
5
|
+
//
|
|
6
|
+
// Out: whoever holds a standing at her asks her methods, and the occupant's
|
|
7
|
+
// notes say what that caller is, a prefix on a store, an account at a
|
|
8
|
+
// vendor. Her own cells say what the thing is.
|
|
9
|
+
//
|
|
10
|
+
// Back: `watch` is the one subscribe verb every faculty has. A being who
|
|
11
|
+
// wants waking mints an invitation and hands it over, this takes it as a
|
|
12
|
+
// standing, and from then on she is asked at her own door as the occupant
|
|
13
|
+
// she named. `occupants.remove` on her side is the whole of unsubscribe: the
|
|
14
|
+
// next wake hears `removed` and the row goes.
|
|
15
|
+
//
|
|
16
|
+
// She holds what the box holds and begins holding it at birth: the
|
|
17
|
+
// connection, the interval, the worker, the route are opened in her
|
|
18
|
+
// constructor, and she is born whether or not the thing is reachable,
|
|
19
|
+
// answering an error object while it is not.
|
|
20
|
+
import { Being, isInvitation, isSilence, isWord, wordOf } from '@nervur-org/nervur';
|
|
21
|
+
export class Faculty extends Being {
|
|
22
|
+
static cells = { watched: {}, minted: 0 };
|
|
23
|
+
// Someone wants waking. She hands the invitation she minted and says what
|
|
24
|
+
// to ask her; this knocks it, takes it under an id of this being's own,
|
|
25
|
+
// and files it. The id never crosses: it is this side's name for the
|
|
26
|
+
// relation, and the caller's name for the same relation is her own.
|
|
27
|
+
//
|
|
28
|
+
// The empty ask is the knock, because every being answers one and asking
|
|
29
|
+
// for a named ask would make every subscriber owe a method she does not
|
|
30
|
+
// otherwise need.
|
|
31
|
+
async watch(args, asker) {
|
|
32
|
+
const inv = args.invitation;
|
|
33
|
+
if (!isInvitation(inv) || typeof args.method !== 'string')
|
|
34
|
+
return { error: 'an invitation and a method' };
|
|
35
|
+
const extra = args.args;
|
|
36
|
+
if (extra !== undefined && (extra === null || typeof extra !== 'object' || Array.isArray(extra)))
|
|
37
|
+
return { error: 'args is one object' };
|
|
38
|
+
const out = await this.knock(inv);
|
|
39
|
+
if (isSilence(out) || isWord(out))
|
|
40
|
+
return { error: 'no way back' };
|
|
41
|
+
const cells = this.cells;
|
|
42
|
+
const id = await this.take(`wake:${(cells.minted += 1)}`, inv);
|
|
43
|
+
if (id === null)
|
|
44
|
+
return { error: 'not taken' };
|
|
45
|
+
cells.watched[id] = { by: asker.id ?? '', method: args.method, args: extra ?? {} };
|
|
46
|
+
await this.welcomed(id);
|
|
47
|
+
return { watching: id };
|
|
48
|
+
}
|
|
49
|
+
// Somebody new is filed, and the answer to her `watch` waits for this. A
|
|
50
|
+
// faculty that owes a new subscriber something at once says it here: the
|
|
51
|
+
// timer wakes her, learns when she is next due and arms the box for it, so
|
|
52
|
+
// a being born long after the box was up is not waiting for a round nobody
|
|
53
|
+
// scheduled. Most faculties owe nothing, and this is why they say nothing.
|
|
54
|
+
async welcomed(_id) { }
|
|
55
|
+
// One round of waking. Each subscriber is asked what she asked to be
|
|
56
|
+
// asked, with whatever this round adds. A subscriber who removed the
|
|
57
|
+
// occupant says `removed` once, and her row goes with the standing; there
|
|
58
|
+
// is nothing to retry and nothing to remember.
|
|
59
|
+
//
|
|
60
|
+
// `only` is one subscriber of the round's own choosing, named by this
|
|
61
|
+
// being's own id for the relation, and it is the same round down to one
|
|
62
|
+
// row: a faculty that wakes somebody just filed learns exactly what a full
|
|
63
|
+
// round would have learned about her.
|
|
64
|
+
//
|
|
65
|
+
// `dropped` is this being herself: her ward took her out, so the standings
|
|
66
|
+
// she holds reach nobody. Null says so, and whoever holds the interval puts
|
|
67
|
+
// it down rather than waking an empty room for as long as the box is up.
|
|
68
|
+
async woke(round = (w) => w.args, only) {
|
|
69
|
+
const cells = this.cells;
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const [id, w] of Object.entries(cells.watched)) {
|
|
72
|
+
if (only !== undefined && id !== only)
|
|
73
|
+
continue;
|
|
74
|
+
const said = await this.standings[id]?.ask(w.method, round(w));
|
|
75
|
+
if (said === undefined || (isWord(said) && wordOf(said) === 'dropped'))
|
|
76
|
+
return null;
|
|
77
|
+
if (isWord(said) && wordOf(said) === 'removed') {
|
|
78
|
+
this.standings.remove(id);
|
|
79
|
+
delete cells.watched[id];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
out.push({ id, by: w.by, out: said });
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
// Who is waiting, by this being's own id for the relation. For a device's
|
|
87
|
+
// own code and for a suite; it crosses no door.
|
|
88
|
+
get watching() {
|
|
89
|
+
return this.cells.watched;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// The three moves a being makes at every birth to hold what her box lends.
|
|
93
|
+
// A standing points at a being and not at a box, so one she wakes up holding
|
|
94
|
+
// names a being on the box she was on when it was made; a restart and a
|
|
95
|
+
// migration are the same silent event to her, so she does these again every
|
|
96
|
+
// time she is constructed and is always pointed at the box she is on.
|
|
97
|
+
//
|
|
98
|
+
// `watch` says she wants waking too: her ward mints the occupant, the
|
|
99
|
+
// faculty takes the standing it makes, and from then on the wake arrives at
|
|
100
|
+
// her door under the id she chose.
|
|
101
|
+
// Every join in flight on this box. A being takes what her box lends at her
|
|
102
|
+
// birth, and birth is synchronous while taking a standing is not, so a
|
|
103
|
+
// harbor that answered "up" before they landed would be a box whose first
|
|
104
|
+
// tick woke nobody and whose first ring reached no platform. The terrain
|
|
105
|
+
// waits on this once, after its wards are hosted; nothing else reads it, and
|
|
106
|
+
// a join that fails leaves nothing behind but a being who asks again.
|
|
107
|
+
const flight = new Set();
|
|
108
|
+
export const settled = async () => {
|
|
109
|
+
while (flight.size) {
|
|
110
|
+
const now = [...flight];
|
|
111
|
+
await Promise.allSettled(now);
|
|
112
|
+
for (const p of now)
|
|
113
|
+
flight.delete(p);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
export function join(b, name, watch, id = name) {
|
|
117
|
+
const p = joining(b, name, watch, id);
|
|
118
|
+
flight.add(p);
|
|
119
|
+
void p.catch(() => { });
|
|
120
|
+
return p;
|
|
121
|
+
}
|
|
122
|
+
async function joining(b, name, watch, id) {
|
|
123
|
+
// One turn, before anything is asked. A being is called at her door and
|
|
124
|
+
// holds her stance from her first line, but her ward sets that door after
|
|
125
|
+
// her constructor returns, so a stance call made inside it reaches a being
|
|
126
|
+
// her ward does not hold yet and is answered as one taken out. Every
|
|
127
|
+
// faculty relation is made at birth, so the wait belongs here rather than
|
|
128
|
+
// in each of them.
|
|
129
|
+
await Promise.resolve();
|
|
130
|
+
b.standings.remove(id);
|
|
131
|
+
b.occupants.remove(`${id}:wake`);
|
|
132
|
+
const got = await b.lend(name, id);
|
|
133
|
+
if (got === null)
|
|
134
|
+
return null;
|
|
135
|
+
if (!watch)
|
|
136
|
+
return got;
|
|
137
|
+
const back = await b.invite(`${id}:wake`);
|
|
138
|
+
if (!back)
|
|
139
|
+
return got;
|
|
140
|
+
await b.standings[id]?.ask('watch', { invitation: back, method: watch.method, ...(watch.args ? { args: watch.args } : {}) });
|
|
141
|
+
return got;
|
|
142
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Ring } from './push.ts';
|
|
2
|
+
export type Account = {
|
|
3
|
+
project_id: string;
|
|
4
|
+
client_email: string;
|
|
5
|
+
private_key: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function isAccount(v: unknown): v is Account;
|
|
8
|
+
export type Doors = {
|
|
9
|
+
oauth: string;
|
|
10
|
+
fcm: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const DOORS: Doors;
|
|
13
|
+
export declare function fcmPush(a: Account, at?: Doors): Ring;
|