nervur 0.22.0 → 0.22.2-3

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 (398) hide show
  1. package/AUTHORING.md +741 -0
  2. package/KIT-SPEC.md +286 -0
  3. package/LICENSE +1 -1
  4. package/NOTICE +6 -2
  5. package/README.md +97 -424
  6. package/dist/app/app-ground.d.ts +18 -0
  7. package/dist/app/app-ground.js +25 -0
  8. package/dist/app/index.d.ts +2 -0
  9. package/dist/app/index.js +5 -0
  10. package/dist/app/native.d.ts +46 -0
  11. package/dist/app/native.js +101 -0
  12. package/dist/being/being.d.ts +229 -0
  13. package/dist/being/being.js +20 -0
  14. package/dist/being/covers.d.ts +9 -0
  15. package/dist/being/covers.js +21 -0
  16. package/dist/being/index.d.ts +3 -0
  17. package/dist/being/index.js +5 -0
  18. package/dist/being/need.d.ts +49 -0
  19. package/dist/being/need.js +33 -0
  20. package/dist/being/schema.d.ts +100 -0
  21. package/dist/being/schema.js +132 -0
  22. package/dist/being/table.d.ts +42 -0
  23. package/dist/being/table.js +288 -0
  24. package/dist/bench/bench-ground.d.ts +55 -0
  25. package/dist/bench/bench-ground.js +101 -0
  26. package/dist/bench/bench.d.ts +87 -0
  27. package/dist/bench/bench.js +260 -0
  28. package/dist/bench/fake-carry.d.ts +32 -0
  29. package/dist/bench/fake-carry.js +56 -0
  30. package/dist/bench/fake-clock.d.ts +17 -0
  31. package/dist/bench/fake-clock.js +34 -0
  32. package/dist/bench/fake-custody.d.ts +9 -0
  33. package/dist/bench/fake-custody.js +10 -0
  34. package/dist/bench/fake-faculty.d.ts +25 -0
  35. package/dist/bench/fake-faculty.js +48 -0
  36. package/dist/bench/fake-keys.d.ts +5 -0
  37. package/dist/bench/fake-keys.js +9 -0
  38. package/dist/bench/fake-memory.d.ts +14 -0
  39. package/dist/bench/fake-memory.js +46 -0
  40. package/dist/bench/fake-network.d.ts +57 -0
  41. package/dist/bench/fake-network.js +169 -0
  42. package/dist/bench/index.d.ts +11 -0
  43. package/dist/bench/index.js +13 -0
  44. package/dist/bench/seeded.d.ts +11 -0
  45. package/dist/bench/seeded.js +37 -0
  46. package/dist/bench/settle.d.ts +2 -0
  47. package/dist/bench/settle.js +49 -0
  48. package/dist/bench/stewards.d.ts +17 -0
  49. package/dist/bench/stewards.js +20 -0
  50. package/dist/bodies/class-list.d.ts +14 -0
  51. package/dist/bodies/class-list.js +33 -0
  52. package/dist/bodies/joined-carry.d.ts +23 -0
  53. package/dist/bodies/joined-carry.js +42 -0
  54. package/dist/bodies/noble-crypto.d.ts +22 -0
  55. package/dist/bodies/noble-crypto.js +88 -0
  56. package/dist/bodies/seed-keys.d.ts +10 -0
  57. package/dist/bodies/seed-keys.js +42 -0
  58. package/dist/bodies/strict-tools.d.ts +10 -0
  59. package/dist/bodies/strict-tools.js +306 -0
  60. package/dist/bodies/web-carry.d.ts +65 -0
  61. package/dist/bodies/web-carry.js +224 -0
  62. package/dist/bodies/web-clock.d.ts +12 -0
  63. package/dist/bodies/web-clock.js +33 -0
  64. package/dist/browser/browser-ground.d.ts +69 -0
  65. package/dist/browser/browser-ground.js +219 -0
  66. package/dist/browser/index.d.ts +4 -0
  67. package/dist/browser/index.js +7 -0
  68. package/dist/browser/indexeddb-memory.d.ts +17 -0
  69. package/dist/browser/indexeddb-memory.js +97 -0
  70. package/dist/browser/locked-custody.d.ts +25 -0
  71. package/dist/browser/locked-custody.js +89 -0
  72. package/dist/browser/origin-classes.d.ts +7 -0
  73. package/dist/browser/origin-classes.js +31 -0
  74. package/dist/foundation.d.ts +150 -0
  75. package/dist/foundation.js +4 -0
  76. package/dist/ground/ground.d.ts +177 -0
  77. package/dist/ground/ground.js +363 -0
  78. package/dist/house/crossing.d.ts +37 -0
  79. package/dist/house/crossing.js +131 -0
  80. package/dist/house/house.d.ts +80 -0
  81. package/dist/house/house.js +1753 -0
  82. package/dist/house/rows-shape.d.ts +103 -0
  83. package/dist/house/rows-shape.js +12 -0
  84. package/dist/house/rows.d.ts +27 -0
  85. package/dist/house/rows.js +168 -0
  86. package/dist/index.d.ts +29 -1
  87. package/dist/index.js +25 -14
  88. package/dist/node/bridge.d.ts +17 -0
  89. package/dist/node/bridge.js +188 -0
  90. package/dist/node/cli.js +170 -0
  91. package/dist/node/custody.d.ts +20 -0
  92. package/dist/node/custody.js +34 -0
  93. package/dist/node/file-keys.d.ts +7 -0
  94. package/dist/node/file-keys.js +30 -0
  95. package/dist/node/file-memory.d.ts +13 -0
  96. package/dist/node/file-memory.js +91 -0
  97. package/dist/node/folder-classes.d.ts +5 -0
  98. package/dist/node/folder-classes.js +40 -0
  99. package/dist/node/gone.d.ts +2 -0
  100. package/dist/node/gone.js +7 -0
  101. package/dist/node/hand.d.ts +29 -0
  102. package/dist/node/hand.js +85 -0
  103. package/dist/node/held-keys.d.ts +14 -0
  104. package/dist/node/held-keys.js +36 -0
  105. package/dist/node/http.d.ts +11 -0
  106. package/dist/node/http.js +90 -0
  107. package/dist/node/index.d.ts +13 -0
  108. package/dist/node/index.js +15 -0
  109. package/dist/node/keychain-keys.d.ts +10 -0
  110. package/dist/node/keychain-keys.js +51 -0
  111. package/dist/node/ledger-memory.d.ts +17 -0
  112. package/dist/node/ledger-memory.js +151 -0
  113. package/dist/node/lock.d.ts +10 -0
  114. package/dist/node/lock.js +78 -0
  115. package/dist/node/node-ground.d.ts +33 -0
  116. package/dist/node/node-ground.js +147 -0
  117. package/dist/node/notify.d.ts +1 -0
  118. package/dist/node/notify.js +18 -0
  119. package/dist/node/tcp-carry.d.ts +39 -0
  120. package/dist/node/tcp-carry.js +216 -0
  121. package/dist/node/websocket.d.ts +6 -0
  122. package/dist/node/websocket.js +118 -0
  123. package/dist/quo/frame.d.ts +20 -0
  124. package/dist/quo/frame.js +48 -0
  125. package/dist/quo/room.d.ts +154 -0
  126. package/dist/quo/room.js +362 -0
  127. package/dist/serve/index.d.ts +27 -0
  128. package/dist/serve/index.js +55 -0
  129. package/package.json +49 -47
  130. package/dist/browser.d.ts +0 -1
  131. package/dist/browser.js +0 -8
  132. package/dist/cli.js +0 -16
  133. package/dist/core/being/being.d.ts +0 -17
  134. package/dist/core/being/being.js +0 -72
  135. package/dist/core/being/digest.d.ts +0 -3
  136. package/dist/core/being/digest.js +0 -17
  137. package/dist/core/being/faculty.d.ts +0 -10
  138. package/dist/core/being/faculty.js +0 -77
  139. package/dist/core/being/index.d.ts +0 -6
  140. package/dist/core/being/index.js +0 -8
  141. package/dist/core/being/kind.d.ts +0 -6
  142. package/dist/core/being/kind.js +0 -34
  143. package/dist/core/being/types.d.ts +0 -61
  144. package/dist/core/being/types.js +0 -4
  145. package/dist/core/being/words.d.ts +0 -13
  146. package/dist/core/being/words.js +0 -14
  147. package/dist/core/browser/index.d.ts +0 -37
  148. package/dist/core/browser/index.js +0 -196
  149. package/dist/core/browser/worker.d.ts +0 -11
  150. package/dist/core/browser/worker.js +0 -37
  151. package/dist/core/cli/command.d.ts +0 -13
  152. package/dist/core/cli/command.js +0 -330
  153. package/dist/core/cli/edge.d.ts +0 -7
  154. package/dist/core/cli/edge.js +0 -79
  155. package/dist/core/cli/harbor.d.ts +0 -25
  156. package/dist/core/cli/harbor.js +0 -115
  157. package/dist/core/contract/index.d.ts +0 -42
  158. package/dist/core/contract/index.js +0 -34
  159. package/dist/core/contract/link.d.ts +0 -4
  160. package/dist/core/contract/link.js +0 -45
  161. package/dist/core/crypto/aes.d.ts +0 -3
  162. package/dist/core/crypto/aes.js +0 -24
  163. package/dist/core/crypto/bytes.d.ts +0 -6
  164. package/dist/core/crypto/bytes.js +0 -33
  165. package/dist/core/crypto/ed25519.d.ts +0 -3
  166. package/dist/core/crypto/ed25519.js +0 -93
  167. package/dist/core/crypto/hash.d.ts +0 -2
  168. package/dist/core/crypto/hash.js +0 -11
  169. package/dist/core/crypto/index.d.ts +0 -7
  170. package/dist/core/crypto/index.js +0 -10
  171. package/dist/core/crypto/json.d.ts +0 -8
  172. package/dist/core/crypto/json.js +0 -250
  173. package/dist/core/crypto/mlkem.d.ts +0 -12
  174. package/dist/core/crypto/mlkem.js +0 -36
  175. package/dist/core/crypto/subtle.d.ts +0 -3
  176. package/dist/core/crypto/subtle.js +0 -11
  177. package/dist/core/crypto/x25519.d.ts +0 -2
  178. package/dist/core/crypto/x25519.js +0 -24
  179. package/dist/core/edge/index.d.ts +0 -164
  180. package/dist/core/edge/index.js +0 -739
  181. package/dist/core/edge/kit.d.ts +0 -2
  182. package/dist/core/edge/kit.js +0 -2
  183. package/dist/core/folder/index.d.ts +0 -18
  184. package/dist/core/folder/index.js +0 -130
  185. package/dist/core/git/http.d.ts +0 -6
  186. package/dist/core/git/http.js +0 -102
  187. package/dist/core/git/index.d.ts +0 -5
  188. package/dist/core/git/index.js +0 -10
  189. package/dist/core/git/object.d.ts +0 -40
  190. package/dist/core/git/object.js +0 -103
  191. package/dist/core/git/pack.d.ts +0 -9
  192. package/dist/core/git/pack.js +0 -171
  193. package/dist/core/git/store.d.ts +0 -23
  194. package/dist/core/git/store.js +0 -120
  195. package/dist/core/git/zlib.d.ts +0 -6
  196. package/dist/core/git/zlib.js +0 -210
  197. package/dist/core/harbor/carrying.d.ts +0 -51
  198. package/dist/core/harbor/carrying.js +0 -69
  199. package/dist/core/harbor/catalogue.d.ts +0 -33
  200. package/dist/core/harbor/catalogue.js +0 -365
  201. package/dist/core/harbor/dna.d.ts +0 -13
  202. package/dist/core/harbor/dna.js +0 -120
  203. package/dist/core/harbor/dock.d.ts +0 -62
  204. package/dist/core/harbor/dock.js +0 -381
  205. package/dist/core/harbor/harbor.d.ts +0 -16
  206. package/dist/core/harbor/harbor.js +0 -438
  207. package/dist/core/harbor/index.d.ts +0 -11
  208. package/dist/core/harbor/index.js +0 -14
  209. package/dist/core/harbor/memory.d.ts +0 -10
  210. package/dist/core/harbor/memory.js +0 -25
  211. package/dist/core/harbor/package.d.ts +0 -20
  212. package/dist/core/harbor/package.js +0 -172
  213. package/dist/core/harbor/probe.d.ts +0 -22
  214. package/dist/core/harbor/probe.js +0 -15
  215. package/dist/core/harbor/registry.d.ts +0 -15
  216. package/dist/core/harbor/registry.js +0 -110
  217. package/dist/core/harbor/root-line.d.ts +0 -7
  218. package/dist/core/harbor/root-line.js +0 -9
  219. package/dist/core/harbor/terrain.d.ts +0 -25
  220. package/dist/core/harbor/terrain.js +0 -28
  221. package/dist/core/http/index.d.ts +0 -2
  222. package/dist/core/http/index.js +0 -154
  223. package/dist/core/http/websocket.d.ts +0 -29
  224. package/dist/core/http/websocket.js +0 -133
  225. package/dist/core/line/answer.d.ts +0 -8
  226. package/dist/core/line/answer.js +0 -60
  227. package/dist/core/line/frame.d.ts +0 -29
  228. package/dist/core/line/frame.js +0 -77
  229. package/dist/core/line/ground.d.ts +0 -3
  230. package/dist/core/line/ground.js +0 -7
  231. package/dist/core/line/index.d.ts +0 -4
  232. package/dist/core/line/index.js +0 -8
  233. package/dist/core/line/web.d.ts +0 -12
  234. package/dist/core/line/web.js +0 -117
  235. package/dist/core/node/index.d.ts +0 -23
  236. package/dist/core/node/index.js +0 -124
  237. package/dist/core/pointer/bodies.d.ts +0 -31
  238. package/dist/core/pointer/bodies.js +0 -118
  239. package/dist/core/pointer/index.d.ts +0 -2
  240. package/dist/core/pointer/index.js +0 -4
  241. package/dist/core/pointer/world.d.ts +0 -45
  242. package/dist/core/pointer/world.js +0 -106
  243. package/dist/core/proof/contracts.d.ts +0 -19
  244. package/dist/core/proof/contracts.js +0 -116
  245. package/dist/core/proof/expect.d.ts +0 -6
  246. package/dist/core/proof/expect.js +0 -18
  247. package/dist/core/proof/index.d.ts +0 -3
  248. package/dist/core/proof/index.js +0 -14
  249. package/dist/core/proof/law.d.ts +0 -25
  250. package/dist/core/proof/law.js +0 -715
  251. package/dist/core/quo/address.d.ts +0 -19
  252. package/dist/core/quo/address.js +0 -84
  253. package/dist/core/quo/door.d.ts +0 -34
  254. package/dist/core/quo/door.js +0 -174
  255. package/dist/core/quo/index.d.ts +0 -9
  256. package/dist/core/quo/index.js +0 -12
  257. package/dist/core/quo/invitation.d.ts +0 -8
  258. package/dist/core/quo/invitation.js +0 -19
  259. package/dist/core/quo/keys.d.ts +0 -40
  260. package/dist/core/quo/keys.js +0 -79
  261. package/dist/core/quo/payload.d.ts +0 -15
  262. package/dist/core/quo/payload.js +0 -55
  263. package/dist/core/quo/relations.d.ts +0 -40
  264. package/dist/core/quo/relations.js +0 -33
  265. package/dist/core/quo/reply.d.ts +0 -13
  266. package/dist/core/quo/reply.js +0 -35
  267. package/dist/core/quo/seal.d.ts +0 -42
  268. package/dist/core/quo/seal.js +0 -78
  269. package/dist/core/quo/standing.d.ts +0 -39
  270. package/dist/core/quo/standing.js +0 -90
  271. package/dist/core/tcp/index.d.ts +0 -27
  272. package/dist/core/tcp/index.js +0 -209
  273. package/dist/core/ward/allowance.d.ts +0 -14
  274. package/dist/core/ward/allowance.js +0 -32
  275. package/dist/core/ward/cells.d.ts +0 -7
  276. package/dist/core/ward/cells.js +0 -74
  277. package/dist/core/ward/house.d.ts +0 -67
  278. package/dist/core/ward/house.js +0 -473
  279. package/dist/core/ward/index.d.ts +0 -5
  280. package/dist/core/ward/index.js +0 -7
  281. package/dist/core/ward/partition.d.ts +0 -50
  282. package/dist/core/ward/partition.js +0 -66
  283. package/dist/core/ward/stance.d.ts +0 -29
  284. package/dist/core/ward/stance.js +0 -186
  285. package/dist/core/ward/ward.d.ts +0 -44
  286. package/dist/core/ward/ward.js +0 -140
  287. package/dist/core.d.ts +0 -10
  288. package/dist/core.js +0 -19
  289. package/dist/defaults/dock/index.d.ts +0 -4
  290. package/dist/defaults/dock/index.js +0 -11
  291. package/dist/defaults/index.d.ts +0 -5
  292. package/dist/defaults/index.js +0 -10
  293. package/dist/defaults/memory/index.d.ts +0 -12
  294. package/dist/defaults/memory/index.js +0 -39
  295. package/dist/defaults/tcp/index.d.ts +0 -16
  296. package/dist/defaults/tcp/index.js +0 -82
  297. package/dist/defaults/ward/index.d.ts +0 -4
  298. package/dist/defaults/ward/index.js +0 -11
  299. package/dist/defaults/web/index.d.ts +0 -15
  300. package/dist/defaults/web/index.js +0 -90
  301. package/dist/edge.d.ts +0 -1
  302. package/dist/edge.js +0 -9
  303. package/dist/kit.d.ts +0 -4
  304. package/dist/kit.js +0 -12
  305. package/dist/node.d.ts +0 -1
  306. package/dist/node.js +0 -8
  307. package/src/browser.ts +0 -10
  308. package/src/cli.ts +0 -15
  309. package/src/core/being/being.ts +0 -84
  310. package/src/core/being/digest.ts +0 -18
  311. package/src/core/being/faculty.ts +0 -76
  312. package/src/core/being/index.ts +0 -8
  313. package/src/core/being/kind.ts +0 -37
  314. package/src/core/being/types.ts +0 -74
  315. package/src/core/being/words.ts +0 -31
  316. package/src/core/browser/index.ts +0 -219
  317. package/src/core/browser/worker.ts +0 -63
  318. package/src/core/cli/command.ts +0 -311
  319. package/src/core/cli/edge.ts +0 -86
  320. package/src/core/cli/harbor.ts +0 -120
  321. package/src/core/contract/index.ts +0 -82
  322. package/src/core/contract/link.ts +0 -58
  323. package/src/core/crypto/aes.ts +0 -26
  324. package/src/core/crypto/bytes.ts +0 -37
  325. package/src/core/crypto/ed25519.ts +0 -92
  326. package/src/core/crypto/hash.ts +0 -14
  327. package/src/core/crypto/index.ts +0 -10
  328. package/src/core/crypto/json.ts +0 -241
  329. package/src/core/crypto/mlkem.ts +0 -38
  330. package/src/core/crypto/subtle.ts +0 -13
  331. package/src/core/crypto/x25519.ts +0 -23
  332. package/src/core/edge/index.ts +0 -865
  333. package/src/core/folder/index.ts +0 -137
  334. package/src/core/git/http.ts +0 -103
  335. package/src/core/git/index.ts +0 -10
  336. package/src/core/git/object.ts +0 -116
  337. package/src/core/git/pack.ts +0 -147
  338. package/src/core/git/store.ts +0 -122
  339. package/src/core/git/zlib.ts +0 -197
  340. package/src/core/harbor/carrying.ts +0 -124
  341. package/src/core/harbor/catalogue.ts +0 -386
  342. package/src/core/harbor/dna.ts +0 -116
  343. package/src/core/harbor/dock.ts +0 -420
  344. package/src/core/harbor/harbor.ts +0 -433
  345. package/src/core/harbor/index.ts +0 -14
  346. package/src/core/harbor/memory.ts +0 -31
  347. package/src/core/harbor/package.ts +0 -195
  348. package/src/core/harbor/probe.ts +0 -39
  349. package/src/core/harbor/registry.ts +0 -107
  350. package/src/core/harbor/root-line.ts +0 -19
  351. package/src/core/harbor/terrain.ts +0 -54
  352. package/src/core/http/index.ts +0 -147
  353. package/src/core/http/websocket.ts +0 -124
  354. package/src/core/line/answer.ts +0 -60
  355. package/src/core/line/frame.ts +0 -83
  356. package/src/core/line/ground.ts +0 -18
  357. package/src/core/line/index.ts +0 -8
  358. package/src/core/line/web.ts +0 -123
  359. package/src/core/node/index.ts +0 -139
  360. package/src/core/pointer/bodies.ts +0 -122
  361. package/src/core/pointer/index.ts +0 -4
  362. package/src/core/pointer/world.ts +0 -134
  363. package/src/core/proof/contracts.ts +0 -134
  364. package/src/core/proof/expect.ts +0 -25
  365. package/src/core/proof/index.ts +0 -14
  366. package/src/core/proof/law.ts +0 -742
  367. package/src/core/quo/address.ts +0 -87
  368. package/src/core/quo/door.ts +0 -180
  369. package/src/core/quo/index.ts +0 -12
  370. package/src/core/quo/invitation.ts +0 -19
  371. package/src/core/quo/keys.ts +0 -91
  372. package/src/core/quo/payload.ts +0 -61
  373. package/src/core/quo/relations.ts +0 -62
  374. package/src/core/quo/reply.ts +0 -38
  375. package/src/core/quo/seal.ts +0 -101
  376. package/src/core/quo/standing.ts +0 -111
  377. package/src/core/stand/main.ts +0 -20
  378. package/src/core/stand/stand.ts +0 -259
  379. package/src/core/tcp/index.ts +0 -227
  380. package/src/core/ward/allowance.ts +0 -47
  381. package/src/core/ward/cells.ts +0 -77
  382. package/src/core/ward/house.ts +0 -524
  383. package/src/core/ward/index.ts +0 -7
  384. package/src/core/ward/partition.ts +0 -117
  385. package/src/core/ward/stance.ts +0 -207
  386. package/src/core/ward/ward.ts +0 -166
  387. package/src/core.ts +0 -55
  388. package/src/defaults/dock/index.ts +0 -12
  389. package/src/defaults/index.ts +0 -10
  390. package/src/defaults/memory/index.ts +0 -50
  391. package/src/defaults/tcp/index.ts +0 -89
  392. package/src/defaults/ward/index.ts +0 -12
  393. package/src/defaults/web/index.ts +0 -97
  394. package/src/edge.ts +0 -11
  395. package/src/index.ts +0 -18
  396. package/src/kit.ts +0 -15
  397. package/src/node.ts +0 -10
  398. /package/dist/{cli.d.ts → node/cli.d.ts} +0 -0
@@ -1,865 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // `nervur/edge`: the edge ground, Cloudflare Workers with a Durable Object
3
- // per harbor. The deployed worker is a shell that never runs a harbor's
4
- // code: each of its objects holds one harbor's storage and its web door,
5
- // and runs the whole harbor, the kit and the modules its `live` commit
6
- // holds, in a child worker Cloudflare's Worker Loader makes of them, as a
7
- // facet of the object keeping its storage. Moving `live` loads the next
8
- // child, so code reaches an edge with no deploy. The shell answers Quo
9
- // over the web, a post or a held line, since a worker takes no inbound
10
- // TCP, handing each box to the child; the harbor asks over TCP and the
11
- // web from the child.
12
- //
13
- // One worker carries many harbors, each a Durable Object by its name,
14
- // answering at `/<name>/quo`, and one secret, `NERVUR_SECRET`, for all of
15
- // them. A harbor born on another ground is carried here in a pack: by
16
- // name, its seed sealed under that secret and its sealed places, bundled
17
- // with the worker and landed the first time its child starts. So a
18
- // harbor is added by a deploy, never by a new secret. Its owner holds the
19
- // owner invitation minted where it was born, and pilots it over the web.
20
- //
21
- // m/<place>/<name> one sealed entry of the harbor's memory
22
- // c/seed the seed, sealed under a key from the secret
23
- //
24
- // A keep is one storage transaction, whole or nothing. The storage, state
25
- // and loader types name only what the kit uses, so no Cloudflare package
26
- // is a dependency.
27
- import type { JsonObject } from '../being/index.ts';
28
- import { Custody, Loader, Memory, moduleOf, type Entropy, type Module } from '../contract/index.ts';
29
- import { concat, decrypt, encrypt, hex, hkdf, isHex, sha256, unhex, utf8 } from '../crypto/index.ts';
30
- import { carrierIn, Harbor, landPlaces, probeGround, readCarried, UNDIALED, type Arrivals, type Carried, type Defaults, type Dialed, type GroundProbe, type Sockets, type TcpListening, type WebListen, type WebListener } from '../harbor/index.ts';
31
- import { answerHeld, answerPost, askFrame, FrameReader } from '../line/index.ts';
32
- import { CryptoEntropy, PointerTerrain } from '../pointer/index.ts';
33
- import { tcpAddress, tcpAt, type TcpAddress } from '../quo/index.ts';
34
-
35
- export type { Carried } from '../harbor/index.ts';
36
-
37
- export const EDGE = 'edge';
38
- const ENTRY = 'm/';
39
- const SEED = 'c/seed';
40
- // The most keys one storage call takes.
41
- const BATCH = 128;
42
- const SEAL_INFO = utf8('nervur edge custody');
43
- const MAX_ID = 0xffff_ffff;
44
-
45
- export interface EdgeStorage {
46
- get(key: string): Promise<unknown>;
47
- put(entries: Record<string, unknown>): Promise<void>;
48
- delete(keys: string[]): Promise<number>;
49
- list(options: { prefix: string }): Promise<Map<string, unknown>>;
50
- transaction<T>(work: (txn: EdgeStorage) => Promise<T>): Promise<T>;
51
- }
52
- export interface EdgeState {
53
- readonly storage: EdgeStorage;
54
- blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
55
- }
56
- export type EdgeEnv = { readonly NERVUR_SECRET?: string };
57
- // What the child hands its harbor: its state, the worker's environment,
58
- // the loader of the modules the child carries, its calls, and a packed
59
- // harbor to land where the storage keeps no seed yet.
60
- export type EdgeGiven = { readonly state: EdgeState; readonly env: EdgeEnv; readonly loader?: EdgeLoader; readonly calls?: ChildCalls; readonly package?: Packed };
61
-
62
- // A module a child carries: the module its worker imported, by its blob's
63
- // id.
64
- export type CarriedModule = { readonly blob: string; readonly module: unknown };
65
-
66
- // Which call of a child runs now: an `AsyncLocalStorage` the child's main
67
- // module hands in.
68
- export type CallContext = { run<R>(call: number, work: () => R): R; getStore(): number | undefined };
69
-
70
- // The calls of a child, each by the number the shell gave it, and what
71
- // each did to the memory: a miss or a refused keep is told to the call it
72
- // happened in and to no other, and so is a keep that landed. A call that
73
- // was refused and kept nothing is asked again whole in the next child. One
74
- // that kept a step before it was refused, a line's seq before it asked
75
- // out, is not, since a box asked again would find that step taken: it
76
- // answers what the child answered, as after a crash.
77
- export class ChildCalls {
78
- readonly #context: CallContext | undefined;
79
- readonly #refused = new Set<number>();
80
- readonly #kept = new Set<number>();
81
- constructor(context?: CallContext) {
82
- this.#context = context;
83
- }
84
-
85
- run<R>(call: number, work: () => R): R {
86
- return this.#context ? this.#context.run(call, work) : work();
87
- }
88
-
89
- // The call running now missed a module, or had a keep refused.
90
- refused(): void {
91
- const call = this.#context?.getStore();
92
- if (call !== undefined) this.#refused.add(call);
93
- }
94
-
95
- // A keep of the call running now landed.
96
- kept(): void {
97
- const call = this.#context?.getStore();
98
- if (call !== undefined) this.#kept.add(call);
99
- }
100
-
101
- // Whether a call that ended kept nothing and was refused, so it is asked
102
- // again; told once per call.
103
- again(call: number): boolean {
104
- const again = this.#refused.delete(call) && !this.#kept.has(call);
105
- this.#kept.delete(call);
106
- return again;
107
- }
108
- }
109
-
110
- // The edge's loader: the modules its child was loaded with, each found by
111
- // its blob's id. A blob it does not carry is a miss, kept with its source,
112
- // so the shell loads the next child carrying it and asks again; a blob the
113
- // shell found does not load here is refused with its reason. From the
114
- // first miss the child is spent and its memory keeps nothing more, since a
115
- // ward keeps every row it was told at once and the call that missed told
116
- // some, the seq of the box it opened among them.
117
- export class EdgeLoader extends Loader {
118
- readonly #carried: ReadonlyMap<string, unknown>;
119
- readonly #refused = new Map<string, string>();
120
- readonly #missed = new Map<string, string>();
121
- readonly #calls: ChildCalls | undefined;
122
- constructor(carried: readonly CarriedModule[] = [], calls?: ChildCalls) {
123
- super();
124
- this.#carried = new Map(carried.map((c) => [c.blob, c.module]));
125
- this.#calls = calls;
126
- }
127
-
128
- get carried(): string[] {
129
- return [...this.#carried.keys()];
130
- }
131
-
132
- refuse(refused: Readonly<Record<string, string>>): void {
133
- for (const [blob, reason] of Object.entries(refused)) this.#refused.set(blob, reason);
134
- }
135
-
136
- get refused(): ReadonlyMap<string, string> {
137
- return this.#refused;
138
- }
139
-
140
- load(source: string, id: string): Promise<Module> {
141
- if (this.#carried.has(id)) return Promise.resolve().then(() => moduleOf(this.#carried.get(id)));
142
- const refused = this.#refused.get(id);
143
- if (refused !== undefined) return Promise.reject(new Error(refused));
144
- this.#missed.set(id, source);
145
- this.#calls?.refused();
146
- return Promise.reject(new Error(`blob ${id} is carried by the next child`));
147
- }
148
-
149
- // Whether the child missed a blob, so it is spent.
150
- get missing(): boolean {
151
- return this.#missed.size > 0;
152
- }
153
-
154
- // Every blob the child missed, with its source.
155
- missed(): Map<string, string> {
156
- return new Map(this.#missed);
157
- }
158
- }
159
-
160
- // One harbor packed for an edge: its seed sealed under the worker's
161
- // secret, as hex of the nonce and the sealed bytes, and its sealed places.
162
- // A pack is every harbor of a worker, by name.
163
- export type Packed = { readonly sealed: string; readonly places: Carried['places'] };
164
- export type Pack = Record<string, Packed>;
165
-
166
- // A harbor's name on the edge: one path segment, so it is where the
167
- // harbor answers, `/<name>/quo`.
168
- export const isHarborName = (name: unknown): name is string => typeof name === 'string' && /^[a-z0-9][a-z0-9-]{0,62}$/.test(name);
169
- const NONCE = 12;
170
-
171
- // A pack as JSON reads, or null for anything else.
172
- export const readPack = (value: unknown): Pack | null => {
173
- if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
174
- const pack: Record<string, Packed> = {};
175
- for (const [name, entry] of Object.entries(value)) {
176
- const { sealed, places } = (entry ?? {}) as { sealed?: unknown; places?: unknown };
177
- const carried = readCarried({ places });
178
- if (!isHarborName(name) || !isHex(sealed) || (sealed as string).length <= NONCE * 2 || carried === null) return null;
179
- pack[name] = { sealed: sealed as string, places: carried.places };
180
- }
181
- return pack;
182
- };
183
-
184
- const sealKey = (secret: string): Promise<Uint8Array> => hkdf(utf8(secret), SEAL_INFO, 32);
185
-
186
- // A seed sealed under a worker's secret, as a pack keeps it.
187
- export const sealSeed = async (secret: string, seed: Uint8Array, entropy: Entropy = new CryptoEntropy()): Promise<string> => {
188
- const nonce = entropy.draw(NONCE);
189
- return hex(concat(nonce, await encrypt(await sealKey(secret), nonce, new Uint8Array(0), seed)));
190
- };
191
-
192
- const chunks = <T>(items: readonly T[]): T[][] => Array.from({ length: Math.ceil(items.length / BATCH) }, (_, i) => items.slice(i * BATCH, (i + 1) * BATCH));
193
- const bytesOf = (value: unknown): Uint8Array => new Uint8Array(value instanceof ArrayBuffer ? value : (value as Uint8Array));
194
-
195
- // A body keeps its keys under `root`, so one object may hold more than one
196
- // keeping.
197
- // A body refuses every keep while `voided` says so: in a child, from the
198
- // moment its loader misses a module, so a spent child keeps nothing more,
199
- // and tells the child's calls which keeps it refused and which landed.
200
- export class EdgeMemory extends Memory {
201
- readonly #storage: EdgeStorage;
202
- readonly #entry: string;
203
- readonly #voided: () => boolean;
204
- readonly #calls: ChildCalls | undefined;
205
- constructor(storage: EdgeStorage, root = '', voided: () => boolean = () => false, calls?: ChildCalls) {
206
- super();
207
- this.#storage = storage;
208
- this.#entry = `${root}${ENTRY}`;
209
- this.#voided = voided;
210
- this.#calls = calls;
211
- }
212
-
213
- #refuse(): Promise<void> | null {
214
- if (!this.#voided()) return null;
215
- this.#calls?.refused();
216
- return Promise.reject(new Error('a module was missed, so this call keeps nothing'));
217
- }
218
-
219
- async read(place: string): Promise<Map<string, Uint8Array>> {
220
- const prefix = `${this.#entry}${place}/`;
221
- const listed = await this.#storage.list({ prefix });
222
- return new Map([...listed].map(([key, value]) => [key.slice(prefix.length), bytesOf(value).slice()]));
223
- }
224
-
225
- write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void> {
226
- const refused = this.#refuse();
227
- if (refused) return refused;
228
- const put: [string, Uint8Array][] = [];
229
- const gone: string[] = [];
230
- for (const [name, bytes] of entries) {
231
- if (bytes === null) gone.push(`${this.#entry}${place}/${name}`);
232
- else put.push([`${this.#entry}${place}/${name}`, bytes.slice()]);
233
- }
234
- return this.#storage
235
- .transaction(async (txn) => {
236
- for (const batch of chunks(put)) await txn.put(Object.fromEntries(batch));
237
- for (const batch of chunks(gone)) await txn.delete(batch);
238
- })
239
- .then(() => this.#calls?.kept());
240
- }
241
-
242
- async places(): Promise<string[]> {
243
- const entry = this.#entry;
244
- const keys = [...(await this.#storage.list({ prefix: entry })).keys()];
245
- return [...new Set(keys.map((key) => key.slice(entry.length, key.indexOf('/', entry.length))))];
246
- }
247
-
248
- async forget(place: string): Promise<void> {
249
- await this.#refuse();
250
- const keys = [...(await this.#storage.list({ prefix: `${this.#entry}${place}/` })).keys()];
251
- await this.#storage.transaction(async (txn) => {
252
- for (const batch of chunks(keys)) await txn.delete(batch);
253
- });
254
- this.#calls?.kept();
255
- }
256
- }
257
-
258
- // The seed, kept sealed under a key the worker's secret gives, so the
259
- // storage alone opens nothing: the one a pack landed, or one drawn the
260
- // first time it is asked for. A worker with no secret stands no harbor.
261
- export class EdgeCustody extends Custody {
262
- readonly #storage: EdgeStorage;
263
- readonly #secret: string | undefined;
264
- readonly #entropy: Entropy;
265
- readonly #key: string;
266
- constructor(storage: EdgeStorage, secret: string | undefined, entropy: Entropy = new CryptoEntropy(), root = '') {
267
- super();
268
- this.#storage = storage;
269
- this.#secret = secret;
270
- this.#entropy = entropy;
271
- this.#key = `${root}${SEED}`;
272
- }
273
-
274
- // Whether a seed is kept, so a pack lands only where none is.
275
- async kept(): Promise<boolean> {
276
- return (await this.#storage.get(this.#key)) !== undefined;
277
- }
278
-
279
- // A packed seed, sealed as `sealSeed` sealed it, kept as it came.
280
- async land(sealed: string, txn: EdgeStorage = this.#storage): Promise<void> {
281
- const bytes = unhex(sealed);
282
- await txn.put({ [this.#key]: { nonce: bytes.slice(0, NONCE), sealed: bytes.slice(NONCE) } });
283
- }
284
-
285
- async seed(): Promise<Uint8Array> {
286
- if (!this.#secret) throw new Error('the worker holds no NERVUR_SECRET');
287
- const key = await sealKey(this.#secret);
288
- return this.#storage.transaction(async (txn) => {
289
- const kept = (await txn.get(this.#key)) as { nonce: Uint8Array; sealed: Uint8Array } | undefined;
290
- if (kept) {
291
- const seed = await decrypt(key, bytesOf(kept.nonce), new Uint8Array(0), bytesOf(kept.sealed));
292
- if (seed === null) throw new Error('the seed does not open under this NERVUR_SECRET');
293
- return seed;
294
- }
295
- const seed = this.#entropy.draw(32);
296
- const nonce = this.#entropy.draw(NONCE);
297
- await txn.put({ [this.#key]: { nonce, sealed: await encrypt(key, nonce, new Uint8Array(0), seed) } });
298
- return seed.slice();
299
- });
300
- }
301
- }
302
-
303
- // One TCP line opened by the worker's `connect`, many asks on it.
304
- type EdgeSocket = {
305
- readonly opened: Promise<unknown>;
306
- readonly closed: Promise<unknown>;
307
- readonly readable: ReadableStream<Uint8Array>;
308
- readonly writable: WritableStream<Uint8Array>;
309
- close(): Promise<void>;
310
- };
311
- type Connect = (address: { hostname: string; port: number }) => EdgeSocket;
312
-
313
- class EdgeLine {
314
- readonly #socket: EdgeSocket;
315
- readonly #writer: WritableStreamDefaultWriter<Uint8Array>;
316
- readonly #waiting = new Map<number, (box: Uint8Array | null) => void>();
317
- #next = 1;
318
- #closed = false;
319
-
320
- constructor(socket: EdgeSocket, onClose: () => void) {
321
- this.#socket = socket;
322
- this.#writer = socket.writable.getWriter();
323
- const reader = new FrameReader();
324
- void (async () => {
325
- try {
326
- for await (const chunk of socket.readable) {
327
- for (const frame of reader.push(new Uint8Array(chunk))) {
328
- if (frame.kind === 'ask') continue;
329
- const done = this.#waiting.get(frame.id);
330
- this.#waiting.delete(frame.id);
331
- done?.(frame.kind === 'reply' ? frame.box : null);
332
- }
333
- if (reader.bad) break;
334
- }
335
- } catch {
336
- // A line that broke answers nothing more.
337
- }
338
- this.#closed = true;
339
- onClose();
340
- for (const done of this.#waiting.values()) done(null);
341
- this.#waiting.clear();
342
- await socket.close().catch(() => undefined);
343
- })();
344
- }
345
-
346
- ask(pk: Uint8Array, box: Uint8Array): Promise<Uint8Array | null> {
347
- if (this.#closed) return Promise.resolve(null);
348
- const id = this.#next;
349
- this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
350
- return new Promise((resolve) => {
351
- this.#waiting.set(id, resolve);
352
- this.#writer.write(askFrame(id, pk, box)).catch(() => resolve(null));
353
- });
354
- }
355
-
356
- close(): Promise<void> {
357
- this.#closed = true;
358
- return this.#socket.close().catch(() => undefined);
359
- }
360
- }
361
-
362
- const sockets = 'cloudflare:sockets';
363
- const connector = async (): Promise<Connect | null> => {
364
- try {
365
- return ((await import(/* @vite-ignore */ sockets)) as { connect: Connect }).connect;
366
- } catch {
367
- return null;
368
- }
369
- };
370
-
371
- // The worker's sockets under the TCP carrier: it dials on the worker's
372
- // `connect`, one line per address, and takes no inbound TCP.
373
- export class EdgeSockets implements Sockets {
374
- readonly #lines = new Map<string, Promise<EdgeLine | null>>();
375
-
376
- async dial(address: string, pk: string, bytes: Uint8Array): Promise<Dialed> {
377
- const at = tcpAddress(address);
378
- if (at === null || !isHex(pk, 64)) return UNDIALED;
379
- const line = await this.#line(at);
380
- return line ? line.ask(unhex(pk), bytes) : UNDIALED;
381
- }
382
-
383
- #line({ host, port }: TcpAddress): Promise<EdgeLine | null> {
384
- const at = tcpAt(host, port);
385
- let line = this.#lines.get(at);
386
- if (!line) {
387
- line = (async () => {
388
- const connect = await connector();
389
- if (!connect) return null;
390
- try {
391
- const socket = connect({ hostname: host, port });
392
- await socket.opened;
393
- return new EdgeLine(socket, () => this.#lines.delete(at));
394
- } catch {
395
- return null;
396
- }
397
- })();
398
- this.#lines.set(at, line);
399
- void line.then((open) => open ?? this.#lines.delete(at));
400
- }
401
- return line;
402
- }
403
-
404
- listen(): Promise<TcpListening> {
405
- return Promise.reject(new Error('an edge takes no inbound TCP'));
406
- }
407
-
408
- async close(): Promise<void> {
409
- const lines = [...this.#lines.values()];
410
- this.#lines.clear();
411
- for (const line of await Promise.all(lines)) await line?.close();
412
- }
413
- }
414
-
415
- // Where the edge's web carrier listens: the object's own requests, at the
416
- // host and path its cells name. A port of 0 is the edge's own https.
417
- type Listening = { at: WebListen | undefined };
418
- const edgeServe = (listening: Listening, listen: WebListen): Promise<WebListener> => {
419
- listening.at = listen;
420
- const authority = listen.port === 0 ? listen.host : `${tcpAt(listen.host, listen.port).slice('tcp://'.length)}`;
421
- const [post, held] = listen.port === 0 ? ['https', 'wss'] : ['http', 'ws'];
422
- return Promise.resolve({
423
- port: listen.port,
424
- at: [`${post}://${authority}${listen.path}`, `${held}://${authority}${listen.path}`],
425
- close: () => {
426
- if (listening.at === listen) listening.at = undefined;
427
- return Promise.resolve();
428
- },
429
- });
430
- };
431
-
432
- export type EdgeParts = { readonly given: EdgeGiven; readonly defaults: Defaults };
433
-
434
- export class EdgeTerrain extends PointerTerrain {
435
- readonly given: EdgeGiven;
436
- readonly #listening: Listening;
437
-
438
- constructor(parts: EdgeParts) {
439
- const entropy = new CryptoEntropy();
440
- const { storage } = parts.given.state;
441
- const loader = parts.given.loader ?? new EdgeLoader();
442
- const listening: Listening = { at: undefined };
443
- super({
444
- entropy,
445
- loader,
446
- custody: new EdgeCustody(storage, parts.given.env.NERVUR_SECRET, entropy),
447
- memory: new EdgeMemory(storage, '', () => loader.missing, parts.given.calls),
448
- tcp: () => new EdgeSockets(),
449
- web: (_ground, listen) => edgeServe(listening, listen),
450
- defaults: parts.defaults,
451
- });
452
- this.given = parts.given;
453
- this.#listening = listening;
454
- }
455
-
456
- // Where the web carrier listens now, which the shell answers at.
457
- get listening(): WebListen | undefined {
458
- return this.#listening.at;
459
- }
460
-
461
- // A packed harbor lands where the object keeps no seed yet: its places
462
- // first and its seed last, so a start cut short lands it whole again,
463
- // and once the seed is kept the storage is the harbor and a new deploy
464
- // leaves it be. A seed sealed under another secret does not open.
465
- override async claim(): Promise<void> {
466
- const packed = this.given.package;
467
- const custody = this.custody as EdgeCustody;
468
- if (!packed || (await custody.kept())) return;
469
- await landPlaces(this.memory, { places: packed.places });
470
- await custody.land(packed.sealed);
471
- }
472
-
473
- }
474
-
475
- const isGiven = (value: unknown): value is EdgeGiven => {
476
- const given = value as Partial<EdgeGiven> | null;
477
- return typeof given?.state?.storage?.transaction === 'function' && typeof given.env === 'object';
478
- };
479
-
480
- // A harbor handed a Durable Object's state and environment, and the
481
- // loader of the modules its child carries.
482
- export const edgeGround: GroundProbe = {
483
- name: EDGE,
484
- fits: ({ given }) => isGiven(given),
485
- terrain: ({ given }, defaults) => new EdgeTerrain({ given: given as EdgeGiven, defaults }),
486
- };
487
-
488
- // The harbor a request is for: the first segment of its path.
489
- export const harborNameOf = (request: Request): string | null => {
490
- const name = new URL(request.url).pathname.split('/')[1];
491
- return isHarborName(name) ? name : null;
492
- };
493
-
494
- // What a child tells its shell after a call, where the shell must move:
495
- // the commit `live` names and its modules' blobs, the modules the next
496
- // child carries, by blob with their sources, and the blobs the child
497
- // missed, which the next child carries beside them.
498
- export type Wanted = {
499
- readonly live: string;
500
- readonly liveBlobs: readonly string[];
501
- readonly modules: Readonly<Record<string, string>>;
502
- readonly missed: readonly string[];
503
- };
504
- export type Opened = { readonly listen: WebListen | null; readonly wanted: Wanted | null };
505
- // `again` where the call kept nothing and was refused, by a miss of its own
506
- // or in a spent child, so it is asked again in the next child.
507
- export type Arrived = { readonly reply: Uint8Array | null; readonly wanted: Wanted | null; readonly again: boolean };
508
-
509
- // The key a child is loaded under: the commit `live` names, where the
510
- // child carries that commit's modules, and else that commit and every
511
- // blob the child carries.
512
- export const childKey = (live: string, blobs: readonly string[], liveBlobs: readonly string[]): string => {
513
- const carried = [...new Set(blobs)].sort();
514
- const same = carried.length === new Set(liveBlobs).size && liveBlobs.every((b) => carried.includes(b));
515
- return same ? live : `${live}+${carried.join('+')}`;
516
- };
517
-
518
- // The whole harbor inside a child: its bodies on the storage the facet
519
- // keeps, its loader over the modules the child's worker imported, and the
520
- // web door the shell hands each box through. It opens the harbor once, and
521
- // after every call says whether the shell must move to another child.
522
- export class HarborChild {
523
- readonly #state: EdgeState;
524
- readonly #env: EdgeEnv;
525
- readonly #key: string;
526
- readonly #loader: EdgeLoader;
527
- readonly #calls: ChildCalls;
528
- #opening: Promise<Harbor> | undefined;
529
- // The terrain the child opened its harbor on, which says where its web
530
- // carrier listens.
531
- #terrain: EdgeTerrain | undefined;
532
- #settled: string | undefined;
533
-
534
- constructor(state: EdgeState, env: EdgeEnv, key: string, carried: readonly CarriedModule[], context: CallContext) {
535
- this.#state = state;
536
- this.#env = env;
537
- this.#key = key;
538
- this.#calls = new ChildCalls(context);
539
- this.#loader = new EdgeLoader(carried, this.#calls);
540
- }
541
-
542
- // The harbor, opened inside the object's block so no call reaches it
543
- // half open, its web carrier listening at `/<name>/quo`. A child that
544
- // missed a module of `live` as the harbor booted stands nothing more:
545
- // the shell moves to the next before it hands this one a call.
546
- async open(name: string, host: string, port: number, packed: Packed | null, refused: Readonly<Record<string, string>>): Promise<Opened> {
547
- this.#loader.refuse(refused);
548
- this.#opening ??= this.#state.blockConcurrencyWhile(async () => {
549
- const terrain = probeGround({ given: { state: this.#state, env: this.#env, loader: this.#loader, calls: this.#calls, ...(packed ? { package: packed } : {}) } }).terrain as EdgeTerrain;
550
- this.#terrain = terrain;
551
- const harbor = await Harbor.open(terrain);
552
- if (this.#loader.missing) return harbor;
553
- await harbor.ask({ method: 'stand' });
554
- const { beings } = ((await harbor.ask({})).answer as { notes: { beings: Record<string, { class: string }> } }).notes;
555
- const web = carrierIn(beings, terrain.defaults.classes, 'web');
556
- if (web === null) throw new Error('no class this edge runs carries web');
557
- if (web.open !== undefined) await harbor.ask(web.open);
558
- if (terrain.listening?.path !== `/${name}/quo`) await harbor.ask({ method: 'ask', args: { being: web.key, method: 'listen', args: { host, port, path: `/${name}/quo` } } });
559
- return harbor;
560
- });
561
- const harbor = await this.#opening;
562
- const at = this.#terrain?.listening;
563
- // A plain copy, since what crosses to the shell is cloned.
564
- const listen = at ? { host: at.host, port: at.port, path: at.path, origins: [...at.origins] } : null;
565
- return { listen, wanted: await this.#wanted(harbor) };
566
- }
567
-
568
- // One box from the web door under the shell's number for the call, its
569
- // reply, and whether it is asked again. A call that lands in a spent
570
- // child is asked again untouched.
571
- async arrive(pk: string, box: Uint8Array, call: number): Promise<Arrived> {
572
- const harbor = await this.#opening;
573
- if (!harbor) return { reply: null, wanted: null, again: false };
574
- if (this.#loader.missing) return { reply: null, wanted: await this.#wanted(harbor), again: true };
575
- const reply = await this.#calls.run(call, () => harbor.arrive(pk, box)).catch(() => null);
576
- return { reply, wanted: await this.#wanted(harbor), again: this.#calls.again(call) };
577
- }
578
-
579
- async #wanted(harbor: Harbor): Promise<Wanted | null> {
580
- const missed = this.#loader.missed();
581
- const catalogue = async (method: string): Promise<JsonObject> => ((await harbor.ask({ method: 'ask', args: { being: 'catalogue', method } })).answer as { answer: JsonObject }).answer;
582
- const live = (await catalogue('live')).live as string;
583
- if (missed.size === 0 && live === this.#settled) return null;
584
- const sources = (await catalogue('sources')).sources as { blob: string; source: string }[];
585
- const modules = new Map(sources.map((s) => [s.blob, s.source]));
586
- for (const [blob, source] of missed) modules.set(blob, source);
587
- for (const blob of this.#loader.refused.keys()) modules.delete(blob);
588
- const liveBlobs = sources.map((s) => s.blob);
589
- if (missed.size === 0 && childKey(live, [...modules.keys()], liveBlobs) === this.#key) {
590
- this.#settled = live;
591
- return null;
592
- }
593
- return { live, liveBlobs, modules: Object.fromEntries(modules), missed: [...missed.keys()] };
594
- }
595
- }
596
-
597
- // The Worker Loader binding, and a child's facet, as the shell uses them.
598
- type WorkerCode = { readonly compatibilityDate: string; readonly compatibilityFlags?: readonly string[]; readonly mainModule: string; readonly modules: Record<string, { js: string }>; readonly env: Record<string, unknown> };
599
- type LoadedWorker = { getDurableObjectClass(name: string): unknown; getEntrypoint(): { fetch(request: Request): Promise<Response> } };
600
- export type WorkerLoader = { get(id: string, code: () => Promise<WorkerCode>): LoadedWorker };
601
- type ChildStub = {
602
- open(name: string, host: string, port: number, packed: Packed | null, refused: Record<string, string>): Promise<Opened>;
603
- arrive(pk: string, box: Uint8Array, call: number): Promise<Arrived>;
604
- };
605
- type Facets = { get(name: string, start: () => Promise<{ class: unknown }>): ChildStub; abort(name: string, reason: unknown): void };
606
- export type ShellState = { readonly facets: Facets };
607
- export type ShellEnv = EdgeEnv & { readonly LOADER?: WorkerLoader };
608
-
609
- const FACET = 'harbor';
610
- const CHILD = 'Harbor';
611
- const MAIN = 'harbor.js';
612
- const PROBE = 'probe.js';
613
- const BOOT = 'boot';
614
- const COMPATIBILITY = '2026-09-01';
615
- // The most moves one call makes before the shell gives up on it.
616
- const ROUNDS = 8;
617
- // A module's file at the child's root, where its `'nervur'` names the kit.
618
- const moduleFile = (blob: string): string => `${blob}.js`;
619
-
620
- // A child's main module: the kit's child as the facet's class, over the
621
- // modules it carries, each imported by its blob's file.
622
- const childMain = (key: string, blobs: readonly string[]): string =>
623
- [
624
- "import { DurableObject } from 'cloudflare:workers';",
625
- "import { AsyncLocalStorage } from 'node:async_hooks';",
626
- "import { HarborChild } from 'nervur';",
627
- ...blobs.map((b, i) => `import * as m${String(i)} from '${moduleFile(b)}';`),
628
- `const carried = [${blobs.map((b, i) => `{ blob: '${b}', module: m${String(i)} }`).join(', ')}];`,
629
- 'export class Harbor extends DurableObject {',
630
- ` #child = new HarborChild(this.ctx, this.env, ${JSON.stringify(key)}, carried, new AsyncLocalStorage());`,
631
- ' open(name, host, port, packed, refused) { return this.#child.open(name, host, port, packed, refused); }',
632
- ' arrive(pk, box, call) { return this.#child.arrive(pk, box, call); }',
633
- '}',
634
- '',
635
- ].join('\n');
636
-
637
- // A probe's main module: the module alone beside the kit, so one that
638
- // does not load fails here and never in a harbor's child.
639
- const probeMain = (blob: string): string => `import '${moduleFile(blob)}';\nexport default { fetch() { return new Response(null, { status: 204 }); } };\n`;
640
-
641
- const said = (e: unknown): string => (e instanceof Error ? e.message : String(e));
642
-
643
- // What a Durable Object class of harbors is made of:
644
- //
645
- // kit the kit as one module's text, `nervur/edge/kit`, which every
646
- // child runs and every module's `'nervur'` names
647
- // pack the harbors carried here, from `nervur edge pack`, each landed
648
- // the first time its child starts; or a function of the
649
- // worker's environment giving them
650
- export type HarborShellParts = {
651
- readonly kit: string;
652
- readonly pack: Pack | ((env: EdgeEnv) => Pack | undefined);
653
- };
654
- export type HarborShell = { fetch(request: Request): Promise<Response> };
655
- export type HarborShellClass = new (state: ShellState, env: ShellEnv) => HarborShell;
656
-
657
- // A Durable Object class whose every instance is one harbor, by the name
658
- // the worker reached it under, the one its pack holds under that name. The
659
- // object is a shell: it keeps the harbor's storage as its child's facet,
660
- // answers the web at `/<name>/quo`, and hands each box to the child it
661
- // loaded for the commit `live` names. A call that misses a module loads
662
- // the next child carrying it and is asked there again, since a miss comes
663
- // before `live` or any cell moves; a module that does not load on its own
664
- // is refused with its reason, and `live` stays. An object answers the one
665
- // name it opened under, and a name its pack does not hold is 404, with
666
- // nothing written. The shell never runs a harbor's code and reads no cell.
667
- export const harborShell = ({ kit, pack }: HarborShellParts): HarborShellClass =>
668
- class implements HarborShell {
669
- readonly #state: ShellState;
670
- readonly #env: ShellEnv;
671
- readonly #refused: Record<string, string> = {};
672
- #name: string | undefined;
673
- #packed: Packed | undefined;
674
- #at: { host: string; port: number } | undefined;
675
- #kit: Promise<string> | undefined;
676
- #key: string | undefined;
677
- #child: ChildStub | undefined;
678
- #listen: WebListen | null = null;
679
- #standing: Promise<void> | undefined;
680
- #turn: Promise<unknown> = Promise.resolve();
681
- // The number the shell gives each call it hands a child.
682
- #calls = 0;
683
- // The calls inside the child now, and the moves waiting for them to end.
684
- #inside = 0;
685
- #ended: (() => void)[] = [];
686
- // Where the child said the shell must go, the latest word of the child
687
- // that stands.
688
- #wanted: Wanted | undefined;
689
-
690
- constructor(state: ShellState, env: ShellEnv) {
691
- this.#state = state;
692
- this.#env = env;
693
- }
694
-
695
- async fetch(request: Request): Promise<Response> {
696
- const name = harborNameOf(request);
697
- if (name === null || (this.#name !== undefined && this.#name !== name)) return new Response(null, { status: 404 });
698
- if (this.#name === undefined) {
699
- const packed = typeof pack === 'function' ? pack(this.#env) : pack;
700
- const held = packed && Object.hasOwn(packed, name) ? packed[name] : undefined;
701
- if (!held) return new Response(null, { status: 404 });
702
- const url = new URL(request.url);
703
- this.#name = name;
704
- this.#packed = held;
705
- this.#at = { host: url.hostname, port: url.port === '' ? 0 : Number(url.port) };
706
- }
707
- this.#standing ??= this.#serial(() => this.#open(BOOT, {}, 0)).catch((e: unknown) => {
708
- this.#standing = undefined;
709
- throw e;
710
- });
711
- await this.#standing;
712
- const listen = this.#listen;
713
- if (!listen) return new Response(null, { status: 404 });
714
- const door: Arrivals = { arrive: (pk, box) => this.#arrive(pk, box) };
715
- if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') return answerPost(door, listen, request);
716
- const offered = (request.headers.get('sec-websocket-protocol') ?? '').split(',').map((p) => p.trim());
717
- if (new URL(request.url).pathname !== listen.path || !offered.includes('quo')) return new Response(null, { status: 400 });
718
- const Pair = (globalThis as unknown as { WebSocketPair: new () => EdgePair }).WebSocketPair;
719
- const pair = new Pair();
720
- const [client, server] = [pair[0], pair[1]];
721
- server.accept();
722
- const side = { send: (body: Uint8Array) => server.send(new Uint8Array(body)), close: () => server.close() };
723
- server.addEventListener('message', (event: MessageEvent) => answerHeld(door, side, typeof event.data === 'string' ? event.data : new Uint8Array(event.data as ArrayBuffer)));
724
- return new Response(null, { status: 101, webSocket: client, headers: { 'sec-websocket-protocol': 'quo' } } as ResponseInit);
725
- }
726
-
727
- // One move at a time.
728
- #serial<T>(work: () => Promise<T>): Promise<T> {
729
- const run = this.#turn.then(work, work);
730
- this.#turn = run.catch(() => undefined);
731
- return run;
732
- }
733
-
734
- #loader(): WorkerLoader {
735
- const loader = this.#env.LOADER;
736
- if (!loader) throw new Error('the worker binds no LOADER: its wrangler.toml names a worker_loaders binding');
737
- return loader;
738
- }
739
-
740
- #kitId(): Promise<string> {
741
- return (this.#kit ??= sha256(utf8(kit)).then((digest) => hex(digest).slice(0, 16)));
742
- }
743
-
744
- // The child under `key`, carrying `modules`, stood as the facet in the
745
- // one before's place, and opened. A staging child carries a blob a
746
- // call missed beside what `live` names, and keeps its key until that
747
- // call, asked again, moves `live`.
748
- async #open(key: string, modules: Readonly<Record<string, string>>, depth: number, staging = false): Promise<void> {
749
- const id = `${await this.#kitId()}:${key}`;
750
- const blobs = Object.keys(modules).sort();
751
- const secret = this.#env.NERVUR_SECRET;
752
- const code: WorkerCode = {
753
- compatibilityDate: COMPATIBILITY,
754
- // `AsyncLocalStorage` alone of Node's modules, to tell calls apart.
755
- compatibilityFlags: ['nodejs_als'],
756
- mainModule: MAIN,
757
- modules: { [MAIN]: { js: childMain(key, blobs) }, nervur: { js: kit }, ...Object.fromEntries(blobs.map((b) => [moduleFile(b), { js: modules[b]! }])) },
758
- env: secret === undefined ? {} : { NERVUR_SECRET: secret },
759
- };
760
- const worker = this.#loader().get(id, () => Promise.resolve(code));
761
- if (this.#child) this.#state.facets.abort(FACET, new Error('live moved'));
762
- const child = this.#state.facets.get(FACET, () => Promise.resolve({ class: worker.getDurableObjectClass(CHILD) }));
763
- this.#child = child;
764
- this.#key = key;
765
- this.#wanted = undefined;
766
- const opened = await child.open(this.#name!, this.#at!.host, this.#at!.port, this.#packed ?? null, { ...this.#refused });
767
- this.#listen = opened.listen;
768
- if (opened.wanted && (!staging || opened.wanted.missed.length > 0)) await this.#move(opened.wanted, depth + 1);
769
- }
770
-
771
- // Where a child said the shell must go: each blob it missed probed, one
772
- // that does not load refused, and the child for what remains loaded.
773
- // A call that missed kept nothing, so a fresh child takes it whole,
774
- // under the same key where every blob it missed is refused.
775
- async #move(wanted: Wanted, depth: number): Promise<void> {
776
- if (depth > ROUNDS) throw new Error('the harbor settles on no child');
777
- for (const blob of wanted.missed) {
778
- if (Object.hasOwn(this.#refused, blob)) continue;
779
- const reason = await this.#probe(blob, wanted.modules[blob] ?? '');
780
- if (reason !== null) this.#refused[blob] = reason;
781
- }
782
- const modules = Object.fromEntries(Object.entries(wanted.modules).filter(([blob]) => !Object.hasOwn(this.#refused, blob)));
783
- const key = childKey(wanted.live, Object.keys(modules), wanted.liveBlobs);
784
- const staging = wanted.missed.length > 0;
785
- if (key === this.#key && !staging) return;
786
- return this.#open(key, modules, depth, staging);
787
- }
788
-
789
- // Null where a module loads on its own beside the kit, and else why not.
790
- async #probe(blob: string, source: string): Promise<string | null> {
791
- const code: WorkerCode = {
792
- compatibilityDate: COMPATIBILITY,
793
- mainModule: PROBE,
794
- modules: { [PROBE]: { js: probeMain(blob) }, nervur: { js: kit }, [moduleFile(blob)]: { js: source } },
795
- env: {},
796
- };
797
- try {
798
- const worker = this.#loader().get(`${await this.#kitId()}:probe:${blob}`, () => Promise.resolve(code));
799
- const answered = await worker.getEntrypoint().fetch(new Request('https://probe.invalid/'));
800
- return answered.status === 204 ? null : `the module does not load on the edge: ${await answered.text()}`;
801
- } catch (e) {
802
- return `the module does not load on the edge: ${said(e)}`;
803
- }
804
- }
805
-
806
- // A box handed to the child, and its answer. Where the child says the
807
- // shell must move, the move waits until no call is inside, so none is
808
- // cut short, and no call enters while it waits. A call that missed a
809
- // module told nothing, and is asked again in the next child.
810
- async #arrive(pk: string, box: Uint8Array): Promise<Uint8Array | null> {
811
- for (let round = 0; round <= ROUNDS; round += 1) {
812
- let turn: Promise<unknown>;
813
- do {
814
- turn = this.#turn;
815
- await turn;
816
- } while (turn !== this.#turn);
817
- const child = this.#child!;
818
- const call = (this.#calls += 1);
819
- this.#inside += 1;
820
- let arrived: Arrived | null;
821
- try {
822
- arrived = await child.arrive(pk, box, call);
823
- } catch {
824
- arrived = null;
825
- } finally {
826
- this.#inside -= 1;
827
- if (this.#inside === 0) for (const end of this.#ended.splice(0)) end();
828
- }
829
- if (arrived === null) return null;
830
- if (arrived.wanted && child === this.#child) {
831
- this.#wanted = arrived.wanted;
832
- void this.#serial(() => this.#moved(child)).catch(() => undefined);
833
- }
834
- if (!arrived.again) return arrived.reply;
835
- }
836
- return null;
837
- }
838
-
839
- // The move the child that stands asked for, once no call is inside it.
840
- async #moved(child: ChildStub): Promise<void> {
841
- if (this.#inside > 0) await new Promise<void>((end) => this.#ended.push(end));
842
- const wanted = this.#wanted;
843
- if (child !== this.#child || !wanted) return;
844
- this.#wanted = undefined;
845
- await this.#move(wanted, 0);
846
- }
847
- };
848
-
849
- // What a worker hands a WebSocket upgrade.
850
- type EdgePair = { 0: EdgeWebSocket; 1: EdgeWebSocket };
851
- type EdgeWebSocket = WebSocket & { accept(): void };
852
-
853
- type Namespace = { idFromName(name: string): unknown; get(id: unknown): { fetch(request: Request): Promise<Response> } };
854
-
855
- // A worker of many harbors: each request goes to the object of the
856
- // harbor its path names, in the Durable Object namespace bound as
857
- // `binding`, and a path naming none is 404.
858
- export const harborWorker = (binding = 'HARBOR') => ({
859
- fetch(request: Request, env: Record<string, unknown>): Promise<Response> {
860
- const name = harborNameOf(request);
861
- const objects = env[binding] as Namespace | undefined;
862
- if (name === null || !objects) return Promise.resolve(new Response(null, { status: 404 }));
863
- return objects.get(objects.idFromName(name)).fetch(request);
864
- },
865
- });