@earendil-works/chord 1.4.0 → 1.5.1

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 (1299) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +115 -90
  3. package/dist/context/index.d.ts +1 -1
  4. package/dist/context/index.d.ts.map +1 -1
  5. package/dist/context/index.js +1 -1
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/contracts/accepts.d.ts +81 -0
  8. package/dist/contracts/accepts.d.ts.map +1 -0
  9. package/dist/contracts/accepts.js +123 -0
  10. package/dist/contracts/accepts.js.map +1 -0
  11. package/dist/contracts/contract.d.ts +121 -0
  12. package/dist/contracts/contract.d.ts.map +1 -0
  13. package/dist/contracts/contract.js +50 -0
  14. package/dist/contracts/contract.js.map +1 -0
  15. package/dist/contracts/declare.d.ts +107 -0
  16. package/dist/contracts/declare.d.ts.map +1 -0
  17. package/dist/contracts/declare.js +136 -0
  18. package/dist/contracts/declare.js.map +1 -0
  19. package/dist/contracts/evolve.d.ts +92 -0
  20. package/dist/contracts/evolve.d.ts.map +1 -0
  21. package/dist/contracts/evolve.js +179 -0
  22. package/dist/contracts/evolve.js.map +1 -0
  23. package/dist/contracts/fingerprint.d.ts +16 -0
  24. package/dist/contracts/fingerprint.d.ts.map +1 -0
  25. package/dist/contracts/fingerprint.js +50 -0
  26. package/dist/contracts/fingerprint.js.map +1 -0
  27. package/dist/contracts/history.d.ts +71 -0
  28. package/dist/contracts/history.d.ts.map +1 -0
  29. package/dist/contracts/history.js +3 -0
  30. package/dist/contracts/history.js.map +1 -0
  31. package/dist/contracts/host-kind.d.ts +40 -0
  32. package/dist/contracts/host-kind.d.ts.map +1 -0
  33. package/dist/contracts/host-kind.js +37 -0
  34. package/dist/contracts/host-kind.js.map +1 -0
  35. package/dist/contracts/index.d.ts +7 -0
  36. package/dist/contracts/index.d.ts.map +1 -0
  37. package/dist/contracts/index.js +6 -0
  38. package/dist/contracts/index.js.map +1 -0
  39. package/dist/contracts/internal.d.ts +10 -0
  40. package/dist/contracts/internal.d.ts.map +1 -0
  41. package/dist/contracts/internal.js +9 -0
  42. package/dist/contracts/internal.js.map +1 -0
  43. package/dist/contracts/names.d.ts +3 -0
  44. package/dist/contracts/names.d.ts.map +1 -0
  45. package/dist/contracts/names.js +13 -0
  46. package/dist/contracts/names.js.map +1 -0
  47. package/dist/contracts/params.d.ts +28 -0
  48. package/dist/contracts/params.d.ts.map +1 -0
  49. package/dist/contracts/params.js +50 -0
  50. package/dist/contracts/params.js.map +1 -0
  51. package/dist/contracts/schema.d.ts +102 -0
  52. package/dist/contracts/schema.d.ts.map +1 -0
  53. package/dist/contracts/schema.js +103 -0
  54. package/dist/contracts/schema.js.map +1 -0
  55. package/dist/contracts/snapshot.d.ts +16 -0
  56. package/dist/contracts/snapshot.d.ts.map +1 -0
  57. package/dist/contracts/snapshot.js +167 -0
  58. package/dist/contracts/snapshot.js.map +1 -0
  59. package/dist/data/args.d.ts +50 -0
  60. package/dist/data/args.d.ts.map +1 -0
  61. package/dist/data/args.js +83 -0
  62. package/dist/data/args.js.map +1 -0
  63. package/dist/data/errors.d.ts +80 -0
  64. package/dist/data/errors.d.ts.map +1 -0
  65. package/dist/data/errors.js +116 -0
  66. package/dist/data/errors.js.map +1 -0
  67. package/dist/data/failure.d.ts +26 -0
  68. package/dist/data/failure.d.ts.map +1 -0
  69. package/dist/data/failure.js +77 -0
  70. package/dist/data/failure.js.map +1 -0
  71. package/dist/data/index.d.ts +4 -0
  72. package/dist/data/index.d.ts.map +1 -0
  73. package/dist/data/index.js +5 -0
  74. package/dist/data/index.js.map +1 -0
  75. package/dist/data/internal.d.ts +7 -0
  76. package/dist/data/internal.d.ts.map +1 -0
  77. package/dist/data/internal.js +8 -0
  78. package/dist/data/internal.js.map +1 -0
  79. package/dist/data/params.d.ts +20 -0
  80. package/dist/data/params.d.ts.map +1 -0
  81. package/dist/data/params.js +31 -0
  82. package/dist/data/params.js.map +1 -0
  83. package/dist/data/principal.d.ts +22 -0
  84. package/dist/data/principal.d.ts.map +1 -0
  85. package/dist/data/principal.js +49 -0
  86. package/dist/data/principal.js.map +1 -0
  87. package/dist/data/validator.d.ts +12 -0
  88. package/dist/data/validator.d.ts.map +1 -0
  89. package/dist/data/validator.js +35 -0
  90. package/dist/data/validator.js.map +1 -0
  91. package/dist/delta/apply-immutable-trusted.js +3 -4
  92. package/dist/delta/apply-immutable-trusted.js.map +1 -1
  93. package/dist/delta/diff.d.ts.map +1 -1
  94. package/dist/delta/diff.js +5 -3
  95. package/dist/delta/diff.js.map +1 -1
  96. package/dist/delta/index.d.ts +9 -1
  97. package/dist/delta/index.d.ts.map +1 -1
  98. package/dist/delta/index.js +15 -11
  99. package/dist/delta/index.js.map +1 -1
  100. package/dist/delta/pending.d.ts +36 -0
  101. package/dist/delta/pending.d.ts.map +1 -0
  102. package/dist/delta/pending.js +143 -0
  103. package/dist/delta/pending.js.map +1 -0
  104. package/dist/delta/tracker.d.ts +12 -0
  105. package/dist/delta/tracker.d.ts.map +1 -1
  106. package/dist/delta/tracker.js +109 -39
  107. package/dist/delta/tracker.js.map +1 -1
  108. package/dist/documents/address-source.d.ts +20 -0
  109. package/dist/documents/address-source.d.ts.map +1 -0
  110. package/dist/documents/address-source.js +44 -0
  111. package/dist/documents/address-source.js.map +1 -0
  112. package/dist/documents/definition.d.ts +84 -0
  113. package/dist/documents/definition.d.ts.map +1 -0
  114. package/dist/documents/definition.js +74 -0
  115. package/dist/documents/definition.js.map +1 -0
  116. package/dist/documents/documents.d.ts +126 -0
  117. package/dist/documents/documents.d.ts.map +1 -0
  118. package/dist/documents/documents.js +248 -0
  119. package/dist/documents/documents.js.map +1 -0
  120. package/dist/documents/fence.d.ts +9 -0
  121. package/dist/documents/fence.d.ts.map +1 -0
  122. package/dist/documents/fence.js +15 -0
  123. package/dist/documents/fence.js.map +1 -0
  124. package/dist/documents/handles.d.ts +76 -0
  125. package/dist/documents/handles.d.ts.map +1 -0
  126. package/dist/documents/handles.js +66 -0
  127. package/dist/documents/handles.js.map +1 -0
  128. package/dist/documents/index.d.ts +4 -0
  129. package/dist/documents/index.d.ts.map +1 -0
  130. package/dist/documents/index.js +3 -0
  131. package/dist/documents/index.js.map +1 -0
  132. package/dist/documents/internal.d.ts +9 -0
  133. package/dist/documents/internal.d.ts.map +1 -0
  134. package/dist/documents/internal.js +9 -0
  135. package/dist/documents/internal.js.map +1 -0
  136. package/dist/documents/keyed-slot.d.ts +103 -0
  137. package/dist/documents/keyed-slot.d.ts.map +1 -0
  138. package/dist/documents/keyed-slot.js +395 -0
  139. package/dist/documents/keyed-slot.js.map +1 -0
  140. package/dist/documents/line.d.ts +7 -0
  141. package/dist/documents/line.d.ts.map +1 -0
  142. package/dist/documents/line.js +14 -0
  143. package/dist/documents/line.js.map +1 -0
  144. package/dist/documents/persistence.d.ts +104 -0
  145. package/dist/documents/persistence.d.ts.map +1 -0
  146. package/dist/documents/persistence.js +129 -0
  147. package/dist/documents/persistence.js.map +1 -0
  148. package/dist/documents/residency.d.ts +31 -0
  149. package/dist/documents/residency.d.ts.map +1 -0
  150. package/dist/documents/residency.js +93 -0
  151. package/dist/documents/residency.js.map +1 -0
  152. package/dist/documents/slot.d.ts +109 -0
  153. package/dist/documents/slot.d.ts.map +1 -0
  154. package/dist/documents/slot.js +181 -0
  155. package/dist/documents/slot.js.map +1 -0
  156. package/dist/documents/transaction.d.ts +143 -0
  157. package/dist/documents/transaction.d.ts.map +1 -0
  158. package/dist/documents/transaction.js +442 -0
  159. package/dist/documents/transaction.js.map +1 -0
  160. package/dist/host/activity.d.ts +31 -0
  161. package/dist/host/activity.d.ts.map +1 -0
  162. package/dist/host/activity.js +40 -0
  163. package/dist/host/activity.js.map +1 -0
  164. package/dist/host/adapted.d.ts +49 -0
  165. package/dist/host/adapted.d.ts.map +1 -0
  166. package/dist/host/adapted.js +214 -0
  167. package/dist/host/adapted.js.map +1 -0
  168. package/dist/host/attempt.d.ts +308 -0
  169. package/dist/host/attempt.d.ts.map +1 -0
  170. package/dist/host/attempt.js +1199 -0
  171. package/dist/host/attempt.js.map +1 -0
  172. package/dist/host/backoff.d.ts +29 -0
  173. package/dist/host/backoff.d.ts.map +1 -0
  174. package/dist/host/backoff.js +69 -0
  175. package/dist/host/backoff.js.map +1 -0
  176. package/dist/host/ctx.d.ts +94 -0
  177. package/dist/host/ctx.d.ts.map +1 -0
  178. package/dist/host/ctx.js +200 -0
  179. package/dist/host/ctx.js.map +1 -0
  180. package/dist/host/deadline.d.ts +12 -0
  181. package/dist/host/deadline.d.ts.map +1 -0
  182. package/dist/host/deadline.js +40 -0
  183. package/dist/host/deadline.js.map +1 -0
  184. package/dist/host/dependencies.d.ts +65 -0
  185. package/dist/host/dependencies.d.ts.map +1 -0
  186. package/dist/host/dependencies.js +115 -0
  187. package/dist/host/dependencies.js.map +1 -0
  188. package/dist/host/destination.d.ts +48 -0
  189. package/dist/host/destination.d.ts.map +1 -0
  190. package/dist/host/destination.js +101 -0
  191. package/dist/host/destination.js.map +1 -0
  192. package/dist/host/facet.d.ts +84 -0
  193. package/dist/host/facet.d.ts.map +1 -0
  194. package/dist/host/facet.js +55 -0
  195. package/dist/host/facet.js.map +1 -0
  196. package/dist/host/follow.d.ts +52 -0
  197. package/dist/host/follow.d.ts.map +1 -0
  198. package/dist/host/follow.js +207 -0
  199. package/dist/host/follow.js.map +1 -0
  200. package/dist/host/host.d.ts +571 -0
  201. package/dist/host/host.d.ts.map +1 -0
  202. package/dist/host/host.js +1318 -0
  203. package/dist/host/host.js.map +1 -0
  204. package/dist/host/index.d.ts +17 -0
  205. package/dist/host/index.d.ts.map +1 -0
  206. package/dist/host/index.js +6 -0
  207. package/dist/host/index.js.map +1 -0
  208. package/dist/host/internal.d.ts +11 -0
  209. package/dist/host/internal.d.ts.map +1 -0
  210. package/dist/host/internal.js +9 -0
  211. package/dist/host/internal.js.map +1 -0
  212. package/dist/host/members.d.ts +50 -0
  213. package/dist/host/members.d.ts.map +1 -0
  214. package/dist/host/members.js +107 -0
  215. package/dist/host/members.js.map +1 -0
  216. package/dist/host/native.d.ts +64 -0
  217. package/dist/host/native.d.ts.map +1 -0
  218. package/dist/host/native.js +126 -0
  219. package/dist/host/native.js.map +1 -0
  220. package/dist/host/object.d.ts +331 -0
  221. package/dist/host/object.d.ts.map +1 -0
  222. package/dist/host/object.js +1296 -0
  223. package/dist/host/object.js.map +1 -0
  224. package/dist/host/old-work.d.ts +34 -0
  225. package/dist/host/old-work.d.ts.map +1 -0
  226. package/dist/host/old-work.js +52 -0
  227. package/dist/host/old-work.js.map +1 -0
  228. package/dist/host/parked.d.ts +23 -0
  229. package/dist/host/parked.d.ts.map +1 -0
  230. package/dist/host/parked.js +79 -0
  231. package/dist/host/parked.js.map +1 -0
  232. package/dist/host/presence.d.ts +34 -0
  233. package/dist/host/presence.d.ts.map +1 -0
  234. package/dist/host/presence.js +117 -0
  235. package/dist/host/presence.js.map +1 -0
  236. package/dist/host/readiness.d.ts +27 -0
  237. package/dist/host/readiness.d.ts.map +1 -0
  238. package/dist/host/readiness.js +134 -0
  239. package/dist/host/readiness.js.map +1 -0
  240. package/dist/host/ref-state.d.ts +53 -0
  241. package/dist/host/ref-state.d.ts.map +1 -0
  242. package/dist/host/ref-state.js +83 -0
  243. package/dist/host/ref-state.js.map +1 -0
  244. package/dist/host/reference-keys.d.ts +36 -0
  245. package/dist/host/reference-keys.d.ts.map +1 -0
  246. package/dist/host/reference-keys.js +168 -0
  247. package/dist/host/reference-keys.js.map +1 -0
  248. package/dist/host/reference-source.d.ts +65 -0
  249. package/dist/host/reference-source.d.ts.map +1 -0
  250. package/dist/host/reference-source.js +285 -0
  251. package/dist/host/reference-source.js.map +1 -0
  252. package/dist/host/reference.d.ts +321 -0
  253. package/dist/host/reference.d.ts.map +1 -0
  254. package/dist/host/reference.js +797 -0
  255. package/dist/host/reference.js.map +1 -0
  256. package/dist/host/reload-cycle.d.ts +16 -0
  257. package/dist/host/reload-cycle.d.ts.map +1 -0
  258. package/dist/host/reload-cycle.js +81 -0
  259. package/dist/host/reload-cycle.js.map +1 -0
  260. package/dist/host/retry-on.d.ts +28 -0
  261. package/dist/host/retry-on.d.ts.map +1 -0
  262. package/dist/host/retry-on.js +52 -0
  263. package/dist/host/retry-on.js.map +1 -0
  264. package/dist/host/revocation.d.ts +16 -0
  265. package/dist/host/revocation.d.ts.map +1 -0
  266. package/dist/host/revocation.js +27 -0
  267. package/dist/host/revocation.js.map +1 -0
  268. package/dist/host/root.d.ts +37 -0
  269. package/dist/host/root.d.ts.map +1 -0
  270. package/dist/host/root.js +68 -0
  271. package/dist/host/root.js.map +1 -0
  272. package/dist/host/route.d.ts +64 -0
  273. package/dist/host/route.d.ts.map +1 -0
  274. package/dist/host/route.js +227 -0
  275. package/dist/host/route.js.map +1 -0
  276. package/dist/host/routines.d.ts +78 -0
  277. package/dist/host/routines.d.ts.map +1 -0
  278. package/dist/host/routines.js +398 -0
  279. package/dist/host/routines.js.map +1 -0
  280. package/dist/host/sleep.d.ts +9 -0
  281. package/dist/host/sleep.d.ts.map +1 -0
  282. package/dist/host/sleep.js +40 -0
  283. package/dist/host/sleep.js.map +1 -0
  284. package/dist/host/staging.d.ts +94 -0
  285. package/dist/host/staging.d.ts.map +1 -0
  286. package/dist/host/staging.js +304 -0
  287. package/dist/host/staging.js.map +1 -0
  288. package/dist/host/unbound-clock.d.ts +23 -0
  289. package/dist/host/unbound-clock.d.ts.map +1 -0
  290. package/dist/host/unbound-clock.js +58 -0
  291. package/dist/host/unbound-clock.js.map +1 -0
  292. package/dist/host/unbound.d.ts +37 -0
  293. package/dist/host/unbound.d.ts.map +1 -0
  294. package/dist/host/unbound.js +88 -0
  295. package/dist/host/unbound.js.map +1 -0
  296. package/dist/host/uuid.d.ts +3 -0
  297. package/dist/host/uuid.d.ts.map +1 -0
  298. package/dist/host/uuid.js +38 -0
  299. package/dist/host/uuid.js.map +1 -0
  300. package/dist/index.d.ts +29 -18
  301. package/dist/index.d.ts.map +1 -1
  302. package/dist/index.js +20 -9
  303. package/dist/index.js.map +1 -1
  304. package/dist/jobs/definition.d.ts +96 -0
  305. package/dist/jobs/definition.d.ts.map +1 -0
  306. package/dist/jobs/definition.js +83 -0
  307. package/dist/jobs/definition.js.map +1 -0
  308. package/dist/jobs/index.d.ts +5 -0
  309. package/dist/jobs/index.d.ts.map +1 -0
  310. package/dist/jobs/index.js +3 -0
  311. package/dist/jobs/index.js.map +1 -0
  312. package/dist/jobs/internal.d.ts +3 -0
  313. package/dist/jobs/internal.d.ts.map +1 -0
  314. package/dist/jobs/internal.js +4 -0
  315. package/dist/jobs/internal.js.map +1 -0
  316. package/dist/jobs/job.d.ts +149 -0
  317. package/dist/jobs/job.d.ts.map +1 -0
  318. package/dist/jobs/job.js +387 -0
  319. package/dist/jobs/job.js.map +1 -0
  320. package/dist/jobs/persisted.d.ts +38 -0
  321. package/dist/jobs/persisted.d.ts.map +1 -0
  322. package/dist/jobs/persisted.js +95 -0
  323. package/dist/jobs/persisted.js.map +1 -0
  324. package/dist/jobs/records.d.ts +231 -0
  325. package/dist/jobs/records.d.ts.map +1 -0
  326. package/dist/jobs/records.js +871 -0
  327. package/dist/jobs/records.js.map +1 -0
  328. package/dist/jobs/run-states.d.ts +69 -0
  329. package/dist/jobs/run-states.d.ts.map +1 -0
  330. package/dist/jobs/run-states.js +228 -0
  331. package/dist/jobs/run-states.js.map +1 -0
  332. package/dist/jobs/run.d.ts +117 -0
  333. package/dist/jobs/run.d.ts.map +1 -0
  334. package/dist/jobs/run.js +204 -0
  335. package/dist/jobs/run.js.map +1 -0
  336. package/dist/jobs/running.d.ts +44 -0
  337. package/dist/jobs/running.d.ts.map +1 -0
  338. package/dist/jobs/running.js +117 -0
  339. package/dist/jobs/running.js.map +1 -0
  340. package/dist/jobs/stored.d.ts +63 -0
  341. package/dist/jobs/stored.d.ts.map +1 -0
  342. package/dist/jobs/stored.js +55 -0
  343. package/dist/jobs/stored.js.map +1 -0
  344. package/dist/link/binary.d.ts +4 -0
  345. package/dist/link/binary.d.ts.map +1 -0
  346. package/dist/link/binary.js +544 -0
  347. package/dist/link/binary.js.map +1 -0
  348. package/dist/link/channel.d.ts +68 -0
  349. package/dist/link/channel.d.ts.map +1 -0
  350. package/dist/link/channel.js +215 -0
  351. package/dist/link/channel.js.map +1 -0
  352. package/dist/link/errors.d.ts +7 -0
  353. package/dist/link/errors.d.ts.map +1 -0
  354. package/dist/link/errors.js +21 -0
  355. package/dist/link/errors.js.map +1 -0
  356. package/dist/link/index.d.ts +8 -0
  357. package/dist/link/index.d.ts.map +1 -0
  358. package/dist/link/index.js +8 -0
  359. package/dist/link/index.js.map +1 -0
  360. package/dist/link/internal.d.ts +9 -0
  361. package/dist/link/internal.d.ts.map +1 -0
  362. package/dist/link/internal.js +10 -0
  363. package/dist/link/internal.js.map +1 -0
  364. package/dist/link/ipc.d.ts +22 -0
  365. package/dist/link/ipc.d.ts.map +1 -0
  366. package/dist/link/ipc.js +64 -0
  367. package/dist/link/ipc.js.map +1 -0
  368. package/dist/link/link.d.ts +34 -0
  369. package/dist/link/link.d.ts.map +1 -0
  370. package/dist/link/link.js +87 -0
  371. package/dist/link/link.js.map +1 -0
  372. package/dist/link/message.d.ts +262 -0
  373. package/dist/link/message.d.ts.map +1 -0
  374. package/dist/link/message.js +156 -0
  375. package/dist/link/message.js.map +1 -0
  376. package/dist/link/part.d.ts +80 -0
  377. package/dist/link/part.d.ts.map +1 -0
  378. package/dist/link/part.js +165 -0
  379. package/dist/link/part.js.map +1 -0
  380. package/dist/link/text.d.ts +6 -0
  381. package/dist/link/text.d.ts.map +1 -0
  382. package/dist/link/text.js +355 -0
  383. package/dist/link/text.js.map +1 -0
  384. package/dist/link/values.d.ts +40 -0
  385. package/dist/link/values.d.ts.map +1 -0
  386. package/dist/link/values.js +121 -0
  387. package/dist/link/values.js.map +1 -0
  388. package/dist/link/websocket.d.ts +26 -0
  389. package/dist/link/websocket.d.ts.map +1 -0
  390. package/dist/link/websocket.js +91 -0
  391. package/dist/link/websocket.js.map +1 -0
  392. package/dist/node/app/app.d.ts +269 -0
  393. package/dist/node/app/app.d.ts.map +1 -0
  394. package/dist/node/app/app.js +513 -0
  395. package/dist/node/app/app.js.map +1 -0
  396. package/dist/node/app/archive.d.ts +37 -0
  397. package/dist/node/app/archive.d.ts.map +1 -0
  398. package/dist/node/app/archive.js +204 -0
  399. package/dist/node/app/archive.js.map +1 -0
  400. package/dist/node/app/blobs.d.ts +23 -0
  401. package/dist/node/app/blobs.d.ts.map +1 -0
  402. package/dist/node/app/blobs.js +64 -0
  403. package/dist/node/app/blobs.js.map +1 -0
  404. package/dist/node/app/data.d.ts +35 -0
  405. package/dist/node/app/data.d.ts.map +1 -0
  406. package/dist/node/app/data.js +44 -0
  407. package/dist/node/app/data.js.map +1 -0
  408. package/dist/node/app/ephemeral-sessions.d.ts +20 -0
  409. package/dist/node/app/ephemeral-sessions.d.ts.map +1 -0
  410. package/dist/node/app/ephemeral-sessions.js +69 -0
  411. package/dist/node/app/ephemeral-sessions.js.map +1 -0
  412. package/dist/node/app/ephemeral.d.ts +11 -0
  413. package/dist/node/app/ephemeral.d.ts.map +1 -0
  414. package/dist/node/app/ephemeral.js +34 -0
  415. package/dist/node/app/ephemeral.js.map +1 -0
  416. package/dist/node/app/files-copy.d.ts +13 -0
  417. package/dist/node/app/files-copy.d.ts.map +1 -0
  418. package/dist/node/app/files-copy.js +86 -0
  419. package/dist/node/app/files-copy.js.map +1 -0
  420. package/dist/node/app/files-entry.d.ts +4 -0
  421. package/dist/node/app/files-entry.d.ts.map +1 -0
  422. package/dist/node/app/files-entry.js +7 -0
  423. package/dist/node/app/files-entry.js.map +1 -0
  424. package/dist/node/app/files-index.d.ts +180 -0
  425. package/dist/node/app/files-index.d.ts.map +1 -0
  426. package/dist/node/app/files-index.js +535 -0
  427. package/dist/node/app/files-index.js.map +1 -0
  428. package/dist/node/app/files.d.ts +363 -0
  429. package/dist/node/app/files.d.ts.map +1 -0
  430. package/dist/node/app/files.js +368 -0
  431. package/dist/node/app/files.js.map +1 -0
  432. package/dist/node/app/folder-views.d.ts +21 -0
  433. package/dist/node/app/folder-views.d.ts.map +1 -0
  434. package/dist/node/app/folder-views.js +55 -0
  435. package/dist/node/app/folder-views.js.map +1 -0
  436. package/dist/node/app/index.d.ts +12 -0
  437. package/dist/node/app/index.d.ts.map +1 -0
  438. package/dist/node/app/index.js +15 -0
  439. package/dist/node/app/index.js.map +1 -0
  440. package/dist/node/app/info.d.ts +66 -0
  441. package/dist/node/app/info.d.ts.map +1 -0
  442. package/dist/node/app/info.js +37 -0
  443. package/dist/node/app/info.js.map +1 -0
  444. package/dist/node/app/invite.d.ts +5 -0
  445. package/dist/node/app/invite.d.ts.map +1 -0
  446. package/dist/node/app/invite.js +64 -0
  447. package/dist/node/app/invite.js.map +1 -0
  448. package/dist/node/app/layout.d.ts +36 -0
  449. package/dist/node/app/layout.d.ts.map +1 -0
  450. package/dist/node/app/layout.js +87 -0
  451. package/dist/node/app/layout.js.map +1 -0
  452. package/dist/node/app/owner.d.ts +31 -0
  453. package/dist/node/app/owner.d.ts.map +1 -0
  454. package/dist/node/app/owner.js +104 -0
  455. package/dist/node/app/owner.js.map +1 -0
  456. package/dist/node/app/pairing.d.ts +55 -0
  457. package/dist/node/app/pairing.d.ts.map +1 -0
  458. package/dist/node/app/pairing.js +183 -0
  459. package/dist/node/app/pairing.js.map +1 -0
  460. package/dist/node/app/secrets.d.ts +16 -0
  461. package/dist/node/app/secrets.d.ts.map +1 -0
  462. package/dist/node/app/secrets.js +72 -0
  463. package/dist/node/app/secrets.js.map +1 -0
  464. package/dist/node/app/sessions.d.ts +72 -0
  465. package/dist/node/app/sessions.d.ts.map +1 -0
  466. package/dist/node/app/sessions.js +222 -0
  467. package/dist/node/app/sessions.js.map +1 -0
  468. package/dist/node/app/tar.d.ts +24 -0
  469. package/dist/node/app/tar.d.ts.map +1 -0
  470. package/dist/node/app/tar.js +167 -0
  471. package/dist/node/app/tar.js.map +1 -0
  472. package/dist/node/app/uploads.d.ts +37 -0
  473. package/dist/node/app/uploads.d.ts.map +1 -0
  474. package/dist/node/app/uploads.js +157 -0
  475. package/dist/node/app/uploads.js.map +1 -0
  476. package/dist/node/app/users.d.ts +37 -0
  477. package/dist/node/app/users.d.ts.map +1 -0
  478. package/dist/node/app/users.js +62 -0
  479. package/dist/node/app/users.js.map +1 -0
  480. package/dist/node/app/worker.d.ts +18 -0
  481. package/dist/node/app/worker.d.ts.map +1 -0
  482. package/dist/node/app/worker.js +84 -0
  483. package/dist/node/app/worker.js.map +1 -0
  484. package/dist/node/compression.d.ts +7 -0
  485. package/dist/node/compression.d.ts.map +1 -0
  486. package/dist/node/compression.js +25 -0
  487. package/dist/node/compression.js.map +1 -0
  488. package/dist/node/http/admission.d.ts +63 -0
  489. package/dist/node/http/admission.d.ts.map +1 -0
  490. package/dist/node/http/admission.js +134 -0
  491. package/dist/node/http/admission.js.map +1 -0
  492. package/dist/node/http/authenticate.d.ts +21 -0
  493. package/dist/node/http/authenticate.d.ts.map +1 -0
  494. package/dist/node/http/authenticate.js +34 -0
  495. package/dist/node/http/authenticate.js.map +1 -0
  496. package/dist/node/http/cookies.d.ts +32 -0
  497. package/dist/node/http/cookies.d.ts.map +1 -0
  498. package/dist/node/http/cookies.js +70 -0
  499. package/dist/node/http/cookies.js.map +1 -0
  500. package/dist/node/http/file.d.ts +23 -0
  501. package/dist/node/http/file.d.ts.map +1 -0
  502. package/dist/node/http/file.js +94 -0
  503. package/dist/node/http/file.js.map +1 -0
  504. package/dist/node/http/headers.d.ts +37 -0
  505. package/dist/node/http/headers.d.ts.map +1 -0
  506. package/dist/node/http/headers.js +136 -0
  507. package/dist/node/http/headers.js.map +1 -0
  508. package/dist/node/http/http.d.ts +111 -0
  509. package/dist/node/http/http.d.ts.map +1 -0
  510. package/dist/node/http/http.js +524 -0
  511. package/dist/node/http/http.js.map +1 -0
  512. package/dist/node/http/incoming.d.ts +19 -0
  513. package/dist/node/http/incoming.d.ts.map +1 -0
  514. package/dist/node/http/incoming.js +31 -0
  515. package/dist/node/http/incoming.js.map +1 -0
  516. package/dist/node/http/index.d.ts +11 -0
  517. package/dist/node/http/index.d.ts.map +1 -0
  518. package/dist/node/http/index.js +12 -0
  519. package/dist/node/http/index.js.map +1 -0
  520. package/dist/node/http/node.d.ts +78 -0
  521. package/dist/node/http/node.d.ts.map +1 -0
  522. package/dist/node/http/node.js +291 -0
  523. package/dist/node/http/node.js.map +1 -0
  524. package/dist/node/http/request.d.ts +60 -0
  525. package/dist/node/http/request.d.ts.map +1 -0
  526. package/dist/node/http/request.js +118 -0
  527. package/dist/node/http/request.js.map +1 -0
  528. package/dist/node/http/response.d.ts +63 -0
  529. package/dist/node/http/response.d.ts.map +1 -0
  530. package/dist/node/http/response.js +202 -0
  531. package/dist/node/http/response.js.map +1 -0
  532. package/dist/node/http/router.d.ts +132 -0
  533. package/dist/node/http/router.d.ts.map +1 -0
  534. package/dist/node/http/router.js +267 -0
  535. package/dist/node/http/router.js.map +1 -0
  536. package/dist/node/http/upgrades.d.ts +61 -0
  537. package/dist/node/http/upgrades.d.ts.map +1 -0
  538. package/dist/node/http/upgrades.js +192 -0
  539. package/dist/node/http/upgrades.js.map +1 -0
  540. package/dist/node/plugins/base-inputs.d.ts +14 -0
  541. package/dist/node/plugins/base-inputs.d.ts.map +1 -0
  542. package/dist/node/plugins/base-inputs.js +33 -0
  543. package/dist/node/plugins/base-inputs.js.map +1 -0
  544. package/dist/node/plugins/build.d.ts +77 -0
  545. package/dist/node/plugins/build.d.ts.map +1 -0
  546. package/dist/node/plugins/build.js +576 -0
  547. package/dist/node/plugins/build.js.map +1 -0
  548. package/dist/node/plugins/css-modules.d.ts +13 -0
  549. package/dist/node/plugins/css-modules.d.ts.map +1 -0
  550. package/dist/node/plugins/css-modules.js +58 -0
  551. package/dist/node/plugins/css-modules.js.map +1 -0
  552. package/dist/node/plugins/discover.d.ts +67 -0
  553. package/dist/node/plugins/discover.d.ts.map +1 -0
  554. package/dist/node/plugins/discover.js +369 -0
  555. package/dist/node/plugins/discover.js.map +1 -0
  556. package/dist/node/plugins/evaluate.d.ts +33 -0
  557. package/dist/node/plugins/evaluate.d.ts.map +1 -0
  558. package/dist/node/plugins/evaluate.js +121 -0
  559. package/dist/node/plugins/evaluate.js.map +1 -0
  560. package/dist/node/plugins/index.d.ts +11 -0
  561. package/dist/node/plugins/index.d.ts.map +1 -0
  562. package/dist/node/plugins/index.js +13 -0
  563. package/dist/node/plugins/index.js.map +1 -0
  564. package/dist/node/plugins/install.d.ts +38 -0
  565. package/dist/node/plugins/install.d.ts.map +1 -0
  566. package/dist/node/plugins/install.js +213 -0
  567. package/dist/node/plugins/install.js.map +1 -0
  568. package/dist/node/plugins/plugins.d.ts +162 -0
  569. package/dist/node/plugins/plugins.d.ts.map +1 -0
  570. package/dist/node/plugins/plugins.js +244 -0
  571. package/dist/node/plugins/plugins.js.map +1 -0
  572. package/dist/node/plugins/runtime.d.ts +57 -0
  573. package/dist/node/plugins/runtime.d.ts.map +1 -0
  574. package/dist/node/plugins/runtime.js +92 -0
  575. package/dist/node/plugins/runtime.js.map +1 -0
  576. package/dist/node/plugins/stack.d.ts +16 -0
  577. package/dist/node/plugins/stack.d.ts.map +1 -0
  578. package/dist/node/plugins/stack.js +61 -0
  579. package/dist/node/plugins/stack.js.map +1 -0
  580. package/dist/node/plugins/testing.d.ts +58 -0
  581. package/dist/node/plugins/testing.d.ts.map +1 -0
  582. package/dist/node/plugins/testing.js +210 -0
  583. package/dist/node/plugins/testing.js.map +1 -0
  584. package/dist/node/plugins/watcher.d.ts +69 -0
  585. package/dist/node/plugins/watcher.d.ts.map +1 -0
  586. package/dist/node/plugins/watcher.js +622 -0
  587. package/dist/node/plugins/watcher.js.map +1 -0
  588. package/dist/node/surfaces/bundle.d.ts +31 -0
  589. package/dist/node/surfaces/bundle.d.ts.map +1 -0
  590. package/dist/node/surfaces/bundle.js +118 -0
  591. package/dist/node/surfaces/bundle.js.map +1 -0
  592. package/dist/node/surfaces/current.d.ts +17 -0
  593. package/dist/node/surfaces/current.d.ts.map +1 -0
  594. package/dist/node/surfaces/current.js +54 -0
  595. package/dist/node/surfaces/current.js.map +1 -0
  596. package/dist/node/surfaces/index.d.ts +6 -0
  597. package/dist/node/surfaces/index.d.ts.map +1 -0
  598. package/dist/node/surfaces/index.js +6 -0
  599. package/dist/node/surfaces/index.js.map +1 -0
  600. package/dist/node/surfaces/published.d.ts +27 -0
  601. package/dist/node/surfaces/published.d.ts.map +1 -0
  602. package/dist/node/surfaces/published.js +142 -0
  603. package/dist/node/surfaces/published.js.map +1 -0
  604. package/dist/node/surfaces/served.d.ts +15 -0
  605. package/dist/node/surfaces/served.d.ts.map +1 -0
  606. package/dist/node/surfaces/served.js +42 -0
  607. package/dist/node/surfaces/served.js.map +1 -0
  608. package/dist/node/surfaces/surface-build.d.ts +53 -0
  609. package/dist/node/surfaces/surface-build.d.ts.map +1 -0
  610. package/dist/node/surfaces/surface-build.js +105 -0
  611. package/dist/node/surfaces/surface-build.js.map +1 -0
  612. package/dist/node/surfaces/tui.d.ts +40 -0
  613. package/dist/node/surfaces/tui.d.ts.map +1 -0
  614. package/dist/node/surfaces/tui.js +55 -0
  615. package/dist/node/surfaces/tui.js.map +1 -0
  616. package/dist/node/surfaces/web.d.ts +56 -0
  617. package/dist/node/surfaces/web.d.ts.map +1 -0
  618. package/dist/node/surfaces/web.js +108 -0
  619. package/dist/node/surfaces/web.js.map +1 -0
  620. package/dist/node/tui/actions.d.ts +49 -0
  621. package/dist/node/tui/actions.d.ts.map +1 -0
  622. package/dist/node/tui/actions.js +195 -0
  623. package/dist/node/tui/actions.js.map +1 -0
  624. package/dist/node/tui/credentials.d.ts +17 -0
  625. package/dist/node/tui/credentials.d.ts.map +1 -0
  626. package/dist/node/tui/credentials.js +54 -0
  627. package/dist/node/tui/credentials.js.map +1 -0
  628. package/dist/node/tui/filter.d.ts +5 -0
  629. package/dist/node/tui/filter.d.ts.map +1 -0
  630. package/dist/node/tui/filter.js +79 -0
  631. package/dist/node/tui/filter.js.map +1 -0
  632. package/dist/node/tui/index.d.ts +6 -0
  633. package/dist/node/tui/index.d.ts.map +1 -0
  634. package/dist/node/tui/index.js +6 -0
  635. package/dist/node/tui/index.js.map +1 -0
  636. package/dist/node/tui/launcher.d.ts +40 -0
  637. package/dist/node/tui/launcher.d.ts.map +1 -0
  638. package/dist/node/tui/launcher.js +439 -0
  639. package/dist/node/tui/launcher.js.map +1 -0
  640. package/dist/node/tui/pipe.d.ts +18 -0
  641. package/dist/node/tui/pipe.d.ts.map +1 -0
  642. package/dist/node/tui/pipe.js +134 -0
  643. package/dist/node/tui/pipe.js.map +1 -0
  644. package/dist/node/tui/sandbox.d.ts +54 -0
  645. package/dist/node/tui/sandbox.d.ts.map +1 -0
  646. package/dist/node/tui/sandbox.js +431 -0
  647. package/dist/node/tui/sandbox.js.map +1 -0
  648. package/dist/node/workers/base.d.ts +30 -0
  649. package/dist/node/workers/base.d.ts.map +1 -0
  650. package/dist/node/workers/base.js +255 -0
  651. package/dist/node/workers/base.js.map +1 -0
  652. package/dist/node/workers/child.d.ts +4 -0
  653. package/dist/node/workers/child.d.ts.map +1 -0
  654. package/dist/node/workers/child.js +6 -0
  655. package/dist/node/workers/child.js.map +1 -0
  656. package/dist/node/workers/control.d.ts +83 -0
  657. package/dist/node/workers/control.d.ts.map +1 -0
  658. package/dist/node/workers/control.js +77 -0
  659. package/dist/node/workers/control.js.map +1 -0
  660. package/dist/node/workers/index.d.ts +4 -0
  661. package/dist/node/workers/index.d.ts.map +1 -0
  662. package/dist/node/workers/index.js +7 -0
  663. package/dist/node/workers/index.js.map +1 -0
  664. package/dist/node/workers/lock.d.ts +30 -0
  665. package/dist/node/workers/lock.d.ts.map +1 -0
  666. package/dist/node/workers/lock.js +180 -0
  667. package/dist/node/workers/lock.js.map +1 -0
  668. package/dist/node/workers/main.d.ts +46 -0
  669. package/dist/node/workers/main.d.ts.map +1 -0
  670. package/dist/node/workers/main.js +230 -0
  671. package/dist/node/workers/main.js.map +1 -0
  672. package/dist/node/workers/process.d.ts +42 -0
  673. package/dist/node/workers/process.d.ts.map +1 -0
  674. package/dist/node/workers/process.js +125 -0
  675. package/dist/node/workers/process.js.map +1 -0
  676. package/dist/node/workers/workers.d.ts +122 -0
  677. package/dist/node/workers/workers.d.ts.map +1 -0
  678. package/dist/node/workers/workers.js +677 -0
  679. package/dist/node/workers/workers.js.map +1 -0
  680. package/dist/output.d.ts +1 -1
  681. package/dist/output.d.ts.map +1 -1
  682. package/dist/output.js +4 -2
  683. package/dist/output.js.map +1 -1
  684. package/dist/react/hooks.d.ts +43 -0
  685. package/dist/react/hooks.d.ts.map +1 -0
  686. package/dist/react/hooks.js +82 -0
  687. package/dist/react/hooks.js.map +1 -0
  688. package/dist/react/index.d.ts +3 -0
  689. package/dist/react/index.d.ts.map +1 -0
  690. package/dist/react/index.js +4 -0
  691. package/dist/react/index.js.map +1 -0
  692. package/dist/react/stores.d.ts +9 -0
  693. package/dist/react/stores.d.ts.map +1 -0
  694. package/dist/react/stores.js +37 -0
  695. package/dist/react/stores.js.map +1 -0
  696. package/dist/registries/contribution.d.ts +50 -0
  697. package/dist/registries/contribution.d.ts.map +1 -0
  698. package/dist/registries/contribution.js +129 -0
  699. package/dist/registries/contribution.js.map +1 -0
  700. package/dist/registries/entries.d.ts +18 -0
  701. package/dist/registries/entries.d.ts.map +1 -0
  702. package/dist/registries/entries.js +42 -0
  703. package/dist/registries/entries.js.map +1 -0
  704. package/dist/registries/index.d.ts +3 -0
  705. package/dist/registries/index.d.ts.map +1 -0
  706. package/dist/registries/index.js +5 -0
  707. package/dist/registries/index.js.map +1 -0
  708. package/dist/registries/internal.d.ts +4 -0
  709. package/dist/registries/internal.d.ts.map +1 -0
  710. package/dist/registries/internal.js +5 -0
  711. package/dist/registries/internal.js.map +1 -0
  712. package/dist/registries/record.d.ts +115 -0
  713. package/dist/registries/record.d.ts.map +1 -0
  714. package/dist/registries/record.js +283 -0
  715. package/dist/registries/record.js.map +1 -0
  716. package/dist/registries/registry.d.ts +74 -0
  717. package/dist/registries/registry.d.ts.map +1 -0
  718. package/dist/registries/registry.js +14 -0
  719. package/dist/registries/registry.js.map +1 -0
  720. package/dist/registries/value.d.ts +48 -0
  721. package/dist/registries/value.d.ts.map +1 -0
  722. package/dist/registries/value.js +224 -0
  723. package/dist/registries/value.js.map +1 -0
  724. package/dist/remote/calls.d.ts +25 -0
  725. package/dist/remote/calls.d.ts.map +1 -0
  726. package/dist/remote/calls.js +62 -0
  727. package/dist/remote/calls.js.map +1 -0
  728. package/dist/remote/consumer.d.ts +43 -0
  729. package/dist/remote/consumer.d.ts.map +1 -0
  730. package/dist/remote/consumer.js +350 -0
  731. package/dist/remote/consumer.js.map +1 -0
  732. package/dist/remote/hold.d.ts +141 -0
  733. package/dist/remote/hold.d.ts.map +1 -0
  734. package/dist/remote/hold.js +579 -0
  735. package/dist/remote/hold.js.map +1 -0
  736. package/dist/remote/host-link.d.ts +77 -0
  737. package/dist/remote/host-link.d.ts.map +1 -0
  738. package/dist/remote/host-link.js +184 -0
  739. package/dist/remote/host-link.js.map +1 -0
  740. package/dist/remote/internal.d.ts +6 -0
  741. package/dist/remote/internal.d.ts.map +1 -0
  742. package/dist/remote/internal.js +6 -0
  743. package/dist/remote/internal.js.map +1 -0
  744. package/dist/remote/path.d.ts +46 -0
  745. package/dist/remote/path.d.ts.map +1 -0
  746. package/dist/remote/path.js +73 -0
  747. package/dist/remote/path.js.map +1 -0
  748. package/dist/remote/provider.d.ts +89 -0
  749. package/dist/remote/provider.d.ts.map +1 -0
  750. package/dist/remote/provider.js +597 -0
  751. package/dist/remote/provider.js.map +1 -0
  752. package/dist/remote/relay.d.ts +69 -0
  753. package/dist/remote/relay.d.ts.map +1 -0
  754. package/dist/remote/relay.js +219 -0
  755. package/dist/remote/relay.js.map +1 -0
  756. package/dist/remote/remote-object.d.ts +138 -0
  757. package/dist/remote/remote-object.d.ts.map +1 -0
  758. package/dist/remote/remote-object.js +492 -0
  759. package/dist/remote/remote-object.js.map +1 -0
  760. package/dist/remote/replica.d.ts +59 -0
  761. package/dist/remote/replica.d.ts.map +1 -0
  762. package/dist/remote/replica.js +125 -0
  763. package/dist/remote/replica.js.map +1 -0
  764. package/dist/sqlite/cloudflare.d.ts +2 -0
  765. package/dist/sqlite/cloudflare.d.ts.map +1 -1
  766. package/dist/sqlite/cloudflare.js +10 -0
  767. package/dist/sqlite/cloudflare.js.map +1 -1
  768. package/dist/sqlite/database.d.ts +5 -2
  769. package/dist/sqlite/database.d.ts.map +1 -1
  770. package/dist/sqlite/database.js.map +1 -1
  771. package/dist/sqlite/node.d.ts +13 -2
  772. package/dist/sqlite/node.d.ts.map +1 -1
  773. package/dist/sqlite/node.js +31 -8
  774. package/dist/sqlite/node.js.map +1 -1
  775. package/dist/state/batch.d.ts +64 -0
  776. package/dist/state/batch.d.ts.map +1 -0
  777. package/dist/state/batch.js +164 -0
  778. package/dist/state/batch.js.map +1 -0
  779. package/dist/state/derive.d.ts +52 -0
  780. package/dist/state/derive.d.ts.map +1 -0
  781. package/dist/state/derive.js +182 -0
  782. package/dist/state/derive.js.map +1 -0
  783. package/dist/state/index.d.ts +6 -0
  784. package/dist/state/index.d.ts.map +1 -0
  785. package/dist/state/index.js +4 -0
  786. package/dist/state/index.js.map +1 -0
  787. package/dist/state/internal.d.ts +6 -0
  788. package/dist/state/internal.d.ts.map +1 -0
  789. package/dist/state/internal.js +8 -0
  790. package/dist/state/internal.js.map +1 -0
  791. package/dist/state/native-source.d.ts +106 -0
  792. package/dist/state/native-source.d.ts.map +1 -0
  793. package/dist/state/native-source.js +240 -0
  794. package/dist/state/native-source.js.map +1 -0
  795. package/dist/state/watchable.d.ts +114 -0
  796. package/dist/state/watchable.d.ts.map +1 -0
  797. package/dist/state/watchable.js +156 -0
  798. package/dist/state/watchable.js.map +1 -0
  799. package/dist/state/writable.d.ts +32 -0
  800. package/dist/state/writable.d.ts.map +1 -0
  801. package/dist/state/writable.js +49 -0
  802. package/dist/state/writable.js.map +1 -0
  803. package/dist/store/adapter.d.ts +67 -0
  804. package/dist/store/adapter.d.ts.map +1 -0
  805. package/dist/store/adapter.js +118 -0
  806. package/dist/store/adapter.js.map +1 -0
  807. package/dist/store/allocator.d.ts +17 -0
  808. package/dist/store/allocator.d.ts.map +1 -0
  809. package/dist/store/allocator.js +30 -0
  810. package/dist/store/allocator.js.map +1 -0
  811. package/dist/store/applier.d.ts +8 -0
  812. package/dist/store/applier.d.ts.map +1 -0
  813. package/dist/store/applier.js +375 -0
  814. package/dist/store/applier.js.map +1 -0
  815. package/dist/store/apply-batch.d.ts +12 -0
  816. package/dist/store/apply-batch.d.ts.map +1 -0
  817. package/dist/store/apply-batch.js +83 -0
  818. package/dist/store/apply-batch.js.map +1 -0
  819. package/dist/store/archive.d.ts +32 -0
  820. package/dist/store/archive.d.ts.map +1 -0
  821. package/dist/store/archive.js +130 -0
  822. package/dist/store/archive.js.map +1 -0
  823. package/dist/store/backend.d.ts +10 -0
  824. package/dist/store/backend.d.ts.map +1 -0
  825. package/dist/store/backend.js +15 -0
  826. package/dist/store/backend.js.map +1 -0
  827. package/dist/store/catalog.d.ts +24 -0
  828. package/dist/store/catalog.d.ts.map +1 -0
  829. package/dist/store/catalog.js +17 -0
  830. package/dist/store/catalog.js.map +1 -0
  831. package/dist/store/claims.d.ts +23 -0
  832. package/dist/store/claims.d.ts.map +1 -0
  833. package/dist/store/claims.js +65 -0
  834. package/dist/store/claims.js.map +1 -0
  835. package/dist/store/columns.d.ts +77 -0
  836. package/dist/store/columns.d.ts.map +1 -0
  837. package/dist/store/columns.js +77 -0
  838. package/dist/store/columns.js.map +1 -0
  839. package/dist/store/engine.d.ts +176 -0
  840. package/dist/store/engine.d.ts.map +1 -0
  841. package/dist/store/engine.js +283 -0
  842. package/dist/store/engine.js.map +1 -0
  843. package/dist/store/evaluate.d.ts +17 -0
  844. package/dist/store/evaluate.d.ts.map +1 -0
  845. package/dist/store/evaluate.js +128 -0
  846. package/dist/store/evaluate.js.map +1 -0
  847. package/dist/store/export.d.ts +17 -0
  848. package/dist/store/export.d.ts.map +1 -0
  849. package/dist/store/export.js +200 -0
  850. package/dist/store/export.js.map +1 -0
  851. package/dist/store/expression.d.ts +150 -0
  852. package/dist/store/expression.d.ts.map +1 -0
  853. package/dist/store/expression.js +268 -0
  854. package/dist/store/expression.js.map +1 -0
  855. package/dist/store/import.d.ts +10 -0
  856. package/dist/store/import.d.ts.map +1 -0
  857. package/dist/store/import.js +276 -0
  858. package/dist/store/import.js.map +1 -0
  859. package/dist/store/index.d.ts +16 -0
  860. package/dist/store/index.d.ts.map +1 -0
  861. package/dist/store/index.js +13 -0
  862. package/dist/store/index.js.map +1 -0
  863. package/dist/store/json-column.d.ts +33 -0
  864. package/dist/store/json-column.d.ts.map +1 -0
  865. package/dist/store/json-column.js +62 -0
  866. package/dist/store/json-column.js.map +1 -0
  867. package/dist/store/jsonl.d.ts +9 -0
  868. package/dist/store/jsonl.d.ts.map +1 -0
  869. package/dist/store/jsonl.js +61 -0
  870. package/dist/store/jsonl.js.map +1 -0
  871. package/dist/store/live-query.d.ts +108 -0
  872. package/dist/store/live-query.d.ts.map +1 -0
  873. package/dist/store/live-query.js +254 -0
  874. package/dist/store/live-query.js.map +1 -0
  875. package/dist/store/outbox.d.ts +26 -0
  876. package/dist/store/outbox.d.ts.map +1 -0
  877. package/dist/store/outbox.js +80 -0
  878. package/dist/store/outbox.js.map +1 -0
  879. package/dist/store/query.d.ts +39 -0
  880. package/dist/store/query.d.ts.map +1 -0
  881. package/dist/store/query.js +97 -0
  882. package/dist/store/query.js.map +1 -0
  883. package/dist/store/refusal.d.ts +15 -0
  884. package/dist/store/refusal.d.ts.map +1 -0
  885. package/dist/store/refusal.js +36 -0
  886. package/dist/store/refusal.js.map +1 -0
  887. package/dist/store/rows.d.ts +80 -0
  888. package/dist/store/rows.d.ts.map +1 -0
  889. package/dist/store/rows.js +183 -0
  890. package/dist/store/rows.js.map +1 -0
  891. package/dist/store/schema.d.ts +25 -0
  892. package/dist/store/schema.d.ts.map +1 -0
  893. package/dist/store/schema.js +115 -0
  894. package/dist/store/schema.js.map +1 -0
  895. package/dist/store/sql.d.ts +73 -0
  896. package/dist/store/sql.d.ts.map +1 -0
  897. package/dist/store/sql.js +234 -0
  898. package/dist/store/sql.js.map +1 -0
  899. package/dist/store/step.d.ts +50 -0
  900. package/dist/store/step.d.ts.map +1 -0
  901. package/dist/store/step.js +52 -0
  902. package/dist/store/step.js.map +1 -0
  903. package/dist/store/store.d.ts +171 -0
  904. package/dist/store/store.d.ts.map +1 -0
  905. package/dist/store/store.js +818 -0
  906. package/dist/store/store.js.map +1 -0
  907. package/dist/store/table-changes.d.ts +29 -0
  908. package/dist/store/table-changes.d.ts.map +1 -0
  909. package/dist/store/table-changes.js +102 -0
  910. package/dist/store/table-changes.js.map +1 -0
  911. package/dist/store/table-evolve.d.ts +74 -0
  912. package/dist/store/table-evolve.d.ts.map +1 -0
  913. package/dist/store/table-evolve.js +201 -0
  914. package/dist/store/table-evolve.js.map +1 -0
  915. package/dist/store/table-handle.d.ts +73 -0
  916. package/dist/store/table-handle.d.ts.map +1 -0
  917. package/dist/store/table-handle.js +101 -0
  918. package/dist/store/table-handle.js.map +1 -0
  919. package/dist/store/table-history.d.ts +75 -0
  920. package/dist/store/table-history.d.ts.map +1 -0
  921. package/dist/store/table-history.js +42 -0
  922. package/dist/store/table-history.js.map +1 -0
  923. package/dist/store/table-migration.d.ts +16 -0
  924. package/dist/store/table-migration.d.ts.map +1 -0
  925. package/dist/store/table-migration.js +202 -0
  926. package/dist/store/table-migration.js.map +1 -0
  927. package/dist/store/table-writes.d.ts +125 -0
  928. package/dist/store/table-writes.d.ts.map +1 -0
  929. package/dist/store/table-writes.js +170 -0
  930. package/dist/store/table-writes.js.map +1 -0
  931. package/dist/store/table.d.ts +80 -0
  932. package/dist/store/table.d.ts.map +1 -0
  933. package/dist/store/table.js +103 -0
  934. package/dist/store/table.js.map +1 -0
  935. package/dist/store/text.d.ts +14 -0
  936. package/dist/store/text.d.ts.map +1 -0
  937. package/dist/store/text.js +51 -0
  938. package/dist/store/text.js.map +1 -0
  939. package/dist/store/writes.d.ts +5 -0
  940. package/dist/store/writes.d.ts.map +1 -0
  941. package/dist/store/writes.js +4 -0
  942. package/dist/store/writes.js.map +1 -0
  943. package/dist/surfaces/active-language.d.ts +20 -0
  944. package/dist/surfaces/active-language.d.ts.map +1 -0
  945. package/dist/surfaces/active-language.js +78 -0
  946. package/dist/surfaces/active-language.js.map +1 -0
  947. package/dist/surfaces/active-theme.d.ts +16 -0
  948. package/dist/surfaces/active-theme.d.ts.map +1 -0
  949. package/dist/surfaces/active-theme.js +47 -0
  950. package/dist/surfaces/active-theme.js.map +1 -0
  951. package/dist/surfaces/client.d.ts +96 -0
  952. package/dist/surfaces/client.d.ts.map +1 -0
  953. package/dist/surfaces/client.js +264 -0
  954. package/dist/surfaces/client.js.map +1 -0
  955. package/dist/surfaces/connection.d.ts +49 -0
  956. package/dist/surfaces/connection.d.ts.map +1 -0
  957. package/dist/surfaces/connection.js +142 -0
  958. package/dist/surfaces/connection.js.map +1 -0
  959. package/dist/surfaces/format.d.ts +5 -0
  960. package/dist/surfaces/format.d.ts.map +1 -0
  961. package/dist/surfaces/format.js +37 -0
  962. package/dist/surfaces/format.js.map +1 -0
  963. package/dist/surfaces/index.d.ts +15 -0
  964. package/dist/surfaces/index.d.ts.map +1 -0
  965. package/dist/surfaces/index.js +18 -0
  966. package/dist/surfaces/index.js.map +1 -0
  967. package/dist/surfaces/language.d.ts +53 -0
  968. package/dist/surfaces/language.d.ts.map +1 -0
  969. package/dist/surfaces/language.js +74 -0
  970. package/dist/surfaces/language.js.map +1 -0
  971. package/dist/surfaces/loaded.d.ts +42 -0
  972. package/dist/surfaces/loaded.d.ts.map +1 -0
  973. package/dist/surfaces/loaded.js +116 -0
  974. package/dist/surfaces/loaded.js.map +1 -0
  975. package/dist/surfaces/locale-choice.d.ts +25 -0
  976. package/dist/surfaces/locale-choice.d.ts.map +1 -0
  977. package/dist/surfaces/locale-choice.js +14 -0
  978. package/dist/surfaces/locale-choice.js.map +1 -0
  979. package/dist/surfaces/messages.d.ts +67 -0
  980. package/dist/surfaces/messages.d.ts.map +1 -0
  981. package/dist/surfaces/messages.js +112 -0
  982. package/dist/surfaces/messages.js.map +1 -0
  983. package/dist/surfaces/page-state.d.ts +14 -0
  984. package/dist/surfaces/page-state.d.ts.map +1 -0
  985. package/dist/surfaces/page-state.js +43 -0
  986. package/dist/surfaces/page-state.js.map +1 -0
  987. package/dist/surfaces/preference.d.ts +32 -0
  988. package/dist/surfaces/preference.d.ts.map +1 -0
  989. package/dist/surfaces/preference.js +72 -0
  990. package/dist/surfaces/preference.js.map +1 -0
  991. package/dist/surfaces/registered.d.ts +33 -0
  992. package/dist/surfaces/registered.d.ts.map +1 -0
  993. package/dist/surfaces/registered.js +51 -0
  994. package/dist/surfaces/registered.js.map +1 -0
  995. package/dist/surfaces/scripts.d.ts +84 -0
  996. package/dist/surfaces/scripts.d.ts.map +1 -0
  997. package/dist/surfaces/scripts.js +28 -0
  998. package/dist/surfaces/scripts.js.map +1 -0
  999. package/dist/surfaces/theme-choice.d.ts +31 -0
  1000. package/dist/surfaces/theme-choice.d.ts.map +1 -0
  1001. package/dist/surfaces/theme-choice.js +18 -0
  1002. package/dist/surfaces/theme-choice.js.map +1 -0
  1003. package/dist/surfaces/themes.d.ts +43 -0
  1004. package/dist/surfaces/themes.d.ts.map +1 -0
  1005. package/dist/surfaces/themes.js +110 -0
  1006. package/dist/surfaces/themes.js.map +1 -0
  1007. package/dist/surfaces/translations.d.ts +29 -0
  1008. package/dist/surfaces/translations.d.ts.map +1 -0
  1009. package/dist/surfaces/translations.js +57 -0
  1010. package/dist/surfaces/translations.js.map +1 -0
  1011. package/dist/sync/batch.d.ts +18 -0
  1012. package/dist/sync/batch.d.ts.map +1 -0
  1013. package/dist/sync/batch.js +125 -0
  1014. package/dist/sync/batch.js.map +1 -0
  1015. package/dist/sync/client.d.ts +166 -0
  1016. package/dist/sync/client.d.ts.map +1 -0
  1017. package/dist/sync/client.js +438 -0
  1018. package/dist/sync/client.js.map +1 -0
  1019. package/dist/sync/files.d.ts +46 -0
  1020. package/dist/sync/files.d.ts.map +1 -0
  1021. package/dist/sync/files.js +13 -0
  1022. package/dist/sync/files.js.map +1 -0
  1023. package/dist/sync/filesystem.d.ts +37 -0
  1024. package/dist/sync/filesystem.d.ts.map +1 -0
  1025. package/dist/sync/filesystem.js +185 -0
  1026. package/dist/sync/filesystem.js.map +1 -0
  1027. package/dist/sync/index.d.ts +8 -0
  1028. package/dist/sync/index.d.ts.map +1 -0
  1029. package/dist/sync/index.js +9 -0
  1030. package/dist/sync/index.js.map +1 -0
  1031. package/dist/sync/lines.d.ts +18 -0
  1032. package/dist/sync/lines.d.ts.map +1 -0
  1033. package/dist/sync/lines.js +46 -0
  1034. package/dist/sync/lines.js.map +1 -0
  1035. package/dist/sync/remote.d.ts +130 -0
  1036. package/dist/sync/remote.d.ts.map +1 -0
  1037. package/dist/sync/remote.js +46 -0
  1038. package/dist/sync/remote.js.map +1 -0
  1039. package/dist/sync/state.d.ts +17 -0
  1040. package/dist/sync/state.d.ts.map +1 -0
  1041. package/dist/sync/state.js +40 -0
  1042. package/dist/sync/state.js.map +1 -0
  1043. package/dist/telemetry-ingest/batch.d.ts +70 -0
  1044. package/dist/telemetry-ingest/batch.d.ts.map +1 -0
  1045. package/dist/telemetry-ingest/batch.js +110 -0
  1046. package/dist/telemetry-ingest/batch.js.map +1 -0
  1047. package/dist/telemetry-ingest/facet.d.ts +21 -0
  1048. package/dist/telemetry-ingest/facet.d.ts.map +1 -0
  1049. package/dist/telemetry-ingest/facet.js +96 -0
  1050. package/dist/telemetry-ingest/facet.js.map +1 -0
  1051. package/dist/telemetry-ingest/index.d.ts +5 -0
  1052. package/dist/telemetry-ingest/index.d.ts.map +1 -0
  1053. package/dist/telemetry-ingest/index.js +8 -0
  1054. package/dist/telemetry-ingest/index.js.map +1 -0
  1055. package/dist/telemetry-ingest/recorder.d.ts +50 -0
  1056. package/dist/telemetry-ingest/recorder.d.ts.map +1 -0
  1057. package/dist/telemetry-ingest/recorder.js +298 -0
  1058. package/dist/telemetry-ingest/recorder.js.map +1 -0
  1059. package/dist/telemetry-ingest/sender.d.ts +14 -0
  1060. package/dist/telemetry-ingest/sender.d.ts.map +1 -0
  1061. package/dist/telemetry-ingest/sender.js +64 -0
  1062. package/dist/telemetry-ingest/sender.js.map +1 -0
  1063. package/dist/telemetry-sqlite/index.d.ts +13 -4
  1064. package/dist/telemetry-sqlite/index.d.ts.map +1 -1
  1065. package/dist/telemetry-sqlite/index.js +81 -241
  1066. package/dist/telemetry-sqlite/index.js.map +1 -1
  1067. package/dist/telemetry-sqlite/node.d.ts +13 -0
  1068. package/dist/telemetry-sqlite/node.d.ts.map +1 -0
  1069. package/dist/telemetry-sqlite/node.js +27 -0
  1070. package/dist/telemetry-sqlite/node.js.map +1 -0
  1071. package/dist/telemetry-sqlite/schema.d.ts +9 -0
  1072. package/dist/telemetry-sqlite/schema.d.ts.map +1 -0
  1073. package/dist/telemetry-sqlite/schema.js +61 -0
  1074. package/dist/telemetry-sqlite/schema.js.map +1 -0
  1075. package/dist/telemetry-sqlite/store.d.ts +3 -0
  1076. package/dist/telemetry-sqlite/store.d.ts.map +1 -1
  1077. package/dist/telemetry-sqlite/store.js +24 -13
  1078. package/dist/telemetry-sqlite/store.js.map +1 -1
  1079. package/dist/telemetry.d.ts +937 -0
  1080. package/dist/telemetry.d.ts.map +1 -0
  1081. package/dist/telemetry.js +470 -0
  1082. package/dist/telemetry.js.map +1 -0
  1083. package/dist/tui/bridge.d.ts +112 -0
  1084. package/dist/tui/bridge.d.ts.map +1 -0
  1085. package/dist/tui/bridge.js +9 -0
  1086. package/dist/tui/bridge.js.map +1 -0
  1087. package/dist/tui/channel.d.ts +44 -0
  1088. package/dist/tui/channel.d.ts.map +1 -0
  1089. package/dist/tui/channel.js +150 -0
  1090. package/dist/tui/channel.js.map +1 -0
  1091. package/dist/tui/frames.d.ts +5 -0
  1092. package/dist/tui/frames.d.ts.map +1 -0
  1093. package/dist/tui/frames.js +52 -0
  1094. package/dist/tui/frames.js.map +1 -0
  1095. package/dist/tui/index.d.ts +7 -0
  1096. package/dist/tui/index.d.ts.map +1 -0
  1097. package/dist/tui/index.js +8 -0
  1098. package/dist/tui/index.js.map +1 -0
  1099. package/dist/tui/runtime.d.ts +46 -0
  1100. package/dist/tui/runtime.d.ts.map +1 -0
  1101. package/dist/tui/runtime.js +101 -0
  1102. package/dist/tui/runtime.js.map +1 -0
  1103. package/dist/tui/scope.d.ts +46 -0
  1104. package/dist/tui/scope.d.ts.map +1 -0
  1105. package/dist/tui/scope.js +141 -0
  1106. package/dist/tui/scope.js.map +1 -0
  1107. package/dist/tui/terminal.d.ts +15 -0
  1108. package/dist/tui/terminal.d.ts.map +1 -0
  1109. package/dist/tui/terminal.js +37 -0
  1110. package/dist/tui/terminal.js.map +1 -0
  1111. package/dist/tui/theme.d.ts +17 -0
  1112. package/dist/tui/theme.d.ts.map +1 -0
  1113. package/dist/tui/theme.js +38 -0
  1114. package/dist/tui/theme.js.map +1 -0
  1115. package/dist/watch.d.ts +194 -0
  1116. package/dist/watch.d.ts.map +1 -0
  1117. package/dist/watch.js +383 -0
  1118. package/dist/watch.js.map +1 -0
  1119. package/dist/web/environment.d.ts +4 -0
  1120. package/dist/web/environment.d.ts.map +1 -0
  1121. package/dist/web/environment.js +37 -0
  1122. package/dist/web/environment.js.map +1 -0
  1123. package/dist/web/index.d.ts +5 -0
  1124. package/dist/web/index.d.ts.map +1 -0
  1125. package/dist/web/index.js +7 -0
  1126. package/dist/web/index.js.map +1 -0
  1127. package/dist/web/language.d.ts +7 -0
  1128. package/dist/web/language.d.ts.map +1 -0
  1129. package/dist/web/language.js +21 -0
  1130. package/dist/web/language.js.map +1 -0
  1131. package/dist/web/page.d.ts +29 -0
  1132. package/dist/web/page.d.ts.map +1 -0
  1133. package/dist/web/page.js +92 -0
  1134. package/dist/web/page.js.map +1 -0
  1135. package/dist/web/scripts.d.ts +23 -0
  1136. package/dist/web/scripts.d.ts.map +1 -0
  1137. package/dist/web/scripts.js +50 -0
  1138. package/dist/web/scripts.js.map +1 -0
  1139. package/dist/web/styles.d.ts +17 -0
  1140. package/dist/web/styles.d.ts.map +1 -0
  1141. package/dist/web/styles.js +218 -0
  1142. package/dist/web/styles.js.map +1 -0
  1143. package/dist/web/themes.d.ts +6 -0
  1144. package/dist/web/themes.d.ts.map +1 -0
  1145. package/dist/web/themes.js +31 -0
  1146. package/dist/web/themes.js.map +1 -0
  1147. package/docs/benchmarks.md +1036 -0
  1148. package/docs/decisions.md +3436 -0
  1149. package/docs/dependencies/book.md +1671 -0
  1150. package/docs/dependencies/index.html +2641 -0
  1151. package/docs/dependencies/model.md +330 -0
  1152. package/docs/design.md +3065 -0
  1153. package/docs/durable-observers/book.md +432 -0
  1154. package/docs/durable-observers/index.html +517 -0
  1155. package/docs/flow-control/book.md +333 -0
  1156. package/docs/flow-control/fold/index.html +265 -0
  1157. package/docs/flow-control/index.html +548 -0
  1158. package/docs/flow-control/window/index.html +228 -0
  1159. package/docs/guide-plugins-hosts/book.md +957 -0
  1160. package/docs/guide-plugins-hosts/index.html +952 -0
  1161. package/docs/http-workers/book.md +378 -0
  1162. package/docs/http-workers/index.html +515 -0
  1163. package/docs/i18n/book.md +217 -0
  1164. package/docs/i18n/index.html +344 -0
  1165. package/docs/layer-1.md +311 -0
  1166. package/docs/layer-2.md +216 -0
  1167. package/docs/p3-features/README.md +6 -0
  1168. package/docs/p3-features/book.md +478 -0
  1169. package/docs/plugin-authoring.md +276 -0
  1170. package/docs/plugin-model/book.md +186 -0
  1171. package/docs/plugin-model/index.html +310 -0
  1172. package/docs/plugin-primer/book.md +273 -0
  1173. package/docs/plugin-primer/index.html +430 -0
  1174. package/docs/slice-1.md +236 -0
  1175. package/docs/slice-10.md +288 -0
  1176. package/docs/slice-11.md +114 -0
  1177. package/docs/slice-12.md +132 -0
  1178. package/docs/slice-13.md +121 -0
  1179. package/docs/slice-2.md +174 -0
  1180. package/docs/slice-3a.md +296 -0
  1181. package/docs/slice-3b.md +197 -0
  1182. package/docs/slice-4.md +146 -0
  1183. package/docs/slice-4a.md +418 -0
  1184. package/docs/slice-4b.md +208 -0
  1185. package/docs/slice-5.md +281 -0
  1186. package/docs/slice-6a.md +676 -0
  1187. package/docs/slice-6b.md +332 -0
  1188. package/docs/slice-7a.md +130 -0
  1189. package/docs/slice-7b/book.md +608 -0
  1190. package/docs/slice-7b/index.html +842 -0
  1191. package/docs/slice-7b.md +255 -0
  1192. package/docs/slice-7c.md +165 -0
  1193. package/docs/slice-8.md +194 -0
  1194. package/docs/slice-9.md +110 -0
  1195. package/docs/slices.md +241 -0
  1196. package/docs/store/book.md +670 -0
  1197. package/docs/store/index.html +861 -0
  1198. package/docs/store/walkthrough.md +193 -0
  1199. package/docs/theming/book.md +223 -0
  1200. package/docs/theming/index.html +371 -0
  1201. package/docs/tutorial/book.md +2169 -0
  1202. package/docs/tutorial/index.html +2112 -0
  1203. package/docs/tutorial/screens/click-after.png +0 -0
  1204. package/docs/tutorial/screens/click-before.png +0 -0
  1205. package/docs/tutorial/screens/edited.png +0 -0
  1206. package/docs/tutorial/screens/french.png +0 -0
  1207. package/docs/tutorial/screens/harbor.png +0 -0
  1208. package/docs/tutorial/screens/page.png +0 -0
  1209. package/docs/tutorial/screens/traces-ada.png +0 -0
  1210. package/docs/tutorial/screens/traces.png +0 -0
  1211. package/package.json +114 -20
  1212. package/src/delta/README.md +69 -0
  1213. package/dist/cell.d.ts +0 -122
  1214. package/dist/cell.d.ts.map +0 -1
  1215. package/dist/cell.js +0 -356
  1216. package/dist/cell.js.map +0 -1
  1217. package/dist/cluster.d.ts +0 -27
  1218. package/dist/cluster.d.ts.map +0 -1
  1219. package/dist/cluster.js +0 -230
  1220. package/dist/cluster.js.map +0 -1
  1221. package/dist/connection.d.ts +0 -270
  1222. package/dist/connection.d.ts.map +0 -1
  1223. package/dist/connection.js +0 -1216
  1224. package/dist/connection.js.map +0 -1
  1225. package/dist/contract.d.ts +0 -208
  1226. package/dist/contract.d.ts.map +0 -1
  1227. package/dist/contract.js +0 -246
  1228. package/dist/contract.js.map +0 -1
  1229. package/dist/declare.d.ts +0 -33
  1230. package/dist/declare.d.ts.map +0 -1
  1231. package/dist/declare.js +0 -2
  1232. package/dist/declare.js.map +0 -1
  1233. package/dist/errors.d.ts +0 -28
  1234. package/dist/errors.d.ts.map +0 -1
  1235. package/dist/errors.js +0 -59
  1236. package/dist/errors.js.map +0 -1
  1237. package/dist/host.d.ts +0 -348
  1238. package/dist/host.d.ts.map +0 -1
  1239. package/dist/host.js +0 -1000
  1240. package/dist/host.js.map +0 -1
  1241. package/dist/link.d.ts +0 -107
  1242. package/dist/link.d.ts.map +0 -1
  1243. package/dist/link.js +0 -355
  1244. package/dist/link.js.map +0 -1
  1245. package/dist/loader.d.ts +0 -4
  1246. package/dist/loader.d.ts.map +0 -1
  1247. package/dist/loader.js +0 -73
  1248. package/dist/loader.js.map +0 -1
  1249. package/dist/manager.d.ts +0 -115
  1250. package/dist/manager.d.ts.map +0 -1
  1251. package/dist/manager.js +0 -345
  1252. package/dist/manager.js.map +0 -1
  1253. package/dist/node.d.ts +0 -4
  1254. package/dist/node.d.ts.map +0 -1
  1255. package/dist/node.js +0 -4
  1256. package/dist/node.js.map +0 -1
  1257. package/dist/object.d.ts +0 -438
  1258. package/dist/object.d.ts.map +0 -1
  1259. package/dist/object.js +0 -1571
  1260. package/dist/object.js.map +0 -1
  1261. package/dist/publish.d.ts +0 -5
  1262. package/dist/publish.d.ts.map +0 -1
  1263. package/dist/publish.js +0 -40
  1264. package/dist/publish.js.map +0 -1
  1265. package/dist/registry.d.ts +0 -25
  1266. package/dist/registry.d.ts.map +0 -1
  1267. package/dist/registry.js +0 -6
  1268. package/dist/registry.js.map +0 -1
  1269. package/dist/reload.d.ts +0 -126
  1270. package/dist/reload.d.ts.map +0 -1
  1271. package/dist/reload.js +0 -653
  1272. package/dist/reload.js.map +0 -1
  1273. package/dist/remote.d.ts +0 -165
  1274. package/dist/remote.d.ts.map +0 -1
  1275. package/dist/remote.js +0 -612
  1276. package/dist/remote.js.map +0 -1
  1277. package/dist/schema.d.ts +0 -21
  1278. package/dist/schema.d.ts.map +0 -1
  1279. package/dist/schema.js +0 -228
  1280. package/dist/schema.js.map +0 -1
  1281. package/dist/state.d.ts +0 -92
  1282. package/dist/state.d.ts.map +0 -1
  1283. package/dist/state.js +0 -287
  1284. package/dist/state.js.map +0 -1
  1285. package/dist/types.d.ts +0 -88
  1286. package/dist/types.d.ts.map +0 -1
  1287. package/dist/types.js +0 -2
  1288. package/dist/types.js.map +0 -1
  1289. package/dist/websocket.d.ts +0 -15
  1290. package/dist/websocket.d.ts.map +0 -1
  1291. package/dist/websocket.js +0 -16
  1292. package/dist/websocket.js.map +0 -1
  1293. package/dist/wire.d.ts +0 -216
  1294. package/dist/wire.d.ts.map +0 -1
  1295. package/dist/wire.js +0 -165
  1296. package/dist/wire.js.map +0 -1
  1297. package/docs/guide.md +0 -814
  1298. package/docs/spec.md +0 -288
  1299. package/docs/tutorial.md +0 -931
@@ -0,0 +1,2112 @@
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3
+ <title>Together: build a shared todo app with Chord</title>
4
+ <style>
5
+
6
+ :root{--paper:#fbf8f2;--ink:#2a2723;--muted:#7b7468;--rule:#e3dbcc;--accent:#b4472f;--blue:#2e6a8a;--sand:#c2b18c;--soft:#f4eee2}
7
+ *{box-sizing:border-box}
8
+ html{background:#efe9dd}
9
+ body{margin:0;color:var(--ink);font:19px/1.68 'Iowan Old Style','Palatino Linotype',Palatino,'Book Antiqua',Georgia,serif;
10
+ -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
11
+ .book{max-width:860px;margin:0 auto;background:var(--paper);box-shadow:0 0 0 1px #e1d9c8,0 30px 80px -40px rgba(60,40,10,.35);padding:0 0 4rem}
12
+ .inner{padding:0 90px}
13
+ h1,h2,h3{font-weight:400;line-height:1.15}
14
+ h2{font-size:2.1rem;margin:.2em 0 .3em;letter-spacing:-.01em}
15
+ h3{font-size:1.05rem;text-transform:uppercase;letter-spacing:.14em;margin:2.4em 0 .6em;color:var(--ink);font-weight:600;
16
+ font-family:'Avenir Next','Helvetica Neue',sans-serif}
17
+ p{margin:0 0 1.05em;hyphens:auto;text-align:left}
18
+ code{font:.8em/1 'SF Mono',Menlo,Consolas,monospace;background:#f1ebde;padding:.1em .35em;border-radius:3px}
19
+ pre{font:.74rem/1.55 'SF Mono',Menlo,Consolas,monospace;background:#f4eee2;border-left:3px solid var(--sand);padding:1rem 1.2rem;margin:1.4rem 0 1.8rem;
20
+ overflow-x:auto;border-radius:0 4px 4px 0;white-space:pre;tab-size:2}
21
+ pre code{background:none;padding:0;font-size:1em}
22
+ em{font-style:italic}
23
+ a{color:var(--blue)}
24
+ .cover{min-height:92vh;display:flex;flex-direction:column;justify-content:center;padding:90px;background:
25
+ radial-gradient(circle at 85% 15%,rgba(180,71,47,.13),transparent 40%),radial-gradient(circle at 10% 90%,rgba(46,106,138,.12),transparent 45%),var(--paper);
26
+ border-bottom:1px solid var(--rule)}
27
+ .kicker{font:600 .78rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.22em;text-transform:uppercase;color:var(--accent)}
28
+ .cover h1{font-size:5.2rem;line-height:.98;margin:.35em 0 .25em;letter-spacing:-.025em}
29
+ .subtitle{font-size:1.45rem;font-style:italic;color:#4a453e;max-width:30em;text-align:left}
30
+ .cover-stats,.stats{display:flex;flex-wrap:wrap;gap:1.2rem 2.4rem;margin:2.2rem 0}
31
+ .stats{margin:1.6rem 0 1.8rem;padding:1.1rem 0;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
32
+ .stat b{display:block;font-size:2.1rem;font-weight:400;line-height:1.1;font-variant-numeric:lining-nums}
33
+ .stat span{font:.78rem/1.3 'Avenir Next','Helvetica Neue',sans-serif;color:var(--muted);letter-spacing:.03em;display:block;max-width:14em}
34
+ .cover-note{font-size:.9rem;color:var(--muted);max-width:34em;text-align:left;margin-top:2rem}
35
+ .toc{padding:4rem 0 2rem}
36
+ .toc ol{list-style:none;padding:0;margin:1rem 0;columns:2;column-gap:3rem}
37
+ .toc li{padding:.3rem 0;border-bottom:1px dotted var(--rule);break-inside:avoid}
38
+ .toc a{color:var(--ink);text-decoration:none}
39
+ .toc a:hover{color:var(--accent)}
40
+ .preface{padding-top:2rem}
41
+ .findings{padding-left:1.4em}
42
+ .findings li{margin:.55em 0;padding-left:.3em}
43
+ .findings li::marker{color:var(--accent);font-weight:600}
44
+ .chapter{padding-top:4.5rem;margin-top:3rem;border-top:1px solid var(--rule)}
45
+ .opener{text-align:center;margin-bottom:2.6rem}
46
+ .numeral{font-size:4.2rem;color:var(--accent);line-height:1;font-variant:small-caps;letter-spacing:.05em}
47
+ .opener h2{font-size:2.5rem;margin:.25em 0}
48
+ .lede{font-style:italic;color:#5a544a;font-size:1.12rem;max-width:30em;margin:0 auto;text-align:center}
49
+ .dropcap::first-letter{float:left;font-size:4.1em;line-height:.82;padding:.06em .09em 0 0;color:var(--accent)}
50
+ .fig{margin:2rem 0 2.2rem}
51
+ .fig.wide{margin-left:-50px;margin-right:-50px}
52
+ .fig svg{display:block;margin:0 auto}
53
+ figcaption{font:.8rem/1.45 'Avenir Next','Helvetica Neue',sans-serif;color:var(--muted);margin-top:.7rem;padding-left:.8rem;border-left:2px solid var(--rule)}
54
+ .fignum{color:var(--ink);font-weight:600}
55
+ .note{font:.86rem/1.55 'Avenir Next','Helvetica Neue',sans-serif;color:#5b5549;background:#f4eee2;border-left:3px solid var(--sand);
56
+ padding:.9rem 1.1rem;margin:1.8rem 0;border-radius:0 4px 4px 0}
57
+ .note.review{border-left-color:var(--accent)}
58
+ .note b.tag{font-weight:600;letter-spacing:.12em;text-transform:uppercase;font-size:.72rem;color:var(--accent);display:block;margin-bottom:.3rem}
59
+ .note code{font-size:.85em}
60
+ table{width:100%;border-collapse:collapse;margin:1.4rem 0 1.8rem;font:.8rem/1.4 'Avenir Next','Helvetica Neue',sans-serif}
61
+ th{text-align:left;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;font-size:.68rem;border-bottom:1.5px solid var(--ink);padding:.45rem .5rem}
62
+ td{padding:.42rem .5rem;border-bottom:1px solid var(--rule);vertical-align:top}
63
+ td code{font-size:.92em}
64
+ table.num td:not(:first-child),table.num th:not(:first-child){text-align:right}
65
+ .epilogue p{font-size:1.04em}
66
+ .appendix{font-size:.92em}
67
+ .appendix ul,.chapter ul{padding-left:1.2em}
68
+ .appendix li,.chapter li{margin:.4em 0}
69
+ .colophon{text-align:center;font:.78rem 'Avenir Next','Helvetica Neue',sans-serif;color:var(--muted);margin-top:4rem;letter-spacing:.05em}
70
+ @media (max-width:760px){.inner{padding:0 22px}.cover{padding:40px 22px}.cover h1{font-size:3.2rem}.fig.wide{margin-left:0;margin-right:0}
71
+ .toc ol{columns:1}p{text-align:left}body{font-size:17px}}
72
+ @media print{html{background:#fff}.book{box-shadow:none}.chapter{break-before:page}.fig{break-inside:avoid}}
73
+ .transcript p b{font:600 .78rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin-right:.3em}
74
+ pre .tk-c{color:#8d8576;font-style:italic}pre .tk-s{color:#5f7a3d}pre .tk-n{color:#a0662a}pre .tk-k{color:#b4472f}pre .tk-t{color:#2e6a8a}pre .tk-f{color:#7a3f6a}
75
+ h4{font:600 .82rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.06em;margin:1.8em 0 .5em;color:var(--muted)}
76
+ .inner ul,.inner ol:not(.findings){padding-left:1.2em}
77
+ .inner li{margin:.35em 0}
78
+ blockquote{margin:1.4rem 0;padding:.2rem 0 .2rem 1.2rem;border-left:3px solid var(--rule);color:#5a544a;font-style:italic}
79
+ .fig svg text{dominant-baseline:auto}
80
+ .fig .replay{font:600 .7rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);
81
+ background:none;border:1px solid var(--rule);border-radius:3px;padding:.2rem .55rem;margin:0 0 0 1rem;cursor:pointer;float:right}
82
+ .fig .replay:hover{border-color:var(--accent)}
83
+ .anim.armed .step{opacity:0}
84
+ .anim.armed.play .step{animation:stepIn .45s ease-out both;animation-delay:calc(var(--i) * var(--pace, .45s))}
85
+ .anim.armed.play .step.edge{animation-name:edgeIn}
86
+ @keyframes stepIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
87
+ @keyframes edgeIn{from{opacity:0}to{opacity:1}}
88
+ .steps{margin:2rem 0 2.2rem;border:1px solid var(--rule);border-radius:6px;background:#fffdf8}
89
+ .steps-head{display:flex;align-items:center;gap:.8rem;padding:.6rem 1rem;border-bottom:1px solid var(--rule);
90
+ font:600 .72rem 'Avenir Next','Helvetica Neue',sans-serif;letter-spacing:.12em;text-transform:uppercase;color:var(--accent)}
91
+ .steps-head .title{flex:1}
92
+ .steps-head .count{color:var(--muted);letter-spacing:.06em}
93
+ .steps-head button{font:600 .72rem 'Avenir Next','Helvetica Neue',sans-serif;color:var(--ink);background:var(--soft);border:1px solid var(--rule);
94
+ border-radius:3px;padding:.25rem .65rem;cursor:pointer}
95
+ .steps-head button:disabled{opacity:.35;cursor:default}
96
+ .step-panel{padding:1rem 1.2rem .4rem}
97
+ .step-panel>h4:first-child{margin-top:.2rem;color:var(--ink)}
98
+ .steps.js .step-panel{display:none}
99
+ .steps.js .step-panel.on{display:block}
100
+ .steps:not(.js) .steps-head button,.steps:not(.js) .steps-head .count{display:none}
101
+ .steps:not(.js) .step-panel+.step-panel{border-top:1px dashed var(--rule)}
102
+ .fig figcaption{display:flow-root}
103
+ .fig svg{max-width:100%;height:auto}
104
+ table{max-width:100%}
105
+ @media (max-width:760px){.inner{padding:0 20px}.cover{padding:48px 22px}.cover h1{font-size:3.2rem}.fig.wide{margin-left:0;margin-right:0}table{display:block;overflow-x:auto}.toc ol{columns:1}}
106
+ </style>
107
+
108
+
109
+ </head>
110
+ <body><div class="book">
111
+ <section class="cover">
112
+ <div class="kicker">A tutorial · Chord v2 and its app kit · 2026-11-03</div>
113
+ <h1>Build It<br>Together</h1>
114
+ <p class="subtitle">One small app, built a step at a time, from an empty server to two people, a page, a terminal, a theme, three languages, an edit applied while they are connected, and one click traced across its processes</p>
115
+ <div class="cover-stats"><div class="stat"><b>11</b><span>steps, one runnable example</span></div><div class="stat"><b>4</b><span>host kinds one plugin runs on</span></div><div class="stat"><b>1</b><span>worker process per list</span></div><div class="stat"><b>0</b><span>restarts for a new theme, a language or an edit</span></div></div>
116
+ <div class="cover-note"><p>The app is <code>packages/chord/test/examples/61-tutorial/</code>, runnable with the line in its <code>main.ts</code>. Every code block
117
+ below is quoted from its files, and every console output from what its steps print:
118
+ <code>test/examples/61-tutorial/tutorial.test.ts</code> fails when a quote or an output drifts. The design is
119
+ <code>packages/chord/docs/design.md</code>; where this tutorial says &quot;the rule&quot;, design.md has the full statement. The last
120
+ chapter is about p2, whose code lives outside this repository.</p>
121
+ </div>
122
+ </section><div class="inner">
123
+ <nav class="toc"><h2>Contents</h2><ol>
124
+ <li><a href="#before-we-begin">Before we begin</a></li>
125
+ <li><a href="#server">I. One Server</a></li>
126
+ <li><a href="#worker">II. A List on Its Own Worker</a></li>
127
+ <li><a href="#routes">III. Routes</a></li>
128
+ <li><a href="#page">IV. A Page</a></li>
129
+ <li><a href="#terminal">V. A Terminal</a></li>
130
+ <li><a href="#theme">VI. A Theme</a></li>
131
+ <li><a href="#user">VII. A Second User</a></li>
132
+ <li><a href="#languages">VIII. Languages</a></li>
133
+ <li><a href="#reload">IX. Changing Code</a></li>
134
+ <li><a href="#export">X. Moving a List</a></li>
135
+ <li><a href="#telemetry">XI. Seeing What Happened</a></li>
136
+ <li><a href="#testing">XII. Testing One Plugin</a></li>
137
+ <li><a href="#p2">XIII. Writing a p2 Plugin</a></li>
138
+ <li><a href="#traps-and-open-questions">Traps and Open Questions</a></li>
139
+ <li><a href="#the-app-s-files">Appendix A: The App&#39;s Files</a></li>
140
+ <li><a href="#reference">Appendix B: Reference</a></li>
141
+ </ol></nav>
142
+
143
+ <section class="preface" id="before-we-begin">
144
+ <h2>Before we begin</h2>
145
+ <p class="dropcap">Mara keeps the household&#39;s shopping list on her laptop. She wants it in a browser tab and in a terminal, live in both.
146
+ Ada, her flatmate, should be able to add coffee from her phone without an account: pick a name and go. And Mara wants
147
+ to change the app while it runs: add a dark theme in the evening, fix a small annoyance in how todos are added, without
148
+ anyone being thrown out.</p>
149
+ <p>That is four kinds of program (a server, a process per list, a page, a terminal client) and three kinds of change (new
150
+ data, a new person, new code) in one small app. Wiring that by hand means a web server, a socket protocol, a way to
151
+ start and stop processes, sessions, a bundler for the page and a reload story for each. Chord does the wiring, and its
152
+ <strong>app kit</strong> (<code>@earendil-works/chord/app</code>) makes the decisions an app would otherwise make itself: where data lives,
153
+ how people sign in, how plugins are found and rolled out.</p>
154
+ <p>This tutorial builds that app. Each chapter adds one thing, shows the code, shows what happens when it runs, and
155
+ states the rule behind it. The app is one directory, <code>test/examples/61-tutorial/</code>; run every step at once from
156
+ <code>packages/chord</code>:</p>
157
+ <pre><code>node --conditions=source --experimental-strip-types test/examples/<span class="tk-n">61</span>-tutorial/main.ts</code></pre>
158
+ <p>With Node 26.5.1 or later the terminal client of chapter V runs too (its sandbox needs that Node); with an older one,
159
+ that step says so and the rest runs.</p>
160
+ <h3 id="ten-things-to-know">Ten things to know</h3>
161
+ <ol class="findings">
162
+ <li><strong>An app is its API, its plugins and a start script.</strong> <code>startServer</code> makes the data directory, the store, the
163
+ server host, HTTP with sign-in, the workers and the plugin roots; <code>startWorker</code> does a worker&#39;s half.</li>
164
+ <li><strong>A host is one participant.</strong> The server, each list&#39;s worker, each browser tab and each terminal are hosts,
165
+ connected by links. Tabs and terminals are clients: they connect and ask, nobody asks them.</li>
166
+ <li><strong>An object is reached by its contract.</strong> A contract names an object type, its params and its members; a <code>remote</code>
167
+ contract also says which host serves it. Any host asks for <code>Todos { list: &quot;groceries&quot; }</code>, and Chord routes the
168
+ request to that list&#39;s worker.</li>
169
+ <li><strong>Who calls is stamped, never sent.</strong> Every call carries a principal the link stamped from the sign-in. A factory
170
+ picks a variant per principal; a todo records who added it from the principal, not from its arguments.</li>
171
+ <li><strong>A plugin has one file per host kind.</strong> <code>server.ts</code>, <code>worker.ts</code>, <code>web.ts</code>, <code>tui.ts</code>: each declares what that kind
172
+ of host serves and contributes. Plugins import only the app&#39;s API.</li>
173
+ <li><strong>Plugins meet in registries.</strong> The page shows <code>Panels</code>, the terminal <code>Screens</code>, the kit&#39;s <code>Themes</code> restyle both;
174
+ plugins contribute, one plugin (or the entry) composes.</li>
175
+ <li><strong>Changes apply when you say so.</strong> Saving a file marks its plugin dirty; <code>applyChanges</code> builds and rolls out to the
176
+ server, the workers, the pages and the terminals, in place.</li>
177
+ <li><strong>Text is rendered per viewer.</strong> A plugin&#39;s messages are typed code, a translation is a plugin, each user picks a
178
+ language, and servers store keys, never sentences.</li>
179
+ <li><strong>One click is one trace.</strong> The server writes the telemetry store (here one SQLite file in the data directory); the
180
+ page and the workers send it their spans; carriers in the calls&#39; headers join the page&#39;s click, the server&#39;s relay
181
+ and the worker&#39;s method into one trace, which <code>TelemetryStore</code> reads back. <code>ctx.trace</code> adds spans of your own.</li>
182
+ <li><strong>Six traps:</strong> load before use; a reference acts as whoever made it; await what you start; a watch starts at the
183
+ current value; the plain <code>t</code> has no language of its own; a trace starts where a span starts.</li>
184
+ </ol>
185
+ <h3 id="words-used-throughout">Words used throughout</h3>
186
+ <p>A <strong>host</strong> is one participant of the app, with an <strong>address</strong> if others can reach it (the server, <code>List({ id })</code>) or
187
+ without one (a <strong>client</strong>: a page, a terminal). A <strong>link</strong> connects two hosts. A <strong>principal</strong> is who calls, typed by
188
+ the app and stamped by links. An <strong>object</strong> is state and methods at one identity; its <strong>contract</strong> (<code>local</code> for its own
189
+ host only, <code>remote</code> for any host) names its <strong>params</strong> (which instance) and members. A remote contract&#39;s <strong>variants</strong>
190
+ are the member sets different principals get. A <strong>factory</strong> declares an object&#39;s parts: <strong>documents</strong> and <strong>tables</strong>
191
+ (stored data, changed in <strong>transactions</strong>), <strong>live queries</strong> (a table&#39;s rows Chord keeps current), <strong>derives</strong>
192
+ (values computed from others), <strong>natives</strong> (things opened and closed, such as a file or a database cursor). A <strong>reference</strong> is what asking for an object returns. A <strong>facet</strong> is a
193
+ plugin&#39;s code for one host kind; a <strong>registry</strong> is an extension point plugins <strong>contribute</strong> to. A <strong>message</strong> is a
194
+ text with a key, a <strong>catalog</strong> a plugin&#39;s messages in one language, a <strong>locale</strong> a language tag (<code>de</code>, <code>pt-BR</code>). A <strong>rollout</strong> applies
195
+ new plugin code to the hosts in order. A <strong>span</strong> is one timed piece of work a host recorded, a <strong>trace</strong> the spans of
196
+ one request across hosts, joined by the <strong>carriers</strong> calls carry.</p>
197
+ </section>
198
+
199
+ <section class="chapter" id="server"><header class="opener"><div class="numeral">I</div><h2>One Server</h2><p class="lede">The API, the principals and a start script: the smallest app the kit runs, and who may come in.</p></header>
200
+ <p class="dropcap">An app built with the kit has a fixed shape: its API in <code>src/api/v&lt;n&gt;/</code>, its built-in plugins in <code>plugins/</code>, a start
201
+ script and a worker entry (design §23, &quot;The app kit&quot;). Everything else is a plugin. This chapter writes the three
202
+ pieces of base code and starts the server.</p>
203
+ <h3 id="the-principals">The principals</h3>
204
+ <p>A <strong>principal</strong> says who is calling. The app defines the type; the kit needs a principal for the server, one for each of
205
+ its workers (an app without worker kinds leaves it out) and one for the owner, whom the owner&#39;s sign-in names. The todo
206
+ app gives the owner a variant of its own, <code>{ kind: &quot;owner&quot; }</code>; another app could make the owner one of its users:</p>
207
+ <!-- quote: src/principals.ts -->
208
+ <pre><code><span class="tk-c">// The todo app's principals: its server and list workers, its owner, and the users who signed in with a name.</span>
209
+
210
+ <span class="tk-c">/** Who someone is in the todo app. */</span>
211
+ <span class="tk-k">export</span> <span class="tk-k">type</span> <span class="tk-t">TodoPrincipal</span> =
212
+ | { <span class="tk-k">readonly</span> kind: <span class="tk-s">"server"</span> }
213
+ | { <span class="tk-k">readonly</span> kind: <span class="tk-s">"worker"</span>; <span class="tk-k">readonly</span> list: string }
214
+ | { <span class="tk-k">readonly</span> kind: <span class="tk-s">"owner"</span> }
215
+ | { <span class="tk-k">readonly</span> kind: <span class="tk-s">"user"</span>; <span class="tk-k">readonly</span> id: string; <span class="tk-k">readonly</span> name: string };</code></pre>
216
+ <p>The user variant is for chapter VII. A principal travels with every call and is part of objects&#39; identities, so it
217
+ stays small: a kind and an id. A user&#39;s profile, if the app had one, would be data in an object keyed by the id.</p>
218
+ <h3 id="the-api">The API</h3>
219
+ <p>Plugins never import Chord. They import the app&#39;s <strong>API</strong>, a module the app names with a version in its path
220
+ (<code>@todo/app/v1</code>). It re-exports the part of Chord the app supports and adds what is the app&#39;s own:</p>
221
+ <!-- quote: src/api/v1/index.ts -->
222
+ <pre><code><span class="tk-k">export</span> { BACKGROUND_CONTEXT } <span class="tk-k">from</span> <span class="tk-s">"../../../../../../src/context/index.ts"</span>;
223
+ <span class="tk-k">export</span> {
224
+ arr,
225
+ bool,
226
+ <span class="tk-k">type</span> <span class="tk-t">Ctx</span>,
227
+ defineMessages,
228
+ defineState,
229
+ defineTable,
230
+ eq,
231
+ forbidden,
232
+ int,
233
+ invalid,
234
+ local,
235
+ <span class="tk-t">MessageSchema</span>,
236
+ <span class="tk-k">type</span> <span class="tk-t">MessagesOf</span>,
237
+ method,
238
+ nul,
239
+ num,
240
+ obj,
241
+ <span class="tk-t">PluginError</span>,
242
+ query,
243
+ remote,
244
+ <span class="tk-k">type</span> <span class="tk-t">Static</span>,
245
+ str,
246
+ <span class="tk-k">type</span> <span class="tk-t">Theme</span>,
247
+ <span class="tk-t">Themes</span>,
248
+ <span class="tk-k">type</span> <span class="tk-t">Translation</span>,
249
+ <span class="tk-t">Translations</span>,
250
+ union,
251
+ } <span class="tk-k">from</span> <span class="tk-s">"../../../../../../src/index.ts"</span>;
252
+ <span class="tk-k">export</span> <span class="tk-k">type</span> { <span class="tk-t">TodoPrincipal</span> } <span class="tk-k">from</span> <span class="tk-s">"../../principals.ts"</span>;</code></pre>
253
+ <p><code>defineMessages</code>, <code>Translations</code> and the message schema are chapter VIII&#39;s. <code>invalid</code> and <code>forbidden</code> make Chord&#39;s own errors: a plugin refusing arguments throws <code>invalid(&quot;args&quot;, message)</code>,
254
+ one refusing a caller <code>forbidden(message)</code>, and the caller gets the same <code>{ code, reason }</code> Chord&#39;s built-in checks
255
+ give (chapter IX). <code>PluginError</code> is for codes of the plugin&#39;s own.</p>
256
+ <p>and a <code>defineFacet</code> bound to the principal type, so every factory sees a <code>TodoPrincipal</code>:</p>
257
+ <!-- quote: src/api/v1/index.ts -->
258
+ <pre><code><span class="tk-c">/** Defines a facet of the todo app: its factories get the app's principals. */</span>
259
+ <span class="tk-k">export</span> <span class="tk-k">function</span> <span class="tk-f">defineFacet</span>(setup: (host: <span class="tk-t">FacetHost</span>&lt;<span class="tk-t">TodoPrincipal</span>&gt;, self: <span class="tk-t">FacetSelf</span>) =&gt; <span class="tk-k">void</span>) {
260
+ <span class="tk-k">return</span> <span class="tk-f">chordFacet</span>&lt;<span class="tk-t">TodoPrincipal</span>&gt;(setup);
261
+ }</code></pre>
262
+ <p>The API is a <strong>host package</strong>: a plugin&#39;s build leaves it out, and each process hands the plugin its own instance when
263
+ it evaluates it. The plugin sees the very <code>Themes</code> registry and <code>List</code> host kind the process has, not a copy. A
264
+ version in the path lets the app ship <code>v2</code> beside <code>v1</code> later; one facet uses one version (design §21, &quot;The plugin
265
+ model&quot;).</p>
266
+ <p>The API is also a real package: a <strong>workspace package</strong> of the app&#39;s repository, named in its <code>package.json</code> with an
267
+ <code>exports</code> entry per module:</p>
268
+ <!-- quote: package.json -->
269
+ <pre><code>{
270
+ <span class="tk-s">"name"</span>: <span class="tk-s">"@todo/app"</span>,
271
+ <span class="tk-s">"version"</span>: <span class="tk-s">"0.0.1"</span>,
272
+ <span class="tk-s">"private"</span>: <span class="tk-k">true</span>,
273
+ <span class="tk-s">"description"</span>: <span class="tk-s">"The tutorial's todo app: its API (v1 and its per-kind modules), base code and built-in plugins"</span>,
274
+ <span class="tk-s">"type"</span>: <span class="tk-s">"module"</span>,
275
+ <span class="tk-s">"exports"</span>: {
276
+ <span class="tk-s">"./v1"</span>: <span class="tk-s">"./src/api/v1/index.ts"</span>,
277
+ <span class="tk-s">"./v1/server"</span>: <span class="tk-s">"./src/api/v1/server.ts"</span>,
278
+ <span class="tk-s">"./v1/web"</span>: <span class="tk-s">"./src/api/v1/web.ts"</span>,
279
+ <span class="tk-s">"./v1/tui"</span>: <span class="tk-s">"./src/api/v1/tui.ts"</span>
280
+ }
281
+ }</code></pre>
282
+ <p>The repository&#39;s root <code>package.json</code> lists it in <code>workspaces</code> (here Chord&#39;s repository lists
283
+ <code>packages/chord/test/examples/61-tutorial</code>), so <code>npm install</code> links <code>node_modules/@todo/app</code> to it. Then Node,
284
+ TypeScript, the tests and esbuild resolve <code>@todo/app/v1</code> alike, from base code, from a plugin&#39;s files and from a test
285
+ importing a plugin&#39;s <code>contract.ts</code>; plugin builds still leave it out and pass the process&#39;s instance.</p>
286
+ <h3 id="the-start-script">The start script</h3>
287
+ <p><code>startServer</code> takes what the app decides and does the rest. <code>start.ts</code> names the API with this process&#39;s modules, the
288
+ facet kinds (which files a plugin may have), and the principals:</p>
289
+ <!-- quote: start.ts -->
290
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">const</span> API = {
291
+ name: <span class="tk-s">"@todo/app"</span>,
292
+ versions: [<span class="tk-s">"v1"</span>],
293
+ modules: { <span class="tk-s">"@todo/app/v1"</span>: v1, <span class="tk-s">"@todo/app/v1/server"</span>: v1server },
294
+ };
295
+
296
+ <span class="tk-c">/** The app's facet kinds: a plugin has one file per kind it runs on (`server.ts`, `worker.ts`, `web.ts`, `tui.ts`). */</span>
297
+ <span class="tk-k">export</span> <span class="tk-k">const</span> KINDS = {
298
+ server: { platform: <span class="tk-s">"node"</span> },
299
+ worker: { platform: <span class="tk-s">"node"</span> },
300
+ web: { platform: <span class="tk-s">"browser"</span>, packages: { react: {}, <span class="tk-s">"react/jsx-runtime"</span>: {} } },
301
+ tui: { platform: <span class="tk-s">"node"</span> },
302
+ } <span class="tk-k">as</span> <span class="tk-k">const</span>;</code></pre>
303
+ <!-- quote: start.ts -->
304
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">function</span> <span class="tk-f">startTodo</span>(
305
+ dataDir: string,
306
+ root: string,
307
+ telemetry: <span class="tk-t">TodoTelemetry</span>,
308
+ context: <span class="tk-t">Context</span>,
309
+ ): <span class="tk-t">Promise</span>&lt;<span class="tk-t">App</span>&lt;<span class="tk-t">TodoPrincipal</span>&gt;&gt; {
310
+ <span class="tk-k">return</span> <span class="tk-f">startServer</span>&lt;<span class="tk-t">TodoPrincipal</span>&gt;(
311
+ {
312
+ name: <span class="tk-s">"todo"</span>,
313
+ dataDir,
314
+ root,
315
+ listen: { host: <span class="tk-s">"127.0.0.1"</span>, port: <span class="tk-n">0</span> },
316
+ api: API,
317
+ kinds: KINDS,
318
+ principal: {
319
+ server: { kind: <span class="tk-s">"server"</span> },
320
+ worker: (address) =&gt; ({ kind: <span class="tk-s">"worker"</span>, list: address.key }),
321
+ owner: { kind: <span class="tk-s">"owner"</span> },
322
+ },
323
+ <span class="tk-c">// The store the server writes: its own spans and metrics, and what the workers and recording pages send.</span>
324
+ telemetry: telemetry.store,
325
+ <span class="tk-c">// Who a span was for: a short key and a user's name (each worker's `startWorker` gets the same hook).</span>
326
+ principalAttributes,
327
+ workers: [
328
+ {
329
+ kind: v1.<span class="tk-t">List</span>,
330
+ entry: <span class="tk-f">fileURLToPath</span>(<span class="tk-k">new</span> <span class="tk-f">URL</span>(<span class="tk-s">"./worker.ts"</span>, <span class="tk-k">import</span>.meta.url)),
331
+ execArgv: [<span class="tk-s">"--conditions=source"</span>, <span class="tk-s">"--experimental-strip-types"</span>, <span class="tk-s">"--no-warnings"</span>],
332
+ start: () =&gt; ({}),
333
+ <span class="tk-c">// Who may reach a list's worker: everyone signed in, and the server itself.</span>
334
+ authorize: (principal) =&gt; principal.kind !== <span class="tk-s">"worker"</span>,
335
+ },
336
+ ],
337
+ <span class="tk-c">// The page, and the owner's trace viewer: the same page, which shows the viewer at `/traces`.</span>
338
+ web: { entry: <span class="tk-f">fileURLToPath</span>(<span class="tk-k">new</span> <span class="tk-f">URL</span>(<span class="tk-s">"./src/web/main.ts"</span>, <span class="tk-k">import</span>.meta.url)), page, paths: [<span class="tk-s">"/"</span>, <span class="tk-s">"/traces"</span>] },
339
+ tui: { entry: <span class="tk-f">fileURLToPath</span>(<span class="tk-k">new</span> <span class="tk-f">URL</span>(<span class="tk-s">"./src/tui/main.ts"</span>, <span class="tk-k">import</span>.meta.url)) },
340
+ onError: (error) =&gt; console.<span class="tk-f">error</span>(<span class="tk-s">"todo:"</span>, error.kind, error.error),
341
+ },
342
+ context,
343
+ );
344
+ }</code></pre>
345
+ <p><code>workers</code> is chapter II, <code>web</code> chapter IV (its second path, <code>/traces</code>, chapter XI), <code>tui</code> chapter V, <code>telemetry</code> and
346
+ <code>principalAttributes</code> chapter XI. <code>listen</code> asks for a free port on loopback, as the example does; a real app picks a
347
+ port. The worker entry is a few lines (its <code>principalAttributes</code> is chapter XI&#39;s too):</p>
348
+ <!-- quote: worker.ts -->
349
+ <pre><code><span class="tk-c">// The todo app's worker entry: one process per list. `startWorker` opens the list's store in its directory, connects</span>
350
+ <span class="tk-c">// to the server and loads the plugins' worker facets. The worker records its spans in memory and sends them to the</span>
351
+ <span class="tk-c">// server, which stores them: its methods called through the server continue the caller's trace.</span>
352
+
353
+ <span class="tk-k">import</span> { startWorker } <span class="tk-k">from</span> <span class="tk-s">"../../../src/node/app/index.ts"</span>;
354
+ <span class="tk-k">import</span> { principalAttributes } <span class="tk-k">from</span> <span class="tk-s">"./src/principals.ts"</span>;
355
+ <span class="tk-k">import</span> { API, KINDS } <span class="tk-k">from</span> <span class="tk-s">"./start.ts"</span>;
356
+
357
+ <span class="tk-f">startWorker</span>({ api: API, kinds: KINDS, principalAttributes, onError: (error) =&gt; console.<span class="tk-f">error</span>(<span class="tk-s">"list:"</span>, error) });</code></pre>
358
+ <h3 id="running-it">Running it</h3>
359
+ <p>Step 1 of <code>main.ts</code> starts the app, tries the page without signing in, then does what a browser does with the owner
360
+ link:</p>
361
+ <!-- quote: main.ts -->
362
+ <pre><code><span class="tk-k">const</span> telemetry = <span class="tk-k">await</span> <span class="tk-f">openTelemetry</span>(<span class="tk-f">telemetryFile</span>(dataDir));
363
+ <span class="tk-k">const</span> app = <span class="tk-k">await</span> <span class="tk-f">startTodo</span>(dataDir, root, telemetry, context);
364
+ <span class="tk-k">const</span> link = <span class="tk-k">new</span> <span class="tk-f">URL</span>(app.<span class="tk-f">ownerLink</span>());
365
+ <span class="tk-k">const</span> url = link.origin;
366
+ console.<span class="tk-f">log</span>(<span class="tk-s">"1. owner link:"</span>, <span class="tk-s">`http://127.0.0.1:&lt;port&gt;${link.pathname}#&lt;token&gt;`</span>);
367
+ console.<span class="tk-f">log</span>(<span class="tk-s">" data directory:"</span>, (<span class="tk-k">await</span> <span class="tk-f">readdir</span>(dataDir)).<span class="tk-f">sort</span>().<span class="tk-f">join</span>(<span class="tk-s">", "</span>));
368
+ console.<span class="tk-f">log</span>(<span class="tk-s">" GET / signed out:"</span>, (<span class="tk-k">await</span> <span class="tk-f">fetch</span>(url)).status);
369
+ <span class="tk-k">const</span> signIn = <span class="tk-k">await</span> <span class="tk-f">fetch</span>(<span class="tk-s">`${url}/_chord/owner`</span>, {
370
+ method: <span class="tk-s">"POST"</span>,
371
+ headers: { <span class="tk-s">"content-type"</span>: <span class="tk-s">"application/json"</span> },
372
+ body: JSON.<span class="tk-f">stringify</span>({ token: link.hash.<span class="tk-f">slice</span>(<span class="tk-n">1</span>) }),
373
+ });
374
+ <span class="tk-k">const</span> cookie = signIn.headers.<span class="tk-f">get</span>(<span class="tk-s">"set-cookie"</span>)!.<span class="tk-f">split</span>(<span class="tk-s">";"</span>)[<span class="tk-n">0</span>]!;
375
+ console.<span class="tk-f">log</span>(<span class="tk-s">" the owner signs in:"</span>, signIn.status, cookie.<span class="tk-f">replace</span>(/=.*/, <span class="tk-s">"=…"</span>));
376
+ console.<span class="tk-f">log</span>(<span class="tk-s">" GET / signed in:"</span>, (<span class="tk-k">await</span> <span class="tk-f">fetch</span>(url, { headers: { cookie } })).status);</code></pre>
377
+ <!-- output: main -->
378
+ <pre class="plain"><code>1. owner link: http://127.0.0.1:&lt;port&gt;/_chord/owner#&lt;token&gt;
379
+ data directory: cache, logs, owner-token, plugins, server, telemetry.sqlite, telemetry.sqlite-shm, telemetry.sqlite-wal, workers
380
+ GET / signed out: 401
381
+ the owner signs in: 200 chord-session=…
382
+ GET / signed in: 200</code></pre>
383
+ <p><code>startServer</code> made the data directory: <code>server/</code> (its store, the cookie secret, its files), <code>workers/</code> (one directory
384
+ per worker), <code>plugins/</code> (the user&#39;s plugin root, beside the app&#39;s built-in one), <code>cache/</code> (each worker kind&#39;s base, written
385
+ again at every start, and Node&#39;s compile cache for the workers), <code>logs/</code> and <code>owner-token</code>;
386
+ <code>telemetry.sqlite</code> (with SQLite&#39;s <code>-shm</code> and <code>-wal</code> files) is the app&#39;s own, opened before the server starts
387
+ (chapter XI). The page
388
+ answered 401: HTTP <strong>denies by default</strong>, before any plugin code runs, unless a route says <code>public: true</code>.</p>
389
+ <p>The <strong>owner link</strong> (<code>/_chord/owner#&lt;token&gt;</code>) is how the owner gets in: a single-use token, valid ten minutes, made at
390
+ each start. It sits in the URL&#39;s fragment, which browsers never send, so it never reaches a log; the link&#39;s page posts
391
+ it and receives a session cookie. A command-line client on the same machine reads <code>owner-token</code> instead (mode 0600,
392
+ chapter III). Nothing is the owner because it comes from loopback: any local process could.</p>
393
+ </section>
394
+
395
+ <section class="chapter" id="worker"><header class="opener"><div class="numeral">II</div><h2>A List on Its Own Worker</h2><p class="lede">A host kind, a remote contract, a worker facet with a table and a live query, and the principal that adds a todo.</p></header>
396
+ <p class="dropcap">Each list gets its own process, a <strong>worker</strong>, with its own store: a list that misbehaves cannot take the others down,
397
+ and an idle list costs nothing. This chapter declares where lists live, what a list offers, and the worker code that
398
+ keeps its todos.</p>
399
+ <h3 id="where-a-list-lives">Where a list lives</h3>
400
+ <p>A <strong>host kind</strong> declares addressable hosts with params; <code>List({ id: &quot;groceries&quot; })</code> is an <strong>address</strong>. The app&#39;s
401
+ host kinds are base code, in the API:</p>
402
+ <!-- quote: src/api/v1/index.ts -->
403
+ <pre><code><span class="tk-c">/** A todo list: a worker process of its own, with its own store. */</span>
404
+ <span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">List</span> = <span class="tk-f">hostKind</span>(<span class="tk-s">"list"</span>, { id: <span class="tk-f">str</span>() });</code></pre>
405
+ <p>A <code>remote</code> contract says, with <code>hostAddress</code>, at which address its objects live. The contract for a list&#39;s todos is
406
+ the lists plugin&#39;s, in its <code>contract.ts</code>, which its facets of every kind import, and so do the steps and its test:</p>
407
+ <!-- quote: plugins/lists/contract.ts -->
408
+ <pre><code><span class="tk-k">import</span> {
409
+ arr,
410
+ bool,
411
+ int,
412
+ <span class="tk-t">List</span>,
413
+ <span class="tk-t">MessageSchema</span>,
414
+ method,
415
+ nul,
416
+ num,
417
+ obj,
418
+ remote,
419
+ <span class="tk-k">type</span> <span class="tk-t">Static</span>,
420
+ str,
421
+ union,
422
+ } <span class="tk-k">from</span> <span class="tk-s">"@todo/app/v1"</span>;
423
+
424
+ <span class="tk-c">/** A todo: its number in the list, its text, whether it is done, who added it and when. */</span>
425
+ <span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Item</span> = <span class="tk-f">obj</span>({ id: <span class="tk-f">int</span>(), text: <span class="tk-f">str</span>(), done: <span class="tk-f">bool</span>(), by: <span class="tk-f">str</span>(), at: <span class="tk-f">num</span>() });
426
+
427
+ <span class="tk-c">/** A todo (see `Item`). */</span>
428
+ <span class="tk-k">export</span> <span class="tk-k">type</span> <span class="tk-t">Item</span> = <span class="tk-t">Static</span>&lt;<span class="tk-k">typeof</span> <span class="tk-t">Item</span>&gt;;
429
+
430
+ <span class="tk-c">/** The list's latest notice, as data each viewer renders in its own language; `null` before the first. */</span>
431
+ <span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Notice</span> = <span class="tk-f">union</span>([<span class="tk-t">MessageSchema</span>, <span class="tk-f">nul</span>()]);
432
+
433
+ <span class="tk-c">/**
434
+ * A list's todos as one principal sees them: the newest 50 (live) and the list's notice, adding one and ticking one off
435
+ * (or on again); the owner also clears the done ones.
436
+ */</span>
437
+ <span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Todos</span> = <span class="tk-f">remote</span>(<span class="tk-s">"todo.todos"</span>, {
438
+ version: <span class="tk-n">1</span>,
439
+ params: { list: <span class="tk-f">str</span>() },
440
+ hostAddress: ({ list }) =&gt; <span class="tk-t">List</span>({ id: list }),
441
+ variants: {
442
+ member: {
443
+ state: { items: <span class="tk-f">arr</span>(<span class="tk-t">Item</span>), notice: <span class="tk-t">Notice</span> },
444
+ methods: { add: <span class="tk-f">method</span>(<span class="tk-f">str</span>()), tick: <span class="tk-f">method</span>(<span class="tk-f">obj</span>({ id: <span class="tk-f">int</span>(), done: <span class="tk-f">bool</span>() })) },
445
+ },
446
+ owner: {
447
+ state: { items: <span class="tk-f">arr</span>(<span class="tk-t">Item</span>), notice: <span class="tk-t">Notice</span> },
448
+ methods: { add: <span class="tk-f">method</span>(<span class="tk-f">str</span>()), tick: <span class="tk-f">method</span>(<span class="tk-f">obj</span>({ id: <span class="tk-f">int</span>(), done: <span class="tk-f">bool</span>() })), clear: <span class="tk-f">method</span>() },
449
+ },
450
+ },
451
+ });</code></pre>
452
+ <p>Its <code>notice</code> is a message as data, which chapter VIII renders in each viewer&#39;s language. <code>params</code> select the
453
+ instance: <code>Todos { list: &quot;groceries&quot; }</code> is one object per principal. Nobody who asks names a
454
+ host: the caller computes the address from the params and sends the request there; a host that cannot serve it passes
455
+ it on to its parent or to a route (design §3, &quot;Host kinds and addresses&quot;). The kit&#39;s <code>workers</code> option is that route:</p>
456
+ <!-- quote: start.ts -->
457
+ <pre><code> workers: [
458
+ {
459
+ kind: v1.<span class="tk-t">List</span>,
460
+ entry: <span class="tk-f">fileURLToPath</span>(<span class="tk-k">new</span> <span class="tk-f">URL</span>(<span class="tk-s">"./worker.ts"</span>, <span class="tk-k">import</span>.meta.url)),
461
+ execArgv: [<span class="tk-s">"--conditions=source"</span>, <span class="tk-s">"--experimental-strip-types"</span>, <span class="tk-s">"--no-warnings"</span>],
462
+ start: () =&gt; ({}),
463
+ <span class="tk-c">// Who may reach a list's worker: everyone signed in, and the server itself.</span>
464
+ authorize: (principal) =&gt; principal.kind !== <span class="tk-s">"worker"</span>,
465
+ },
466
+ ],</code></pre>
467
+ <p><code>authorize</code> runs for every request routed to a
468
+ list before anything starts; the first request for an address starts
469
+ its worker, and a worker idle for five minutes (the default <code>idleMs</code>) stops. <strong>Variants</strong> are the member sets a
470
+ contract offers: the owner&#39;s <code>Todos</code> has <code>clear</code>, a member&#39;s does not. The factory picks one per principal.</p>
471
+ <h3 id="the-worker-facet">The worker facet</h3>
472
+ <p>The lists plugin is a directory with a <code>package.json</code> (its name is the plugin&#39;s name) and one file per host kind it
473
+ runs on. <code>worker.ts</code> is its <strong>worker facet</strong>: what every list&#39;s worker serves. It keeps the todos in a <strong>table</strong> of the
474
+ worker&#39;s store:</p>
475
+ <!-- quote: plugins/lists/worker.ts -->
476
+ <pre><code><span class="tk-c">/** Every todo of the list, by its number. */</span>
477
+ <span class="tk-k">const</span> <span class="tk-t">Items</span> = <span class="tk-f">defineTable</span>(<span class="tk-s">"todo.items"</span>, {
478
+ version: <span class="tk-n">1</span>,
479
+ columns: { id: <span class="tk-f">int</span>(), text: <span class="tk-f">str</span>(), done: <span class="tk-f">bool</span>(), by: <span class="tk-f">str</span>(), at: <span class="tk-f">num</span>() },
480
+ key: [<span class="tk-s">"id"</span>],
481
+ });
482
+
483
+ <span class="tk-c">/** The newest todos first. */</span>
484
+ <span class="tk-k">const</span> NEWEST = <span class="tk-f">query</span>(<span class="tk-t">Items</span>).<span class="tk-f">orderBy</span>((item) =&gt; item.id, <span class="tk-s">"desc"</span>);
485
+
486
+ <span class="tk-c">/** How many todos the live query holds. */</span>
487
+ <span class="tk-k">const</span> WINDOW = <span class="tk-n">50</span>;</code></pre>
488
+ <p>A table holds many records with queries and pages; it lives in an object, never in a facet. Persisted data lives only
489
+ in <code>local</code> objects with empty params (one per host and type), so its address never depends on who asked (design §9,
490
+ &quot;Lifetime, persistence&quot;). That object is the plugin&#39;s store, <code>Store</code>, a local contract of its own file:</p>
491
+ <!-- quote: plugins/lists/worker.ts -->
492
+ <pre><code><span class="tk-c">/** The list's todos, once per worker: empty params, so it lives as long as the worker and may keep data in its store. */</span>
493
+ <span class="tk-k">const</span> <span class="tk-t">Store</span> = <span class="tk-f">local</span>(<span class="tk-s">"todo.store"</span>, {
494
+ version: <span class="tk-n">1</span>,
495
+ params: {},
496
+ members: {
497
+ items: <span class="tk-f">arr</span>(<span class="tk-t">Item</span>),
498
+ notice: <span class="tk-t">Notice</span>,
499
+ add: <span class="tk-f">method</span>(<span class="tk-f">obj</span>({ text: <span class="tk-f">str</span>(), by: <span class="tk-f">str</span>() })),
500
+ tick: <span class="tk-f">method</span>(<span class="tk-f">obj</span>({ id: <span class="tk-f">int</span>(), done: <span class="tk-f">bool</span>() })),
501
+ clear: <span class="tk-f">method</span>(<span class="tk-f">obj</span>({ by: <span class="tk-f">str</span>() })),
502
+ },
503
+ });</code></pre>
504
+ <p>A <code>local</code> object with empty params lives as long as its host: it starts with the worker and is never ended by a count
505
+ of references. Its factory declares its parts and returns its members:</p>
506
+ <!-- quote: plugins/lists/worker.ts -->
507
+ <pre><code> host.<span class="tk-f">serve</span>(<span class="tk-t">Store</span>, (_params, self) =&gt; {
508
+ <span class="tk-k">const</span> items = self.<span class="tk-f">table</span>(<span class="tk-t">Items</span>);
509
+ <span class="tk-c">// The newest todos, live: read before the store goes live, read again after every commit writing the table.</span>
510
+ <span class="tk-k">const</span> newest = self.<span class="tk-f">query</span>(items, NEWEST, WINDOW);
511
+ <span class="tk-k">const</span> notices = self.<span class="tk-f">state</span>(<span class="tk-t">Notices</span>);
512
+ <span class="tk-k">return</span> {
513
+ <span class="tk-c">// Oldest first, as the list shows them.</span>
514
+ items: self.<span class="tk-f">derive</span>([newest], ([rows]) =&gt; [...rows].<span class="tk-f">reverse</span>()),
515
+ notice: self.<span class="tk-f">derive</span>([notices], ([value]) =&gt; value.notice),</code></pre>
516
+ <p>A <strong>factory</strong> is synchronous and only declares; Chord activates the object afterwards. <code>self.table</code> declares the
517
+ table. <code>self.query(items, NEWEST, WINDOW)</code> declares a <strong>live query</strong> over it: the rows of <code>NEWEST</code>, at most 50, as a
518
+ value the object can expose or derive from. Chord runs it before the store goes live, and again after every commit
519
+ of the store that writes <code>items</code>, and sends watchers only what changed. The store&#39;s derive turns the rows around,
520
+ oldest first, for the list.</p>
521
+ <p>A live query reads only its own object&#39;s tables: only the store writes them, so its own commits are exactly the
522
+ moments the rows can change, and a commit that writes no table with a query costs nothing. The limit is required:
523
+ each such commit runs the query once more, at most <code>WINDOW</code> rows. That is cheap for a list a person edits and wrong
524
+ for a table written hundreds of times a second (design §11, &quot;there may be dragons&quot;).</p>
525
+ <p>Writes go through <strong>transactions</strong>: <code>self.tx(fn, ctx)</code> runs <code>fn</code> with <code>tx</code>, and what it stages is committed at once
526
+ when <code>fn</code> returns, or not at all if it throws. The live query follows in the same commit; the store keeps nothing
527
+ else in memory, not even the next todo&#39;s number, which it reads in the transaction:</p>
528
+ <!-- quote: plugins/lists/worker.ts -->
529
+ <pre><code> add: <span class="tk-k">async</span> ({ text, by }, ctx) =&gt; {
530
+ <span class="tk-c">// A span of its own under the method's: a trace shows the check, and why it refused.</span>
531
+ <span class="tk-k">await</span> ctx.<span class="tk-f">trace</span>(<span class="tk-s">"todos.validate"</span>, { chars: text.length }, <span class="tk-k">async</span> () =&gt; {
532
+ <span class="tk-c">// Refused as Chord refuses arguments that fail their schema: the caller gets `invalid/args`.</span>
533
+ <span class="tk-k">if</span> (text === <span class="tk-s">""</span>) <span class="tk-k">throw</span> <span class="tk-f">invalid</span>(<span class="tk-s">"args"</span>, <span class="tk-s">"a todo needs text"</span>);
534
+ });
535
+ <span class="tk-k">await</span> self.<span class="tk-f">tx</span>(<span class="tk-k">async</span> (tx) =&gt; {
536
+ <span class="tk-c">// The number after the newest todo's: the store's transactions run one after another.</span>
537
+ <span class="tk-k">const</span> { items: last } = <span class="tk-k">await</span> tx.<span class="tk-f">page</span>(items, NEWEST, <span class="tk-n">1</span>);
538
+ tx.<span class="tk-f">put</span>(items, { id: (last[<span class="tk-n">0</span>]?.id ?? <span class="tk-n">0</span>) + <span class="tk-n">1</span>, text, done: <span class="tk-k">false</span>, by, at: <span class="tk-t">Date</span>.<span class="tk-f">now</span>() });
539
+ }, ctx);
540
+ <span class="tk-k">return</span> <span class="tk-k">null</span>;</code></pre>
541
+ <p>A todo without text is refused with <code>invalid(&quot;args&quot;, …)</code>, from the API: the error crosses every link as it is, so a
542
+ caller on a page or in a terminal gets <code>{ code: &quot;invalid&quot;, reason: &quot;args&quot; }</code>, as for arguments that fail the
543
+ contract&#39;s schema. The check runs in a span of its own (<code>ctx.trace</code>), which chapter XI shows in a trace.</p>
544
+ <h3 id="the-view-per-principal">The view per principal</h3>
545
+ <p><code>Todos</code> is the remote face of the store, one object per principal. Its factory gets the principal the link stamped:</p>
546
+ <!-- quote: plugins/lists/worker.ts -->
547
+ <pre><code> host.<span class="tk-f">serve</span>(<span class="tk-t">Todos</span>, (_params, principal, self) =&gt; {
548
+ <span class="tk-k">const</span> store = self.<span class="tk-f">get</span>(<span class="tk-t">Store</span>, {});
549
+ <span class="tk-c">// Who adds a todo comes from the principal the link stamped, never from the arguments.</span>
550
+ <span class="tk-k">const</span> by = principal.kind === <span class="tk-s">"user"</span> ? principal.name : principal.kind;
551
+ <span class="tk-k">const</span> members = {
552
+ items: self.<span class="tk-f">derive</span>([store.items], ([items]) =&gt; items),
553
+ notice: self.<span class="tk-f">derive</span>([store.notice], ([notice]) =&gt; notice),
554
+ add: (text: string, ctx: <span class="tk-t">Ctx</span>) =&gt; store.<span class="tk-f">add</span>({ text, by }, ctx),
555
+ tick: (tick: { id: number; done: boolean }, ctx: <span class="tk-t">Ctx</span>) =&gt; store.<span class="tk-f">tick</span>(tick, ctx),
556
+ };
557
+ <span class="tk-k">if</span> (principal.kind !== <span class="tk-s">"owner"</span>) <span class="tk-k">return</span> { variant: <span class="tk-s">"member"</span>, members };
558
+ <span class="tk-k">return</span> { variant: <span class="tk-s">"owner"</span>, members: { ...members, clear: (ctx: <span class="tk-t">Ctx</span>) =&gt; store.<span class="tk-f">clear</span>({ by }, ctx) } };
559
+ });</code></pre>
560
+ <p><code>self.get(Store, {})</code> declares a dependency: the view goes live once the store is, and holds a <strong>reference</strong> to it
561
+ for its whole life. <code>by</code> comes from the principal. A client could put anything in the arguments of <code>add</code>; it cannot
562
+ change what its link stamped. That is the rule for every plugin: identity from the principal, never from arguments.</p>
563
+ <h3 id="running-it">Running it</h3>
564
+ <p>Step 2 asks for the groceries list twice, from a root acting as the owner and from a root of the server itself:</p>
565
+ <!-- quote: main.ts -->
566
+ <pre><code><span class="tk-k">const</span> OWNER = { kind: <span class="tk-s">"owner"</span> } <span class="tk-k">as</span> <span class="tk-k">const</span>;
567
+ <span class="tk-k">await</span> app.host.<span class="tk-f">root</span>(
568
+ <span class="tk-k">async</span> (ctx) =&gt; {
569
+ <span class="tk-k">const</span> todos = <span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> });
570
+ <span class="tk-k">await</span> todos.<span class="tk-f">add</span>(<span class="tk-s">"milk"</span>, ctx);
571
+ <span class="tk-k">await</span> todos.<span class="tk-f">add</span>(<span class="tk-s">"eggs"</span>, ctx);
572
+ <span class="tk-k">await</span> todos.<span class="tk-f">tick</span>({ id: <span class="tk-n">1</span>, done: <span class="tk-k">true</span> }, ctx);
573
+ <span class="tk-k">await</span> <span class="tk-f">until</span>(() =&gt; todos.items.value[<span class="tk-n">0</span>]?.done === <span class="tk-k">true</span>);
574
+ console.<span class="tk-f">log</span>(<span class="tk-s">"2. as the owner:"</span>, todos.variant, <span class="tk-s">"|"</span>, <span class="tk-f">show</span>(todos.items.value));
575
+ },
576
+ context,
577
+ { principal: OWNER },
578
+ );
579
+ <span class="tk-k">await</span> app.host.<span class="tk-f">root</span>(<span class="tk-k">async</span> (ctx) =&gt; {
580
+ <span class="tk-k">const</span> todos = <span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> });
581
+ <span class="tk-k">await</span> todos.<span class="tk-f">add</span>(<span class="tk-s">"bread"</span>, ctx);
582
+ <span class="tk-k">await</span> <span class="tk-f">until</span>(() =&gt; todos.items.value.length === <span class="tk-n">3</span>);
583
+ console.<span class="tk-f">log</span>(<span class="tk-s">" as the server:"</span>, todos.variant, <span class="tk-s">"|"</span>, <span class="tk-f">show</span>(todos.items.value.<span class="tk-f">slice</span>(-<span class="tk-n">1</span>)));
584
+ }, context);</code></pre>
585
+ <!-- output: main -->
586
+ <pre class="plain"><code>2. as the owner: owner | [x] milk (owner), [ ] eggs (owner)
587
+ as the server: member | [ ] bread (server)
588
+ workers: list|groceries
589
+ workers/list/list%7Cgroceries: store.sqlite, store.sqlite-shm, store.sqlite-wal</code></pre>
590
+ <p>A <strong>root</strong> (<code>host.root(fn, ctx, { principal })</code>) is a call scope for code outside objects; its <code>ctx</code> carries the
591
+ principal and cancellation, and the references <code>ctx.get</code> takes end when <code>fn</code> settles. The first request started the
592
+ list&#39;s worker, <code>list|groceries</code>, with its own store in <code>workers/list/list%7Cgroceries/</code>.</p>
593
+ <h3 id="trap-who-a-reference-acts-as">Trap: who a reference acts as</h3>
594
+ <p>The two roots made the same call and got different todos: <code>milk (owner)</code> and <code>bread (server)</code>. <strong>A reference acts as
595
+ whoever made it.</strong> <code>ctx.get</code> in a root or a method acts as that context&#39;s principal; <code>host.get</code>, and <code>self.get</code> in a
596
+ <code>local</code> object, act as the host; <code>self.get</code> in a remote object acts as that object&#39;s principal (design §4). So a route
597
+ whose request came from the owner reaches the worker as the owner (chapter III), and a guest&#39;s call cannot borrow the
598
+ server&#39;s authority. The surprise is the reverse case: a server method that wants to act as the server towards a worker
599
+ must not use its caller&#39;s <code>ctx.get</code>; it takes the reference with <code>self.get</code> in a local server object, or in a root of
600
+ the server&#39;s own.</p>
601
+ <h3 id="trap-await-what-you-start">Trap: await what you start</h3>
602
+ <p>Every write above is awaited, and that is not style. What is started with a context ends with it: a root&#39;s scope seals
603
+ when <code>fn</code> settles, a method&#39;s context ends with its call. A <code>void todos.add(text, ctx)</code> inside a root that returns at
604
+ once is cancelled before it commits; Chord rejects it and reports <code>transaction_cancelled</code> through <code>onError</code>, so an
605
+ empty <code>catch</code> cannot hide a write that never happened (design §8). Fire and forget only with a context that does not
606
+ end, as the page does with <code>BACKGROUND_CONTEXT</code> in chapter IV.</p>
607
+ </section>
608
+
609
+ <section class="chapter" id="routes"><header class="opener"><div class="numeral">III</div><h2>Routes</h2><p class="lede">A server facet: a JSON API for command-line clients, running as whoever sent the request.</p></header>
610
+ <p class="dropcap">The page will talk Chord over a WebSocket. A script on Mara&#39;s machine is happier with HTTP. A <strong>server facet</strong>
611
+ contributes routes to the kit&#39;s <code>Http</code> registry, which the API&#39;s server module re-exports:</p>
612
+ <!-- quote: src/api/v1/server.ts -->
613
+ <pre><code><span class="tk-k">export</span> { <span class="tk-t">Http</span> } <span class="tk-k">from</span> <span class="tk-s">"../../../../../../src/node/http/index.ts"</span>;</code></pre>
614
+ <!-- quote: plugins/lists/server.ts -->
615
+ <pre><code><span class="tk-c">// The lists plugin on the server: a JSON API for command-line clients. A route runs as whoever sent the request (the</span>
616
+ <span class="tk-c">// kit answered 401 before this code runs when nobody signed in), so its `ctx.get` reaches the list's worker as them.</span>
617
+
618
+ <span class="tk-k">import</span> { defineFacet } <span class="tk-k">from</span> <span class="tk-s">"@todo/app/v1"</span>;
619
+ <span class="tk-k">import</span> { <span class="tk-t">Http</span> } <span class="tk-k">from</span> <span class="tk-s">"@todo/app/v1/server"</span>;
620
+ <span class="tk-k">import</span> { <span class="tk-t">Todos</span> } <span class="tk-k">from</span> <span class="tk-s">"./contract.ts"</span>;
621
+
622
+ <span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) =&gt; {
623
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Http</span>, {
624
+ routes: {
625
+ <span class="tk-s">"GET /api/lists/:list"</span>: <span class="tk-k">async</span> (request, ctx) =&gt; {
626
+ <span class="tk-k">const</span> todos = <span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: request.params.list! });
627
+ <span class="tk-k">return</span> { kind: <span class="tk-s">"json"</span>, value: todos.items.value };
628
+ },
629
+ <span class="tk-s">"POST /api/lists/:list"</span>: <span class="tk-k">async</span> (request, ctx) =&gt; {
630
+ <span class="tk-k">const</span> todos = <span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: request.params.list! });
631
+ <span class="tk-k">await</span> todos.<span class="tk-f">add</span>(<span class="tk-t">String</span>(<span class="tk-k">await</span> request.<span class="tk-f">json</span>()), ctx);
632
+ <span class="tk-k">return</span> { kind: <span class="tk-s">"json"</span>, value: <span class="tk-s">"added"</span> };
633
+ },
634
+ },
635
+ });
636
+ });</code></pre>
637
+ <p>A <strong>registry</strong> is an extension point: one owner declares how contributions combine (here the kit&#39;s HTTP component
638
+ builds a router), plugins contribute one value each. <code>self.contribute</code> in a facet adds the plugin&#39;s routes while the
639
+ plugin is loaded. A route runs in a root with the request&#39;s principal, so <code>ctx.get(Todos, …)</code> reaches the list&#39;s
640
+ worker as whoever signed in, and gets their variant.</p>
641
+ <p>Step 3 is a command-line client with the owner token file:</p>
642
+ <!-- quote: main.ts -->
643
+ <pre><code><span class="tk-k">const</span> token = (<span class="tk-k">await</span> <span class="tk-f">readFile</span>(app.ownerTokenFile, <span class="tk-s">"utf8"</span>)).<span class="tk-f">trim</span>();
644
+ <span class="tk-k">const</span> cli = { authorization: <span class="tk-s">`Bearer ${token}`</span> };
645
+ <span class="tk-k">const</span> posted = <span class="tk-k">await</span> <span class="tk-f">fetch</span>(<span class="tk-s">`${url}/api/lists/groceries`</span>, {
646
+ method: <span class="tk-s">"POST"</span>,
647
+ headers: { ...cli, <span class="tk-s">"content-type"</span>: <span class="tk-s">"application/json"</span> },
648
+ body: JSON.<span class="tk-f">stringify</span>(<span class="tk-s">"butter"</span>),
649
+ });
650
+ console.<span class="tk-f">log</span>(<span class="tk-s">"3. POST /api/lists/groceries:"</span>, <span class="tk-k">await</span> posted.<span class="tk-f">json</span>());
651
+ <span class="tk-k">const</span> listed = (<span class="tk-k">await</span> (<span class="tk-k">await</span> <span class="tk-f">fetch</span>(<span class="tk-s">`${url}/api/lists/groceries`</span>, { headers: cli })).<span class="tk-f">json</span>()) <span class="tk-k">as</span> <span class="tk-t">Item</span>[];
652
+ console.<span class="tk-f">log</span>(<span class="tk-s">" GET /api/lists/groceries:"</span>, <span class="tk-f">show</span>(listed));
653
+ console.<span class="tk-f">log</span>(<span class="tk-s">" GET without a token:"</span>, (<span class="tk-k">await</span> <span class="tk-f">fetch</span>(<span class="tk-s">`${url}/api/lists/groceries`</span>)).status);</code></pre>
654
+ <!-- output: main -->
655
+ <pre class="plain"><code>3. POST /api/lists/groceries: added
656
+ GET /api/lists/groceries: [x] milk (owner), [ ] eggs (owner), [ ] bread (server), [ ] butter (owner)
657
+ GET without a token: 401</code></pre>
658
+ <p><code>butter</code> is the owner&#39;s: the route acted as the request&#39;s principal. Without a token the kit answered 401 before the
659
+ route ran. Another machine has no token file; it <strong>pairs</strong> instead: it asks <code>POST /_chord/pair</code> for a code, the owner
660
+ approves the code in a signed-in browser, and the client&#39;s next poll returns its token (example 54).</p>
661
+ </section>
662
+
663
+ <section class="chapter" id="page"><header class="opener"><div class="numeral">IV</div><h2>A Page</h2><p class="lede">A web entry, a web facet with React, the panels registry, and why a watch starts at the current value.</p></header>
664
+ <p class="dropcap">A page is a client host in the browser. The kit builds its <strong>base bundle</strong> from the app&#39;s web entry right after the
665
+ server listens (Chord, React, the app&#39;s web modules; the page&#39;s first request waits for it), serves the plugins&#39; web facets as small scripts beside it, and serves the page through
666
+ the app&#39;s template (design §23, &quot;Web and TUI&quot;).</p>
667
+ <h3 id="the-page-39-s-registry">The page&#39;s registry</h3>
668
+ <p>Plugins put panels on the page through a registry the API&#39;s web module declares, with Chord&#39;s React hooks re-exported
669
+ for them:</p>
670
+ <!-- quote: src/api/v1/web.ts -->
671
+ <pre><code><span class="tk-k">import</span> <span class="tk-k">type</span> { <span class="tk-t">ComponentType</span> } <span class="tk-k">from</span> <span class="tk-s">"react"</span>;
672
+ <span class="tk-k">import</span> { defineRegistry } <span class="tk-k">from</span> <span class="tk-s">"../../../../../../src/index.ts"</span>;
673
+
674
+ <span class="tk-k">export</span> { useHold, useHost, useMessages, usePageState, useWatch } <span class="tk-k">from</span> <span class="tk-s">"../../../../../../src/react/index.ts"</span>;
675
+
676
+ <span class="tk-c">/** A panel of the page: its name (a key, never shown) and its React component, which shows its own title. */</span>
677
+ <span class="tk-k">export</span> <span class="tk-k">type</span> <span class="tk-t">Panel</span> = { <span class="tk-k">readonly</span> name: string; <span class="tk-k">readonly</span> <span class="tk-t">Component</span>: <span class="tk-t">ComponentType</span> };
678
+
679
+ <span class="tk-c">/** The page's panels, one per contributing plugin, in plugin-name order. */</span>
680
+ <span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Panels</span> = <span class="tk-f">defineRegistry</span>&lt;<span class="tk-t">Panel</span>, <span class="tk-k">readonly</span> <span class="tk-t">Panel</span>[]&gt;(<span class="tk-s">"todo.panels"</span>, { version: <span class="tk-n">1</span> });</code></pre>
681
+ <p>A panel shows its own title, in the page&#39;s language; its <code>name</code> is only a key. The web entry serves <code>Panels</code> (its
682
+ <code>compose</code> keeps the contributions in plugin-name order), renders every panel, and starts the page&#39;s connection:</p>
683
+ <!-- quote: src/web/main.ts -->
684
+ <pre><code><span class="tk-c">/** The page's own facet: it serves `Panels`, composing the contributions in plugin-name order. */</span>
685
+ <span class="tk-k">const</span> core = <span class="tk-f">defineFacet</span>((host) =&gt; host.<span class="tk-f">serve</span>(v1web.<span class="tk-t">Panels</span>, { compose: (table) =&gt; table.<span class="tk-f">map</span>(({ value }) =&gt; value) }));
686
+
687
+ <span class="tk-k">function</span> <span class="tk-t">Page</span>() {
688
+ <span class="tk-k">const</span> panels = <span class="tk-f">useRegistry</span>(v1web.<span class="tk-t">Panels</span>) ?? [];
689
+ <span class="tk-c">// The page's one other path (`start.ts` serves the page at `/` and `/traces`).</span>
690
+ <span class="tk-k">if</span> (location.pathname === <span class="tk-s">"/traces"</span>) <span class="tk-k">return</span> <span class="tk-f">h</span>(<span class="tk-s">"main"</span>, { className: <span class="tk-s">"wide"</span> }, <span class="tk-f">h</span>(<span class="tk-s">"h1"</span>, <span class="tk-k">null</span>, <span class="tk-s">"Todo"</span>), <span class="tk-f">h</span>(<span class="tk-t">TraceViewer</span>));
691
+ <span class="tk-k">return</span> <span class="tk-f">h</span>(
692
+ <span class="tk-s">"main"</span>,
693
+ <span class="tk-k">null</span>,
694
+ <span class="tk-f">h</span>(<span class="tk-s">"h1"</span>, <span class="tk-k">null</span>, <span class="tk-s">"Todo"</span>),
695
+ ...panels.<span class="tk-f">map</span>(({ name, <span class="tk-t">Component</span> }) =&gt; <span class="tk-f">h</span>(<span class="tk-s">"section"</span>, { key: name }, <span class="tk-f">h</span>(<span class="tk-t">Component</span>))),
696
+ );
697
+ }
698
+
699
+ <span class="tk-k">void</span> <span class="tk-f">startPage</span>({
700
+ connect: () =&gt; <span class="tk-k">new</span> <span class="tk-t">WebSocket</span>(<span class="tk-s">`${location.origin.replace(/^http/, "ws")}/chord`</span>),
701
+ modules: { <span class="tk-s">"@todo/app/v1"</span>: v1, <span class="tk-s">"@todo/app/v1/web"</span>: v1web, react: <span class="tk-t">React</span>, <span class="tk-s">"react/jsx-runtime"</span>: jsx },
702
+ core: [{ name: <span class="tk-s">"@todo/web"</span>, facet: core }],
703
+ <span class="tk-c">// The page records its clicks and the calls under them, and sends them to the server every few seconds.</span>
704
+ telemetry: <span class="tk-k">true</span>,
705
+ render: (host) =&gt; <span class="tk-f">createRoot</span>(document.<span class="tk-f">getElementById</span>(<span class="tk-s">"root"</span>)!).<span class="tk-f">render</span>(<span class="tk-f">h</span>(<span class="tk-t">HostProvider</span>, { host }, <span class="tk-f">h</span>(<span class="tk-t">Page</span>))),
706
+ });</code></pre>
707
+ <p>The path <code>/traces</code> is the owner&#39;s trace viewer, and <code>telemetry</code> what it shows (chapter XI). <code>startPage</code> makes the page&#39;s host, connects it to <code>/chord</code>
708
+ (reconnecting after drops), loads the current plugin scripts with the entry&#39;s core facets, and keeps them current.
709
+ <code>HostProvider</code> gives the React tree that host.</p>
710
+ <h3 id="the-web-facet">The web facet</h3>
711
+ <p><code>plugins/lists/web.ts</code> contributes a panel. Its component holds the list&#39;s <code>Todos</code> while it is mounted and re-renders
712
+ when its items change:</p>
713
+ <!-- quote: plugins/lists/web.ts -->
714
+ <pre><code><span class="tk-k">function</span> <span class="tk-t">TodoList</span>() {
715
+ <span class="tk-k">const</span> { t } = <span class="tk-f">useMessages</span>(messages);
716
+ <span class="tk-k">const</span> host = <span class="tk-f">useHost</span>();
717
+ <span class="tk-k">const</span> list = location.hash.<span class="tk-f">slice</span>(<span class="tk-n">1</span>) || <span class="tk-s">"groceries"</span>;
718
+ <span class="tk-k">const</span> todos = <span class="tk-f">useHold</span>(<span class="tk-t">Todos</span>, { list });
719
+ <span class="tk-k">const</span> items = <span class="tk-f">useWatch</span>(todos?.items) ?? [];
720
+ <span class="tk-k">const</span> notice = <span class="tk-f">useWatch</span>(todos?.notice);
721
+ <span class="tk-c">// Kept by the page, not the component: the new code's component finds the half-typed todo.</span>
722
+ <span class="tk-k">const</span> [text, setText] = <span class="tk-f">usePageState</span>(<span class="tk-s">`lists.draft.${list}`</span>, <span class="tk-s">""</span>);
723
+ <span class="tk-k">const</span> add = (event: { <span class="tk-f">preventDefault</span>(): <span class="tk-k">void</span> }) =&gt; {
724
+ event.<span class="tk-f">preventDefault</span>();
725
+ <span class="tk-c">// An empty todo too: the list's store refuses it, and the click's trace shows where.</span>
726
+ <span class="tk-k">if</span> (todos === <span class="tk-k">undefined</span>) <span class="tk-k">return</span>;
727
+ <span class="tk-c">// The click is a trace of its own: a root span of the page, the call to the list's worker under it.</span>
728
+ <span class="tk-k">const</span> trace = { name: <span class="tk-s">"page.add"</span>, attributes: { chars: text.length } };
729
+ <span class="tk-k">void</span> host.<span class="tk-f">root</span>((ctx) =&gt; todos.<span class="tk-f">add</span>(text, ctx), context, { trace }).<span class="tk-k">catch</span>(() =&gt; {});
730
+ <span class="tk-f">setText</span>(<span class="tk-s">""</span>);
731
+ };
732
+ <span class="tk-k">return</span> <span class="tk-f">h</span>(
733
+ <span class="tk-s">"div"</span>,
734
+ { className: <span class="tk-s">"lists-panel"</span> },
735
+ <span class="tk-f">h</span>(<span class="tk-s">"h2"</span>, <span class="tk-k">null</span>, <span class="tk-f">t</span>(<span class="tk-s">"title"</span>)),
736
+ <span class="tk-f">h</span>(
737
+ <span class="tk-s">"ul"</span>,
738
+ <span class="tk-k">null</span>,
739
+ ...items.<span class="tk-f">map</span>((item) =&gt;
740
+ <span class="tk-f">h</span>(
741
+ <span class="tk-s">"li"</span>,
742
+ {
743
+ key: item.id,
744
+ className: item.done ? <span class="tk-s">"lists-done"</span> : <span class="tk-k">undefined</span>,
745
+ onClick: () =&gt; <span class="tk-k">void</span> todos?.<span class="tk-f">tick</span>({ id: item.id, done: !item.done }, context),
746
+ },
747
+ item.text,
748
+ <span class="tk-f">h</span>(<span class="tk-s">"span"</span>, { className: <span class="tk-s">"lists-by"</span> }, item.by),
749
+ ),
750
+ ),
751
+ ),
752
+ <span class="tk-f">h</span>(
753
+ <span class="tk-s">"form"</span>,
754
+ { onSubmit: add },
755
+ <span class="tk-f">h</span>(<span class="tk-s">"input"</span>, {
756
+ value: text,
757
+ placeholder: <span class="tk-f">t</span>(<span class="tk-s">"placeholder"</span>),
758
+ onChange: (event: { target: { value: string } }) =&gt; <span class="tk-f">setText</span>(event.target.value),
759
+ }),
760
+ ),
761
+ todos?.variant === <span class="tk-s">"owner"</span>
762
+ ? <span class="tk-f">h</span>(<span class="tk-s">"button"</span>, { <span class="tk-k">type</span>: <span class="tk-s">"button"</span>, onClick: () =&gt; <span class="tk-k">void</span> todos.<span class="tk-f">clear</span>(context) }, <span class="tk-f">t</span>(<span class="tk-s">"clearDone"</span>))
763
+ : <span class="tk-k">null</span>,
764
+ notice === <span class="tk-k">undefined</span> || notice === <span class="tk-k">null</span> ? <span class="tk-k">null</span> : <span class="tk-f">h</span>(<span class="tk-s">"p"</span>, { className: <span class="tk-s">"lists-notice"</span> }, t.<span class="tk-f">render</span>(notice)),
765
+ );
766
+ }
767
+
768
+ <span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) =&gt; {
769
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Translations</span>, messages.translations);
770
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Panels</span>, { name: <span class="tk-s">"groceries"</span>, <span class="tk-t">Component</span>: <span class="tk-t">TodoList</span> });
771
+ });</code></pre>
772
+ <p><code>useMessages(messages)</code> gives the plugin&#39;s texts in the page&#39;s language (chapter VIII; <code>messages</code> is the plugin&#39;s
773
+ <code>messages.ts</code>, and the facet contributes its catalogs to the page&#39;s <code>Translations</code>). <code>useHold</code> holds a reference while
774
+ the component is mounted and releases it on unmount; it is <code>undefined</code> on the first render. <code>useWatch</code> follows a member. <code>usePageState</code> is <code>useState</code> whose value the page keeps under a key instead of the
775
+ component: chapter IX shows why the half-typed todo needs it. Clicks call methods with <code>BACKGROUND_CONTEXT</code>, which never ends, so a click&#39;s call
776
+ is not cancelled when the handler returns; adding a todo runs in <code>host.root</code> (<code>useHost</code> gives the page&#39;s host), which
777
+ makes the click a trace of its own (chapter XI). <code>todos?.variant === &quot;owner&quot;</code> narrows the reference: only the owner&#39;s has
778
+ <code>clear</code>, and TypeScript knows it. The panel&#39;s look reads the active theme&#39;s CSS variables, with class names prefixed
779
+ by the plugin&#39;s name (a <code>*.module.css</code> file would scope them instead):</p>
780
+ <!-- quote: plugins/lists/lists.css -->
781
+ <pre class="plain"><code>.lists-panel input { width: 100%; box-sizing: border-box; padding: 0.4rem; background: var(--bg); color: var(--fg);
782
+ border: 1px solid var(--accent); border-radius: var(--radius, 0); }</code></pre>
783
+ <p>The template in <code>start.ts</code> puts the assets the kit made into the app&#39;s own HTML:</p>
784
+ <!-- quote: start.ts -->
785
+ <pre><code><span class="tk-k">function</span> <span class="tk-f">page</span>(request: { <span class="tk-k">readonly</span> nonce: string }, assets: <span class="tk-t">PageAssets</span>): string {
786
+ <span class="tk-k">return</span> <span class="tk-s">`&lt;!doctype html&gt;&lt;html lang="en"&gt;&lt;head&gt;&lt;meta charset="utf-8"&gt;&lt;title&gt;Todo&lt;/title&gt;${pageScripts(assets, request.nonce)}&lt;/head&gt;&lt;body&gt;&lt;div id="root"&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;`</span>;
787
+ }</code></pre>
788
+ <h3 id="running-it">Running it</h3>
789
+ <p>Step 4 reads the page as a browser gets it and watches the list from the server:</p>
790
+ <!-- quote: main.ts -->
791
+ <pre><code><span class="tk-k">const</span> html = <span class="tk-k">await</span> (<span class="tk-k">await</span> <span class="tk-f">fetch</span>(url, { headers: cli })).<span class="tk-f">text</span>();
792
+ console.<span class="tk-f">log</span>(<span class="tk-s">"4. the page loads:"</span>);
793
+ <span class="tk-k">for</span> (<span class="tk-k">const</span> [, asset] <span class="tk-k">of</span> html.<span class="tk-f">matchAll</span>(/(?:src|href)=<span class="tk-s">"([^"</span>]+)"/g)) console.<span class="tk-f">log</span>(<span class="tk-s">` ${hashless(asset!)}`</span>);
794
+ <span class="tk-k">const</span> groceries = <span class="tk-k">await</span> app.host.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> }, context);
795
+ <span class="tk-k">const</span> watch = <span class="tk-k">await</span> groceries.items.<span class="tk-f">watch</span>(context);
796
+ console.<span class="tk-f">log</span>(<span class="tk-s">" watch.value:"</span>, <span class="tk-f">done</span>(watch.value), <span class="tk-s">"done"</span>);
797
+ <span class="tk-k">const</span> changed = <span class="tk-t">Promise</span>.<span class="tk-f">withResolvers</span>&lt;<span class="tk-k">readonly</span> <span class="tk-t">Item</span>[]&gt;();
798
+ watch.<span class="tk-f">start</span>(<span class="tk-k">async</span> (call) =&gt; {
799
+ <span class="tk-k">if</span> (call.kind === <span class="tk-s">"value"</span>) changed.<span class="tk-f">resolve</span>(call.value);
800
+ });
801
+ <span class="tk-k">await</span> groceries.<span class="tk-f">tick</span>({ id: <span class="tk-n">2</span>, done: <span class="tk-k">true</span> }, context);
802
+ console.<span class="tk-f">log</span>(<span class="tk-s">" the listener's first call:"</span>, <span class="tk-f">done</span>(<span class="tk-k">await</span> changed.promise), <span class="tk-s">"done"</span>);
803
+ <span class="tk-k">await</span> watch.<span class="tk-f">stop</span>();</code></pre>
804
+ <!-- output: main -->
805
+ <pre class="plain"><code>4. the page loads:
806
+ /_chord/web/base.&lt;hash&gt;.css
807
+ /_chord/web/plugin/@todo~lists.&lt;hash&gt;.&lt;hash&gt;.css
808
+ /_chord/web/plugins.&lt;hash&gt;.js
809
+ /_chord/web/base.&lt;hash&gt;.js
810
+ watch.value: 1 done
811
+ the listener's first call: 2 done</code></pre>
812
+ <p>The page is the template, the base bundle (<code>base.&lt;hash&gt;.js</code> and <code>.css</code>), one combined file with every plugin&#39;s web
813
+ script and one stylesheet per plugin with CSS: two scripts on a cold load, cacheable forever by their hashes. After the
814
+ owner opened the owner link in Chromium, the page shows the list:</p>
815
+ <figure class="fig"><p><img src="screens/page.png" alt="The todo page"></p>
816
+ <figcaption><span class="fignum">Figure 1.</span> The page after the owner signed in: the groceries list, live, each todo with who added it; the owner&#39;s variant shows &quot;Clear done&quot;.</figcaption></figure>
817
+ <h3 id="trap-a-watch-starts-at-the-current-value">Trap: a watch starts at the current value</h3>
818
+ <p><code>watch.value</code> was the value when the watch was taken (one todo done); the listener&#39;s first call was the change after
819
+ it (two done). <strong>A watch&#39;s listener gets only changes.</strong> Code that renders from a listener alone draws nothing until
820
+ the first change. Read <code>handle.value</code> first, then <code>start</code> (design §11, &quot;Watchables&quot;). <code>useWatch</code> and the terminal&#39;s
821
+ <code>TuiScope.watch</code> do this for you; <code>self.on</code>, an object&#39;s routine, is the one consumer whose first call is the current
822
+ value.</p>
823
+ </section>
824
+
825
+ <section class="chapter" id="terminal"><header class="opener"><div class="numeral">V</div><h2>A Terminal</h2><p class="lede">The same plugin in a terminal: a terminal facet with TuiScope, run in the launcher&#39;s sandbox.</p></header>
826
+ <p class="dropcap">The terminal client is a client host too, run by a <strong>launcher</strong>: a trusted Node program that signs in, downloads the
827
+ terminal&#39;s base bundle and the plugins&#39; terminal scripts, and runs them in a <strong>sandbox</strong>, a child process with no file
828
+ system, network, processes or terminal of its own. The launcher relays its link and draws its frames. Code from the
829
+ server never touches Mara&#39;s machine directly (decision &quot;The terminal&#39;s sandbox is a child process&quot;).</p>
830
+ <p>The API&#39;s terminal module declares the parts of the screen:</p>
831
+ <!-- quote: src/api/v1/tui.ts -->
832
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">type</span> <span class="tk-t">Screen</span> = {
833
+ <span class="tk-k">readonly</span> name: string;
834
+ <span class="tk-k">readonly</span> open: (scope: <span class="tk-t">TuiScope</span>, theme: <span class="tk-t">TerminalTheme</span>) =&gt; () =&gt; <span class="tk-k">readonly</span> string[];
835
+ };
836
+
837
+ <span class="tk-c">/** The terminal's parts, in plugin-name order. */</span>
838
+ <span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Screens</span> = <span class="tk-f">defineRegistry</span>&lt;<span class="tk-t">Screen</span>, <span class="tk-k">readonly</span> <span class="tk-t">Screen</span>[]&gt;(<span class="tk-s">"todo.screens"</span>, { version: <span class="tk-n">1</span> });</code></pre>
839
+ <p>The terminal entry serves <code>Screens</code> and redraws on every change. A <strong><code>TuiScope</code></strong> holds references, follows values and
840
+ registries, calls one callback on every change, and releases everything when disposed; it is the terminal&#39;s
841
+ counterpart of the React hooks, for any terminal library:</p>
842
+ <!-- quote: src/tui/main.ts -->
843
+ <pre><code><span class="tk-c">/** The terminal's own facet: it serves `Screens`, composing the contributions in plugin-name order. */</span>
844
+ <span class="tk-k">const</span> core = <span class="tk-f">defineFacet</span>((host) =&gt; host.<span class="tk-f">serve</span>(v1tui.<span class="tk-t">Screens</span>, { compose: (table) =&gt; table.<span class="tk-f">map</span>(({ value }) =&gt; value) }));
845
+
846
+ <span class="tk-k">void</span> <span class="tk-f">startTerminalRuntime</span>({
847
+ modules: { <span class="tk-s">"@todo/app/v1"</span>: v1, <span class="tk-s">"@todo/app/v1/tui"</span>: v1tui },
848
+ core: [{ name: <span class="tk-s">"@todo/tui"</span>, facet: core }],
849
+ render: (runtime) =&gt; {
850
+ <span class="tk-k">const</span> { terminal, theme } = runtime;
851
+ <span class="tk-k">const</span> screens = <span class="tk-k">new</span> <span class="tk-t">TuiScope</span>(runtime.host, () =&gt; <span class="tk-f">draw</span>()).<span class="tk-f">registry</span>(v1tui.<span class="tk-t">Screens</span>);
852
+ <span class="tk-k">let</span> opened: { screens: <span class="tk-k">readonly</span> v1tui.<span class="tk-t">Screen</span>[]; scope: <span class="tk-t">TuiScope</span>; lines: (() =&gt; <span class="tk-k">readonly</span> string[])[] } | <span class="tk-k">undefined</span>;
853
+ <span class="tk-k">const</span> draw = (): <span class="tk-k">void</span> =&gt; {
854
+ <span class="tk-k">const</span> current = <span class="tk-f">screens</span>() ?? [];
855
+ <span class="tk-k">if</span> (opened?.screens !== current) {
856
+ opened?.scope.<span class="tk-f">dispose</span>();
857
+ <span class="tk-k">const</span> scope = <span class="tk-k">new</span> <span class="tk-t">TuiScope</span>(runtime.host, () =&gt; <span class="tk-f">draw</span>());
858
+ opened = { screens: current, scope, lines: current.<span class="tk-f">map</span>((screen) =&gt; screen.<span class="tk-f">open</span>(scope, theme)) };
859
+ }
860
+ <span class="tk-k">const</span> lines = opened.lines.<span class="tk-f">flatMap</span>((read) =&gt; <span class="tk-f">read</span>());
861
+ terminal.<span class="tk-f">write</span>(<span class="tk-s">`\x1b[2J\x1b[H${lines.join("\r\n")}\r\n`</span>);
862
+ };
863
+ theme.<span class="tk-f">onChange</span>(draw);
864
+ terminal.<span class="tk-f">onInput</span>((data) =&gt; {
865
+ <span class="tk-k">if</span> (data === <span class="tk-s">"q"</span>) runtime.<span class="tk-f">exit</span>(<span class="tk-n">0</span>);
866
+ });
867
+ <span class="tk-f">draw</span>();
868
+ },
869
+ });</code></pre>
870
+ <p>When a reload changes <code>Screens</code>, the entry disposes the old scope and opens the screens again. The lists plugin&#39;s
871
+ terminal facet draws its title, the todos and the list&#39;s notice; <code>scope.messages(messages)</code> is the terminal&#39;s
872
+ <code>useMessages</code> (chapter VIII):</p>
873
+ <!-- quote: plugins/lists/tui.ts -->
874
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) =&gt; {
875
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Translations</span>, messages.translations);
876
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Screens</span>, {
877
+ name: <span class="tk-s">"groceries"</span>,
878
+ open: (scope, theme) =&gt; {
879
+ <span class="tk-k">const</span> t = scope.<span class="tk-f">messages</span>(messages);
880
+ <span class="tk-k">const</span> todos = scope.<span class="tk-f">hold</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> });
881
+ <span class="tk-k">const</span> items = scope.<span class="tk-f">watch</span>(todos.items);
882
+ <span class="tk-k">const</span> notice = scope.<span class="tk-f">watch</span>(todos.notice);
883
+ <span class="tk-k">return</span> () =&gt; {
884
+ <span class="tk-k">const</span> shown = <span class="tk-f">notice</span>();
885
+ <span class="tk-k">return</span> [
886
+ theme.<span class="tk-f">fg</span>(<span class="tk-s">"accent"</span>, <span class="tk-f">t</span>(<span class="tk-s">"title"</span>)),
887
+ ...(<span class="tk-f">items</span>() ?? []).<span class="tk-f">map</span>((item) =&gt;
888
+ item.done ? theme.<span class="tk-f">fg</span>(<span class="tk-s">"muted"</span>, <span class="tk-s">`[x] ${item.text}`</span>) : <span class="tk-s">`[ ] ${item.text} ${theme.fg("muted", item.by)}`</span>,
889
+ ),
890
+ ...(shown === <span class="tk-k">undefined</span> || shown === <span class="tk-k">null</span> ? [] : [theme.<span class="tk-f">fg</span>(<span class="tk-s">"warning"</span>, t.<span class="tk-f">render</span>(shown))]),
891
+ ];
892
+ };
893
+ },
894
+ });
895
+ });</code></pre>
896
+ <p><code>theme.fg(&quot;muted&quot;, …)</code> colours by role, never by a fixed colour. Step 5 runs the launcher on a scripted terminal, its
897
+ languages fixed (by default they are the machine&#39;s system locale):</p>
898
+ <!-- quote: main.ts -->
899
+ <pre><code><span class="tk-k">if</span> (<span class="tk-f">sandboxAvailable</span>()) {
900
+ <span class="tk-k">const</span> output = <span class="tk-t">Object</span>.<span class="tk-f">assign</span>(<span class="tk-k">new</span> <span class="tk-t">PassThrough</span>(), { columns: <span class="tk-n">60</span>, rows: <span class="tk-n">12</span> });
901
+ output.<span class="tk-f">on</span>(<span class="tk-s">"data"</span>, (chunk: <span class="tk-t">Buffer</span>) =&gt; frames.<span class="tk-f">push</span>(chunk.<span class="tk-f">toString</span>()));
902
+ terminal = <span class="tk-k">await</span> <span class="tk-f">startTerminal</span>({
903
+ url,
904
+ credentials: { kind: <span class="tk-s">"file"</span>, path: app.ownerTokenFile },
905
+ input: <span class="tk-k">new</span> <span class="tk-t">PassThrough</span>(),
906
+ output,
907
+ colors: <span class="tk-s">"truecolor"</span>,
908
+ <span class="tk-c">// The machine's languages (its system locale by default), here fixed for the book's output.</span>
909
+ locales: [<span class="tk-s">"en-US"</span>],
910
+ });
911
+ <span class="tk-k">await</span> <span class="tk-f">until</span>(() =&gt; <span class="tk-f">screen</span>(frames).<span class="tk-f">includes</span>(<span class="tk-s">"butter"</span>));
912
+ console.<span class="tk-f">log</span>(<span class="tk-s">"5. the terminal shows:"</span>);
913
+ <span class="tk-k">for</span> (<span class="tk-k">const</span> line <span class="tk-k">of</span> <span class="tk-f">screen</span>(frames).<span class="tk-f">trim</span>().<span class="tk-f">split</span>(<span class="tk-s">"\r\n"</span>)) console.<span class="tk-f">log</span>(<span class="tk-s">` ${line}`</span>);
914
+ } <span class="tk-k">else</span> console.<span class="tk-f">log</span>(<span class="tk-s">"5. (the terminal client needs Node 26.5.1 or later for its sandbox)"</span>);</code></pre>
915
+ <!-- output: tui -->
916
+ <pre class="plain"><code>5. the terminal shows:
917
+ Groceries
918
+ [x] milk
919
+ [x] eggs
920
+ [ ] bread server
921
+ [ ] butter owner</code></pre>
922
+ </section>
923
+
924
+ <section class="chapter" id="theme"><header class="opener"><div class="numeral">VI</div><h2>A Theme</h2><p class="lede">A theme is data in a registry; a plugin added while the app runs restyles the page and the terminal in place.</p></header>
925
+ <p class="dropcap">Mara wants the evening look. A theme is data: colours by role, a few web extras, terminal overrides. The kit&#39;s <code>Themes</code>
926
+ registry collects them; each user picks one dark and one light theme, the system preference chooses (decision &quot;Looks
927
+ and themes&quot;). The Harbor theme is a plugin with its data in one file:</p>
928
+ <!-- quote: later/harbor/theme.ts -->
929
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">const</span> harbor: <span class="tk-t">Theme</span> = {
930
+ id: <span class="tk-s">"harbor"</span>,
931
+ name: <span class="tk-s">"Harbor"</span>,
932
+ mode: <span class="tk-s">"dark"</span>,
933
+ colors: {
934
+ accent: <span class="tk-s">"#f2a65a"</span>,
935
+ bg: <span class="tk-s">"#12232e"</span>,
936
+ fg: <span class="tk-s">"#d8e2e8"</span>,
937
+ muted: <span class="tk-s">"#6b8796"</span>,
938
+ error: <span class="tk-s">"#e5676b"</span>,
939
+ warning: <span class="tk-s">"#e9c46a"</span>,
940
+ success: <span class="tk-s">"#7fb685"</span>,
941
+ },
942
+ web: { radius: <span class="tk-s">"6px"</span> },
943
+ tui: { muted: <span class="tk-s">"66"</span> },
944
+ };</code></pre>
945
+ <p>and two facets contributing it, one for pages and one for terminals (registries are per host, so each kind gets its
946
+ own contribution):</p>
947
+ <!-- quote: later/harbor/web.ts -->
948
+ <pre><code><span class="tk-k">import</span> { defineFacet, <span class="tk-t">Themes</span> } <span class="tk-k">from</span> <span class="tk-s">"@todo/app/v1"</span>;
949
+ <span class="tk-k">import</span> { harbor } <span class="tk-k">from</span> <span class="tk-s">"./theme.ts"</span>;
950
+
951
+ <span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) =&gt; self.<span class="tk-f">contribute</span>(<span class="tk-t">Themes</span>, harbor));</code></pre>
952
+ <p>The plugin is not in the app&#39;s root at start. Step 6 copies it in while everything runs and applies the change:</p>
953
+ <!-- quote: main.ts -->
954
+ <pre><code><span class="tk-k">await</span> <span class="tk-f">cp</span>(<span class="tk-f">fileURLToPath</span>(<span class="tk-k">new</span> <span class="tk-f">URL</span>(<span class="tk-s">"./later/harbor/"</span>, <span class="tk-k">import</span>.meta.url)), <span class="tk-f">join</span>(root, <span class="tk-s">"harbor"</span>), { recursive: <span class="tk-k">true</span> });
955
+ <span class="tk-k">await</span> <span class="tk-f">until</span>(() =&gt; app.plugins.state.value.set);
956
+ <span class="tk-k">const</span> added = <span class="tk-k">await</span> app.<span class="tk-f">applyChanges</span>(context);
957
+ console.<span class="tk-f">log</span>(<span class="tk-s">"6. harbor added, rolled out to:"</span>, added.steps.<span class="tk-f">map</span>(({ name }) =&gt; name).<span class="tk-f">join</span>(<span class="tk-s">", "</span>));
958
+ <span class="tk-k">const</span> scripts = <span class="tk-k">await</span> app.host.<span class="tk-f">get</span>(<span class="tk-t">WebScripts</span>, {}, context);
959
+ console.<span class="tk-f">log</span>(<span class="tk-s">" the page's plugins:"</span>, scripts.set.value.plugins.<span class="tk-f">map</span>(({ name }) =&gt; name).<span class="tk-f">join</span>(<span class="tk-s">", "</span>));
960
+ scripts.<span class="tk-f">release</span>();</code></pre>
961
+ <!-- output: main -->
962
+ <pre class="plain"><code>6. harbor added, rolled out to: server, list, web, tui
963
+ the page's plugins: @todo/french, @todo/harbor, @todo/lists</code></pre>
964
+ <!-- output: tui -->
965
+ <pre class="plain"><code> the terminal redraws its title in Harbor's accent: "\u001b[38;2;242;166;90mGroceries"</code></pre>
966
+ <p>A directory appearing in a plugin root marks the plugin set changed (<code>app.plugins.state.value.set</code>); nothing happens
967
+ until <code>applyChanges</code>, which builds the new plugin and rolls it out: the server, the list workers, then the page and
968
+ terminal surfaces. The page&#39;s host learned of the new script set through Chord, fetched Harbor&#39;s web script and loaded
969
+ it in place; the terminal did the same with its terminal script and redrew in Harbor&#39;s accent. With the browser
970
+ preferring dark:</p>
971
+ <figure class="fig"><p><img src="screens/harbor.png" alt="The page in the Harbor theme"></p>
972
+ <figcaption><span class="fignum">Figure 2.</span> The same page after the Harbor plugin was added and applied: no reload, the CSS variables on <code>:root</code> changed.</figcaption></figure>
973
+ </section>
974
+
975
+ <section class="chapter" id="user"><header class="opener"><div class="numeral">VII</div><h2>A Second User</h2><p class="lede">A one-line login plugin, a client host of Ada&#39;s, and why a host must load before it asks.</p></header>
976
+ <p class="dropcap">Ada joins with a name. The kit ships an ephemeral login as a facet (<code>POST /_chord/login { name }</code> signs in as <code>{ kind: &quot;user&quot;, id, name }</code>, no account stored); the API&#39;s server module exports it for the app&#39;s principal type:</p>
977
+ <!-- quote: src/api/v1/server.ts -->
978
+ <pre><code><span class="tk-c">/** The kit's ephemeral login: `POST /_chord/login { name }` signs in as `{ kind: "user", id, name }`. */</span>
979
+ <span class="tk-k">export</span> <span class="tk-k">const</span> login = <span class="tk-f">ephemeralLoginFacet</span>&lt;<span class="tk-t">TodoPrincipal</span>&gt;();</code></pre>
980
+ <p>and the app adds it as a <strong>single-file plugin</strong>, a file named <code>&lt;name&gt;.&lt;kind&gt;.ts</code> at the root&#39;s top:</p>
981
+ <!-- quote: plugins/login.server.ts -->
982
+ <pre><code><span class="tk-c">// The kit's pick-a-name login, added to the app as a single-file plugin: a server facet, one line.</span>
983
+
984
+ <span class="tk-k">import</span> { login } <span class="tk-k">from</span> <span class="tk-s">"@todo/app/v1/server"</span>;
985
+
986
+ <span class="tk-k">export</span> <span class="tk-k">default</span> login;</code></pre>
987
+ <p>Step 7 does what Ada&#39;s page does: logs in, then connects a client host of her own over the links endpoint with her
988
+ cookie:</p>
989
+ <!-- quote: main.ts -->
990
+ <pre><code><span class="tk-k">const</span> login = <span class="tk-k">await</span> <span class="tk-f">fetch</span>(<span class="tk-s">`${url}/_chord/login`</span>, {
991
+ method: <span class="tk-s">"POST"</span>,
992
+ headers: { <span class="tk-s">"content-type"</span>: <span class="tk-s">"application/json"</span> },
993
+ body: JSON.<span class="tk-f">stringify</span>({ name: <span class="tk-s">"ada"</span> }),
994
+ });
995
+ <span class="tk-k">const</span> adaCookie = login.headers.<span class="tk-f">get</span>(<span class="tk-s">"set-cookie"</span>)!.<span class="tk-f">split</span>(<span class="tk-s">";"</span>)[<span class="tk-n">0</span>]!;
996
+ <span class="tk-k">const</span> ada = <span class="tk-f">createHost</span>&lt;<span class="tk-t">TodoPrincipal</span>&gt;({ onError: (error) =&gt; console.<span class="tk-f">error</span>(<span class="tk-s">"ada:"</span>, error.kind) });
997
+ <span class="tk-k">try</span> {
998
+ ada.<span class="tk-f">hold</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> }, context);
999
+ } <span class="tk-k">catch</span> (error) {
1000
+ console.<span class="tk-f">log</span>(<span class="tk-s">"7. ada's hold before load:"</span>, (error <span class="tk-k">as</span> <span class="tk-t">Error</span>).message);
1001
+ }
1002
+ <span class="tk-k">await</span> ada.<span class="tk-f">load</span>([], context);
1003
+ <span class="tk-k">const</span> socket = <span class="tk-k">new</span> <span class="tk-t">WebSocket</span>(<span class="tk-s">`${url.replace(/^http/, "ws")}/chord`</span>, { headers: { cookie: adaCookie } });
1004
+ ada.<span class="tk-f">connect</span>(<span class="tk-f">webSocketLink</span>(socket <span class="tk-k">as</span> <span class="tk-k">unknown</span> <span class="tk-k">as</span> <span class="tk-t">WebSocketLike</span>), { principal: { kind: <span class="tk-s">"server"</span> } }, context);
1005
+ <span class="tk-k">const</span> adas = <span class="tk-k">await</span> ada.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> }, context);
1006
+ <span class="tk-k">await</span> adas.<span class="tk-f">add</span>(<span class="tk-s">"coffee"</span>, context);
1007
+ console.<span class="tk-f">log</span>(<span class="tk-s">" ada's variant:"</span>, adas.variant);
1008
+ <span class="tk-k">if</span> (adas.variant === <span class="tk-s">"member"</span>) {
1009
+ <span class="tk-c">// @ts-expect-error `clear` is the owner's: TypeScript refuses it, and so does the worker.</span>
1010
+ <span class="tk-k">const</span> clearing: <span class="tk-t">Promise</span>&lt;<span class="tk-k">null</span>&gt; = adas.<span class="tk-f">clear</span>(context);
1011
+ <span class="tk-k">await</span> clearing.<span class="tk-k">catch</span>((error: <span class="tk-t">ChordError</span>) =&gt; console.<span class="tk-f">log</span>(<span class="tk-s">" ada's clear:"</span>, <span class="tk-s">`${error.code}/${error.reason}`</span>));
1012
+ }
1013
+ <span class="tk-k">await</span> <span class="tk-f">until</span>(() =&gt; groceries.items.value.<span class="tk-f">some</span>(({ by }) =&gt; by === <span class="tk-s">"ada"</span>));
1014
+ console.<span class="tk-f">log</span>(<span class="tk-s">" the server's reference sees:"</span>, <span class="tk-f">show</span>(groceries.items.value.<span class="tk-f">slice</span>(-<span class="tk-n">1</span>)));</code></pre>
1015
+ <!-- output: main -->
1016
+ <pre class="plain"><code>7. ada's hold before load: todo.todos: the host has not loaded; call host.load first (a host with nothing to load calls load([]))
1017
+ ada's variant: member
1018
+ ada's clear: invalid/member
1019
+ the server's reference sees: [ ] coffee (ada)</code></pre>
1020
+ <h3 id="trap-load-before-use">Trap: load before use</h3>
1021
+ <p>Ada&#39;s first <code>hold</code> threw at once: <strong>a host serves and binds nothing before <code>load</code>.</strong> Requests made before it would wait
1022
+ for a publication nobody started, so Chord refuses them with <code>not_ready</code> (&quot;call <code>host.load</code> first&quot;); a client with no
1023
+ plugins of its own calls <code>load([])</code> (design §3). <code>startPage</code>, the launcher and the kit do it for you; code that makes
1024
+ a host itself must not forget.</p>
1025
+ <p>Ada got the <code>member</code> variant; her <code>clear</code> is refused twice, by TypeScript (the <code>@ts-expect-error</code>) and by the worker
1026
+ (<code>invalid/member</code>), because what a reference offers is decided by the provider&#39;s variant, not by the caller&#39;s code.
1027
+ Her todo is hers: <code>coffee (ada)</code>.</p>
1028
+ </section>
1029
+
1030
+ <section class="chapter" id="languages"><header class="opener"><div class="numeral">VIII</div><h2>Languages</h2><p class="lede">Messages in code, a French plugin, a language chosen live, and a notice each viewer reads in its own language.</p></header>
1031
+ <p class="dropcap">Mara reads German; Ada would rather read French. Both look at the same list, served by the same worker, so its text
1032
+ cannot be formatted once for everyone: <strong>text people read is rendered per viewer, never by a server</strong> (decision
1033
+ &quot;Messages and languages in the app kit&quot;). A plugin writes its texts once, in code; languages are contributions; the
1034
+ language is a preference of each user.</p>
1035
+ <h3 id="messages-in-code">Messages in code</h3>
1036
+ <p>The lists plugin keeps its texts in one file, <code>messages.ts</code>, which every facet that renders imports (and the worker,
1037
+ below):</p>
1038
+ <!-- quote: plugins/lists/messages.ts -->
1039
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">const</span> messages = <span class="tk-f">defineMessages</span>(<span class="tk-s">"todo.lists"</span>, {
1040
+ en: {
1041
+ title: <span class="tk-s">"Groceries"</span>,
1042
+ placeholder: <span class="tk-s">"Add a todo"</span>,
1043
+ clearDone: <span class="tk-s">"Clear done"</span>,
1044
+ open: (n: number) =&gt; (n === <span class="tk-n">1</span> ? <span class="tk-s">"1 open"</span> : <span class="tk-s">`${n} open`</span>),
1045
+ cleared: (by: string) =&gt; <span class="tk-s">`${by} cleared the done todos`</span>,
1046
+ },
1047
+ de: {
1048
+ title: <span class="tk-s">"Einkäufe"</span>,
1049
+ placeholder: <span class="tk-s">"Neues Todo"</span>,
1050
+ clearDone: <span class="tk-s">"Erledigte entfernen"</span>,
1051
+ open: (n) =&gt; <span class="tk-s">`${n} offen`</span>,
1052
+ cleared: (by) =&gt; <span class="tk-s">`${by} hat die erledigten Todos entfernt`</span>,
1053
+ },
1054
+ });</code></pre>
1055
+ <p>The first catalog defines the keys and their argument types, and the English catalog is the source of truth; the
1056
+ German one gets its functions&#39; parameter types from it, and a missing key, an unknown one or a wrong argument
1057
+ (<code>t(&quot;open&quot;, &quot;3&quot;)</code>) is a type error, not a blank in production. The namespace (<code>todo.lists</code>) keeps two plugins&#39; <code>title</code>
1058
+ apart. Functions cover plurals and word order; dates and numbers inside them go through <code>formatTime</code> and
1059
+ <code>formatNumber</code>, which use the locale being rendered.</p>
1060
+ <p>The panel renders with <code>useMessages(messages)</code> and the terminal screen with <code>scope.messages(messages)</code> (chapters IV
1061
+ and V): both give a <code>t</code> bound to their host&#39;s language, and re-render or redraw when it changes. Each facet that
1062
+ renders also contributes the plugin&#39;s own catalogs to its host&#39;s <code>Translations</code>:</p>
1063
+ <!-- quote: plugins/lists/web.ts -->
1064
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) =&gt; {
1065
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Translations</span>, messages.translations);
1066
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Panels</span>, { name: <span class="tk-s">"groceries"</span>, <span class="tk-t">Component</span>: <span class="tk-t">TodoList</span> });
1067
+ });</code></pre>
1068
+ <p>Registries are per host, and a host learns from them which languages its plugins offer: the page matches the
1069
+ browser&#39;s languages against them before it picks one.</p>
1070
+ <h3 id="a-translation-plugin">A translation plugin</h3>
1071
+ <p>French comes from a plugin of its own, written by someone who never touches the lists plugin. Its catalog is typed by
1072
+ the lists plugin&#39;s messages, a type-only import erased at build:</p>
1073
+ <!-- quote: plugins/french/fr.ts -->
1074
+ <pre><code><span class="tk-k">import</span> <span class="tk-k">type</span> { <span class="tk-t">MessagesOf</span>, <span class="tk-t">Translation</span> } <span class="tk-k">from</span> <span class="tk-s">"@todo/app/v1"</span>;
1075
+ <span class="tk-k">import</span> <span class="tk-k">type</span> { messages } <span class="tk-k">from</span> <span class="tk-s">"../lists/messages.ts"</span>;
1076
+
1077
+ <span class="tk-k">const</span> lists: <span class="tk-t">MessagesOf</span>&lt;<span class="tk-k">typeof</span> messages&gt; = {
1078
+ title: <span class="tk-s">"Courses"</span>,
1079
+ placeholder: <span class="tk-s">"Nouvelle tâche"</span>,
1080
+ clearDone: <span class="tk-s">"Retirer les tâches faites"</span>,
1081
+ open: (n) =&gt; <span class="tk-s">`${n} à faire`</span>,
1082
+ cleared: (by) =&gt; <span class="tk-s">`${by} a retiré les tâches faites`</span>,
1083
+ };
1084
+
1085
+ <span class="tk-k">export</span> <span class="tk-k">const</span> french: <span class="tk-k">readonly</span> <span class="tk-t">Translation</span>[] = [{ namespace: <span class="tk-s">"todo.lists"</span>, locale: <span class="tk-s">"fr"</span>, messages: lists }];</code></pre>
1086
+ <p>and its web and terminal facets contribute that list, the same line in each:</p>
1087
+ <!-- quote: plugins/french/web.ts -->
1088
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) =&gt; self.<span class="tk-f">contribute</span>(<span class="tk-t">Translations</span>, french));</code></pre>
1089
+ <p>A contribution is a list, so one plugin can translate the app and every plugin at once. A catalog may be partial: a
1090
+ missing key falls back to the locale&#39;s base (<code>pt-BR</code> to <code>pt</code>), then to the namespace&#39;s first catalog, key by key, and
1091
+ each fallback is reported once (the page&#39;s <code>onError</code>, kind <code>messages</code>). Two catalogs for one namespace and locale: the
1092
+ first by plugin name wins and the other is reported; a translation contributed with <code>override: true</code> replaces the
1093
+ plugin&#39;s own (a better German).</p>
1094
+ <h3 id="choosing-a-language">Choosing a language</h3>
1095
+ <p>The language is a preference each user keeps on the server, <code>LocaleChoice</code>, one per principal like the theme choice of
1096
+ chapter VI. Until a user chooses, the device decides: the browser&#39;s <code>navigator.languages</code> on a page, the system locale
1097
+ in a terminal (the launcher&#39;s <code>locales</code>), matched against the languages the catalogs offer, else English. Step 8 has
1098
+ the owner choose German while the terminal runs:</p>
1099
+ <!-- quote: main.ts -->
1100
+ <pre><code><span class="tk-k">const</span> ownerChooses = (locale: string) =&gt;
1101
+ app.host.<span class="tk-f">root</span>(<span class="tk-k">async</span> (ctx) =&gt; (<span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">LocaleChoice</span>, {})).<span class="tk-f">choose</span>(locale, ctx), context, { principal: OWNER });
1102
+ <span class="tk-k">await</span> <span class="tk-f">ownerChooses</span>(<span class="tk-s">"de"</span>);</code></pre>
1103
+ <!-- output: main -->
1104
+ <pre class="plain"><code>8. the owner chooses German
1105
+ ada's pick: fr</code></pre>
1106
+ <!-- output: tui -->
1107
+ <pre class="plain"><code> the terminal shows:
1108
+ Einkäufe
1109
+ [x] milk
1110
+ [x] eggs
1111
+ [ ] bread server
1112
+ [ ] butter owner
1113
+ [ ] coffee ada</code></pre>
1114
+ <p>Every client of the owner holds the owner&#39;s <code>LocaleChoice</code>, so the terminal redrew in German at once; nothing
1115
+ restarted. Ada&#39;s pick (French) is hers and changes nothing for the owner. A page does the same and also sets <code>lang</code>
1116
+ and <code>dir</code> on its root element (right to left for Arabic or Hebrew, through the app&#39;s logical CSS properties).</p>
1117
+ <h3 id="text-from-the-server">Text from the server</h3>
1118
+ <p>Clearing the done todos should tell everyone who cleared them. If the worker wrote &quot;owner hat die erledigten Todos
1119
+ entfernt&quot;, Ada would read German too. So the list&#39;s state carries a <strong>message as data</strong>, a key and its parameters,
1120
+ declared with the kit&#39;s generic schema:</p>
1121
+ <!-- quote: plugins/lists/contract.ts -->
1122
+ <pre><code><span class="tk-c">/** The list's latest notice, as data each viewer renders in its own language; `null` before the first. */</span>
1123
+ <span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Notice</span> = <span class="tk-f">union</span>([<span class="tk-t">MessageSchema</span>, <span class="tk-f">nul</span>()]);</code></pre>
1124
+ <p>The store writes it with <code>messages.message</code>, typed against the English catalog like <code>t</code> (<code>messages.message(&quot;cleared&quot;, 42)</code> does not compile), and each viewer renders it with <code>t.render</code>:</p>
1125
+ <!-- quote: plugins/lists/worker.ts -->
1126
+ <pre><code> clear: <span class="tk-k">async</span> ({ by }, ctx) =&gt; {
1127
+ <span class="tk-k">await</span> self.<span class="tk-f">tx</span>(<span class="tk-k">async</span> (tx) =&gt; tx.<span class="tk-f">deleteWhere</span>(items, (item) =&gt; <span class="tk-f">eq</span>(item.done, <span class="tk-k">true</span>)), ctx);
1128
+ <span class="tk-c">// The key and its parameters, typed against the English catalog: every viewer renders its own text.</span>
1129
+ <span class="tk-k">await</span> notices.<span class="tk-f">change</span>((draft) =&gt; {
1130
+ draft.notice = messages.<span class="tk-f">message</span>(<span class="tk-s">"cleared"</span>, by);
1131
+ }, ctx);
1132
+ <span class="tk-k">return</span> <span class="tk-k">null</span>;
1133
+ },</code></pre>
1134
+ <!-- output: main -->
1135
+ <pre class="plain"><code> the notice: { namespace: 'todo.lists', key: 'cleared', params: [ 'owner' ] }
1136
+ the server renders it: owner cleared the done todos</code></pre>
1137
+ <!-- output: tui -->
1138
+ <pre class="plain"><code> the terminal renders it: owner hat die erledigten Todos entfernt</code></pre>
1139
+ <p>Parameters are plain data (strings, numbers, ISO dates), so a message crosses links like any state. Across a link
1140
+ the member is the generic schema, so <code>t.render</code> checks the namespace and the key at runtime: a message of another
1141
+ namespace or a key the catalogs no longer have shows the key, reported. Never translated: the model&#39;s prompts and
1142
+ answers, log lines, error codes.</p>
1143
+ <h3 id="trap-the-plain-t">Trap: the plain <code>t</code></h3>
1144
+ <p>The server&#39;s <code>messages.render(…)</code> printed English. <strong>The plain <code>t</code> has no language of its own:</strong> inside a render it
1145
+ uses the language being rendered (a message function calling another plugin&#39;s <code>t</code>, <code>formatTime</code>), outside one the
1146
+ first catalog. That is right for a server, a test or a log line, and wrong in a component: a page or a terminal
1147
+ renders with <code>useMessages(t)</code> or <code>scope.messages(t)</code>, bound to its host&#39;s language. There is no module-wide current
1148
+ language, because one process runs many hosts and a server renders for many people.</p>
1149
+ <p>On the page, <code>screenshots.ts</code> has the owner choose French and clear the done todos while a todo is half typed:</p>
1150
+ <!-- quote: screenshots.ts -->
1151
+ <pre><code> <span class="tk-k">await</span> app.host.<span class="tk-f">root</span>(
1152
+ <span class="tk-k">async</span> (ctx) =&gt; {
1153
+ <span class="tk-k">await</span> (<span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">LocaleChoice</span>, {})).<span class="tk-f">choose</span>(<span class="tk-s">"fr"</span>, ctx);
1154
+ <span class="tk-k">const</span> todos = <span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> });
1155
+ <span class="tk-k">if</span> (todos.variant === <span class="tk-s">"owner"</span>) <span class="tk-k">await</span> todos.<span class="tk-f">clear</span>(ctx);
1156
+ },
1157
+ context,
1158
+ { principal: { kind: <span class="tk-s">"owner"</span> } },
1159
+ );</code></pre>
1160
+ <!-- output: screenshots -->
1161
+ <pre class="plain"><code>wrote docs/tutorial/screens/french.png
1162
+ after choosing French: lang="fr", "oat mi" still typed
1163
+ the page did not reload: true</code></pre>
1164
+ <figure class="fig"><p><img src="screens/french.png" alt="The page in French"></p>
1165
+ <figcaption><span class="fignum">Figure 3.</span> The page after the owner chose French: the lists plugin&#39;s texts from the French plugin, the notice rendered from its key, the half-typed &quot;oat mi&quot; kept; no reload.</figcaption></figure>
1166
+ </section>
1167
+
1168
+ <section class="chapter" id="reload"><header class="opener"><div class="numeral">IX</div><h2>Changing Code</h2><p class="lede">An edit while Ada is connected: the watcher marks, applyChanges rolls out, every object takes its fate.</p></header>
1169
+ <p class="dropcap">Ada typed &quot; tea &quot; with spaces, and a blank todo should be refused like an empty one. The fix is one line of the lists
1170
+ plugin&#39;s worker facet: the view trims the text before the store sees it. Step 9 makes the edit in the running app&#39;s
1171
+ copy of the plugins, as an editor or an agent would, and applies it:</p>
1172
+ <!-- quote: main.ts -->
1173
+ <pre><code><span class="tk-k">const</span> BEFORE = <span class="tk-s">"add: (text: string, ctx: Ctx) =&gt; store.add({ text, by }, ctx),"</span>;
1174
+ <span class="tk-k">const</span> AFTER = <span class="tk-s">"add: (text: string, ctx: Ctx) =&gt; store.add({ text: text.trim(), by }, ctx),"</span>;
1175
+ <span class="tk-k">const</span> workerFile = <span class="tk-f">join</span>(root, <span class="tk-s">"lists"</span>, <span class="tk-s">"worker.ts"</span>);
1176
+ <span class="tk-k">await</span> <span class="tk-f">writeFile</span>(workerFile, (<span class="tk-k">await</span> <span class="tk-f">readFile</span>(workerFile, <span class="tk-s">"utf8"</span>)).<span class="tk-f">replace</span>(BEFORE, AFTER));
1177
+ <span class="tk-k">await</span> <span class="tk-f">until</span>(() =&gt; app.plugins.state.value.dirty.<span class="tk-f">includes</span>(<span class="tk-s">"@todo/lists"</span>));
1178
+ <span class="tk-k">const</span> applied = <span class="tk-k">await</span> app.<span class="tk-f">applyChanges</span>(context);
1179
+ console.<span class="tk-f">log</span>(<span class="tk-s">"9. lists changed, rolled out to:"</span>, applied.steps.<span class="tk-f">map</span>(({ name }) =&gt; name).<span class="tk-f">join</span>(<span class="tk-s">", "</span>));
1180
+ <span class="tk-k">for</span> (<span class="tk-k">const</span> host <span class="tk-k">of</span> applied.steps.<span class="tk-f">find</span>(({ name }) =&gt; name === <span class="tk-s">"list"</span>)!.hosts) {
1181
+ <span class="tk-k">const</span> fates = <span class="tk-s">"report"</span> <span class="tk-k">in</span> host ? host.report.objects.<span class="tk-f">map</span>(({ object, kind }) =&gt; <span class="tk-s">`${object} ${kind}`</span>) : [];
1182
+ console.<span class="tk-f">log</span>(<span class="tk-s">` on ${host.host}:`</span>, [...<span class="tk-k">new</span> <span class="tk-t">Set</span>(fates)].<span class="tk-f">join</span>(<span class="tk-s">", "</span>));
1183
+ }
1184
+ <span class="tk-k">await</span> adas.<span class="tk-f">add</span>(<span class="tk-s">" tea "</span>, context);
1185
+ <span class="tk-k">await</span> <span class="tk-f">until</span>(() =&gt; groceries.items.value.<span class="tk-f">at</span>(-<span class="tk-n">1</span>)?.text === <span class="tk-s">"tea"</span>);
1186
+ console.<span class="tk-f">log</span>(<span class="tk-s">" ada adds ' tea ':"</span>, <span class="tk-f">show</span>(groceries.items.value.<span class="tk-f">slice</span>(-<span class="tk-n">1</span>)));
1187
+ <span class="tk-k">const</span> refused = <span class="tk-k">await</span> adas.<span class="tk-f">add</span>(<span class="tk-s">" "</span>, context).<span class="tk-f">then</span>(
1188
+ () =&gt; <span class="tk-s">"added"</span>,
1189
+ (error: <span class="tk-t">ChordError</span>) =&gt; ({ code: error.code, reason: error.reason }),
1190
+ );
1191
+ console.<span class="tk-f">log</span>(<span class="tk-s">" ada adds ' ':"</span>, refused);</code></pre>
1192
+ <!-- output: main -->
1193
+ <pre class="plain"><code>9. lists changed, rolled out to: server, list, web, tui
1194
+ on list|groceries: todo.store restarted, todo.todos(groceries) restarted
1195
+ ada adds ' tea ': [ ] tea (ada)
1196
+ ada adds ' ': { code: 'invalid', reason: 'args' }</code></pre>
1197
+ <p>The watcher marked <code>@todo/lists</code> dirty when the file changed; <code>applyChanges</code> rebuilt it (one esbuild call per facet)
1198
+ and rolled it out host by host. On each host Chord stages the new code (it runs the new factories against a recording
1199
+ <code>self</code> to see what they declare), commits it in one step, and gives each object of the changed plugin its <strong>fate</strong>
1200
+ (design §21):</p>
1201
+ <table>
1202
+ <thead>
1203
+ <tr>
1204
+ <th>The object</th>
1205
+ <th>Fate</th>
1206
+ </tr>
1207
+ </thead>
1208
+ <tbody><tr>
1209
+ <td>live</td>
1210
+ <td><strong>restarted</strong>: running calls finish on the old code, new calls wait for the new code, the same documents and tables</td>
1211
+ </tr>
1212
+ <tr>
1213
+ <td>not live (failed, parked, activating)</td>
1214
+ <td><strong>retried</strong> on the new code</td>
1215
+ </tr>
1216
+ <tr>
1217
+ <td>its new code waits for a dependency</td>
1218
+ <td><strong>parked</strong> until the dependency is live</td>
1219
+ </tr>
1220
+ <tr>
1221
+ <td>its type was removed</td>
1222
+ <td><strong>ended</strong>; its holders wait until something serves it again</td>
1223
+ </tr>
1224
+ </tbody></table>
1225
+ <p>Both the store and the view were live, so both <strong>restarted</strong>. The sequence on the list&#39;s worker:</p>
1226
+ <ol>
1227
+ <li>The old attempts stay open: calls already running finish on the old code, and so does every call they make with
1228
+ their <code>ctx</code> (<strong>old work</strong>: the view&#39;s running <code>add</code> reaches the store&#39;s old attempt, never waiting for the new
1229
+ one). Calls arriving now are new work: they wait at a gate.</li>
1230
+ <li>Each old attempt is sealed once its calls drained and the requests its new factory made in staging are ready (the
1231
+ view&#39;s new code needs the store, whose old attempt still serves it). That takes as long as the running calls, at
1232
+ most <code>reloadDrainTimeout</code> (5 s); a call still running then is cut.</li>
1233
+ <li>The new attempt runs its factory, its live query runs again (the same read as at the first activation), and it
1234
+ goes live: the gate opens, and references move to its members by name, each with one frame from its last value.</li>
1235
+ </ol>
1236
+ <p>Ada&#39;s reference stayed <code>live</code> while the old code drained, showed <code>loading</code> for a moment, then the new code&#39;s values;
1237
+ her next add was trimmed. Long work does not belong in a method: a call longer than the drain bound is cut at a
1238
+ reload, so it goes into a job.</p>
1239
+ <p>Ada&#39;s blank todo is now empty after trimming, so the store refused it with
1240
+ <code>invalid(&quot;args&quot;, …)</code> (chapter II): her client got <code>{ code: &quot;invalid&quot;, reason: &quot;args&quot; }</code>, Chord&#39;s own error, which
1241
+ crossed the worker&#39;s link and the server&#39;s as it was. Had the edit broken the plugin, that plugin alone would have kept its old code
1242
+ and the report would name the error.</p>
1243
+ <p>A contract that changes shape needs a new <code>version</code>; old consumers keep working when the provider maps their version
1244
+ (<code>accepts</code>), otherwise they wait with <code>contract_mismatch</code> until they reload. A stored document or table that changes
1245
+ shape bumps its own version with a migration. Contract versions protect callers, storage versions protect data.</p>
1246
+ <h3 id="on-the-page">On the page</h3>
1247
+ <p>An edit of a web facet applies in place too: the page fetches the one changed script and reloads its host, with no
1248
+ page reload. <code>screenshots.ts</code> adds a count of open todos to the panel (<code>t(&quot;open&quot;, n)</code>, in the page&#39;s language) while half a
1249
+ todo is typed:</p>
1250
+ <!-- quote: screenshots.ts -->
1251
+ <pre><code><span class="tk-k">const</span> BEFORE = <span class="tk-s">`h(
1252
+ "ul",`</span>;</code></pre>
1253
+ <!-- output: screenshots -->
1254
+ <pre class="plain"><code>wrote docs/tutorial/screens/page.png
1255
+ wrote docs/tutorial/screens/harbor.png
1256
+ wrote docs/tutorial/screens/edited.png
1257
+ typed "oat mi"; after the edit: "oat mi"
1258
+ the page did not reload: true</code></pre>
1259
+ <figure class="fig"><p><img src="screens/edited.png" alt="The page after the edit"></p>
1260
+ <figcaption><span class="fignum">Figure 4.</span> After the edit of the web facet, applied in place: the count appears, and the half-typed &quot;oat mi&quot; is still in the box: the page kept it under its key while React mounted the new component.</figcaption></figure>
1261
+ <p>The page did not reload, and everything outside the changed plugin&#39;s components kept its state. The changed panel
1262
+ itself is new code: its component is a new function, which React mounts anew, so its own <code>useState</code> would start over
1263
+ (design §21). What must survive a plugin update (a half-typed todo, a scroll position) is <strong>page state</strong>:
1264
+ <code>usePageState(key, initial)</code> keeps the value in the page&#39;s host under the key, not in the component, so the new
1265
+ component finds it; a terminal&#39;s <code>TuiScope</code> has the same store as <code>scope.pageState(key, initial)</code>. Page state is any
1266
+ value, never sent anywhere, and gone with the page.</p>
1267
+ </section>
1268
+
1269
+ <section class="chapter" id="export"><header class="opener"><div class="numeral">X</div><h2>Moving a List</h2><p class="lede">Export a worker&#39;s data while it runs, import it under another address.</p></header>
1270
+ <p class="dropcap">A list&#39;s data is what its worker created: its store and its files. Step 10 exports the groceries list while its worker
1271
+ runs and imports it as a new list:</p>
1272
+ <!-- quote: main.ts -->
1273
+ <pre><code><span class="tk-k">const</span> archive = <span class="tk-f">join</span>(base, <span class="tk-s">"groceries.tar"</span>);
1274
+ <span class="tk-k">await</span> app.<span class="tk-f">exportData</span>(<span class="tk-t">List</span>({ id: <span class="tk-s">"groceries"</span> }), archive, context);
1275
+ console.<span class="tk-f">log</span>(<span class="tk-s">"10. exported groceries:"</span>, (<span class="tk-k">await</span> <span class="tk-f">readFile</span>(archive)).length &gt; <span class="tk-n">0</span> ? <span class="tk-s">"an archive"</span> : <span class="tk-s">"nothing"</span>);
1276
+ <span class="tk-k">await</span> app.<span class="tk-f">importData</span>(archive, <span class="tk-t">List</span>({ id: <span class="tk-s">"picnic"</span> }), context);
1277
+ <span class="tk-k">const</span> picnic = <span class="tk-k">await</span> app.host.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"picnic"</span> }, context);
1278
+ console.<span class="tk-f">log</span>(<span class="tk-s">" imported as picnic:"</span>, <span class="tk-f">show</span>(picnic.items.value));
1279
+ picnic.<span class="tk-f">release</span>();
1280
+ <span class="tk-k">await</span> app.<span class="tk-f">importData</span>(archive, <span class="tk-t">List</span>({ id: <span class="tk-s">"groceries"</span> }), context).<span class="tk-k">catch</span>((error: <span class="tk-t">Error</span>) =&gt; {
1281
+ console.<span class="tk-f">log</span>(<span class="tk-s">" importing over groceries:"</span>, error.message.<span class="tk-f">replace</span>(base, <span class="tk-s">"…"</span>));
1282
+ });</code></pre>
1283
+ <!-- output: main -->
1284
+ <pre class="plain"><code>10. exported groceries: an archive
1285
+ imported as picnic: [ ] bread (server), [ ] butter (owner), [ ] coffee (ada), [ ] tea (ada)
1286
+ importing over groceries: importData: list|groceries has data (its worker runs)</code></pre>
1287
+ <p><code>exportData</code> stops nothing: the running worker writes its own store at one consistent point (the store&#39;s export holds
1288
+ its commit queue while it reads) and its files, as one archive with a manifest naming the app. With no worker running,
1289
+ the server takes the worker&#39;s process lock and reads the store itself. <code>importData</code> refuses an address that has data;
1290
+ replacing a list is a delete, then an import (decision &quot;App kit naming and export&quot;). Registering an imported list in
1291
+ an index of the app&#39;s, if it had one, is the app&#39;s.</p>
1292
+ </section>
1293
+
1294
+ <section class="chapter" id="telemetry"><header class="opener"><div class="numeral">XI</div><h2>Seeing What Happened</h2><p class="lede">One telemetry store on the server, one trace from a click in the browser to a commit on a worker, a span of your own, whom each span was for, and a small viewer for the owner.</p></header>
1295
+ <p class="dropcap">Ada says that adding a todo from her phone feels slow some evenings. Her todo crosses three processes: her page, the
1296
+ server&#39;s relay and the list&#39;s worker. A log line per process would say what each one did, but not which lines belong
1297
+ to her click, or where the time went. Chord records <strong>telemetry</strong> through pi-telemetry: <strong>spans</strong> (a named, timed
1298
+ piece of work with attributes), <strong>events</strong> on spans, and <strong>metrics</strong> (counters and histograms). Each host has one
1299
+ <strong>adapter</strong>, given to <code>createHost({ telemetry })</code> and NOOP by default, which decides where they go. A <strong>carrier</strong> in
1300
+ each call&#39;s header names the caller&#39;s span, so the spans of one request, recorded by different processes, form one
1301
+ <strong>trace</strong> (design §29). Mara wants to see them without reading SQL: a small trace viewer would be nice. This chapter
1302
+ stores telemetry in one place, follows a click from the browser to the worker, adds a span, names whom each span was
1303
+ for, reads the store from a script, and ends with the viewer.</p>
1304
+ <h3 id="one-store-on-the-server">One store, on the server</h3>
1305
+ <p>The app keeps its telemetry in one SQLite file in the data directory, through Chord&#39;s store
1306
+ (<code>@earendil-works/chord/telemetry-sqlite</code>), and the server is the only process that writes it. The app&#39;s base code opens
1307
+ it for the server:</p>
1308
+ <!-- quote: src/telemetry.ts -->
1309
+ <pre><code><span class="tk-c">/** The telemetry store's file in the data directory `dataDir`. */</span>
1310
+ <span class="tk-k">export</span> <span class="tk-k">function</span> <span class="tk-f">telemetryFile</span>(dataDir: string): string {
1311
+ <span class="tk-k">return</span> <span class="tk-f">join</span>(dataDir, <span class="tk-s">"telemetry.sqlite"</span>);
1312
+ }
1313
+
1314
+ <span class="tk-c">/** Opens the telemetry store at `file` for this process (its directory made, private, when missing). */</span>
1315
+ <span class="tk-k">export</span> <span class="tk-k">async</span> <span class="tk-k">function</span> <span class="tk-f">openTelemetry</span>(file: string): <span class="tk-t">Promise</span>&lt;<span class="tk-t">TodoTelemetry</span>&gt; {
1316
+ <span class="tk-c">// Spans carry who called what: the data directory is private before the store's first file appears in it.</span>
1317
+ <span class="tk-k">await</span> <span class="tk-f">mkdir</span>(<span class="tk-f">dirname</span>(file), { recursive: <span class="tk-k">true</span>, mode: 0o700 });
1318
+ <span class="tk-k">const</span> db = <span class="tk-k">await</span> <span class="tk-f">openNodeSqliteDatabase</span>(file);
1319
+ <span class="tk-k">const</span> store = <span class="tk-k">await</span> <span class="tk-f">createSqliteTelemetry</span>(db);
1320
+ <span class="tk-k">return</span> {
1321
+ store,
1322
+ close: <span class="tk-k">async</span> () =&gt; {
1323
+ <span class="tk-k">await</span> store.<span class="tk-f">close</span>();
1324
+ <span class="tk-k">await</span> db.<span class="tk-f">close</span>();
1325
+ },
1326
+ };
1327
+ }</code></pre>
1328
+ <p><code>createSqliteTelemetry</code> returns a <strong>recorder</strong>: an adapter that keeps each settled span in memory and writes them in
1329
+ one batch a second, one row per span with its events and links inline and its start and end in microseconds (a
1330
+ monotonic clock anchored to the wall clock, so spans of one process keep their order and spacing); metrics are summed
1331
+ per series and 10 seconds; rows older than seven days are deleted in the same batch, and when 10,000 spans wait, new
1332
+ ones are dropped and counted. Recording never runs SQL. The same object is the store&#39;s <strong>read interface</strong>,
1333
+ <code>TelemetryStore</code> (below).</p>
1334
+ <p>Step 1 opened the store before the server (chapter I), and <code>startTodo</code> gives it to <code>startServer</code>:</p>
1335
+ <!-- quote: start.ts -->
1336
+ <pre><code> <span class="tk-c">// The store the server writes: its own spans and metrics, and what the workers and recording pages send.</span>
1337
+ telemetry: telemetry.store,</code></pre>
1338
+ <p>With a store, the kit does the rest. Every list&#39;s worker records in memory and sends its finished spans and metrics to
1339
+ the server once a second, over the link it already has; nothing tells a worker where the file is, and the worker&#39;s
1340
+ entry has no telemetry of its own to set up:</p>
1341
+ <!-- quote: worker.ts -->
1342
+ <pre><code><span class="tk-f">startWorker</span>({ api: API, kinds: KINDS, principalAttributes, onError: (error) =&gt; console.<span class="tk-f">error</span>(<span class="tk-s">"list:"</span>, error) });</code></pre>
1343
+ <p>A page records when the app asks it to, in its web entry:</p>
1344
+ <!-- quote: src/web/main.ts -->
1345
+ <pre><code> <span class="tk-c">// The page records its clicks and the calls under them, and sends them to the server every few seconds.</span>
1346
+ telemetry: <span class="tk-k">true</span>,</code></pre>
1347
+ <p>The page keeps its finished spans in memory and sends them every three seconds, and at once when it is hidden (a tab
1348
+ switched away may not come back). Both send to an object the kit serves on the server, <code>WorkerTelemetry</code> and
1349
+ <code>PageTelemetry</code>, whose <code>ingest(batch)</code> hands the spans to the store; its calls record no span of their own, or every
1350
+ batch would write a span about itself. A worker is the server&#39;s own child: its batches are written as they are,
1351
+ marked with its host. A page is anyone&#39;s: sign-in and the link&#39;s limits apply as for any call, and the server checks
1352
+ everything (next section).</p>
1353
+ <h3 id="one-trace-from-the-browser">One trace from the browser</h3>
1354
+ <p>The lists plugin&#39;s panel starts a trace where Ada&#39;s click starts: in its form&#39;s <code>submit</code>, a root span of the page&#39;s host
1355
+ with the call under it:</p>
1356
+ <!-- quote: plugins/lists/web.ts -->
1357
+ <pre><code> <span class="tk-c">// The click is a trace of its own: a root span of the page, the call to the list's worker under it.</span>
1358
+ <span class="tk-k">const</span> trace = { name: <span class="tk-s">"page.add"</span>, attributes: { chars: text.length } };
1359
+ <span class="tk-k">void</span> host.<span class="tk-f">root</span>((ctx) =&gt; todos.<span class="tk-f">add</span>(text, ctx), context, { trace }).<span class="tk-k">catch</span>(() =&gt; {});</code></pre>
1360
+ <p><code>host.root(fn, context, { trace })</code> runs <code>fn</code> in a new root span of the host&#39;s adapter, <code>ctx.telemetry</code> that span, so
1361
+ the call of <code>add</code> starts its <code>chord.call</code> under it and puts that span&#39;s carrier in the call&#39;s header (<code>useHost</code> gives a
1362
+ component its page&#39;s host). Without a recording adapter it starts nothing. <code>screenshots.ts</code> has the owner signed in
1363
+ to the real page in Chromium (the app&#39;s page from chapter IV, by now in Harbor and French), types honey and submits,
1364
+ then submits again with nothing typed:</p>
1365
+ <!-- quote: screenshots.ts -->
1366
+ <pre><code> <span class="tk-k">await</span> browser.<span class="tk-k">type</span>(<span class="tk-s">"honey"</span>);
1367
+ <span class="tk-k">await</span> <span class="tk-f">shoot</span>(<span class="tk-s">"click-before.png"</span>);
1368
+ <span class="tk-k">const</span> submit = <span class="tk-s">"document.querySelector('.lists-panel form').requestSubmit()"</span>;
1369
+ <span class="tk-k">await</span> browser.<span class="tk-f">evaluate</span>(submit);</code></pre>
1370
+ <!-- output: screenshots -->
1371
+ <pre class="plain"><code>wrote docs/tutorial/screens/click-before.png
1372
+ wrote docs/tutorial/screens/click-after.png</code></pre>
1373
+ <figure class="fig wide"><div style="display: flex; gap: 1rem; align-items: flex-start"><img src="screens/click-before.png" alt="The page with honey typed" style="width: 50%; min-width: 0"><img src="screens/click-after.png" alt="The page after the click: honey in the list" style="width: 50%; min-width: 0"></div><figcaption><span class="fignum">Figure 5.</span> The click, on the real page: honey typed (left), then submitted (right). The list shows honey once the worker&#39;s store committed it and its live query reached the page; the page has recorded <code>page.add</code> and the <code>chord.call</code> under it, and sends them to the server within three seconds.</figcaption></figure>
1374
+ <p>It then reads the store until the worker&#39;s spans of both clicks are there and prints honey&#39;s trace with pi-telemetry&#39;s
1375
+ <code>renderTrace</code> (ids and lengths of time masked, which differ at every run), and opens the owner&#39;s trace viewer (the last
1376
+ section builds it) on the same trace:</p>
1377
+ <!-- output: screenshots -->
1378
+ <pre class="plain"><code>adding honey from the page:
1379
+ page.add chord.host="browser" chars=5 chord.principal="owner"
1380
+ chord.call chord.host="browser" type="todo.todos" member="add" object="todo.todos(groceries)" address.kind="list" outcome="ok" chord.principal="owner"
1381
+ chord.relay chord.host="server" type="todo.todos" member="#0" upstream.kind="list" outcome="ok"
1382
+ chord.method chord.host="list|groceries" type="todo.todos" member="add" object="todo.todos(groceries)" principal.kind="owner" chord.principal="owner" outcome="ok"
1383
+ chord.method chord.host="list|groceries" type="todo.store" member="add" object="todo.store" principal.kind="worker" chord.principal="worker:list|groceries" outcome="ok"
1384
+ · chord.commit documents=0 ops=0 rows=1 duration=&lt;ms&gt;
1385
+ todos.validate chord.host="list|groceries" chars=5</code></pre>
1386
+ <figure class="fig wide"><p><img src="screens/traces.png" alt="The trace viewer with the click&#39;s trace"></p>
1387
+ <figcaption><span class="fignum">Figure 6.</span> The same trace in the owner&#39;s trace viewer at <code>/traces</code>, in the Harbor theme: the tree <code>renderTrace</code> printed above, each span with the host that recorded it (<code>browser</code>, <code>server</code>, <code>list|groceries</code>) and its length as a bar on the trace&#39;s time line, all written by the server. Starts are stored to the microsecond, so even bars of a fraction of a millisecond line up. On the left the newest traces: the refused click in red, each with whom it was for; the rest the app&#39;s own work (relays, activations, HTTP requests).</figcaption></figure>
1388
+ <p>Three processes recorded these six spans, and the server wrote them all. Each span says which host recorded it:
1389
+ <code>chord.host</code> is the host&#39;s address key (<code>server</code>, <code>list|groceries</code>), and <code>browser</code> for what a page sent. The page
1390
+ recorded <code>page.add</code> and <code>chord.call</code>, the consumer&#39;s call until its reply. The server recorded <code>chord.relay</code>: it
1391
+ forwarded the call to the list&#39;s worker without knowing member names (<code>#0</code> is the method&#39;s index). The worker recorded
1392
+ the rest: <code>chord.method</code> for the owner&#39;s <code>Todos</code> view, then the view&#39;s call of the store, which is a method span too,
1393
+ by <code>worker</code>, because the store acts as its host (chapter II). The commit is an event on the span its transaction ran
1394
+ under, and the renderer prints a span&#39;s events before its children. It counts the documents it changed, their ops and
1395
+ the table rows its writes changed: the store keeps todos in a table, so one row and no documents. <code>chord.principal</code>
1396
+ says whom a span was for (below).</p>
1397
+ <figure class="fig anim"><svg viewBox="0 0 720 390" width="100%" role="img" aria-label="One click, one trace: each process starts its spans with its own adapter, the carrier in each call's header names the parent span on the other side, and the page and the worker send their spans to the server, which writes them." xmlns="http://www.w3.org/2000/svg" style="font-family:'Avenir Next','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:760px"><defs><marker id="f7m" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#7b7468"/></marker><marker id="f7a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#b4472f"/></marker><marker id="f7b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#2e6a8a"/></marker></defs><line x1="130.7" y1="52" x2="130.7" y2="382" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="45.7" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="130.7" y="27" font-size="11" fill="#2a2723" text-anchor="middle">page</text><text x="130.7" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">browser</text><line x1="360" y1="52" x2="360" y2="382" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="275" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="360" y="27" font-size="11" fill="#2a2723" text-anchor="middle">server</text><text x="360" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">the store</text><line x1="589.3" y1="52" x2="589.3" y2="382" stroke="#c9bfae" stroke-dasharray="3 3"/><rect x="504.3" y="10" width="170" height="42" rx="4" fill="#fff" stroke="#c9bfae"/><text x="589.3" y="27" font-size="11" fill="#2a2723" text-anchor="middle">list|groceries</text><text x="589.3" y="41" font-size="9.5" fill="#7b7468" text-anchor="middle">worker</text><g class="step edge" style="--i:1"><path d="M130.7 72 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f7m)"/><text x="172.7" y="82" font-size="10" fill="#2a2723" text-anchor="start">page.add, chord.call</text></g><g class="step edge" style="--i:2"><line x1="130.7" y1="118" x2="357" y2="118" stroke="#b4472f" marker-end="url(#f7a)"/><text x="245.3" y="112" font-size="10" fill="#b4472f" text-anchor="middle">call add, carrier of chord.call</text></g><g class="step edge" style="--i:3"><path d="M360 144 h34 v14 h-32" fill="none" stroke="#7b7468" marker-end="url(#f7m)"/><text x="402" y="154" font-size="10" fill="#2a2723" text-anchor="start">chord.relay</text></g><g class="step edge" style="--i:4"><line x1="360" y1="190" x2="586.3" y2="190" stroke="#b4472f" marker-end="url(#f7a)"/><text x="474.7" y="184" font-size="10" fill="#b4472f" text-anchor="middle">call #0, carrier of chord.relay</text></g><g class="step edge" style="--i:5"><path d="M589.3 216 h-34 v14 h32" fill="none" stroke="#7b7468" marker-end="url(#f7m)"/><text x="547.3" y="226" font-size="10" fill="#2a2723" text-anchor="end">chord.method ×2, todos.validate, commit</text></g><g class="step edge" style="--i:6"><line x1="589.3" y1="262" x2="363" y2="262" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f7m)"/><text x="474.7" y="256" font-size="10" fill="#2a2723" text-anchor="middle">reply</text></g><g class="step edge" style="--i:7"><line x1="360" y1="294" x2="133.7" y2="294" stroke="#7b7468" stroke-dasharray="4 3" marker-end="url(#f7m)"/><text x="245.3" y="288" font-size="10" fill="#2a2723" text-anchor="middle">reply</text></g><g class="step edge" style="--i:8"><line x1="589.3" y1="326" x2="363" y2="326" stroke="#b4472f" marker-end="url(#f7a)"/><text x="474.7" y="320" font-size="10" fill="#b4472f" text-anchor="middle">ingest: its spans (every second)</text></g><g class="step edge" style="--i:9"><line x1="130.7" y1="358" x2="357" y2="358" stroke="#b4472f" marker-end="url(#f7a)"/><text x="245.3" y="352" font-size="10" fill="#b4472f" text-anchor="middle">ingest: its spans (every 3 s, or hidden)</text></g></svg><figcaption><button class="replay" type="button">Replay</button><span class="fignum">Figure 7.</span> One click, one trace: each process starts its spans with its own adapter, the carrier in each call&#39;s header names the parent span on the other side, and the page and the worker send their spans to the server, which writes them.</figcaption></figure>
1398
+ <p>The carrier is a field of the <code>call</code> and <code>hold</code> headers, at most 8 keys of at most 256 characters each. The receiver
1399
+ reads it with its own adapter; an absent or malformed one gives a new root, never an error. With the NOOP adapter
1400
+ nothing is injected and no span starts: a span site costs one check (§30).</p>
1401
+ <p>What a page sends, the server treats as untrusted. A batch is checked by its schema: at most 100 spans, every field
1402
+ bounded (names 128 characters, 32 attributes of at most 256 characters, 16 events, 8 links, a 500-character error
1403
+ message), ids printable ASCII and no control characters anywhere; a batch outside it is refused whole. A page&#39;s span
1404
+ ids are <code>browser:&lt;its host&#39;s incarnation&gt;:&lt;n&gt;</code>, and the server takes only spans whose id, trace and parent carry the
1405
+ calling page&#39;s own prefix: a page can neither write into another sender&#39;s trace nor pose as a server span, and the
1406
+ store never replaces a stored span. A page may send 1,200 spans a minute; past that they are dropped and counted in
1407
+ <code>ingest</code>&#39;s answer. The server marks each span <code>chord.host: browser</code> and stamps it with the principal of the link it came
1408
+ over, dropping the page&#39;s own <code>chord.*</code> attributes: what a page says about itself counts for nothing.</p>
1409
+ <p>The trace holds the click and nothing else. Work that serves many requests is its own trace, <strong>linked</strong> to the span that
1410
+ first needed it: a worker&#39;s start (<code>chord.resolve</code>), an object&#39;s activation (<code>chord.activate</code>). A link is a reference to
1411
+ a span of another trace; the renderer prints it and never follows it.</p>
1412
+ <h3 id="trap-a-trace-starts-where-a-span-starts">Trap: a trace starts where a span starts</h3>
1413
+ <p>The panel took its reference to <code>Todos</code> before the click (<code>useHold</code>), under <code>BACKGROUND_CONTEXT</code>, and that hold is in no
1414
+ trace of the page: <strong>a context without a recording span records nothing.</strong> A host gives such work no span of its own,
1415
+ so its calls start no <code>chord.call</code> and send no carrier, and the provider&#39;s <code>chord.method</code> becomes a root of its own trace
1416
+ (the server&#39;s relay of the page&#39;s hold is one: <code>chord.relay</code> with <code>member=&quot;hold&quot;</code>). So were the server&#39;s calls in step 2:
1417
+ on the worker each is a root <code>chord.method</code>. Start a span where the work begins, a click handler or a job
1418
+ (<code>host.root(fn, context, { trace })</code>), and pass its <code>ctx</code> down. The kit&#39;s HTTP component does this for every request: a
1419
+ <code>chord.http</code> span (method, route pattern, status), with the route&#39;s <code>ctx</code> under it. Step 11 adds honey from the command
1420
+ line, as step 3 did; the request is the trace&#39;s root, the server&#39;s call to the worker under it:</p>
1421
+ <!-- output: main -->
1422
+ <pre class="plain"><code>11. adding honey from the command line:
1423
+ chord.http chord.host="server" method="POST" route="POST /api/lists/:list" status=200
1424
+ chord.hold chord.host="server" type="todo.todos" object="todo.todos(groceries)" address.kind="list" outcome="bound"
1425
+ chord.call chord.host="server" type="todo.todos" member="add" object="todo.todos(groceries)" address.kind="list" chord.principal="owner" outcome="ok"
1426
+ chord.method chord.host="list|groceries" type="todo.todos" member="add" object="todo.todos(groceries)" principal.kind="owner" chord.principal="owner" outcome="ok"
1427
+ chord.method chord.host="list|groceries" type="todo.store" member="add" object="todo.store" principal.kind="worker" chord.principal="worker:list|groceries" outcome="ok"
1428
+ · chord.commit documents=0 ops=0 rows=1 duration=&lt;ms&gt;
1429
+ todos.validate chord.host="list|groceries" chars=5</code></pre>
1430
+ <h3 id="a-span-of-your-own">A span of your own</h3>
1431
+ <p>The store&#39;s <code>add</code> checks the text in a span of its own:</p>
1432
+ <!-- quote: plugins/lists/worker.ts -->
1433
+ <pre><code> <span class="tk-k">await</span> ctx.<span class="tk-f">trace</span>(<span class="tk-s">"todos.validate"</span>, { chars: text.length }, <span class="tk-k">async</span> () =&gt; {
1434
+ <span class="tk-c">// Refused as Chord refuses arguments that fail their schema: the caller gets `invalid/args`.</span>
1435
+ <span class="tk-k">if</span> (text === <span class="tk-s">""</span>) <span class="tk-k">throw</span> <span class="tk-f">invalid</span>(<span class="tk-s">"args"</span>, <span class="tk-s">"a todo needs text"</span>);
1436
+ });</code></pre>
1437
+ <p><code>ctx.trace(name, attributes, fn)</code> starts a child of the method&#39;s span and gives <code>fn</code> a child <code>ctx</code> with the same
1438
+ principal and <code>get</code>, so calls made inside are parented under it, and names its host as Chord&#39;s spans do. It is the last
1439
+ line of both traces above: <code>todos.validate chord.host=&quot;list|groceries&quot; chars=5</code>. Its attributes are small, flat values;
1440
+ the todo&#39;s text is data and stays out of telemetry, its length is enough. When <code>fn</code> throws, the span fails and the error
1441
+ passes through as it was: the caller still gets <code>invalid/args</code>. A check this small does not need a span of its own; a
1442
+ lookup, a model request or a file read does.</p>
1443
+ <h3 id="whom-a-span-was-for">Whom a span was for</h3>
1444
+ <p>Method and call spans record whom they were for: by default <code>chord.principal</code>, the principal&#39;s key (its canonical
1445
+ JSON). An app may record other attributes instead, through a hook each host takes (<code>principalAttributes</code>): the todo
1446
+ app&#39;s records a short key and, for a user, the name she signed in with:</p>
1447
+ <!-- quote: src/principals.ts -->
1448
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">function</span> <span class="tk-f">principalAttributes</span>(principal: <span class="tk-t">TodoPrincipal</span>): <span class="tk-t">Readonly</span>&lt;<span class="tk-t">Record</span>&lt;string, string&gt;&gt; {
1449
+ <span class="tk-k">let</span> attributes = recorded.<span class="tk-f">get</span>(principal);
1450
+ <span class="tk-k">if</span> (attributes === <span class="tk-k">undefined</span>) {
1451
+ attributes =
1452
+ principal.kind === <span class="tk-s">"user"</span>
1453
+ ? { <span class="tk-s">"chord.principal"</span>: <span class="tk-s">`user:${principal.id}`</span>, <span class="tk-s">"user.name"</span>: principal.name }
1454
+ : { <span class="tk-s">"chord.principal"</span>: principal.kind === <span class="tk-s">"worker"</span> ? <span class="tk-s">`worker:${principal.list}`</span> : principal.kind };
1455
+ recorded.<span class="tk-f">set</span>(principal, attributes);
1456
+ }
1457
+ <span class="tk-k">return</span> attributes;
1458
+ }</code></pre>
1459
+ <p>Chord calls the hook for every span it records with a principal, so it must be synchronous and cheap; any caching is
1460
+ the hook&#39;s own (here a <code>WeakMap</code> by principal: a link stamps one principal object on all its calls). <code>startTodo</code> gives
1461
+ it to the server (<code>principalAttributes</code> beside <code>telemetry</code>) and the worker&#39;s entry to <code>startWorker</code> (above): each host
1462
+ has its own, the same function. The server also stamps a page&#39;s spans with it, for the principal of the link the batch
1463
+ came over: <code>page.add</code> above was the owner&#39;s. Only what the hook returns is recorded, never other app data, and the
1464
+ attributes are for the owner&#39;s eyes: the store, like the viewer below, is the owner&#39;s.</p>
1465
+ <h3 id="reading">Reading</h3>
1466
+ <p>The store is read through <code>TelemetryStore</code>, pi-telemetry&#39;s read interface modeled on Jaeger&#39;s: <code>findTraces</code> (complete
1467
+ traces, newest first, filtered by their first span&#39;s name, attributes, errors or a time window, paged by a cursor),
1468
+ <code>getTrace(id)</code> and <code>findMetrics</code> (a metric&#39;s rows, one per series and 10 seconds, with count, sum, min and max).
1469
+ <code>trace.ts</code> is a script for a data directory, the app running or not. It opens the file with Chord&#39;s read-only store,
1470
+ <code>openTelemetryStore</code> (<code>@earendil-works/chord/telemetry-sqlite/node</code>), which implements only the read interface and
1471
+ opens SQLite read-only: it never writes the file the server writes, never creates or migrates it, and refuses a file
1472
+ without a store or with a store of another version.</p>
1473
+ <!-- quote: trace.ts -->
1474
+ <pre><code><span class="tk-c">// The shared file, read-only: the app's processes may be writing it meanwhile.</span>
1475
+ <span class="tk-k">const</span> store = <span class="tk-k">await</span> <span class="tk-f">openTelemetryStore</span>(<span class="tk-f">telemetryFile</span>(dataDir));
1476
+ <span class="tk-k">try</span> {
1477
+ <span class="tk-k">if</span> (id !== <span class="tk-k">undefined</span>) {
1478
+ <span class="tk-k">const</span> trace = <span class="tk-k">await</span> store.<span class="tk-f">getTrace</span>(id);
1479
+ console.<span class="tk-f">log</span>(trace === <span class="tk-k">undefined</span> ? <span class="tk-s">`no trace ${id}`</span> : <span class="tk-f">renderTrace</span>(trace.spans));
1480
+ } <span class="tk-k">else</span> {
1481
+ <span class="tk-c">// Complete traces, newest first; a trace's first span (its root, by start time) names it, and the app's</span>
1482
+ <span class="tk-c">// `principalAttributes` hook named whom each span was for.</span>
1483
+ <span class="tk-k">const</span> attributes = values.principal === <span class="tk-k">undefined</span> ? <span class="tk-k">undefined</span> : { <span class="tk-s">"chord.principal"</span>: values.principal };
1484
+ <span class="tk-k">const</span> { traces } = <span class="tk-k">await</span> store.<span class="tk-f">findTraces</span>({ name: values.name, attributes, limit: <span class="tk-n">10</span> });
1485
+ <span class="tk-k">for</span> (<span class="tk-k">const</span> { traceId, spans } <span class="tk-k">of</span> traces) {
1486
+ <span class="tk-k">const</span> start = <span class="tk-t">Math</span>.<span class="tk-f">min</span>(...spans.<span class="tk-f">map</span>((span) =&gt; span.startedMs));
1487
+ <span class="tk-k">const</span> length = <span class="tk-t">Math</span>.<span class="tk-f">max</span>(...spans.<span class="tk-f">map</span>((span) =&gt; span.endedMs)) - start;
1488
+ <span class="tk-k">const</span> failed = spans.<span class="tk-f">some</span>((span) =&gt; span.status.status === <span class="tk-s">"error"</span>) ? <span class="tk-s">" error"</span> : <span class="tk-s">""</span>;
1489
+ console.<span class="tk-f">log</span>(<span class="tk-s">`${traceId} ${spans[0]!.name} ${spans.length} spans ${length.toFixed(2)} ms${failed}`</span>);
1490
+ }
1491
+ <span class="tk-c">// Metrics are stored per series and 10 s: a series is one set of attributes, here the direction.</span>
1492
+ <span class="tk-k">for</span> (<span class="tk-k">const</span> direction <span class="tk-k">of</span> [<span class="tk-s">"sent"</span>, <span class="tk-s">"received"</span>]) {
1493
+ <span class="tk-k">const</span> rows = <span class="tk-k">await</span> store.<span class="tk-f">findMetrics</span>({ name: <span class="tk-s">"chord.link.bytes"</span>, attributes: { direction } });
1494
+ <span class="tk-k">const</span> bytes = rows.<span class="tk-f">reduce</span>((sum, row) =&gt; sum + row.sum, <span class="tk-n">0</span>);
1495
+ <span class="tk-k">const</span> messages = rows.<span class="tk-f">reduce</span>((sum, row) =&gt; sum + row.count, <span class="tk-n">0</span>);
1496
+ console.<span class="tk-f">log</span>(<span class="tk-s">`chord.link.bytes ${direction}: ${bytes} bytes in ${messages} messages`</span>);
1497
+ }
1498
+ }</code></pre>
1499
+ <p><code>screenshots.ts</code> runs it twice from a process of its own while the app runs: the newest <code>page.add</code> traces with a span
1500
+ for the owner, then the refused click by its id (ids, lengths and counts masked):</p>
1501
+ <!-- output: screenshots -->
1502
+ <pre class="plain"><code>trace.ts &lt;data directory&gt; --name page.add --principal owner:
1503
+ &lt;id&gt; page.add 6 spans &lt;ms&gt; error
1504
+ &lt;id&gt; page.add 6 spans &lt;ms&gt;
1505
+ chord.link.bytes sent: &lt;n&gt; bytes in &lt;n&gt; messages
1506
+ chord.link.bytes received: &lt;n&gt; bytes in &lt;n&gt; messages
1507
+ trace.ts &lt;data directory&gt; &lt;the refused one's id&gt;:
1508
+ page.add chord.host="browser" chars=0 chord.principal="owner" error trace_failed: a todo needs text
1509
+ chord.call chord.host="browser" type="todo.todos" member="add" object="todo.todos(groceries)" address.kind="list" outcome="error" chord.principal="owner" error call_failed: a todo needs text
1510
+ chord.relay chord.host="server" type="todo.todos" member="#0" upstream.kind="list" outcome="error" error relay_failed: a todo needs text
1511
+ chord.method chord.host="list|groceries" type="todo.todos" member="add" object="todo.todos(groceries)" principal.kind="owner" chord.principal="owner" outcome="error" error method_failed: a todo needs text
1512
+ chord.method chord.host="list|groceries" type="todo.store" member="add" object="todo.store" principal.kind="worker" chord.principal="worker:list|groceries" outcome="error" error method_failed: a todo needs text
1513
+ todos.validate chord.host="list|groceries" chars=0 error trace_failed: a todo needs text</code></pre>
1514
+ <p>A trace&#39;s id is its root span&#39;s. The refused add failed on every span of its path, each with a fixed category
1515
+ (<code>trace_failed</code>, <code>call_failed</code>, <code>relay_failed</code>, <code>method_failed</code>) and the message cut at 500 characters; a cancelled call
1516
+ is not an error. It has no commit: nothing was written. <code>chord.link.bytes</code> counts the encoded messages of the server&#39;s
1517
+ and the workers&#39; links (a page sends no metrics), each message once where it was sent and once where it was received.
1518
+ A metric carries no ids, only attributes with few values (<code>direction</code>), so its series stay few. A metric&#39;s current 10
1519
+ seconds are sent and written only once they are over, or when a process closes.</p>
1520
+ <h3 id="a-trace-viewer">A trace viewer</h3>
1521
+ <p>Two halves make the viewer: routes on the server that read the store, and a page that shows what they answer. Server
1522
+ code of an app is either the start script or a plugin, and routes come from plugins (chapter III), so the routes are a
1523
+ small plugin, <code>plugins/traces/</code>. Its server facet serves one host-lifetime object, which opens the store&#39;s file
1524
+ read-only as a native and contributes the routes while it is live:</p>
1525
+ <!-- quote: plugins/traces/server.ts -->
1526
+ <pre><code> host.<span class="tk-f">serve</span>(<span class="tk-t">Traces</span>, (_params, self) =&gt; {
1527
+ <span class="tk-k">const</span> info = self.<span class="tk-f">get</span>(<span class="tk-t">AppInfo</span>, {});
1528
+ <span class="tk-c">// The server records to the same file with its own recorder; this object only reads it.</span>
1529
+ <span class="tk-k">const</span> telemetry = self.<span class="tk-f">open</span>(
1530
+ <span class="tk-k">async</span> () =&gt; <span class="tk-f">openTelemetryStore</span>(<span class="tk-f">telemetryFile</span>(info.dataDir.value)),
1531
+ (store) =&gt; store.<span class="tk-f">close</span>(),
1532
+ );
1533
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Http</span>, {
1534
+ routes: {
1535
+ <span class="tk-s">"GET /api/traces"</span>: <span class="tk-k">async</span> (request, ctx) =&gt; {
1536
+ <span class="tk-f">ownerOnly</span>(ctx);
1537
+ <span class="tk-c">// `?principal=user:u1`: the traces with a span for that principal (`principalAttributes`).</span>
1538
+ <span class="tk-k">const</span> principal = request.query.<span class="tk-f">get</span>(<span class="tk-s">"principal"</span>);
1539
+ <span class="tk-k">const</span> attributes = principal === <span class="tk-k">null</span> ? <span class="tk-k">undefined</span> : { <span class="tk-s">"chord.principal"</span>: principal };
1540
+ <span class="tk-k">const</span> { traces } = <span class="tk-k">await</span> telemetry.value.<span class="tk-f">findTraces</span>({ limit: RECENT, attributes });
1541
+ <span class="tk-k">return</span> { kind: <span class="tk-s">"json"</span>, value: traces.<span class="tk-f">map</span>(({ traceId, spans }) =&gt; <span class="tk-f">summaryOf</span>(traceId, spans)) };
1542
+ },
1543
+ <span class="tk-s">"GET /api/traces/:id"</span>: <span class="tk-k">async</span> (request, ctx) =&gt; {
1544
+ <span class="tk-f">ownerOnly</span>(ctx);
1545
+ <span class="tk-k">const</span> trace = <span class="tk-k">await</span> telemetry.value.<span class="tk-f">getTrace</span>(request.params.id!);
1546
+ <span class="tk-k">if</span> (trace === <span class="tk-k">undefined</span>) <span class="tk-k">return</span> { kind: <span class="tk-s">"error"</span>, status: <span class="tk-n">404</span> };
1547
+ <span class="tk-k">return</span> { kind: <span class="tk-s">"json"</span>, value: { id: trace.traceId, spans: <span class="tk-f">rowsOf</span>(<span class="tk-f">clamped</span>(trace.spans)) } };
1548
+ },
1549
+ },
1550
+ });
1551
+ <span class="tk-k">return</span> {};
1552
+ });</code></pre>
1553
+ <p>The kit gives a plugin the server&#39;s process values as objects: <code>AppInfo</code> names the data directory, and the API&#39;s server
1554
+ module re-exports it with <code>telemetryFile</code> and Chord&#39;s <code>openTelemetryStore</code>. <code>self.open</code> declares a native, opened once
1555
+ <code>AppInfo</code> is live and closed when the object ends; a reload of the plugin restarts the object and opens the file anew.
1556
+ An object may contribute when it lives as long as its host (<code>local</code>, empty params; decision R21). Spans name who called
1557
+ what, so only the owner reads them:</p>
1558
+ <!-- quote: plugins/traces/server.ts -->
1559
+ <pre><code><span class="tk-k">function</span> <span class="tk-f">ownerOnly</span>(ctx: <span class="tk-t">Ctx</span>): <span class="tk-k">void</span> {
1560
+ <span class="tk-k">if</span> (ctx.principal?.kind !== <span class="tk-s">"owner"</span>) <span class="tk-k">throw</span> <span class="tk-f">forbidden</span>(<span class="tk-s">"the traces are the owner's"</span>);
1561
+ }</code></pre>
1562
+ <p>A route&#39;s principal is <code>undefined</code> only on a public route, which these are not; a signed-in member gets 403. The list
1563
+ filters by whom the traces were for (<code>?principal=</code>), and shows each with its user&#39;s name or principal key. <code>rowsOf</code>
1564
+ orders a trace&#39;s spans as a tree, parents before their children, each row&#39;s host its span&#39;s <code>chord.host</code>, and turns
1565
+ attributes, events and links into text. A page&#39;s spans carry the page&#39;s clock, which may be off by seconds against the
1566
+ server&#39;s, so a span of the page could show starting after the server span under it. Before drawing, <code>clamped</code> moves
1567
+ the page&#39;s spans of the trace by the least amount that puts every server span under a page span inside it
1568
+ (<code>plugins/traces/clock.ts</code>, with its own test):</p>
1569
+ <!-- quote: plugins/traces/clock.ts -->
1570
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">function</span> <span class="tk-f">clamped</span>(spans: <span class="tk-k">readonly</span> <span class="tk-t">StoredSpan</span>[]): <span class="tk-k">readonly</span> <span class="tk-t">StoredSpan</span>[] {
1571
+ <span class="tk-k">const</span> skew = <span class="tk-f">skewOf</span>(spans);
1572
+ <span class="tk-k">if</span> (skew === <span class="tk-n">0</span>) <span class="tk-k">return</span> spans;
1573
+ <span class="tk-k">return</span> spans.<span class="tk-f">map</span>((span) =&gt;
1574
+ <span class="tk-f">hostOf</span>(span) === <span class="tk-s">"browser"</span> ? { ...span, startedMs: span.startedMs + skew, endedMs: span.endedMs + skew } : span,
1575
+ );
1576
+ }</code></pre>
1577
+ <p>The page is the app&#39;s own page at a second path: <code>start.ts</code> serves it at <code>/</code> and <code>/traces</code> (chapter I), and the web
1578
+ entry picks the viewer by the path:</p>
1579
+ <!-- quote: src/web/main.ts -->
1580
+ <pre><code> <span class="tk-c">// The page's one other path (`start.ts` serves the page at `/` and `/traces`).</span>
1581
+ <span class="tk-k">if</span> (location.pathname === <span class="tk-s">"/traces"</span>) <span class="tk-k">return</span> <span class="tk-f">h</span>(<span class="tk-s">"main"</span>, { className: <span class="tk-s">"wide"</span> }, <span class="tk-f">h</span>(<span class="tk-s">"h1"</span>, <span class="tk-k">null</span>, <span class="tk-s">"Todo"</span>), <span class="tk-f">h</span>(<span class="tk-t">TraceViewer</span>));</code></pre>
1582
+ <p><code>TraceViewer</code> (<code>src/web/traces.ts</code>) fetches the list and the trace chosen in the URL&#39;s fragment, and draws each span
1583
+ with its host&#39;s name, its length as a bar on the trace&#39;s time line, its attributes and events, a failed span in the
1584
+ theme&#39;s error colour, and each link as the ids it refers to, never fetched. The bars are positioned with inline
1585
+ styles, which the page&#39;s CSP allows only because React sets them through the DOM:</p>
1586
+ <!-- quote: src/web/traces.ts -->
1587
+ <pre><code> <span class="tk-k">const</span> bar = {
1588
+ left: <span class="tk-s">`${(row.offsetMs / total) * 100}%`</span>,
1589
+ width: <span class="tk-s">`${Math.max((row.durationMs / total) * 100, 0.5)}%`</span>,
1590
+ };</code></pre>
1591
+ <p>After the two clicks, <code>screenshots.ts</code> opened the viewer on honey&#39;s trace (the figure in &quot;One trace from the browser&quot;),
1592
+ then types <code>user:u1</code> into its filter:</p>
1593
+ <!-- quote: screenshots.ts -->
1594
+ <pre><code> <span class="tk-k">await</span> browser.<span class="tk-f">open</span>(<span class="tk-s">`${origin}/traces#${encodeURIComponent(trace.traceId)}`</span>);
1595
+ <span class="tk-k">await</span> browser.<span class="tk-f">wait</span>(<span class="tk-s">"document.querySelectorAll('.traces-tree li').length === 6"</span>);
1596
+ <span class="tk-k">await</span> <span class="tk-f">shoot</span>(<span class="tk-s">"traces.png"</span>);</code></pre>
1597
+ <!-- output: screenshots -->
1598
+ <pre class="plain"><code>wrote docs/tutorial/screens/traces.png
1599
+ the viewer shows page.add: 6 spans on 3 hosts
1600
+ wrote docs/tutorial/screens/traces-ada.png
1601
+ the viewer for user:u1 lists chord.method for ada</code></pre>
1602
+ <figure class="fig wide"><p><img src="screens/traces-ada.png" alt="The trace viewer filtered by principal"></p>
1603
+ <figcaption><span class="fignum">Figure 8.</span> The traces for Ada (<code>user:u1</code>): the coffee <code>screenshots.ts</code> adds at its start as her (a root of the server acting as her), recorded by the list&#39;s worker; the app&#39;s hook named her <code>ada</code>.</figcaption></figure>
1604
+ <p>The list is mostly the app&#39;s own work: each HTTP request, relayed hold and activation is a trace of its own, as the
1605
+ trap above says. A real viewer would filter by name too (<code>findTraces({ name })</code>) or by error (<code>errors: true</code>), both of
1606
+ which the read interface has; this one is about 330 lines of TypeScript in three files and 20 of CSS, on Chord&#39;s
1607
+ read-only store.</p>
1608
+ </section>
1609
+
1610
+ <section class="chapter" id="testing"><header class="opener"><div class="numeral">XII</div><h2>Testing One Plugin</h2><p class="lede">testPlugin builds a plugin as production does and loads it alone on a host of its own.</p></header>
1611
+ <p class="dropcap">The steps above test the whole app. Most rules are checked faster on one plugin. <code>testPlugin</code> builds the plugin as
1612
+ production builds it (the same esbuild call, the API&#39;s imports checked), evaluates it with the app&#39;s real API module and
1613
+ loads it on a new host with an in-memory store, here at a list&#39;s address so it may serve <code>Todos</code>:</p>
1614
+ <!-- quote: plugins/lists/worker.test.ts -->
1615
+ <pre><code> <span class="tk-k">let</span> t: <span class="tk-t">TestedPlugin</span>&lt;v1.<span class="tk-t">TodoPrincipal</span>&gt;;
1616
+ <span class="tk-f">beforeEach</span>(<span class="tk-k">async</span> () =&gt; {
1617
+ t = <span class="tk-k">await</span> <span class="tk-f">testPlugin</span>&lt;v1.<span class="tk-t">TodoPrincipal</span>&gt;(
1618
+ <span class="tk-f">fileURLToPath</span>(<span class="tk-k">new</span> <span class="tk-f">URL</span>(<span class="tk-s">"./"</span>, <span class="tk-k">import</span>.meta.url)),
1619
+ {
1620
+ kind: <span class="tk-s">"worker"</span>,
1621
+ api: { name: <span class="tk-s">"@todo/app"</span>, versions: [<span class="tk-s">"v1"</span>] },
1622
+ modules: { <span class="tk-s">"@todo/app/v1"</span>: v1 },
1623
+ address: v1.<span class="tk-t">List</span>({ id: <span class="tk-s">"groceries"</span> }),
1624
+ principal: { kind: <span class="tk-s">"worker"</span>, list: <span class="tk-s">"list|groceries"</span> },
1625
+ },
1626
+ context,
1627
+ );
1628
+ });
1629
+ <span class="tk-f">afterEach</span>(() =&gt; t.<span class="tk-f">close</span>(context));</code></pre>
1630
+ <p>Each test runs code as a principal of its choice with <code>t.root</code>:</p>
1631
+ <!-- quote: plugins/lists/worker.test.ts -->
1632
+ <pre><code> <span class="tk-f">it</span>(<span class="tk-s">"lets the owner clear the done todos"</span>, <span class="tk-k">async</span> () =&gt; {
1633
+ <span class="tk-k">await</span> t.<span class="tk-f">root</span>(
1634
+ <span class="tk-k">async</span> (ctx) =&gt; {
1635
+ <span class="tk-k">const</span> todos = <span class="tk-k">await</span> ctx.<span class="tk-f">get</span>(<span class="tk-t">Todos</span>, { list: <span class="tk-s">"groceries"</span> });
1636
+ <span class="tk-k">if</span> (todos.variant !== <span class="tk-s">"owner"</span>) <span class="tk-k">throw</span> <span class="tk-k">new</span> <span class="tk-t">Error</span>(<span class="tk-s">`the owner got ${todos.variant}`</span>);
1637
+ <span class="tk-k">await</span> todos.<span class="tk-f">add</span>(<span class="tk-s">"milk"</span>, ctx);
1638
+ <span class="tk-k">await</span> todos.<span class="tk-f">add</span>(<span class="tk-s">"eggs"</span>, ctx);
1639
+ <span class="tk-k">await</span> todos.<span class="tk-f">tick</span>({ id: <span class="tk-n">1</span>, done: <span class="tk-k">true</span> }, ctx);
1640
+ <span class="tk-k">await</span> todos.<span class="tk-f">clear</span>(ctx);
1641
+ <span class="tk-f">expect</span>(todos.items.value.<span class="tk-f">map</span>(({ text }) =&gt; text)).<span class="tk-f">toEqual</span>([<span class="tk-s">"eggs"</span>]);
1642
+ <span class="tk-f">expect</span>(todos.notice.value).<span class="tk-f">toEqual</span>({ namespace: <span class="tk-s">"todo.lists"</span>, key: <span class="tk-s">"cleared"</span>, params: [<span class="tk-s">"owner"</span>] });
1643
+ },
1644
+ context,
1645
+ { principal: OWNER },
1646
+ );
1647
+ <span class="tk-f">expect</span>(t.<span class="tk-f">unserved</span>()).<span class="tk-f">toEqual</span>([]);</code></pre>
1648
+ <p>What the plugin uses and no plugin of the test serves would wait unbound; <code>t.unserved()</code> lists those names, so a test
1649
+ knows which stub facets to add. Registries it contributes to that nobody serves are collected (<code>t.contributions(R)</code>).
1650
+ Run it like any test:</p>
1651
+ <pre><code>node ../../node_modules/vitest/dist/cli.js --run test/examples/<span class="tk-n">61</span>-tutorial/plugins/lists/worker.test.ts</code></pre>
1652
+ <p>The test imports the API by its package name and <code>Todos</code> from the plugin&#39;s own <code>contract.ts</code>, as the facets do:</p>
1653
+ <!-- quote: plugins/lists/worker.test.ts -->
1654
+ <pre><code><span class="tk-k">import</span> * <span class="tk-k">as</span> v1 <span class="tk-k">from</span> <span class="tk-s">"@todo/app/v1"</span>;</code></pre>
1655
+ <!-- quote: plugins/lists/worker.test.ts -->
1656
+ <pre><code><span class="tk-k">import</span> { <span class="tk-t">Todos</span> } <span class="tk-k">from</span> <span class="tk-s">"./contract.ts"</span>;</code></pre>
1657
+ <p>Both resolve because the API is a workspace package npm linked (chapter I); the plugin&#39;s build bundles its own copy of
1658
+ <code>contract.ts</code>, and the two bind, since contracts are data (the same name, version and schema). A third test checks
1659
+ the refusal of chapter II:</p>
1660
+ <!-- quote: plugins/lists/worker.test.ts -->
1661
+ <pre><code> <span class="tk-k">const</span> refused = <span class="tk-k">await</span> todos.<span class="tk-f">add</span>(<span class="tk-s">""</span>, ctx).<span class="tk-k">catch</span>((error: <span class="tk-t">ChordError</span>) =&gt; error);
1662
+ <span class="tk-f">expect</span>(refused).<span class="tk-f">toMatchObject</span>({ code: <span class="tk-s">"invalid"</span>, reason: <span class="tk-s">"args"</span> });</code></pre>
1663
+ </section>
1664
+
1665
+ <section class="chapter" id="p2"><header class="opener"><div class="numeral">XIII</div><h2>Writing a p2 Plugin</h2><p class="lede">What is different in p2, and one small plugin: a tool for the agent and a chip for the people.</p></header>
1666
+ <p class="dropcap">p2, the coding agent&#39;s server with its web page and terminal, runs on Chord v2 but not on the app kit. It is older than
1667
+ the kit and keeps its own decisions: its own data directory and SQLite files, its own sign-in, devices and sharing,
1668
+ its own page and terminal bundles. It uses Chord&#39;s components directly: <code>createHost</code> with p2&#39;s principals,
1669
+ <code>createWorkers</code> for one worker per session, <code>createPlugins</code> to find, build, watch and roll out its plugins. What a
1670
+ plugin author sees is the same model: contracts, factories, registries, facets per host kind.</p>
1671
+ <table>
1672
+ <thead>
1673
+ <tr>
1674
+ <th></th>
1675
+ <th>The todo app (kit)</th>
1676
+ <th>p2</th>
1677
+ </tr>
1678
+ </thead>
1679
+ <tbody><tr>
1680
+ <td>host kinds</td>
1681
+ <td><code>Server</code>, <code>List({ id })</code></td>
1682
+ <td><code>Server</code>, <code>Session({ id })</code> (<code>serverHost</code>, <code>sessionHost</code>)</td>
1683
+ </tr>
1684
+ <tr>
1685
+ <td>principals</td>
1686
+ <td>server, worker, owner, user</td>
1687
+ <td><code>owner</code> (with <code>access</code>), <code>guest</code> (with <code>sessions</code> and <code>revision</code>), <code>worker</code>, <code>server</code>, <code>host</code></td>
1688
+ </tr>
1689
+ <tr>
1690
+ <td>API</td>
1691
+ <td><code>@todo/app/v1</code>, <code>/server</code>, <code>/web</code>, <code>/tui</code></td>
1692
+ <td><code>@p2/app/v1</code> (browser-safe), <code>/node</code> (server and worker), <code>/server</code>, <code>/worker</code>, <code>/web</code>, <code>/tui</code></td>
1693
+ </tr>
1694
+ <tr>
1695
+ <td>other host packages</td>
1696
+ <td>React for the web</td>
1697
+ <td>pi-ai, pi-durable, pi-schema, pi-env, pi-tui, <code>ws</code>, <code>yaml</code>: plugins import them directly</td>
1698
+ </tr>
1699
+ <tr>
1700
+ <td>plugins</td>
1701
+ <td><code>plugins/&lt;name&gt;/</code>, <code>package.json</code> named <code>@todo/&lt;name&gt;</code></td>
1702
+ <td><code>apps/p2/plugins/&lt;name&gt;/</code>, <code>package.json</code> named <code>&lt;name&gt;</code></td>
1703
+ </tr>
1704
+ <tr>
1705
+ <td>a Chord store</td>
1706
+ <td>the kit&#39;s, per host: persisted documents and tables</td>
1707
+ <td>none: durable data is Durable&#39;s (documents with a conversation) or p2&#39;s own files</td>
1708
+ </tr>
1709
+ <tr>
1710
+ <td>page and terminal</td>
1711
+ <td>kit surfaces, <code>Panels</code> and <code>Screens</code></td>
1712
+ <td>p2&#39;s bundles; the page&#39;s <code>Slots</code> registry, <code>useObject</code>, <code>useReadable</code></td>
1713
+ </tr>
1714
+ <tr>
1715
+ <td>applying changes</td>
1716
+ <td><code>app.applyChanges</code></td>
1717
+ <td>the owner&#39;s Reload button, <code>npm run reload</code> in <code>apps/p2</code>, the <code>reload_plugins</code> tool</td>
1718
+ </tr>
1719
+ </tbody></table>
1720
+ <h3 id="the-scratchpad-plugin">The scratchpad plugin</h3>
1721
+ <p>The example gives the agent a <code>scratch_note</code> tool and shows the session&#39;s notes to everyone who may see the session.
1722
+ Its contract places one object per session on the session&#39;s worker:</p>
1723
+ <!-- p2: apps/p2/plugins/scratchpad/contract.ts -->
1724
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Scratchpad</span> = <span class="tk-f">remote</span>(<span class="tk-s">"p2.scratchpad"</span>, {
1725
+ version: <span class="tk-n">1</span>,
1726
+ params: { session: param.<span class="tk-f">str</span>() },
1727
+ hostAddress: sessionHost,
1728
+ variants: { reader: { state: { notes: <span class="tk-f">arr</span>(<span class="tk-f">str</span>()) } } },
1729
+ });</code></pre>
1730
+ <p>The worker facet holds the notes in a worker-wide store (a local object with empty params, as in chapter II). It gives
1731
+ the agent its tool through p2&#39;s <code>Extensions</code> registry, a keyed registry of Durable extensions, and serves the view to
1732
+ whoever <code>canView</code> the session:</p>
1733
+ <!-- p2: apps/p2/plugins/scratchpad/worker.ts -->
1734
+ <pre><code><span class="tk-k">const</span> <span class="tk-t">Notes</span> = <span class="tk-f">defineState</span>(<span class="tk-s">"p2.scratchpad.notes"</span>, {
1735
+ schema: <span class="tk-f">obj</span>({ items: <span class="tk-f">arr</span>(<span class="tk-f">str</span>()) }),
1736
+ version: <span class="tk-n">1</span>,
1737
+ initial: () =&gt; ({ items: [] }),
1738
+ });</code></pre>
1739
+ <!-- p2: apps/p2/plugins/scratchpad/worker.ts -->
1740
+ <pre><code><span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((host) =&gt; {
1741
+ host.<span class="tk-f">serve</span>(<span class="tk-t">ScratchpadStore</span>, (_params, self) =&gt; {
1742
+ <span class="tk-k">const</span> notes = self.<span class="tk-f">state</span>(<span class="tk-t">Notes</span>);
1743
+ self.<span class="tk-f">contribute</span>(<span class="tk-t">Extensions</span>, {
1744
+ scratchpad: <span class="tk-f">defineExtension</span>({
1745
+ name: <span class="tk-s">"scratchpad"</span>,
1746
+ tools: [
1747
+ <span class="tk-f">defineTool</span>({
1748
+ name: <span class="tk-s">"scratch_note"</span>,
1749
+ description:
1750
+ <span class="tk-s">"Keep a short note for this session (a decision, an open question). Everyone viewing the session sees the notes."</span>,
1751
+ parameters: <span class="tk-f">obj</span>({ text: <span class="tk-f">str</span>({ minLength: <span class="tk-n">1</span>, maxLength: <span class="tk-n">500</span> }) }),
1752
+ replay: <span class="tk-s">"unsafe"</span>,
1753
+ execute: <span class="tk-k">async</span> ({ text }, _api, context) =&gt; {
1754
+ <span class="tk-k">await</span> notes.<span class="tk-f">change</span>((draft) =&gt; {
1755
+ draft.items.<span class="tk-f">push</span>(text);
1756
+ }, context);
1757
+ <span class="tk-k">return</span> {
1758
+ output: [{ <span class="tk-k">type</span>: <span class="tk-s">"text"</span> <span class="tk-k">as</span> <span class="tk-k">const</span>, text: <span class="tk-s">`Noted (${notes.value.items.length} in all).`</span> }],
1759
+ };
1760
+ },
1761
+ }),
1762
+ ],
1763
+ }),
1764
+ });
1765
+ <span class="tk-k">return</span> { notes: self.<span class="tk-f">derive</span>([notes], ([value]) =&gt; value.items) };
1766
+ });
1767
+ host.<span class="tk-f">serve</span>(<span class="tk-t">Scratchpad</span>, ({ session }, principal, self) =&gt; {
1768
+ <span class="tk-k">if</span> (!<span class="tk-f">canView</span>(principal, session)) <span class="tk-k">throw</span> <span class="tk-f">forbidden</span>(<span class="tk-s">"viewers only"</span>);
1769
+ <span class="tk-k">const</span> store = self.<span class="tk-f">get</span>(<span class="tk-t">ScratchpadStore</span>, {});
1770
+ <span class="tk-k">return</span> { variant: <span class="tk-s">"reader"</span>, members: { notes: self.<span class="tk-f">derive</span>([store.notes], ([notes]) =&gt; notes) } };
1771
+ });
1772
+ });</code></pre>
1773
+ <p>The notes are an in-memory document: p2 opens no Chord store, so <code>persist: true</code> is not available, and the notes last
1774
+ as long as the session&#39;s worker. Data that must outlive it goes into a Durable document (p2&#39;s chat-mode keeps its switch
1775
+ that way). The tool runs inside Durable with a Durable <code>Context</code>, which is Chord&#39;s own context type, so the store&#39;s
1776
+ handle takes it and the write is awaited. The page gets a chip in the composer&#39;s bar through <code>Slots</code>:</p>
1777
+ <!-- p2: apps/p2/plugins/scratchpad/web.tsx -->
1778
+ <pre><code><span class="tk-k">function</span> <span class="tk-t">NotesChip</span>() {
1779
+ <span class="tk-k">const</span> { session } = <span class="tk-f">useRoute</span>();
1780
+ <span class="tk-k">const</span> pad = <span class="tk-f">useObject</span>(<span class="tk-t">Scratchpad</span>, session === <span class="tk-k">undefined</span> ? <span class="tk-k">undefined</span> : { session });
1781
+ <span class="tk-k">const</span> notes = <span class="tk-f">useReadable</span>(pad?.notes) ?? [];
1782
+ <span class="tk-k">if</span> (notes.length === <span class="tk-n">0</span>) <span class="tk-k">return</span> <span class="tk-k">null</span>;
1783
+ <span class="tk-k">return</span> (
1784
+ &lt;span className=<span class="tk-s">"chip"</span> title={notes.<span class="tk-f">join</span>(<span class="tk-s">"\n"</span>)}&gt;
1785
+ <span class="tk-t">Notes</span> · {notes.length}
1786
+ &lt;/span&gt;
1787
+ );
1788
+ }
1789
+
1790
+ <span class="tk-k">export</span> <span class="tk-k">default</span> <span class="tk-f">defineFacet</span>((_host, self) =&gt; self.<span class="tk-f">contribute</span>(<span class="tk-t">Slots</span>, { <span class="tk-s">"composer:scratchpad"</span>: <span class="tk-t">NotesChip</span> }));</code></pre>
1791
+ <h3 id="testing-it-in-p2">Testing it in p2</h3>
1792
+ <p>p2&#39;s tests load facets on a bare host with <code>testHost</code> (<code>test/p2/helpers.ts</code>), stub what they need, and reach the host
1793
+ as any principal over a memory link with <code>fixture.client(principal)</code>:</p>
1794
+ <!-- p2: apps/p2/test/p2/scratchpad.test.ts -->
1795
+ <pre><code> <span class="tk-f">it</span>(<span class="tk-s">"keeps the agent's notes and shows them to the session's viewers"</span>, <span class="tk-k">async</span> () =&gt; {
1796
+ <span class="tk-k">const</span> extensions = <span class="tk-k">await</span> fixture.host.<span class="tk-f">registry</span>(<span class="tk-t">Extensions</span>, BACKGROUND_CONTEXT);
1797
+ <span class="tk-k">const</span> tool = extensions.value.<span class="tk-f">get</span>(<span class="tk-s">"scratchpad"</span>)!.tools![<span class="tk-n">0</span>]!;
1798
+ <span class="tk-k">await</span> tool.<span class="tk-f">execute</span>({ text: <span class="tk-s">"ship on friday"</span> }, <span class="tk-k">undefined</span> <span class="tk-k">as</span> <span class="tk-k">never</span>, BACKGROUND_CONTEXT);
1799
+ <span class="tk-k">const</span> pad = <span class="tk-k">await</span> fixture.<span class="tk-f">client</span>(OWNER).<span class="tk-f">get</span>(<span class="tk-t">Scratchpad</span>, { session: <span class="tk-s">"s1"</span> }, BACKGROUND_CONTEXT);
1800
+ assert.<span class="tk-f">deepEqual</span>(<span class="tk-k">await</span> <span class="tk-f">until</span>(pad.notes, (notes) =&gt; notes?.length === <span class="tk-n">1</span>), [<span class="tk-s">"ship on friday"</span>]);
1801
+ });</code></pre>
1802
+ <p>Run from <code>apps/p2</code> with <code>node --import ./src/source.ts --test test/p2/scratchpad.test.ts</code>. Load the plugin into a running
1803
+ p2 with the owner&#39;s Reload button or <code>npm run reload -- scratchpad</code>; a new plugin directory is picked up by the same
1804
+ reload. The trap of chapter II applies with p2&#39;s names: a server method that must reach a session&#39;s worker as the
1805
+ server runs in a root of the server&#39;s own (<code>serverOf(self)().root</code>), not through its caller&#39;s <code>ctx.get</code>.</p>
1806
+ </section>
1807
+
1808
+ <section class="chapter epilogue" id="traps-and-open-questions"><header class="opener"><div class="numeral">∴</div><h2>Traps and Open Questions</h2><p class="lede">The six traps once more, and what in Chord v2 was hard to explain while writing this.</p></header>
1809
+ <p class="dropcap"><strong>The traps.</strong> Load before use: a host binds nothing before <code>load</code>; a client with nothing to load calls <code>load([])</code>. A
1810
+ reference acts as whoever made it: <code>ctx.get</code> as the caller, <code>host.get</code> and a local object&#39;s <code>self.get</code> as the host.
1811
+ Await what you start: a write started with a context that ends is cancelled and reported <code>transaction_cancelled</code>. A
1812
+ watch starts at the current value: read <code>handle.value</code>, then <code>start</code>; the listener gets changes only. The plain <code>t</code>
1813
+ has no language of its own: components and screens render with <code>useMessages(t)</code> and <code>scope.messages(t)</code>. A trace
1814
+ starts where a span starts: work under a context without a recording span records nothing, and its provider&#39;s span is
1815
+ a root of its own.</p>
1816
+ <p><strong>Found while writing the example, and fixed in Chord since.</strong> Integer table keys were typed <code>string</code> (a leftover
1817
+ of the switch from TypeBox), so <code>tx.update(items, { id }, ...)</code> with a number failed <code>tsc</code>; the store now uses it. A
1818
+ reloaded web plugin lost its own React state (now <code>usePageState</code>,
1819
+ chapter IX), a plugin&#39;s <code>contract.ts</code> could not be imported outside builds (the API is now a workspace package,
1820
+ chapter I), plugins could not throw Chord&#39;s <code>invalid</code> (Chord exports <code>invalid</code> and <code>forbidden</code>, chapter II), and a
1821
+ live window over a table needed a one-time read in <code>self.open</code>, which made the store restart at every reload of its
1822
+ plugin (now <code>self.query</code>, a live query Chord runs, chapters II and IX).</p>
1823
+ <p><strong>Found while writing the telemetry chapter, and fixed in Chord since.</strong> <code>chord.commit</code> counted documents and their
1824
+ ops only, so a commit that wrote table rows reported <code>documents=0 ops=0</code> (it counts <code>rows</code> now). Spans named no host,
1825
+ and the viewer told processes apart by the SQLite store&#39;s id format (every Chord span carries <code>chord.host</code> now). The
1826
+ store kept a span&#39;s start in whole milliseconds, so the viewer&#39;s bars shifted by up to one (starts are microseconds
1827
+ now, the store&#39;s version 2, which a recorder migrates its file to). Reading needed a recorder that recorded nothing
1828
+ (<code>openTelemetryStore</code> reads read-only). A page could not record, so the chapter&#39;s click was a client host in Node
1829
+ standing in for the page (<code>startPage({ telemetry: true })</code> now, its spans sent to the server). And every process wrote
1830
+ the one file itself, each worker told its path (the server is the store&#39;s only writer now, the workers and pages
1831
+ sending it their spans), and spans said nothing of whom they were for (<code>principalAttributes</code>).</p>
1832
+ </section>
1833
+
1834
+ <section class="chapter appendix" id="the-app-s-files"><header class="opener"><div class="numeral">A</div><h2>The App&#39;s Files</h2></header>
1835
+ <table>
1836
+ <thead>
1837
+ <tr>
1838
+ <th>File</th>
1839
+ <th>Kind</th>
1840
+ <th>What it is</th>
1841
+ </tr>
1842
+ </thead>
1843
+ <tbody><tr>
1844
+ <td><code>src/principals.ts</code></td>
1845
+ <td>base</td>
1846
+ <td>the principal type, <code>principalAttributes</code> (whom a span was for)</td>
1847
+ </tr>
1848
+ <tr>
1849
+ <td><code>package.json</code></td>
1850
+ <td>base</td>
1851
+ <td>the API as the workspace package <code>@todo/app</code>, its <code>exports</code></td>
1852
+ </tr>
1853
+ <tr>
1854
+ <td><code>src/api/v1/index.ts</code></td>
1855
+ <td>base, every kind</td>
1856
+ <td><code>@todo/app/v1</code>: Chord&#39;s part (with <code>invalid</code>, <code>forbidden</code>, <code>defineMessages</code>, <code>Translations</code>), <code>defineFacet</code>, <code>List</code></td>
1857
+ </tr>
1858
+ <tr>
1859
+ <td><code>src/api/v1/server.ts</code></td>
1860
+ <td>base, server</td>
1861
+ <td><code>Http</code>, the ephemeral <code>login</code>, <code>AppInfo</code>, <code>telemetryFile</code>, <code>openTelemetryStore</code></td>
1862
+ </tr>
1863
+ <tr>
1864
+ <td><code>src/api/v1/web.ts</code></td>
1865
+ <td>base, web</td>
1866
+ <td><code>Panels</code>, <code>useHold</code>, <code>useHost</code>, <code>useMessages</code>, <code>usePageState</code>, <code>useWatch</code></td>
1867
+ </tr>
1868
+ <tr>
1869
+ <td><code>src/api/v1/tui.ts</code></td>
1870
+ <td>base, tui</td>
1871
+ <td><code>Screens</code></td>
1872
+ </tr>
1873
+ <tr>
1874
+ <td><code>start.ts</code>, <code>worker.ts</code></td>
1875
+ <td>base</td>
1876
+ <td><code>startServer</code> with the telemetry store, <code>startWorker</code>, each with <code>principalAttributes</code></td>
1877
+ </tr>
1878
+ <tr>
1879
+ <td><code>src/telemetry.ts</code></td>
1880
+ <td>base</td>
1881
+ <td>the telemetry store&#39;s file in the data directory, <code>openTelemetry</code> (the server&#39;s)</td>
1882
+ </tr>
1883
+ <tr>
1884
+ <td><code>src/web/main.ts</code>, <code>src/web/style.css</code></td>
1885
+ <td>base, web entry</td>
1886
+ <td><code>startPage</code> (recording), the panels, the viewer at <code>/traces</code></td>
1887
+ </tr>
1888
+ <tr>
1889
+ <td><code>src/web/traces.ts</code></td>
1890
+ <td>base, web entry</td>
1891
+ <td>the owner&#39;s trace viewer</td>
1892
+ </tr>
1893
+ <tr>
1894
+ <td><code>src/tui/main.ts</code></td>
1895
+ <td>base, terminal entry</td>
1896
+ <td><code>startTerminalRuntime</code>, the screens</td>
1897
+ </tr>
1898
+ <tr>
1899
+ <td><code>plugins/lists/</code></td>
1900
+ <td>plugin</td>
1901
+ <td>contract (<code>Item</code>, <code>Notice</code>, <code>Todos</code>), messages (English, German), worker (table, store, views), server (routes), web (panel), tui (screen), its test</td>
1902
+ </tr>
1903
+ <tr>
1904
+ <td><code>plugins/french/</code></td>
1905
+ <td>plugin</td>
1906
+ <td>French for the lists plugin, for pages and terminals</td>
1907
+ </tr>
1908
+ <tr>
1909
+ <td><code>plugins/login.server.ts</code></td>
1910
+ <td>single-file plugin</td>
1911
+ <td>the kit&#39;s ephemeral login</td>
1912
+ </tr>
1913
+ <tr>
1914
+ <td><code>plugins/traces/</code></td>
1915
+ <td>plugin</td>
1916
+ <td>the trace viewer&#39;s routes over the telemetry store (server), the owner&#39;s only; the page&#39;s clock moved onto the server&#39;s (<code>clock.ts</code>, its test)</td>
1917
+ </tr>
1918
+ <tr>
1919
+ <td><code>later/harbor/</code></td>
1920
+ <td>plugin, added in step 6</td>
1921
+ <td>the Harbor theme for pages and terminals</td>
1922
+ </tr>
1923
+ <tr>
1924
+ <td><code>main.ts</code>, <code>screenshots.ts</code>, <code>tutorial.test.ts</code></td>
1925
+ <td>example</td>
1926
+ <td>the steps, the screenshots (with the page&#39;s traced click), the checks</td>
1927
+ </tr>
1928
+ <tr>
1929
+ <td><code>trace.ts</code></td>
1930
+ <td>example</td>
1931
+ <td>a data directory&#39;s newest traces, one trace, a metric</td>
1932
+ </tr>
1933
+ </tbody></table>
1934
+ </section>
1935
+
1936
+ <section class="chapter appendix" id="reference"><header class="opener"><div class="numeral">B</div><h2>Reference</h2></header>
1937
+ <table>
1938
+ <thead>
1939
+ <tr>
1940
+ <th>Call</th>
1941
+ <th>Chapter</th>
1942
+ <th>What it does</th>
1943
+ </tr>
1944
+ </thead>
1945
+ <tbody><tr>
1946
+ <td><code>startServer&lt;P&gt;({ name, dataDir, root, listen, api, kinds, principal, workers, web, tui, onError }, ctx)</code></td>
1947
+ <td>I</td>
1948
+ <td>the app&#39;s server, with the kit</td>
1949
+ </tr>
1950
+ <tr>
1951
+ <td><code>startWorker({ api, kinds, principalAttributes, onError })</code></td>
1952
+ <td>I, XI</td>
1953
+ <td>a worker&#39;s entry</td>
1954
+ </tr>
1955
+ <tr>
1956
+ <td><code>app.ownerLink()</code>, <code>app.ownerTokenFile</code></td>
1957
+ <td>I, III</td>
1958
+ <td>the owner&#39;s sign-in, for a browser and for local clients</td>
1959
+ </tr>
1960
+ <tr>
1961
+ <td><code>hostKind(name, params)</code>, <code>remote(name, { version, params, hostAddress, variants })</code>, <code>local(name, { version, params, members })</code></td>
1962
+ <td>II</td>
1963
+ <td>host kinds and contracts</td>
1964
+ </tr>
1965
+ <tr>
1966
+ <td><code>host.serve(Type, factory)</code>; factory <code>self</code>: <code>table</code>, <code>query</code>, <code>derive</code>, <code>tx</code>, <code>get</code>, <code>contribute</code></td>
1967
+ <td>II</td>
1968
+ <td>an object&#39;s parts</td>
1969
+ </tr>
1970
+ <tr>
1971
+ <td><code>host.root(fn, ctx, { principal })</code>, <code>ctx.get(Type, params)</code></td>
1972
+ <td>II</td>
1973
+ <td>code outside objects, as a principal</td>
1974
+ </tr>
1975
+ <tr>
1976
+ <td><code>invalid(reason, message, details?)</code>, <code>forbidden(message, details?)</code></td>
1977
+ <td>II, IX</td>
1978
+ <td>Chord&#39;s errors, thrown by a plugin</td>
1979
+ </tr>
1980
+ <tr>
1981
+ <td><code>self.contribute(Http, { routes })</code></td>
1982
+ <td>III</td>
1983
+ <td>routes, denied by default</td>
1984
+ </tr>
1985
+ <tr>
1986
+ <td><code>startPage</code>, <code>HostProvider</code>, <code>useHold</code>, <code>useWatch</code>, <code>useRegistry</code></td>
1987
+ <td>IV</td>
1988
+ <td>the page</td>
1989
+ </tr>
1990
+ <tr>
1991
+ <td><code>startTerminalRuntime</code>, <code>TuiScope</code>, <code>startTerminal</code></td>
1992
+ <td>V</td>
1993
+ <td>the terminal</td>
1994
+ </tr>
1995
+ <tr>
1996
+ <td><code>usePageState(key, initial)</code>, <code>scope.pageState(key, initial)</code></td>
1997
+ <td>IV, IX</td>
1998
+ <td>state the page keeps across plugin reloads</td>
1999
+ </tr>
2000
+ <tr>
2001
+ <td><code>self.contribute(Themes, theme)</code></td>
2002
+ <td>VI</td>
2003
+ <td>a theme</td>
2004
+ </tr>
2005
+ <tr>
2006
+ <td><code>ephemeralLoginFacet&lt;P&gt;()</code>, <code>host.load([], ctx)</code>, <code>host.connect(link, { principal }, ctx)</code></td>
2007
+ <td>VII</td>
2008
+ <td>a second user&#39;s client</td>
2009
+ </tr>
2010
+ <tr>
2011
+ <td><code>defineMessages(namespace, catalogs)</code>, <code>t.translations</code>, <code>self.contribute(Translations, list)</code></td>
2012
+ <td>IV, V, VIII</td>
2013
+ <td>a plugin&#39;s messages and its catalogs</td>
2014
+ </tr>
2015
+ <tr>
2016
+ <td><code>useMessages(t)</code>, <code>scope.messages(t)</code>, <code>t.message(key, ...args)</code>, <code>t.render(message)</code>, <code>MessageSchema</code></td>
2017
+ <td>IV, V, VIII</td>
2018
+ <td>texts in the viewer&#39;s language, messages as data</td>
2019
+ </tr>
2020
+ <tr>
2021
+ <td><code>MessagesOf&lt;typeof t&gt;</code>, <code>LocaleChoice</code></td>
2022
+ <td>VIII</td>
2023
+ <td>a translation plugin, the user&#39;s language</td>
2024
+ </tr>
2025
+ <tr>
2026
+ <td><code>app.plugins.state</code>, <code>app.applyChanges(ctx)</code></td>
2027
+ <td>VI, IX</td>
2028
+ <td>marks and rollouts</td>
2029
+ </tr>
2030
+ <tr>
2031
+ <td><code>app.exportData(address, path, ctx)</code>, <code>app.importData(path, address, ctx)</code></td>
2032
+ <td>X</td>
2033
+ <td>moving a worker&#39;s data</td>
2034
+ </tr>
2035
+ <tr>
2036
+ <td><code>createSqliteTelemetry(db)</code>, <code>startServer({ telemetry, principalAttributes })</code>, <code>startPage({ telemetry: true })</code>, <code>createHost({ telemetry, principalAttributes })</code></td>
2037
+ <td>XI</td>
2038
+ <td>the telemetry store on the server, pages and workers sending it their spans</td>
2039
+ </tr>
2040
+ <tr>
2041
+ <td><code>host.root(fn, ctx, { trace })</code>, <code>ctx.trace(name, attributes, fn)</code></td>
2042
+ <td>XI</td>
2043
+ <td>spans of your own</td>
2044
+ </tr>
2045
+ <tr>
2046
+ <td><code>openTelemetryStore(path)</code>, <code>store.findTraces({ name, attributes, errors, startMin, startMax, limit, cursor })</code>, <code>store.getTrace(id)</code>, <code>store.findMetrics({ name, attributes })</code>, <code>renderTrace(spans)</code></td>
2047
+ <td>XI</td>
2048
+ <td>reading traces and metrics</td>
2049
+ </tr>
2050
+ <tr>
2051
+ <td><code>AppInfo</code>, <code>self.open(open, close)</code></td>
2052
+ <td>XI</td>
2053
+ <td>the data directory, a native (the viewer&#39;s store)</td>
2054
+ </tr>
2055
+ <tr>
2056
+ <td><code>testPlugin(dir, { kind, api, modules, address, principal, stubs }, ctx)</code></td>
2057
+ <td>XII</td>
2058
+ <td>one plugin alone</td>
2059
+ </tr>
2060
+ </tbody></table>
2061
+ </section>
2062
+
2063
+ <p class="colophon">Together · first edition · 2026-11-03 · Chord v2 slices 1-13, the app kit, example 61</p>
2064
+ </div></div>
2065
+ <script>(function activate(root) {
2066
+ const replay = (f) => {
2067
+ f.classList.remove("play");
2068
+ void f.getBoundingClientRect();
2069
+ f.classList.add("play");
2070
+ };
2071
+ const io = new IntersectionObserver(
2072
+ (es) => {
2073
+ for (const e of es)
2074
+ if (e.isIntersecting) {
2075
+ e.target.classList.add("play");
2076
+ io.unobserve(e.target);
2077
+ }
2078
+ },
2079
+ { threshold: 0.3 },
2080
+ );
2081
+ root.querySelectorAll(".anim:not(.armed)").forEach((f) => {
2082
+ f.classList.add("armed");
2083
+ io.observe(f);
2084
+ const b = f.querySelector(".replay");
2085
+ if (b) b.onclick = () => replay(f);
2086
+ });
2087
+ root.querySelectorAll(".steps:not(.js)").forEach((s) => {
2088
+ s.classList.add("js");
2089
+ const ps = [...s.querySelectorAll(":scope>.step-panel")];
2090
+ let i = 0;
2091
+ const c = s.querySelector(".count"),
2092
+ p = s.querySelector(".prev"),
2093
+ n = s.querySelector(".next");
2094
+ const show = () => {
2095
+ ps.forEach((x, j) => x.classList.toggle("on", j === i));
2096
+ c.textContent = `${i + 1} / ${ps.length}`;
2097
+ p.disabled = i === 0;
2098
+ n.disabled = i === ps.length - 1;
2099
+ ps[i].querySelectorAll(".anim").forEach(replay);
2100
+ };
2101
+ p.onclick = () => {
2102
+ i--;
2103
+ show();
2104
+ };
2105
+ n.onclick = () => {
2106
+ i++;
2107
+ show();
2108
+ };
2109
+ show();
2110
+ });
2111
+ })(document)</script>
2112
+ </body></html>