@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.
Files changed (285) hide show
  1. package/GETTING_STARTED.md +128 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +37 -0
  5. package/api/quo-api.md +196 -0
  6. package/api/route.ts +227 -0
  7. package/beings/GLOSSARY.md +118 -0
  8. package/beings/WORLDS.md +567 -0
  9. package/beings/avatar.ts +101 -0
  10. package/beings/carry.ts +159 -0
  11. package/beings/clock.ts +552 -0
  12. package/beings/courier.ts +107 -0
  13. package/beings/desk.ts +128 -0
  14. package/beings/doorbell.ts +129 -0
  15. package/beings/envoy.ts +218 -0
  16. package/beings/index.ts +16 -0
  17. package/beings/join.ts +51 -0
  18. package/beings/link.ts +104 -0
  19. package/beings/look.ts +139 -0
  20. package/beings/porter.ts +43 -0
  21. package/beings/presence.ts +39 -0
  22. package/beings/quo-app-bells.md +280 -0
  23. package/beings/quo-dock.md +1942 -0
  24. package/beings/setup.ts +45 -0
  25. package/beings/side.ts +60 -0
  26. package/beings/user.ts +459 -0
  27. package/beings/way.ts +65 -0
  28. package/cli/bundles.ts +20 -0
  29. package/cli/client.ts +73 -0
  30. package/cli/daemon.ts +581 -0
  31. package/cli/estate/Caddyfile +33 -0
  32. package/cli/estate/quo.service +35 -0
  33. package/cli/estate/systems.quo.plist +44 -0
  34. package/cli/estate.ts +48 -0
  35. package/cli/folder.ts +27 -0
  36. package/cli/oauth.ts +32 -0
  37. package/cli/pilot.ts +38 -0
  38. package/cli/quo-dock.md +1942 -0
  39. package/cli/quo.ts +203 -0
  40. package/cli/stand.ts +160 -0
  41. package/dist/api/route.d.ts +21 -0
  42. package/dist/api/route.js +193 -0
  43. package/dist/beings/avatar.d.ts +32 -0
  44. package/dist/beings/avatar.js +100 -0
  45. package/dist/beings/carry.d.ts +12 -0
  46. package/dist/beings/carry.js +174 -0
  47. package/dist/beings/clock.d.ts +161 -0
  48. package/dist/beings/clock.js +528 -0
  49. package/dist/beings/courier.d.ts +50 -0
  50. package/dist/beings/courier.js +96 -0
  51. package/dist/beings/desk.d.ts +53 -0
  52. package/dist/beings/desk.js +135 -0
  53. package/dist/beings/doorbell.d.ts +78 -0
  54. package/dist/beings/doorbell.js +133 -0
  55. package/dist/beings/envoy.d.ts +54 -0
  56. package/dist/beings/envoy.js +211 -0
  57. package/dist/beings/index.d.ts +14 -0
  58. package/dist/beings/index.js +16 -0
  59. package/dist/beings/join.d.ts +15 -0
  60. package/dist/beings/join.js +42 -0
  61. package/dist/beings/link.d.ts +14 -0
  62. package/dist/beings/link.js +91 -0
  63. package/dist/beings/look.d.ts +30 -0
  64. package/dist/beings/look.js +104 -0
  65. package/dist/beings/porter.d.ts +14 -0
  66. package/dist/beings/porter.js +41 -0
  67. package/dist/beings/presence.d.ts +8 -0
  68. package/dist/beings/presence.js +21 -0
  69. package/dist/beings/setup.d.ts +3 -0
  70. package/dist/beings/setup.js +39 -0
  71. package/dist/beings/side.d.ts +32 -0
  72. package/dist/beings/side.js +37 -0
  73. package/dist/beings/user.d.ts +315 -0
  74. package/dist/beings/user.js +462 -0
  75. package/dist/beings/way.d.ts +19 -0
  76. package/dist/beings/way.js +28 -0
  77. package/dist/cli/bundles.d.ts +5 -0
  78. package/dist/cli/bundles.js +17 -0
  79. package/dist/cli/client.d.ts +15 -0
  80. package/dist/cli/client.js +58 -0
  81. package/dist/cli/daemon.d.ts +50 -0
  82. package/dist/cli/daemon.js +584 -0
  83. package/dist/cli/estate/Caddyfile +33 -0
  84. package/dist/cli/estate/quo.service +35 -0
  85. package/dist/cli/estate/systems.quo.plist +44 -0
  86. package/dist/cli/estate.d.ts +5 -0
  87. package/dist/cli/estate.js +50 -0
  88. package/dist/cli/folder.d.ts +2 -0
  89. package/dist/cli/folder.js +25 -0
  90. package/dist/cli/oauth.d.ts +2 -0
  91. package/dist/cli/oauth.js +32 -0
  92. package/dist/cli/pilot.d.ts +5 -0
  93. package/dist/cli/pilot.js +22 -0
  94. package/dist/cli/quo.d.ts +2 -0
  95. package/dist/cli/quo.js +201 -0
  96. package/dist/cli/stand.d.ts +37 -0
  97. package/dist/cli/stand.js +110 -0
  98. package/dist/dock/agents.d.ts +102 -0
  99. package/dist/dock/agents.js +105 -0
  100. package/dist/dock/apns.d.ts +27 -0
  101. package/dist/dock/apns.js +98 -0
  102. package/dist/dock/console.d.ts +6 -0
  103. package/dist/dock/console.js +20 -0
  104. package/dist/dock/dialers.d.ts +56 -0
  105. package/dist/dock/dialers.js +75 -0
  106. package/dist/dock/faculty.d.ts +24 -0
  107. package/dist/dock/faculty.js +142 -0
  108. package/dist/dock/fcm.d.ts +13 -0
  109. package/dist/dock/fcm.js +77 -0
  110. package/dist/dock/grants.d.ts +41 -0
  111. package/dist/dock/grants.js +99 -0
  112. package/dist/dock/http2.d.ts +4 -0
  113. package/dist/dock/http2.js +68 -0
  114. package/dist/dock/index.d.ts +41 -0
  115. package/dist/dock/index.js +113 -0
  116. package/dist/dock/local.d.ts +13 -0
  117. package/dist/dock/local.js +44 -0
  118. package/dist/dock/location.d.ts +19 -0
  119. package/dist/dock/location.js +11 -0
  120. package/dist/dock/notifications.d.ts +38 -0
  121. package/dist/dock/notifications.js +60 -0
  122. package/dist/dock/push.d.ts +79 -0
  123. package/dist/dock/push.js +132 -0
  124. package/dist/dock/service.d.ts +4 -0
  125. package/dist/dock/service.js +107 -0
  126. package/dist/dock/timer.d.ts +52 -0
  127. package/dist/dock/timer.js +149 -0
  128. package/dist/dock/webpush.d.ts +12 -0
  129. package/dist/dock/webpush.js +144 -0
  130. package/dist/harbor/browser.d.ts +15 -0
  131. package/dist/harbor/browser.js +82 -0
  132. package/dist/harbor/capacitor.d.ts +28 -0
  133. package/dist/harbor/capacitor.js +218 -0
  134. package/dist/harbor/disk.d.ts +27 -0
  135. package/dist/harbor/disk.js +249 -0
  136. package/dist/harbor/edge/alarm.d.ts +10 -0
  137. package/dist/harbor/edge/alarm.js +38 -0
  138. package/dist/harbor/edge/edge.d.ts +54 -0
  139. package/dist/harbor/edge/edge.js +370 -0
  140. package/dist/harbor/edge/exercise.d.ts +7 -0
  141. package/dist/harbor/edge/exercise.js +232 -0
  142. package/dist/harbor/edge/given.d.ts +26 -0
  143. package/dist/harbor/edge/given.js +42 -0
  144. package/dist/harbor/edge/platform.d.ts +39 -0
  145. package/dist/harbor/edge/storage.d.ts +30 -0
  146. package/dist/harbor/edge/storage.js +180 -0
  147. package/dist/harbor/edge/worker.d.ts +17 -0
  148. package/dist/harbor/edge/worker.js +67 -0
  149. package/dist/harbor/files.d.ts +15 -0
  150. package/dist/harbor/files.js +220 -0
  151. package/dist/harbor/http.d.ts +19 -0
  152. package/dist/harbor/http.js +177 -0
  153. package/dist/harbor/idb.d.ts +15 -0
  154. package/dist/harbor/idb.js +0 -0
  155. package/dist/harbor/quo.d.ts +10 -0
  156. package/dist/harbor/quo.js +42 -0
  157. package/dist/harbor/resolve.d.ts +16 -0
  158. package/dist/harbor/resolve.js +17 -0
  159. package/dist/harbor/seal.d.ts +3 -0
  160. package/dist/harbor/seal.js +25 -0
  161. package/dist/harbor/sealed.d.ts +35 -0
  162. package/dist/harbor/sealed.js +162 -0
  163. package/dist/harbor/tauri.d.ts +16 -0
  164. package/dist/harbor/tauri.js +59 -0
  165. package/dist/harbor/ward.d.ts +9 -0
  166. package/dist/harbor/ward.js +15 -0
  167. package/dist/human/app.d.ts +32 -0
  168. package/dist/human/app.js +527 -0
  169. package/dist/human/apps.d.ts +10 -0
  170. package/dist/human/apps.js +29 -0
  171. package/dist/human/dom.d.ts +2 -0
  172. package/dist/human/dom.js +15 -0
  173. package/dist/human/door.d.ts +19 -0
  174. package/dist/human/door.js +92 -0
  175. package/dist/human/guest.d.ts +3 -0
  176. package/dist/human/guest.js +33 -0
  177. package/dist/human/html.d.ts +55 -0
  178. package/dist/human/html.js +451 -0
  179. package/dist/human/local.d.ts +7 -0
  180. package/dist/human/local.js +10 -0
  181. package/dist/human/mark.d.ts +8 -0
  182. package/dist/human/mark.js +47 -0
  183. package/dist/human/move.d.ts +17 -0
  184. package/dist/human/move.js +90 -0
  185. package/dist/human/screen.d.ts +19 -0
  186. package/dist/human/screen.js +119 -0
  187. package/dist/human/style.d.ts +2 -0
  188. package/dist/human/style.js +60 -0
  189. package/dist/human/tab.bundle.txt +4775 -0
  190. package/dist/human/tab.d.ts +15 -0
  191. package/dist/human/tab.js +409 -0
  192. package/dist/human/tree.d.ts +50 -0
  193. package/dist/human/tree.js +157 -0
  194. package/dist/human/wake.d.ts +34 -0
  195. package/dist/human/wake.js +179 -0
  196. package/dist/human/web.d.ts +33 -0
  197. package/dist/human/web.js +184 -0
  198. package/dist/human/worker.bundle.txt +3868 -0
  199. package/dist/human/worker.d.ts +1 -0
  200. package/dist/human/worker.js +81 -0
  201. package/dist/human/worlds.d.ts +24 -0
  202. package/dist/human/worlds.js +99 -0
  203. package/dist/mcp/agent.d.ts +10 -0
  204. package/dist/mcp/agent.js +78 -0
  205. package/dist/mcp/allow.d.ts +20 -0
  206. package/dist/mcp/allow.js +88 -0
  207. package/dist/mcp/http.d.ts +27 -0
  208. package/dist/mcp/http.js +92 -0
  209. package/dist/mcp/oauth.d.ts +91 -0
  210. package/dist/mcp/oauth.js +241 -0
  211. package/dist/mcp/route.d.ts +15 -0
  212. package/dist/mcp/route.js +42 -0
  213. package/dist/mcp/runner.d.ts +45 -0
  214. package/dist/mcp/runner.js +99 -0
  215. package/dist/mcp/server.d.ts +11 -0
  216. package/dist/mcp/server.js +102 -0
  217. package/dock/agents.ts +121 -0
  218. package/dock/apns.ts +138 -0
  219. package/dock/console.ts +23 -0
  220. package/dock/dialers.ts +85 -0
  221. package/dock/faculty.ts +148 -0
  222. package/dock/fcm.ts +103 -0
  223. package/dock/grants.ts +111 -0
  224. package/dock/http2.ts +70 -0
  225. package/dock/index.ts +133 -0
  226. package/dock/local.ts +48 -0
  227. package/dock/location.ts +22 -0
  228. package/dock/notifications.ts +67 -0
  229. package/dock/push.ts +161 -0
  230. package/dock/quo-app-bells.md +280 -0
  231. package/dock/quo-dock.md +1942 -0
  232. package/dock/service.ts +100 -0
  233. package/dock/timer.ts +174 -0
  234. package/dock/webpush.ts +182 -0
  235. package/harbor/browser.ts +97 -0
  236. package/harbor/capacitor.ts +223 -0
  237. package/harbor/disk.ts +237 -0
  238. package/harbor/edge/alarm.ts +48 -0
  239. package/harbor/edge/edge.ts +407 -0
  240. package/harbor/edge/exercise.ts +241 -0
  241. package/harbor/edge/given.ts +106 -0
  242. package/harbor/edge/platform.d.ts +39 -0
  243. package/harbor/edge/storage.ts +179 -0
  244. package/harbor/edge/text.d.ts +15 -0
  245. package/harbor/edge/worker.ts +70 -0
  246. package/harbor/edge/wrangler.toml +30 -0
  247. package/harbor/files.ts +221 -0
  248. package/harbor/http.ts +188 -0
  249. package/harbor/idb.ts +0 -0
  250. package/harbor/quo-harbor.md +850 -0
  251. package/harbor/quo.ts +44 -0
  252. package/harbor/resolve.ts +31 -0
  253. package/harbor/seal.ts +26 -0
  254. package/harbor/sealed.ts +183 -0
  255. package/harbor/tauri.ts +79 -0
  256. package/harbor/ward.ts +28 -0
  257. package/human/app.ts +580 -0
  258. package/human/apps.ts +47 -0
  259. package/human/dom.ts +21 -0
  260. package/human/door.ts +113 -0
  261. package/human/guest.ts +34 -0
  262. package/human/html.ts +490 -0
  263. package/human/local.ts +30 -0
  264. package/human/mark.ts +53 -0
  265. package/human/move.ts +122 -0
  266. package/human/quo-human.md +1042 -0
  267. package/human/screen.ts +136 -0
  268. package/human/style.ts +63 -0
  269. package/human/tab.bundle.txt +4775 -0
  270. package/human/tab.ts +446 -0
  271. package/human/tree.ts +217 -0
  272. package/human/wake.ts +191 -0
  273. package/human/web.ts +259 -0
  274. package/human/worker.bundle.txt +3868 -0
  275. package/human/worker.ts +124 -0
  276. package/human/worlds.ts +127 -0
  277. package/mcp/agent.ts +94 -0
  278. package/mcp/allow.ts +131 -0
  279. package/mcp/http.ts +92 -0
  280. package/mcp/oauth.ts +270 -0
  281. package/mcp/quo-mcp.md +343 -0
  282. package/mcp/route.ts +45 -0
  283. package/mcp/runner.ts +136 -0
  284. package/mcp/server.ts +101 -0
  285. package/package.json +121 -0
@@ -0,0 +1,528 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The clock. A harbor is processes, storage, a network, entropy and a clock,
3
+ // and of those only entropy crosses the edge into a being, as `random` in the
4
+ // ground. The clock does not cross and must not: a being who could read one
5
+ // would be a being who could not be moved, since the moment she read would be
6
+ // her box's and no other. So a clock reaches a being the one way anything
7
+ // reaches a being, as an ask.
8
+ //
9
+ // One class, two placements. On its own daemon she wakes beings of her own
10
+ // harbor, on standings the root placed; on a service she wakes beings in other
11
+ // estates, across a wire, on standings their roots placed. The woken being
12
+ // cannot tell which one woke her, and should not: moving from one to the other
13
+ // is a standing taken and a line moved, never a class rewritten.
14
+ //
15
+ // She reads no clock. `tick` takes the moment, and it comes from the one
16
+ // timer on the device, the `timer` faculty in her dock: at her birth she asks
17
+ // to be woken, and every tick arrives at her door as the occupant `timer`
18
+ // with the moment in its args. So a tick can be replayed exactly, and the
19
+ // suite needs no fake timers. `list` and `next` take the moment for the same
20
+ // reason: what she says is next is computed from a moment she was handed.
21
+ //
22
+ // Nobody is woken without a schedule saying so. Every tick answers when she
23
+ // is next due, the timer arms the box for the earliest such moment across
24
+ // every Clock who lent her, and a schedule kept or dropped says the new
25
+ // moment at her door at once. A box holding no schedule never ticks.
26
+ //
27
+ // One source, one list. `kept` is her own cell, and every schedule she holds
28
+ // is in it: what the device always does, placed once by the root with `keep`,
29
+ // and what an occupant registered on the service placement. Nothing is read
30
+ // from a file and nothing is set on the class, so a schedule outlives the
31
+ // process the way every cell does, and is gone when it is dropped.
32
+ import { Being, isSilence, isWord, wordOf, OWNER } from '@nervur-org/nervur';
33
+ import { TIMER, join } from '../dock/index.js';
34
+ // The root and nobody else. `OWNER` is one of the ward's own words, refused at
35
+ // every mint, so no occupant can ever wear it: an ask gated this way is the
36
+ // device's own and cannot be reached through a door.
37
+ const root = (_occ, asker) => asker.id === OWNER;
38
+ // The box's timer and nobody else. The id is the one she minted when she
39
+ // invited her back, so nobody else can wear it: one id names one record, and
40
+ // a wake is the timer's to cause and comes through her own door.
41
+ const WOKE = `${TIMER}:wake`;
42
+ const timer = (_occ, asker) => asker.id === WOKE;
43
+ // The root, or whoever the root invited. What she holds and when it goes next
44
+ // is read through a door like anything else: the owner creates and places, and
45
+ // the watching is done by somebody with a standing, through a gate. A
46
+ // placement whose occupants must not see each other's schedules says so in its
47
+ // own `asks`, since a subclass replaces the map rather than merging into it.
48
+ const watching = (occ, asker) => occ !== undefined || asker.id === OWNER;
49
+ // What one wake answered with. The three words as the door and the ward say
50
+ // them, `error` for a being who answered an error object, and `unplaced` for a
51
+ // schedule naming a standing the root never placed.
52
+ const said = (out) => (isSilence(out) ? 'silence' : isWord(out) ? wordOf(out) : out !== null && typeof out === 'object' && !Array.isArray(out) && typeof out.error === 'string' ? 'error' : 'answered');
53
+ const MINUTE = 60_000;
54
+ const floor = (ms) => Math.floor(ms / MINUTE) * MINUTE;
55
+ // ---- cron. Five fields, minute hour day-of-month month day-of-week, with
56
+ // `*`, `a`, `a-b`, `*/n`, `a-b/n` and comma lists, and the three-letter names
57
+ // for months and weekdays. Nothing else: no seconds, no `@daily`, no `L` or
58
+ // `#`. A field that will not parse is a schedule that is refused where it was
59
+ // written, never a schedule that quietly never fires.
60
+ const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
61
+ const WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
62
+ function field(spec, lo, hi, names = []) {
63
+ const out = new Set();
64
+ const one = (s) => {
65
+ const i = names.indexOf(s.toLowerCase());
66
+ const n = i >= 0 ? i + lo : Number(s);
67
+ if (!Number.isInteger(n) || n < lo || n > hi + (hi === 6 ? 1 : 0))
68
+ throw new Error(`${s} is not a value between ${lo} and ${hi}`);
69
+ return hi === 6 && n === 7 ? 0 : n; // seven is Sunday, as every crontab says
70
+ };
71
+ for (const part of spec.split(',')) {
72
+ const [range, step] = part.split('/');
73
+ if (range === undefined || range === '')
74
+ throw new Error(`${part} is not a field`);
75
+ const by = step === undefined ? 1 : Number(step);
76
+ if (!Number.isInteger(by) || by < 1)
77
+ throw new Error(`${part} does not step by a whole number`);
78
+ let from = lo, to = hi;
79
+ if (range !== '*') {
80
+ const ends = range.split('-');
81
+ if (ends.length > 2)
82
+ throw new Error(`${part} is not a range`);
83
+ from = one(ends[0]);
84
+ to = ends.length === 2 ? one(ends[1]) : from;
85
+ if (ends.length === 1 && step !== undefined)
86
+ to = hi; // `5/2` is `5-hi/2`, as every crontab says
87
+ }
88
+ if (to < from)
89
+ throw new Error(`${part} ends before it starts`);
90
+ for (let n = from; n <= to; n += by)
91
+ out.add(n);
92
+ }
93
+ return [...out].sort((a, b) => a - b);
94
+ }
95
+ export function parseCron(spec) {
96
+ const f = spec.trim().split(/\s+/);
97
+ if (f.length !== 5)
98
+ throw new Error('a cron is five fields: minute hour day-of-month month day-of-week');
99
+ return {
100
+ minutes: field(f[0], 0, 59),
101
+ hours: field(f[1], 0, 23),
102
+ days: field(f[2], 1, 31),
103
+ months: field(f[3], 1, 12, MONTHS),
104
+ weekdays: field(f[4], 0, 6, WEEKDAYS),
105
+ anyDay: f[2] === '*',
106
+ anyWeekday: f[4] === '*',
107
+ };
108
+ }
109
+ // The classic rule, and the one everybody trips on: two restricted day fields
110
+ // are an or, not an and, so `0 3 1 * mon` is the first of the month and every
111
+ // Monday, not the first of the month when it is a Monday.
112
+ const dateMatches = (c, month, day, weekday) => {
113
+ if (!c.months.includes(month))
114
+ return false;
115
+ const d = c.days.includes(day), w = c.weekdays.includes(weekday);
116
+ return c.anyDay && c.anyWeekday ? true : c.anyDay ? w : c.anyWeekday ? d : d || w;
117
+ };
118
+ // ---- civil time. A zone is a calendar, not a clock: nothing here reads the
119
+ // moment, it only says what a moment is called somewhere and what a name is a
120
+ // moment of. The civil day in Bucharest is not the UTC day, which is the whole
121
+ // reason a schedule carries a zone.
122
+ const FORMATS = new Map();
123
+ function format(tz) {
124
+ let f = FORMATS.get(tz);
125
+ if (!f)
126
+ FORMATS.set(tz, (f = new Intl.DateTimeFormat('en-US', { timeZone: tz, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })));
127
+ return f;
128
+ }
129
+ export function civil(ms, tz) {
130
+ const p = {};
131
+ for (const { type, value } of format(tz).formatToParts(ms))
132
+ p[type] = value;
133
+ return { year: Number(p.year), month: Number(p.month), day: Number(p.day), hour: Number(p.hour), minute: Number(p.minute) };
134
+ }
135
+ // What the zone is doing at that moment, in milliseconds, civil minus UTC.
136
+ const offset = (ms, tz) => {
137
+ const c = civil(ms, tz);
138
+ return Date.UTC(c.year, c.month - 1, c.day, c.hour, c.minute) - ms;
139
+ };
140
+ // A civil name back to a moment. The guess is corrected once, which is enough
141
+ // for every zone: an hour that daylight saving skipped lands on the hour that
142
+ // replaced it, and an hour it repeated fires once, in the second of the two.
143
+ export function fromCivil(tz, year, month, day, hour, minute) {
144
+ const guess = Date.UTC(year, month - 1, day, hour, minute);
145
+ const first = guess - offset(guess, tz);
146
+ const again = guess - offset(first, tz);
147
+ return again;
148
+ }
149
+ const nextDay = (c) => {
150
+ const d = new Date(Date.UTC(c.year, c.month - 1, c.day + 1));
151
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
152
+ };
153
+ const weekdayOf = (year, month, day) => new Date(Date.UTC(year, month - 1, day)).getUTCDay();
154
+ // The first minute strictly after `after` that this cron names in this zone,
155
+ // or null when it names none inside four years and a bit. A schedule that
156
+ // fires only on the twenty-ninth of February is inside that reach from
157
+ // wherever it is asked; anything outside it was written wrong.
158
+ export function nextFire(c, tz, after) {
159
+ const from = floor(after) + MINUTE;
160
+ let date = civil(from, tz);
161
+ for (let n = 0; n < 1500; n += 1) {
162
+ if (dateMatches(c, date.month, date.day, weekdayOf(date.year, date.month, date.day))) {
163
+ const hit = inDay(c, tz, date.year, date.month, date.day, from);
164
+ if (hit !== null)
165
+ return hit;
166
+ }
167
+ date = nextDay(date);
168
+ }
169
+ return null;
170
+ }
171
+ const HOUR = 3_600_000;
172
+ const SPAN = 23 * HOUR + 59 * MINUTE;
173
+ // The first moment this cron names inside one civil day, at or after `from`,
174
+ // or null when it names none there. Two paths, because the day a zone moves is
175
+ // a different problem from every other day.
176
+ //
177
+ // On an ordinary day the offset holds from midnight to midnight, so a civil
178
+ // name and a moment differ by one constant. The fields are already ascending,
179
+ // so the first name at or after `from` is the answer: two reads of the zone,
180
+ // no allocation, and nothing built that is then thrown away. A cron naming
181
+ // every minute of every hour costs the same as one naming a single morning.
182
+ //
183
+ // On the day a zone moves, ascending civil is not ascending UTC. An hour the
184
+ // clocks skipped lands on the hour that replaced it, so a civil 03:30 that
185
+ // never happened can come out later than the 04:00 after it. There every
186
+ // moment is built and sorted, which is what that day costs, and it is the only
187
+ // day that costs it.
188
+ function inDay(c, tz, year, month, day, from) {
189
+ const midnight = fromCivil(tz, year, month, day, 0, 0);
190
+ if (fromCivil(tz, year, month, day, 23, 59) - midnight === SPAN) {
191
+ for (const h of c.hours)
192
+ for (const mi of c.minutes) {
193
+ const ms = midnight + h * HOUR + mi * MINUTE;
194
+ if (ms >= from)
195
+ return ms;
196
+ }
197
+ return null;
198
+ }
199
+ const moments = [];
200
+ for (const h of c.hours)
201
+ for (const mi of c.minutes)
202
+ moments.push(fromCivil(tz, year, month, day, h, mi));
203
+ moments.sort((a, b) => a - b);
204
+ return moments.find((ms) => ms >= from) ?? null;
205
+ }
206
+ // ---- a schedule as it is written. One reader, so the root placing one on a
207
+ // device and an occupant registering on the service are held to one shape
208
+ // and one set of words for what is wrong with theirs.
209
+ export function readSchedule(v) {
210
+ if (v === null || typeof v !== 'object' || Array.isArray(v))
211
+ return 'a schedule is an object';
212
+ const s = v;
213
+ if (typeof s.id !== 'string' || !/^[\w.:-]+$/.test(s.id))
214
+ return 'a schedule has an id, a word';
215
+ if (typeof s.ask !== 'string' || !/^[\w.:-]+\.[\w-]+$/.test(s.ask))
216
+ return `${s.id}: ask is <standing>.<name>`;
217
+ if ((s.cron === undefined) === (s.at === undefined))
218
+ return `${s.id}: a schedule is a cron or an at, and not both`;
219
+ const out = { id: s.id, ask: s.ask };
220
+ if (s.cron !== undefined) {
221
+ if (typeof s.cron !== 'string')
222
+ return `${s.id}: cron is five fields as a string`;
223
+ try {
224
+ parseCron(s.cron);
225
+ }
226
+ catch (e) {
227
+ return `${s.id}: ${e instanceof Error ? e.message : String(e)}`;
228
+ }
229
+ out.cron = s.cron;
230
+ }
231
+ if (s.at !== undefined) {
232
+ if (typeof s.at !== 'string' || !Number.isFinite(Date.parse(s.at)))
233
+ return `${s.id}: at is a moment written out`;
234
+ out.at = s.at;
235
+ }
236
+ if (s.tz !== undefined) {
237
+ if (typeof s.tz !== 'string')
238
+ return `${s.id}: tz is a zone name`;
239
+ try {
240
+ format(s.tz);
241
+ }
242
+ catch {
243
+ return `${s.id}: ${s.tz} is not a zone this device knows`;
244
+ }
245
+ out.tz = s.tz;
246
+ }
247
+ if (s.args !== undefined) {
248
+ if (s.args === null || typeof s.args !== 'object' || Array.isArray(s.args))
249
+ return `${s.id}: args is an object`;
250
+ out.args = s.args;
251
+ }
252
+ if (s.note !== undefined)
253
+ out.note = s.note;
254
+ if (s.time !== undefined) {
255
+ if (typeof s.time !== 'number' || !Number.isFinite(s.time) || s.time <= 0)
256
+ return `${s.id}: time is how long the wake may spend`;
257
+ out.time = s.time;
258
+ }
259
+ if (s.catchUp !== undefined) {
260
+ if (typeof s.catchUp !== 'boolean')
261
+ return `${s.id}: catchUp is true or false`;
262
+ out.catchUp = s.catchUp;
263
+ }
264
+ return out;
265
+ }
266
+ // A schedule with no zone is UTC, said out loud rather than taken from the
267
+ // device: the device's own zone is a thing that changes under a running
268
+ // daemon, and a schedule that means a civil hour somewhere names the where.
269
+ const zoneOf = (s) => s.tz ?? 'UTC';
270
+ // She reads no clock, so every ask of hers that needs one takes it as an
271
+ // argument. `format` says what that argument is, and a side that has a clock
272
+ // of its own fills it rather than asking a human to type milliseconds: the
273
+ // ask says what the argument is, and each side decides what to do with that.
274
+ const NOW = { type: 'object', properties: { now: { type: 'number', format: 'moment', description: 'the moment, in milliseconds' } }, required: ['now'] };
275
+ export class Clock extends Being {
276
+ static cells = { kept: {}, fires: {} };
277
+ // At her birth she lends the box's timer and invites her back, so from then
278
+ // on the moment arrives as an ask at her own door, under the id she minted
279
+ // and by nobody else. A box with no timer to lend leaves her holding
280
+ // nothing, and she waits for a tick a suite or an operator hands her.
281
+ //
282
+ // Again at every birth: a standing points at a being and not at a box, and
283
+ // a restart and a migration are one silent event she cannot tell apart.
284
+ // Birth is synchronous and taking a standing is not, so the promise is
285
+ // kept and whoever needs it waits on it.
286
+ joined;
287
+ constructor(stance) {
288
+ super(stance);
289
+ this.joined = join(this, TIMER, { method: 'tick' });
290
+ }
291
+ static asks = {
292
+ tick: { description: 'fire everything due at this moment, and answer the tally and when she is next due', input: NOW, for: timer },
293
+ list: { description: 'every schedule she holds, its last fire and when it is next', input: NOW, for: watching },
294
+ next: { description: 'the earliest schedule due after this moment', input: NOW, for: watching },
295
+ keep: {
296
+ description: 'add a schedule, or replace the one of that id, until it is dropped',
297
+ input: {
298
+ type: 'object',
299
+ properties: { id: { type: 'string' }, ask: { type: 'string', description: '<standing>.<name>, split at the last dot' }, args: { type: 'object' }, note: {}, cron: { type: 'string' }, at: { type: 'string' }, tz: { type: 'string' }, time: { type: 'number' }, catchUp: { type: 'boolean' } },
300
+ required: ['id', 'ask'],
301
+ },
302
+ for: root,
303
+ },
304
+ drop: { description: 'take one of her schedules out, with what it last fired', input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, for: root },
305
+ };
306
+ #kept() {
307
+ return this.cells.kept;
308
+ }
309
+ // The root's own, and the root's alone. One way to declare a schedule: what
310
+ // the device always does, placed once when the root stands the estate up,
311
+ // and what something decided while it was running, a call back in three
312
+ // days, are the same ask and land in the same cell, and each stays until
313
+ // it is dropped. A second keep under an id replaces the first.
314
+ //
315
+ // On a service this ask is the occupants' door as well, and a root there
316
+ // places the device's own the same way. The asker is in the signature of
317
+ // both, unused here and used by a placement that opens the gate: an ask is
318
+ // `(args, asker)` everywhere, and a base that took one argument would be a
319
+ // base no subclass could widen.
320
+ async keep(args, _asker) {
321
+ const one = readSchedule(args);
322
+ if (typeof one === 'string')
323
+ return { error: one };
324
+ this.#kept()[one.id] = one;
325
+ await this.told();
326
+ return { kept: one.id };
327
+ }
328
+ async drop(args, _asker) {
329
+ if (typeof args.id !== 'string')
330
+ return { error: 'a drop names an id' };
331
+ if (!Object.hasOwn(this.#kept(), args.id))
332
+ return { error: 'no such schedule of hers' };
333
+ delete this.#kept()[args.id];
334
+ delete this.#fires()[args.id];
335
+ await this.told();
336
+ return { dropped: args.id };
337
+ }
338
+ // The box's timer holds one moment for the whole device, and a schedule
339
+ // kept or dropped moves when this Clock is next due. So she says the new
340
+ // moment at her timer's door rather than waiting for a round that may be a
341
+ // day away: a schedule kept at noon for one minute past noon fires at one
342
+ // minute past noon on a box that was asleep.
343
+ //
344
+ // She reads no clock to do it. The moment comes from the timer, who is the
345
+ // one being on the device that reads one, and `due` only ever brings the
346
+ // next round forward. A Clock lent no timer says nothing and waits for a
347
+ // moment somebody hands her.
348
+ //
349
+ // A placement whose occupants keep schedules of their own calls this where
350
+ // it keeps them, for the same reason the base does.
351
+ async told() {
352
+ if (!Object.hasOwn(this.cells.standings, TIMER))
353
+ return;
354
+ const timer = this.standings[TIMER];
355
+ const said = await timer?.ask('now', {});
356
+ const now = said !== null && typeof said === 'object' ? said.now : undefined;
357
+ if (typeof now !== 'number' || !Number.isFinite(now))
358
+ return;
359
+ // A schedule due at this very moment is a round now, not a round at the
360
+ // next moment after it: what she was handed is a minute already begun,
361
+ // and the schedule kept inside it names it.
362
+ const at = this.schedules().some((s) => this.#ready(s, now)) ? now : this.#soonest(floor(now));
363
+ if (at !== null)
364
+ await timer?.ask('due', { at });
365
+ }
366
+ // One list, her own cell and nothing else.
367
+ schedules() {
368
+ return Object.values(this.#kept());
369
+ }
370
+ #fires() {
371
+ return this.cells.fires;
372
+ }
373
+ // When this schedule goes next, after a moment. Null for a one-shot that has
374
+ // fired and for a cron that names nothing inside its reach.
375
+ #next(s, after) {
376
+ if (s.at !== undefined) {
377
+ const at = Date.parse(s.at);
378
+ return this.#fires()[s.id] ? null : at;
379
+ }
380
+ return nextFire(parseCron(s.cron), zoneOf(s), after);
381
+ }
382
+ // The earliest moment any schedule of hers is due after a minute. It is
383
+ // what she answers a tick with, and it is the whole of what the box's timer
384
+ // needs from her: a box whose Clocks all say null arms nothing and sleeps
385
+ // until somebody keeps something. A schedule whose wake was unreached is
386
+ // due again at the next minute, because nothing was delivered, and a
387
+ // schedule that will not parse is no moment at all.
388
+ #soonest(after) {
389
+ let soonest = null;
390
+ for (const s of this.schedules()) {
391
+ let at = null;
392
+ try {
393
+ at = this.#fires()[s.id]?.word === 'unreached' ? after + MINUTE : this.#next(s, after);
394
+ }
395
+ catch {
396
+ continue;
397
+ }
398
+ if (at !== null && (soonest === null || at < soonest))
399
+ soonest = at;
400
+ }
401
+ return soonest;
402
+ }
403
+ // Whether this schedule is due at this moment. A wake that was unreached is
404
+ // retried, because nothing was delivered; `late` is not, because the far
405
+ // door may have heard and be working still; and silence is never retried by
406
+ // anyone. Catch-up fires once for a gap, however long it was, never once per
407
+ // minute the box was down.
408
+ #due(s, now) {
409
+ const f = this.#fires()[s.id];
410
+ // Once a minute, whatever came back. The daemon ticks once a minute, so
411
+ // this only shows when a tick is replayed, and a tick replayed must fire
412
+ // nothing twice: that is what makes a moment handed in worth handing in.
413
+ if (f && f.at >= floor(now))
414
+ return false;
415
+ if (f?.word === 'unreached')
416
+ return true;
417
+ if (s.at !== undefined)
418
+ return !f && Date.parse(s.at) <= now;
419
+ const from = s.catchUp && f ? f.at : floor(now) - MINUTE;
420
+ const n = nextFire(parseCron(s.cron), zoneOf(s), from);
421
+ return n !== null && n <= now;
422
+ }
423
+ // The same question outside a tick, where a schedule that will not parse is
424
+ // no moment rather than an error: a tick says what is wrong with one, and
425
+ // nothing else of hers is allowed to fail on it.
426
+ #ready(s, now) {
427
+ try {
428
+ return this.#due(s, now);
429
+ }
430
+ catch {
431
+ return false;
432
+ }
433
+ }
434
+ // One wake: her standing, the ask on it, and what came back written down.
435
+ // Every send on one standing waits for the one before it, which is what a
436
+ // relation already is, so an estate with fifty schedules at one minute
437
+ // serializes on its own lane and delays nobody else.
438
+ async #wake(s, minute) {
439
+ const dot = s.ask.lastIndexOf('.');
440
+ const id = s.ask.slice(0, dot), name = s.ask.slice(dot + 1);
441
+ let word = 'unplaced';
442
+ if (Object.hasOwn(this.cells.standings, id)) {
443
+ const st = this.standings[id];
444
+ word = said(await st.ask(name, s.args ?? {}, s.time === undefined ? undefined : { time: s.time }));
445
+ }
446
+ const was = this.#fires()[s.id];
447
+ this.#fires()[s.id] = { at: minute, word, tries: word === 'unreached' ? (was?.word === 'unreached' ? was.tries : 0) + 1 : 1 };
448
+ return { id: s.id, ask: s.ask, word };
449
+ }
450
+ async tick(args) {
451
+ const now = args.now;
452
+ if (typeof now !== 'number' || !Number.isFinite(now))
453
+ return { error: 'a tick carries the moment: { now }' };
454
+ const minute = floor(now);
455
+ let due;
456
+ try {
457
+ due = this.schedules().filter((s) => this.#due(s, now));
458
+ }
459
+ catch (e) {
460
+ return { error: e instanceof Error ? e.message : String(e) };
461
+ }
462
+ const fired = await Promise.all(due.map((s) => this.#wake(s, minute)));
463
+ // What she is due next, after this tick and the fires it wrote down. The
464
+ // timer reads it off the answer and arms for the earliest across every
465
+ // Clock who lent her, so nobody is woken without a schedule saying so.
466
+ return { now: minute, fired, next: this.#soonest(minute) };
467
+ }
468
+ list(args) {
469
+ const now = args.now;
470
+ if (typeof now !== 'number' || !Number.isFinite(now))
471
+ return { error: 'a list is asked at a moment: { now }' };
472
+ const schedules = this.schedules().map((s) => {
473
+ const f = this.#fires()[s.id];
474
+ let next = null;
475
+ try {
476
+ next = this.#next(s, Math.max(now, f?.at ?? now));
477
+ }
478
+ catch {
479
+ next = null; // a schedule that will not parse says so where it was written
480
+ }
481
+ return Object.assign({}, s, { last: f?.at ?? null, word: f?.word ?? null, tries: f?.tries ?? 0, next });
482
+ });
483
+ return { now, schedules };
484
+ }
485
+ next(args) {
486
+ const now = args.now;
487
+ if (typeof now !== 'number' || !Number.isFinite(now))
488
+ return { error: 'a next is asked at a moment: { now }' };
489
+ let soonest = null;
490
+ for (const s of this.schedules()) {
491
+ let at = null;
492
+ try {
493
+ at = this.#next(s, now);
494
+ }
495
+ catch {
496
+ continue;
497
+ }
498
+ if (at !== null && (soonest === null || at < soonest.at))
499
+ soonest = { id: s.id, at };
500
+ }
501
+ return { next: soonest };
502
+ }
503
+ }
504
+ // Her id, and the key she is booted under. The daemon looks for exactly this
505
+ // key in `main`, so a device with a clock has her here and nowhere else.
506
+ export const CLOCK = 'clock';
507
+ // The root's setup: the clock booted, and one standing at each being she is to
508
+ // wake, under that being's own key. The same two asks a device's ground takes,
509
+ // because it is the same act: the owner creates and places, and the work is
510
+ // hers from then on.
511
+ export async function setupClock(hosted, wakes = []) {
512
+ const out = (await hosted.ask('boot', { key: CLOCK, class: 'Clock' }));
513
+ if (out.error)
514
+ throw new Error(`clock: ${out.error}`);
515
+ for (const being of wakes)
516
+ await placeClock(hosted, being);
517
+ }
518
+ // One being the clock may wake: she invites the clock under `clock`, and the
519
+ // clock takes the standing under the being's own key, which is what a
520
+ // schedule's `ask` names.
521
+ export async function placeClock(hosted, being) {
522
+ const inv = (await hosted.ask('invite', { being, id: CLOCK }));
523
+ if ('error' in inv)
524
+ throw new Error(`clock: ${inv.error}`);
525
+ const placed = (await hosted.ask('knock', { being: CLOCK, id: being, invitation: inv }));
526
+ if (placed.taken !== being)
527
+ throw new Error(`clock: ${placed.error ?? `she could not take ${being}`}`);
528
+ }
@@ -0,0 +1,50 @@
1
+ import { Being } from '@nervur-org/nervur';
2
+ import type { Invitation, JsonObject, OccupantRecord } from '@nervur-org/nervur';
3
+ export declare const MOVER = "mover";
4
+ export type Moving = {
5
+ world: string;
6
+ at: string;
7
+ name: string;
8
+ seed: string;
9
+ partition: JsonObject;
10
+ record: JsonObject;
11
+ };
12
+ declare const invited: (occ: OccupantRecord | undefined) => occ is OccupantRecord;
13
+ export declare function isMoving(v: unknown): v is Moving;
14
+ export declare class Courier extends Being {
15
+ static asks: {
16
+ hand: {
17
+ description: string;
18
+ input: {
19
+ type: string;
20
+ };
21
+ for: typeof invited;
22
+ };
23
+ done: {
24
+ description: string;
25
+ input: {
26
+ type: string;
27
+ };
28
+ for: typeof invited;
29
+ };
30
+ };
31
+ giving: (() => Promise<Moving | {
32
+ error: string;
33
+ }>) | undefined;
34
+ gone: (() => Promise<{
35
+ deleted: string;
36
+ } | {
37
+ error: string;
38
+ }>) | undefined;
39
+ hand(): Promise<JsonObject>;
40
+ done(): Promise<JsonObject>;
41
+ fetch(invitation: Invitation): Promise<Moving | {
42
+ error: string;
43
+ }>;
44
+ landed(): Promise<{
45
+ deleted: string;
46
+ } | {
47
+ error: string;
48
+ }>;
49
+ }
50
+ export {};
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The courier: a ward moving from one device to another as an ask. A tab
3
+ // holds a world in a local ward of its own, its seed and every standing it
4
+ // won there, and a human who wants that world on their phone has no account
5
+ // to log into and nothing to type: the ward itself moves, whole, and the far
6
+ // world is never told, because the pk does not change and every standing
7
+ // still points at her.
8
+ //
9
+ // Both ends are this class, in a ward of its own on each device, and the
10
+ // device **giving** is the one that shows an invitation. That is not a
11
+ // preference. A device is reachable only through a listener it dials, and a
12
+ // phone that has joined no world dials nothing at all, so it cannot be
13
+ // knocked; the device already in the world holds a socket to that world's
14
+ // rendezvous and can be. So the invitation travels with the reach note of
15
+ // that rendezvous, the receiving device dials it, and knocks.
16
+ //
17
+ // The move is two asks over one standing, because the answer to the first
18
+ // may be lost. `hand` answers with the ward, and nothing is deleted; the
19
+ // device that took it puts it on its own harbor, and only then asks `done`,
20
+ // which is where the copy here goes. A move interrupted between them leaves
21
+ // two copies of a ward that has not been touched since it was saved, and
22
+ // the human tries again; a move that deleted first and lost the answer
23
+ // would leave none.
24
+ //
25
+ // Nothing of the ward is kept here on either side. What is given comes from
26
+ // the shell holding this device when it is asked for, and what arrives goes
27
+ // back to the shell, since a partition written into another ward's cells is
28
+ // a copy nobody asked for.
29
+ import { Being, isSilence, isWord, wordOf } from '@nervur-org/nervur';
30
+ // The id each end knows the other by for the length of one move.
31
+ export const MOVER = 'mover';
32
+ const HEX = /^(?:[0-9a-f]{2})+$/;
33
+ const PK = /^[0-9a-f]{128}$/;
34
+ const invited = (occ) => occ !== undefined;
35
+ export function isMoving(v) {
36
+ if (v === null || typeof v !== 'object' || Array.isArray(v))
37
+ return false;
38
+ const m = v;
39
+ const object = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
40
+ return typeof m.world === 'string' && PK.test(m.world) && typeof m.at === 'string' && typeof m.name === 'string' && typeof m.seed === 'string' && HEX.test(m.seed) && object(m.partition) && object(m.record);
41
+ }
42
+ const answered = (out) => {
43
+ if (isSilence(out))
44
+ return { error: 'silence' };
45
+ if (isWord(out))
46
+ return { error: wordOf(out) };
47
+ if (out === null || typeof out !== 'object' || Array.isArray(out))
48
+ return { error: 'the other device answered nothing' };
49
+ return out;
50
+ };
51
+ export class Courier extends Being {
52
+ static asks = {
53
+ hand: { description: 'the ward this device is giving away: its world, where that world lives, and the ward itself', input: { type: 'object' }, for: invited },
54
+ done: { description: 'the ward arrived on the other device: delete the copy here', input: { type: 'object' }, for: invited },
55
+ };
56
+ // The shell holding the device that gives: what to hand over, and what to
57
+ // do when the other end says it landed. Set while a move is offered and
58
+ // gone after, so a courier nobody is holding gives nothing.
59
+ giving;
60
+ gone;
61
+ // ---- what the device taking asks
62
+ async hand() {
63
+ if (!this.giving)
64
+ return { error: 'this device is giving nothing away' };
65
+ return await this.giving();
66
+ }
67
+ async done() {
68
+ if (!this.gone)
69
+ return { error: 'this device is giving nothing away' };
70
+ return await this.gone();
71
+ }
72
+ // ---- what the shell taking calls. Not asks: nobody reaches these through a door.
73
+ // Knock the far courier, take the ward she answers with, and hold her:
74
+ // the standing is what the second ask goes through, and a knock is spent
75
+ // the moment it is answered.
76
+ async fetch(invitation) {
77
+ const out = answered(await this.knock(invitation, 'hand', {}));
78
+ if ('error' in out && typeof out.error === 'string')
79
+ return { error: out.error };
80
+ if (!isMoving(out))
81
+ return { error: 'the other device answered with something that is not a ward' };
82
+ if ((await this.take(MOVER, invitation)) === null)
83
+ return { error: 'this device could not hold the other one' };
84
+ return out;
85
+ }
86
+ // It is here: the other device may delete its copy.
87
+ async landed() {
88
+ const standing = this.standings[MOVER];
89
+ if (!standing)
90
+ return { error: 'this device is not holding the other one' };
91
+ const out = answered(await standing.ask('done', {}));
92
+ if (typeof out.deleted === 'string')
93
+ return { deleted: out.deleted };
94
+ return { error: typeof out.error === 'string' ? out.error : 'the other device would not let go' };
95
+ }
96
+ }