cursedbelt-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/LICENSE +15 -0
  2. package/dist/client/wsWire.d.ts +49 -0
  3. package/dist/client/wsWire.js +57 -0
  4. package/dist/config/appConfig.d.ts +361 -0
  5. package/dist/config/appConfig.js +308 -0
  6. package/dist/core/activity/index.d.ts +8 -0
  7. package/dist/core/activity/index.js +7 -0
  8. package/dist/core/activity/model.d.ts +115 -0
  9. package/dist/core/activity/model.js +63 -0
  10. package/dist/core/analytics/index.d.ts +18 -0
  11. package/dist/core/analytics/index.js +18 -0
  12. package/dist/core/analytics/range.d.ts +186 -0
  13. package/dist/core/analytics/range.js +271 -0
  14. package/dist/core/analytics/schedule.d.ts +301 -0
  15. package/dist/core/analytics/schedule.js +372 -0
  16. package/dist/core/category/index.d.ts +3 -0
  17. package/dist/core/category/index.js +6 -0
  18. package/dist/core/category/memoryStore.d.ts +21 -0
  19. package/dist/core/category/memoryStore.js +97 -0
  20. package/dist/core/category/tree.d.ts +26 -0
  21. package/dist/core/category/tree.js +113 -0
  22. package/dist/core/category/types.d.ts +48 -0
  23. package/dist/core/category/types.js +8 -0
  24. package/dist/core/color.d.ts +14 -0
  25. package/dist/core/color.js +32 -0
  26. package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
  27. package/dist/core/colorScheme/colorSchemeStore.js +123 -0
  28. package/dist/core/colorScheme/index.d.ts +7 -0
  29. package/dist/core/colorScheme/index.js +7 -0
  30. package/dist/core/domainOwners.d.ts +19 -0
  31. package/dist/core/domainOwners.js +15 -0
  32. package/dist/core/download.d.ts +14 -0
  33. package/dist/core/download.js +37 -0
  34. package/dist/core/events/ccEventBus.d.ts +44 -0
  35. package/dist/core/events/ccEventBus.js +70 -0
  36. package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
  37. package/dist/core/file-tree/fileTreeModel.js +312 -0
  38. package/dist/core/folder-tree/actions.d.ts +84 -0
  39. package/dist/core/folder-tree/actions.js +119 -0
  40. package/dist/core/folder-tree/index.d.ts +3 -0
  41. package/dist/core/folder-tree/index.js +20 -0
  42. package/dist/core/folder-tree/path.d.ts +76 -0
  43. package/dist/core/folder-tree/path.js +146 -0
  44. package/dist/core/folder-tree/tree.d.ts +86 -0
  45. package/dist/core/folder-tree/tree.js +184 -0
  46. package/dist/core/layout/index.d.ts +1 -0
  47. package/dist/core/layout/index.js +7 -0
  48. package/dist/core/master-lock/index.d.ts +14 -0
  49. package/dist/core/master-lock/index.js +14 -0
  50. package/dist/core/master-lock/kdf.d.ts +63 -0
  51. package/dist/core/master-lock/kdf.js +117 -0
  52. package/dist/core/master-lock/policy.d.ts +32 -0
  53. package/dist/core/master-lock/policy.js +52 -0
  54. package/dist/core/master-lock/presence.d.ts +52 -0
  55. package/dist/core/master-lock/presence.js +61 -0
  56. package/dist/core/master-lock/wire.d.ts +139 -0
  57. package/dist/core/master-lock/wire.js +81 -0
  58. package/dist/core/media/bgRemoval.d.ts +27 -0
  59. package/dist/core/media/bgRemoval.js +101 -0
  60. package/dist/core/media/ffmpeg.d.ts +57 -0
  61. package/dist/core/media/ffmpeg.js +126 -0
  62. package/dist/core/media/format.d.ts +20 -0
  63. package/dist/core/media/format.js +54 -0
  64. package/dist/core/media/index.d.ts +12 -0
  65. package/dist/core/media/index.js +18 -0
  66. package/dist/core/media/mediaItem.d.ts +15 -0
  67. package/dist/core/media/mediaItem.js +34 -0
  68. package/dist/core/media/renderPlan.d.ts +63 -0
  69. package/dist/core/media/renderPlan.js +232 -0
  70. package/dist/core/media/segment.d.ts +79 -0
  71. package/dist/core/media/segment.js +247 -0
  72. package/dist/core/media/size.d.ts +1 -0
  73. package/dist/core/media/size.js +3 -0
  74. package/dist/core/media/trim.d.ts +21 -0
  75. package/dist/core/media/trim.js +40 -0
  76. package/dist/core/media/types.d.ts +148 -0
  77. package/dist/core/media/types.js +11 -0
  78. package/dist/core/media/uploadLimits.d.ts +7 -0
  79. package/dist/core/media/uploadLimits.js +16 -0
  80. package/dist/core/media/validate.d.ts +9 -0
  81. package/dist/core/media/validate.js +121 -0
  82. package/dist/core/media/videoCuts.d.ts +152 -0
  83. package/dist/core/media/videoCuts.js +284 -0
  84. package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
  85. package/dist/core/navigation/breadcrumbCompat.js +21 -0
  86. package/dist/core/navigation/coverageTest.d.ts +24 -0
  87. package/dist/core/navigation/coverageTest.js +48 -0
  88. package/dist/core/navigation/index.d.ts +6 -0
  89. package/dist/core/navigation/index.js +10 -0
  90. package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
  91. package/dist/core/navigation/migrateNavPreferences.js +63 -0
  92. package/dist/core/navigation/navigationModel.d.ts +165 -0
  93. package/dist/core/navigation/navigationModel.js +14 -0
  94. package/dist/core/navigation/navigationSelectors.d.ts +63 -0
  95. package/dist/core/navigation/navigationSelectors.js +167 -0
  96. package/dist/core/navigation/searchCatalogue.d.ts +23 -0
  97. package/dist/core/navigation/searchCatalogue.js +76 -0
  98. package/dist/core/net/reconnectSchedule.d.ts +46 -0
  99. package/dist/core/net/reconnectSchedule.js +38 -0
  100. package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
  101. package/dist/core/palette/ShortcutRegistry.js +52 -0
  102. package/dist/core/palette/fuzzyRank.d.ts +18 -0
  103. package/dist/core/palette/fuzzyRank.js +73 -0
  104. package/dist/core/palette/hotkeys.d.ts +28 -0
  105. package/dist/core/palette/hotkeys.js +118 -0
  106. package/dist/core/palette/index.d.ts +3 -0
  107. package/dist/core/palette/index.js +8 -0
  108. package/dist/core/schema/projection.d.ts +84 -0
  109. package/dist/core/schema/projection.js +184 -0
  110. package/dist/core/session/claims.d.ts +145 -0
  111. package/dist/core/session/claims.js +157 -0
  112. package/dist/core/sharing/index.d.ts +5 -0
  113. package/dist/core/sharing/index.js +4 -0
  114. package/dist/core/sharing/model.d.ts +195 -0
  115. package/dist/core/sharing/model.js +150 -0
  116. package/dist/core/site/siteCopy.d.ts +92 -0
  117. package/dist/core/site/siteCopy.js +91 -0
  118. package/dist/core/slots.d.ts +22 -0
  119. package/dist/core/slots.js +31 -0
  120. package/dist/core/styles/constructableSheet.d.ts +51 -0
  121. package/dist/core/styles/constructableSheet.js +80 -0
  122. package/dist/core/tokens/registry.d.ts +119 -0
  123. package/dist/core/tokens/registry.js +447 -0
  124. package/dist/core/uiScale/index.d.ts +8 -0
  125. package/dist/core/uiScale/index.js +8 -0
  126. package/dist/core/uiScale/scale.d.ts +36 -0
  127. package/dist/core/uiScale/scale.js +47 -0
  128. package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
  129. package/dist/core/uiScale/uiScaleStore.js +63 -0
  130. package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
  131. package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
  132. package/dist/core/wire/index.d.ts +17 -0
  133. package/dist/core/wire/index.js +17 -0
  134. package/dist/core/wire/ingress.d.ts +44 -0
  135. package/dist/core/wire/ingress.js +134 -0
  136. package/dist/core/wire/marshalWorker.d.ts +70 -0
  137. package/dist/core/wire/marshalWorker.js +273 -0
  138. package/dist/core/wire/marshalling.d.ts +122 -0
  139. package/dist/core/wire/marshalling.js +126 -0
  140. package/dist/core/wire/wireHub.d.ts +80 -0
  141. package/dist/core/wire/wireHub.js +116 -0
  142. package/dist/ctgr/base64url.d.ts +8 -0
  143. package/dist/ctgr/base64url.js +39 -0
  144. package/dist/ctgr/chunkAccumulator.d.ts +214 -0
  145. package/dist/ctgr/chunkAccumulator.js +307 -0
  146. package/dist/ctgr/chunkStore.d.ts +24 -0
  147. package/dist/ctgr/chunkStore.js +125 -0
  148. package/dist/ctgr/codec.d.ts +44 -0
  149. package/dist/ctgr/codec.js +153 -0
  150. package/dist/ctgr/compress.d.ts +2 -0
  151. package/dist/ctgr/compress.js +20 -0
  152. package/dist/ctgr/crc32.d.ts +2 -0
  153. package/dist/ctgr/crc32.js +23 -0
  154. package/dist/ctgr/digest.d.ts +1 -0
  155. package/dist/ctgr/digest.js +15 -0
  156. package/dist/ctgr/index.d.ts +9 -0
  157. package/dist/ctgr/index.js +17 -0
  158. package/dist/ctgr/types.d.ts +64 -0
  159. package/dist/ctgr/types.js +21 -0
  160. package/dist/ctgr/v0compat.d.ts +27 -0
  161. package/dist/ctgr/v0compat.js +128 -0
  162. package/dist/index.d.ts +15 -0
  163. package/dist/index.js +17 -0
  164. package/dist/shared/errors.d.ts +101 -0
  165. package/dist/shared/errors.js +99 -0
  166. package/dist/testing/bytes.d.ts +32 -0
  167. package/dist/testing/bytes.js +33 -0
  168. package/dist/testing/domInspect.d.ts +59 -0
  169. package/dist/testing/domInspect.js +115 -0
  170. package/dist/testing/fakeFetch.d.ts +23 -0
  171. package/dist/testing/fakeFetch.js +21 -0
  172. package/dist/testing/index.d.ts +3 -0
  173. package/dist/testing/index.js +11 -0
  174. package/package.json +373 -0
  175. package/src/client/wsWire.ts +83 -0
  176. package/src/config/appConfig.spec.ts +158 -0
  177. package/src/config/appConfig.ts +372 -0
  178. package/src/core/activity/index.ts +21 -0
  179. package/src/core/activity/model.ts +160 -0
  180. package/src/core/analytics/index.ts +18 -0
  181. package/src/core/analytics/range.spec.ts +280 -0
  182. package/src/core/analytics/range.ts +383 -0
  183. package/src/core/analytics/schedule.spec.ts +311 -0
  184. package/src/core/analytics/schedule.ts +624 -0
  185. package/src/core/category/category.spec.ts +144 -0
  186. package/src/core/category/index.ts +19 -0
  187. package/src/core/category/memoryStore.ts +104 -0
  188. package/src/core/category/tree.ts +118 -0
  189. package/src/core/category/types.ts +59 -0
  190. package/src/core/color.spec.ts +35 -0
  191. package/src/core/color.ts +35 -0
  192. package/src/core/colorScheme/colorScheme.spec.ts +127 -0
  193. package/src/core/colorScheme/colorSchemeStore.ts +184 -0
  194. package/src/core/colorScheme/index.ts +23 -0
  195. package/src/core/domainOwners.ts +32 -0
  196. package/src/core/download.spec.ts +38 -0
  197. package/src/core/download.ts +35 -0
  198. package/src/core/events/ccEventBus.spec.ts +90 -0
  199. package/src/core/events/ccEventBus.ts +120 -0
  200. package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
  201. package/src/core/file-tree/fileTreeModel.ts +511 -0
  202. package/src/core/folder-tree/actions.spec.ts +103 -0
  203. package/src/core/folder-tree/actions.ts +153 -0
  204. package/src/core/folder-tree/index.ts +46 -0
  205. package/src/core/folder-tree/path.spec.ts +113 -0
  206. package/src/core/folder-tree/path.ts +148 -0
  207. package/src/core/folder-tree/tree.spec.ts +157 -0
  208. package/src/core/folder-tree/tree.ts +244 -0
  209. package/src/core/layout/index.ts +7 -0
  210. package/src/core/master-lock/index.ts +50 -0
  211. package/src/core/master-lock/kdf.spec.ts +95 -0
  212. package/src/core/master-lock/kdf.ts +145 -0
  213. package/src/core/master-lock/policy.ts +55 -0
  214. package/src/core/master-lock/presence.spec.ts +73 -0
  215. package/src/core/master-lock/presence.ts +71 -0
  216. package/src/core/master-lock/wire.ts +155 -0
  217. package/src/core/media/bgRemoval.spec.ts +120 -0
  218. package/src/core/media/bgRemoval.ts +123 -0
  219. package/src/core/media/ffmpeg.spec.ts +135 -0
  220. package/src/core/media/ffmpeg.ts +162 -0
  221. package/src/core/media/format.spec.ts +41 -0
  222. package/src/core/media/format.ts +58 -0
  223. package/src/core/media/index.ts +105 -0
  224. package/src/core/media/mediaItem.spec.ts +46 -0
  225. package/src/core/media/mediaItem.ts +40 -0
  226. package/src/core/media/renderPlan.spec.ts +140 -0
  227. package/src/core/media/renderPlan.ts +318 -0
  228. package/src/core/media/segment.spec.ts +159 -0
  229. package/src/core/media/segment.ts +352 -0
  230. package/src/core/media/size.spec.ts +19 -0
  231. package/src/core/media/size.ts +3 -0
  232. package/src/core/media/trim.spec.ts +75 -0
  233. package/src/core/media/trim.ts +60 -0
  234. package/src/core/media/types.ts +147 -0
  235. package/src/core/media/uploadLimits.ts +18 -0
  236. package/src/core/media/validate.spec.ts +106 -0
  237. package/src/core/media/validate.ts +131 -0
  238. package/src/core/media/videoCuts.spec.ts +327 -0
  239. package/src/core/media/videoCuts.ts +355 -0
  240. package/src/core/navigation/breadcrumbCompat.ts +44 -0
  241. package/src/core/navigation/coverageTest.ts +54 -0
  242. package/src/core/navigation/index.ts +50 -0
  243. package/src/core/navigation/migrateNavPreferences.ts +119 -0
  244. package/src/core/navigation/navigation.spec.ts +388 -0
  245. package/src/core/navigation/navigationModel.ts +185 -0
  246. package/src/core/navigation/navigationSelectors.ts +206 -0
  247. package/src/core/navigation/searchCatalogue.ts +89 -0
  248. package/src/core/net/reconnectSchedule.spec.ts +49 -0
  249. package/src/core/net/reconnectSchedule.ts +78 -0
  250. package/src/core/palette/ShortcutRegistry.ts +69 -0
  251. package/src/core/palette/fuzzyRank.ts +78 -0
  252. package/src/core/palette/hotkeys.ts +132 -0
  253. package/src/core/palette/index.ts +22 -0
  254. package/src/core/palette/palette.spec.ts +160 -0
  255. package/src/core/schema/projection.spec.ts +122 -0
  256. package/src/core/schema/projection.ts +225 -0
  257. package/src/core/session/claims.ts +181 -0
  258. package/src/core/sharing/index.ts +33 -0
  259. package/src/core/sharing/model.spec.ts +104 -0
  260. package/src/core/sharing/model.ts +296 -0
  261. package/src/core/site/siteCopy.ts +115 -0
  262. package/src/core/slots.ts +38 -0
  263. package/src/core/styles/constructableSheet.ts +86 -0
  264. package/src/core/tokens/registry.spec.ts +160 -0
  265. package/src/core/tokens/registry.ts +536 -0
  266. package/src/core/uiScale/index.ts +32 -0
  267. package/src/core/uiScale/scale.ts +63 -0
  268. package/src/core/uiScale/uiScale.spec.ts +77 -0
  269. package/src/core/uiScale/uiScaleStore.ts +100 -0
  270. package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
  271. package/src/core/wire/index.ts +56 -0
  272. package/src/core/wire/ingress.spec.ts +276 -0
  273. package/src/core/wire/ingress.ts +182 -0
  274. package/src/core/wire/marshalWorker.spec.ts +196 -0
  275. package/src/core/wire/marshalWorker.ts +361 -0
  276. package/src/core/wire/marshalling.spec.ts +139 -0
  277. package/src/core/wire/marshalling.ts +238 -0
  278. package/src/core/wire/wireHub.spec.ts +137 -0
  279. package/src/core/wire/wireHub.ts +213 -0
  280. package/src/ctgr/base64url.ts +44 -0
  281. package/src/ctgr/chunkAccumulator.ts +403 -0
  282. package/src/ctgr/chunkStore.spec.ts +101 -0
  283. package/src/ctgr/chunkStore.ts +168 -0
  284. package/src/ctgr/codec.spec.ts +208 -0
  285. package/src/ctgr/codec.ts +202 -0
  286. package/src/ctgr/compress.ts +21 -0
  287. package/src/ctgr/crc32.ts +23 -0
  288. package/src/ctgr/digest.ts +14 -0
  289. package/src/ctgr/index.ts +39 -0
  290. package/src/ctgr/types.ts +80 -0
  291. package/src/ctgr/v0compat.spec.ts +73 -0
  292. package/src/ctgr/v0compat.ts +156 -0
  293. package/src/declaredDepsAreImported.spec.ts +87 -0
  294. package/src/index.ts +25 -0
  295. package/src/leafSubpathsImportNothing.spec.ts +287 -0
  296. package/src/lockfileIsTracked.spec.ts +50 -0
  297. package/src/namedSubpathsResolve.spec.ts +122 -0
  298. package/src/noPathDeps.spec.ts +24 -0
  299. package/src/publishShape.spec.ts +89 -0
  300. package/src/shared/errors.ts +153 -0
  301. package/src/skippedTestsAreAnswered.spec.ts +221 -0
  302. package/src/testTmpRoot.spec.ts +102 -0
  303. package/src/testing/bytes.ts +38 -0
  304. package/src/testing/domInspect.spec.ts +79 -0
  305. package/src/testing/domInspect.ts +127 -0
  306. package/src/testing/fakeFetch.ts +26 -0
  307. package/src/testing/index.ts +11 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The composable ingress seam (05 §B1, owner decision E16/D8) — the ONE thing
3
+ * between `marshalInbound` and a route that accepts external data.
4
+ *
5
+ * ## Why this exists rather than a webhook endpoint
6
+ *
7
+ * D8 asked which ingress should call `marshalInbound` first: an inbound webhook,
8
+ * a CSV/spreadsheet import, or neither. The owner delegated the choice with a
9
+ * binding condition — *"ensure the architecture is ready to add it later, so we
10
+ * don't have to refactor and can just build on top of it."*
11
+ *
12
+ * The answer is neither, yet, and the condition is the deliverable. Shipping a
13
+ * webhook endpoint that no product asked for would create a new orphan seam of
14
+ * exactly the kind Law #1 exists to prevent, and it would guess wrong about the
15
+ * part that actually differs between a webhook and an import (authentication,
16
+ * routing, what the marshalled value is handed to). What does NOT differ — and
17
+ * what every ingress would otherwise re-derive, differently — is this:
18
+ *
19
+ * 1. reading the body as bytes without buffering a stream into RAM twice;
20
+ * 2. finding the marshaller for the token being fed;
21
+ * 3. owning a worker per `workerEntry` — a live OS thread, so exactly one
22
+ * thing must own disposing of it — and resolving it lazily and cached;
23
+ * 4. handing those failures up as errors that already KNOW their HTTP status,
24
+ * so no route re-derives the mapping. The marshalling tier's codes are
25
+ * unchanged and deliberately distinct: 413 over the declared ceiling, 400
26
+ * for a payload `validate` refuses (the caller's shape is wrong), 502 when
27
+ * the TRANSFORM OUTPUT violates the token's canonical schema (an upstream
28
+ * provider changed shape under us — not our error budget), 503 for a
29
+ * payload that needs offload from a marshaller with no worker.
30
+ *
31
+ * Step 3 is the one that would go wrong. `createMarshalWorkerClient` spawns a
32
+ * thread per call; a route handler that called it per request would leak one
33
+ * OS thread per webhook, and the symptom would be a server that gets slower for
34
+ * a week and then dies. Nothing about that is discoverable from the marshalling
35
+ * API, which is precisely why it belongs here once instead of in each ingress.
36
+ *
37
+ * ## Adding an ingress later
38
+ *
39
+ * It is a route, not a refactor:
40
+ *
41
+ * ```ts
42
+ * const ingress = createMarshalledIngress({ registry });
43
+ * app.post('/api/hooks/:token', session, async (c) => {
44
+ * const value = await ingress.ingestRequest(c.req.param('token'), c.req.raw);
45
+ * await service.acceptExternal(scopeOf(c), value);
46
+ * return c.json({ success: true }, 202);
47
+ * });
48
+ * ```
49
+ *
50
+ * A CSV import is the same three lines with a different marshaller and a
51
+ * different service call. Neither needs to know that a worker exists.
52
+ */
53
+ import { AppError } from '../../shared/errors';
54
+ import { createMarshalWorkerClient, type MarshalWorkerClient } from './marshalWorker';
55
+ import {
56
+ type CCWireMarshaller,
57
+ marshalInbound,
58
+ type MarshallerRegistry,
59
+ type MarshalWorker,
60
+ } from './marshalling';
61
+
62
+ /** No marshaller is registered for the token an ingress was asked to feed. */
63
+ export const INGRESS_TOKEN_UNKNOWN = 'INGRESS_TOKEN_UNKNOWN';
64
+
65
+ export interface MarshalledIngressOptions {
66
+ /** The registry the app's wire tokens were registered on. */
67
+ readonly registry: MarshallerRegistry;
68
+ /**
69
+ * Per-payload worker ceiling. A marshalling worker that hangs would otherwise
70
+ * hold the request open for as long as the client is willing to wait.
71
+ */
72
+ readonly workerTimeoutMs?: number;
73
+ /**
74
+ * Spawn override — the seam a test uses to run the offload path without a real
75
+ * OS thread. Production never sets it.
76
+ */
77
+ readonly spawn?: (entry: string | URL) => unknown;
78
+ }
79
+
80
+ export interface MarshalledIngress {
81
+ /**
82
+ * Marshal an already-decoded payload for `tokenId`.
83
+ *
84
+ * @throws {AppError} `INGRESS_TOKEN_UNKNOWN` (404) when nothing is registered;
85
+ * `MARSHALLER_PAYLOAD_TOO_LARGE` (413/503), `MARSHALLER_VALIDATION_FAILED`
86
+ * (400) or `MARSHALLER_SCHEMA_VIOLATION` (502) from the marshaller itself.
87
+ */
88
+ ingest<Out = unknown>(tokenId: string, payload: unknown): Promise<Out>;
89
+ /**
90
+ * Read a `Request` body and marshal it for `tokenId`. JSON bodies are parsed;
91
+ * anything else is handed to the marshaller as bytes, because a marshaller for
92
+ * a CSV or a binary feed wants the bytes and re-encoding them would be lossy.
93
+ */
94
+ ingestRequest<Out = unknown>(tokenId: string, request: Request): Promise<Out>;
95
+ /**
96
+ * Stop every worker this ingress spawned. Idempotent. A host calls it on
97
+ * shutdown; forgetting to is a thread leak, which is why the ingress owns the
98
+ * threads rather than each route.
99
+ */
100
+ dispose(): Promise<void>;
101
+ }
102
+
103
+ /**
104
+ * Build the ingress. One per app — it caches a worker per `workerEntry`, and two
105
+ * ingresses over the same registry would each spawn their own set.
106
+ */
107
+ export const createMarshalledIngress = (options: MarshalledIngressOptions): MarshalledIngress => {
108
+ /** entry → client. The cache IS the point: see the header, step 3. */
109
+ const workers = new Map<string, MarshalWorkerClient>();
110
+ let disposed = false;
111
+
112
+ const resolveWorker = (marshaller: CCWireMarshaller<never, never>): MarshalWorker | undefined => {
113
+ const entry = marshaller.workerEntry;
114
+ if (entry === undefined) return undefined;
115
+ if (disposed)
116
+ throw new AppError('This ingress has been disposed and cannot spawn a marshalling worker.', {
117
+ code: 'INGRESS_DISPOSED',
118
+ status: 503,
119
+ isOperational: false,
120
+ context: { marshaller: marshaller.id },
121
+ });
122
+ const existing = workers.get(entry);
123
+ if (existing) return existing;
124
+ const client = createMarshalWorkerClient({
125
+ entry,
126
+ ...(options.workerTimeoutMs === undefined ? {} : { timeoutMs: options.workerTimeoutMs }),
127
+ ...(options.spawn === undefined ? {} : { spawn: options.spawn as never }),
128
+ });
129
+ workers.set(entry, client);
130
+ return client;
131
+ };
132
+
133
+ const marshallerFor = (tokenId: string): CCWireMarshaller => {
134
+ const marshaller = options.registry.forToken(tokenId);
135
+ if (!marshaller)
136
+ // 404, not 400: from outside, a token with no marshaller and a token that
137
+ // does not exist are the same fact, and distinguishing them would make
138
+ // this endpoint an enumeration oracle for the app's wire surface.
139
+ throw new AppError(`No marshaller is registered for wire token '${tokenId}'.`, {
140
+ code: INGRESS_TOKEN_UNKNOWN,
141
+ status: 404,
142
+ context: { tokenId },
143
+ });
144
+ return marshaller;
145
+ };
146
+
147
+ return {
148
+ async ingest<Out>(tokenId: string, payload: unknown): Promise<Out> {
149
+ return (await marshalInbound(marshallerFor(tokenId) as CCWireMarshaller<unknown, Out>, payload, {
150
+ resolveWorker,
151
+ })) as Out;
152
+ },
153
+
154
+ async ingestRequest<Out>(tokenId: string, request: Request): Promise<Out> {
155
+ const contentType = request.headers.get('content-type') ?? '';
156
+ // JSON is parsed here so the marshaller sees a value rather than a string
157
+ // it would have to parse itself (and parse DIFFERENTLY per marshaller).
158
+ // A malformed body is the CALLER's error — 400 — and must not read as a
159
+ // validation failure of a payload we never managed to decode.
160
+ const payload = contentType.includes('application/json')
161
+ ? await request.json().catch(() => {
162
+ throw new AppError('Request body is not valid JSON.', {
163
+ code: 'INGRESS_BODY_MALFORMED',
164
+ status: 400,
165
+ context: { tokenId, contentType },
166
+ });
167
+ })
168
+ : new Uint8Array(await request.arrayBuffer());
169
+ return await this.ingest<Out>(tokenId, payload);
170
+ },
171
+
172
+ async dispose(): Promise<void> {
173
+ disposed = true;
174
+ const clients = [...workers.values()];
175
+ workers.clear();
176
+ // `allSettled`: one thread that refuses to die must not prevent the rest
177
+ // from being reaped, and a shutdown path that throws leaves the others
178
+ // running forever.
179
+ await Promise.allSettled(clients.map((client) => client.dispose()));
180
+ },
181
+ };
182
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The marshalling Worker (05 §B1) — proven over a REAL thread.
3
+ *
4
+ * The seam has had an injectable `MarshalWorker` since it was written, and the
5
+ * old suite exercised that seam with a fake. What it could never show is the
6
+ * thing the 503 was actually about: that a payload really does cross a thread
7
+ * boundary and come back correct. So the first block here spawns the genuine
8
+ * `node:worker_threads` worker and drives it end to end; the fake-spawn block
9
+ * below covers the lifecycle edges (timeout, crash, dispose) that a real thread
10
+ * cannot be made to hit deterministically.
11
+ */
12
+ import { describe, expect, it } from 'bun:test';
13
+ import { z } from 'zod';
14
+ import {
15
+ createMarshalWorkerClient,
16
+ MARSHALLER_WORKER_FAILED,
17
+ MARSHALLER_WORKER_TIMEOUT,
18
+ } from './marshalWorker';
19
+ import { type CCWireMarshaller, marshalInbound, requiresWorkerOffload } from './marshalling';
20
+
21
+ const schema = z.object({ rows: z.array(z.object({ id: z.string() })) });
22
+ const WORKER_ENTRY = new URL('./__fixtures__/rowsMarshalWorker.ts', import.meta.url);
23
+
24
+ const marshaller = (over: Partial<CCWireMarshaller<unknown, z.infer<typeof schema>>> = {}) =>
25
+ ({
26
+ id: 'rows',
27
+ tokenId: 'knowledge:records',
28
+ maxPayloadBytes: 10 * 1024 * 1024,
29
+ workerEntry: WORKER_ENTRY.pathname,
30
+ validate: () => true,
31
+ transform: (inbound: unknown) => ({ rows: (inbound as { rows: { id: string }[] }).rows }),
32
+ targetZodSchema: schema,
33
+ ...over,
34
+ }) as CCWireMarshaller<unknown, z.infer<typeof schema>>;
35
+
36
+ describe('a real marshalling worker', () => {
37
+ it('runs validate + transform off-thread and returns the result', async () => {
38
+ const client = createMarshalWorkerClient({ entry: WORKER_ENTRY, timeoutMs: 20_000 });
39
+ try {
40
+ const result = await client.run('rows', { rows: [{ id: 'a' }] });
41
+ // The `worker:` prefix can only have been added by the fixture module,
42
+ // which this process never imported — so the work genuinely happened there.
43
+ expect(result).toEqual({ rows: [{ id: 'worker:a' }] });
44
+ } finally {
45
+ await client.dispose();
46
+ }
47
+ }, 30_000);
48
+
49
+ it('multiplexes concurrent payloads onto one thread, each getting its own reply', async () => {
50
+ const client = createMarshalWorkerClient({ entry: WORKER_ENTRY, timeoutMs: 20_000 });
51
+ try {
52
+ const results = await Promise.all(
53
+ ['a', 'b', 'c', 'd'].map((id) => client.run<{ rows: { id: string }[] }, z.infer<typeof schema>>('rows', { rows: [{ id }] })),
54
+ );
55
+ // Correlation by request id is the whole reason this is not a queue of
56
+ // one: replies may arrive in any order and must not be swapped.
57
+ expect(results.map((r) => r.rows[0]?.id)).toEqual(['worker:a', 'worker:b', 'worker:c', 'worker:d']);
58
+ } finally {
59
+ await client.dispose();
60
+ }
61
+ }, 30_000);
62
+
63
+ it('reports a validate() rejection as the caller’s fault, not the server’s', async () => {
64
+ const client = createMarshalWorkerClient({ entry: WORKER_ENTRY, timeoutMs: 20_000 });
65
+ try {
66
+ await expect(client.run('rows', { notRows: true })).rejects.toMatchObject({
67
+ code: 'MARSHALLER_VALIDATION_FAILED',
68
+ status: 400,
69
+ });
70
+ } finally {
71
+ await client.dispose();
72
+ }
73
+ }, 30_000);
74
+
75
+ it('survives a throwing transform — one bad payload is not an outage', async () => {
76
+ const client = createMarshalWorkerClient({ entry: WORKER_ENTRY, timeoutMs: 20_000 });
77
+ try {
78
+ await expect(client.run('rows', { rows: [{ id: 'boom' }] })).rejects.toMatchObject({
79
+ code: MARSHALLER_WORKER_FAILED,
80
+ });
81
+ // The thread must still be there. A worker that dies on a malformed body
82
+ // turns one hostile webhook into a denial of service.
83
+ const after = await client.run('rows', { rows: [{ id: 'after' }] });
84
+ expect(after).toEqual({ rows: [{ id: 'worker:after' }] });
85
+ } finally {
86
+ await client.dispose();
87
+ }
88
+ }, 30_000);
89
+
90
+ it('is the DEFAULT path: marshalInbound resolves the marshaller’s own workerEntry', async () => {
91
+ const client = createMarshalWorkerClient({ entry: WORKER_ENTRY, timeoutMs: 20_000 });
92
+ try {
93
+ let ranOnMainThread = false;
94
+ const result = await marshalInbound(
95
+ marshaller({
96
+ transform: () => {
97
+ ranOnMainThread = true;
98
+ return { rows: [] };
99
+ },
100
+ }),
101
+ { rows: [{ id: 'x' }] },
102
+ // No `worker:` — the point of the commit is that an ingress route wires
103
+ // nothing per-marshaller; it supplies one resolver and offload happens.
104
+ { offloadThresholdBytes: 4, resolveWorker: () => client },
105
+ );
106
+ expect(result).toEqual({ rows: [{ id: 'worker:x' }] });
107
+ expect(ranOnMainThread).toBe(false);
108
+ } finally {
109
+ await client.dispose();
110
+ }
111
+ }, 30_000);
112
+ });
113
+
114
+ describe('marshal worker lifecycle', () => {
115
+ /** A worker that never answers — the only honest way to test a timeout. */
116
+ const silentSpawn = () => ({
117
+ post: () => {},
118
+ onMessage: () => {},
119
+ onError: () => {},
120
+ terminate: () => {},
121
+ });
122
+
123
+ it('times out a payload rather than holding the request open forever', async () => {
124
+ const client = createMarshalWorkerClient({ entry: 'silent', timeoutMs: 20, spawn: silentSpawn });
125
+ await expect(client.run('rows', { rows: [] })).rejects.toMatchObject({
126
+ code: MARSHALLER_WORKER_TIMEOUT,
127
+ status: 504,
128
+ });
129
+ await client.dispose();
130
+ });
131
+
132
+ it('fails every payload in flight when the thread dies, instead of leaving each to time out', async () => {
133
+ let fail: ((error: unknown) => void) | undefined;
134
+ const client = createMarshalWorkerClient({
135
+ entry: 'crashy',
136
+ timeoutMs: 60_000, // far longer than the test — only the crash can settle these
137
+ spawn: () => ({
138
+ post: () => {},
139
+ onMessage: () => {},
140
+ onError: (handler) => {
141
+ fail = handler;
142
+ },
143
+ terminate: () => {},
144
+ }),
145
+ });
146
+ const inFlight = [client.run('rows', {}), client.run('rows', {})];
147
+ fail?.(new Error('thread died'));
148
+ for (const promise of inFlight)
149
+ await expect(promise).rejects.toMatchObject({ code: MARSHALLER_WORKER_FAILED });
150
+ });
151
+
152
+ it('refuses work after dispose rather than queueing onto a dead thread', async () => {
153
+ let terminated = false;
154
+ const client = createMarshalWorkerClient({
155
+ entry: 'disposable',
156
+ spawn: () => ({ ...silentSpawn(), terminate: () => { terminated = true; } }),
157
+ });
158
+ await client.dispose();
159
+ expect(terminated).toBe(true);
160
+ await expect(client.run('rows', {})).rejects.toMatchObject({ code: MARSHALLER_WORKER_FAILED });
161
+ });
162
+ });
163
+
164
+ describe('requiresWorkerOffload — a derived fact, not a declared one', () => {
165
+ it('is false for a marshaller that can never exceed the main-thread limit', () => {
166
+ // 256KB ceiling: no payload it accepts can reach the 512KB boundary, so
167
+ // demanding a worker of it would be noise.
168
+ expect(requiresWorkerOffload({ maxPayloadBytes: 256 * 1024 })).toBe(false);
169
+ });
170
+
171
+ it('is true for a marshaller whose ceiling is above it', () => {
172
+ expect(requiresWorkerOffload({ maxPayloadBytes: 2 * 1024 * 1024 })).toBe(true);
173
+ });
174
+
175
+ it('names the missing artifact when an offload-needing marshaller has no entry', async () => {
176
+ // The 503 that D1 was about. It is still reachable in code — V3 is what makes
177
+ // it unreachable in a verified app — and when it fires it must say what to
178
+ // build, not merely that something is missing.
179
+ const error = await marshalInbound(
180
+ marshaller({ workerEntry: undefined }),
181
+ { rows: [{ id: 'x' }] },
182
+ { offloadThresholdBytes: 1 },
183
+ // A two-armed `then`, not `.catch`: `.catch` leaves the RESOLVED type in the
184
+ // union, so `error.code` was `undefined` on the path where the call did not
185
+ // reject at all — the exact case this test exists to rule out.
186
+ ).then(
187
+ () => {
188
+ throw new Error('expected MARSHALLER_PAYLOAD_TOO_LARGE, but the call resolved');
189
+ },
190
+ (e: unknown) => e as { code: string; context?: Record<string, unknown> },
191
+ );
192
+
193
+ expect(error.code).toBe('MARSHALLER_PAYLOAD_TOO_LARGE');
194
+ expect(String(error.context?.hint)).toContain('workerEntry');
195
+ });
196
+ });