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,23 @@
1
+ /**
2
+ * A REAL marshalling worker entry, used by `marshalWorker.spec.ts` to prove the
3
+ * round-trip over an actual OS thread rather than over an injected fake.
4
+ *
5
+ * This is also the shape every production entry has, so it doubles as the
6
+ * reference an app author copies: construct the marshaller, hand it to
7
+ * `serveMarshalWorker`, done. The host never imports this file — it names it,
8
+ * and the worker imports it.
9
+ */
10
+ import { z } from 'zod';
11
+ import { serveMarshalWorker } from '../marshalWorker';
12
+ const schema = z.object({ rows: z.array(z.object({ id: z.string() })) });
13
+ await serveMarshalWorker({
14
+ id: 'rows',
15
+ validate: (inbound) => Array.isArray(inbound?.rows),
16
+ transform: (inbound) => {
17
+ // `boom` is the spec's hook for "the transform throws" — a worker must
18
+ // report that failure and stay alive for the next payload.
19
+ if (inbound.rows.some((row) => row.id === 'boom'))
20
+ throw new Error('transform exploded');
21
+ return { rows: inbound.rows.map((row) => ({ id: `worker:${row.id}` })) };
22
+ },
23
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `cursedbelt-core/wire` — the wire tier's public surface.
3
+ *
4
+ * This entry exists because of Law #1, for the same reason `client/index.ts`
5
+ * gives that reason for `createWireHub`: an ingress route has to be able to
6
+ * IMPORT the marshalling seam, and a worker entry has to be able to import
7
+ * `serveMarshalWorker`. Until the Worker commit (05 §B1) neither symbol had any
8
+ * export path at all — `marshalInbound` was reachable only from inside this
9
+ * package, which is most of why it went three arcs with zero callers.
10
+ *
11
+ * `wireHub` is re-exported from `cursedbelt-cc/client` as well; that entry is where
12
+ * a shell host looks, this one is where the wire tier lives.
13
+ */
14
+ export { type CCWireMarshaller, createMarshallerRegistry, MARSHALLER_PAYLOAD_TOO_LARGE, MARSHALLER_SCHEMA_VIOLATION, MARSHALLER_VALIDATION_FAILED, MARSHALLER_WORKER_OFFLOAD_BYTES, type MarshallerRegistry, type MarshalOptions, type MarshalWorker, marshalInbound, payloadBytes, requiresWorkerOffload, } from './marshalling';
15
+ export { createMarshalWorkerClient, MARSHAL_WORKER_DEFAULT_TIMEOUT_MS, MARSHALLER_WORKER_FAILED, MARSHALLER_WORKER_TIMEOUT, type MarshalWorkerClient, type MarshalWorkerClientOptions, type MarshalWorkerRequest, type MarshalWorkerResponse, serveMarshalWorker, } from './marshalWorker';
16
+ export { createMarshalledIngress, INGRESS_TOKEN_UNKNOWN, type MarshalledIngress, type MarshalledIngressOptions, } from './ingress';
17
+ export { createInstanceRuntime, createWireHub, type InstanceRuntime, type InstanceRuntimeOptions, type WireEnvelope, type WireHub, type WireHubOptions, } from './wireHub';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `cursedbelt-core/wire` — the wire tier's public surface.
3
+ *
4
+ * This entry exists because of Law #1, for the same reason `client/index.ts`
5
+ * gives that reason for `createWireHub`: an ingress route has to be able to
6
+ * IMPORT the marshalling seam, and a worker entry has to be able to import
7
+ * `serveMarshalWorker`. Until the Worker commit (05 §B1) neither symbol had any
8
+ * export path at all — `marshalInbound` was reachable only from inside this
9
+ * package, which is most of why it went three arcs with zero callers.
10
+ *
11
+ * `wireHub` is re-exported from `cursedbelt-cc/client` as well; that entry is where
12
+ * a shell host looks, this one is where the wire tier lives.
13
+ */
14
+ export { createMarshallerRegistry, MARSHALLER_PAYLOAD_TOO_LARGE, MARSHALLER_SCHEMA_VIOLATION, MARSHALLER_VALIDATION_FAILED, MARSHALLER_WORKER_OFFLOAD_BYTES, marshalInbound, payloadBytes, requiresWorkerOffload, } from './marshalling';
15
+ export { createMarshalWorkerClient, MARSHAL_WORKER_DEFAULT_TIMEOUT_MS, MARSHALLER_WORKER_FAILED, MARSHALLER_WORKER_TIMEOUT, serveMarshalWorker, } from './marshalWorker';
16
+ export { createMarshalledIngress, INGRESS_TOKEN_UNKNOWN, } from './ingress';
17
+ export { createInstanceRuntime, createWireHub, } from './wireHub';
@@ -0,0 +1,44 @@
1
+ import { type MarshallerRegistry } from './marshalling';
2
+ /** No marshaller is registered for the token an ingress was asked to feed. */
3
+ export declare const INGRESS_TOKEN_UNKNOWN = "INGRESS_TOKEN_UNKNOWN";
4
+ export interface MarshalledIngressOptions {
5
+ /** The registry the app's wire tokens were registered on. */
6
+ readonly registry: MarshallerRegistry;
7
+ /**
8
+ * Per-payload worker ceiling. A marshalling worker that hangs would otherwise
9
+ * hold the request open for as long as the client is willing to wait.
10
+ */
11
+ readonly workerTimeoutMs?: number;
12
+ /**
13
+ * Spawn override — the seam a test uses to run the offload path without a real
14
+ * OS thread. Production never sets it.
15
+ */
16
+ readonly spawn?: (entry: string | URL) => unknown;
17
+ }
18
+ export interface MarshalledIngress {
19
+ /**
20
+ * Marshal an already-decoded payload for `tokenId`.
21
+ *
22
+ * @throws {AppError} `INGRESS_TOKEN_UNKNOWN` (404) when nothing is registered;
23
+ * `MARSHALLER_PAYLOAD_TOO_LARGE` (413/503), `MARSHALLER_VALIDATION_FAILED`
24
+ * (400) or `MARSHALLER_SCHEMA_VIOLATION` (502) from the marshaller itself.
25
+ */
26
+ ingest<Out = unknown>(tokenId: string, payload: unknown): Promise<Out>;
27
+ /**
28
+ * Read a `Request` body and marshal it for `tokenId`. JSON bodies are parsed;
29
+ * anything else is handed to the marshaller as bytes, because a marshaller for
30
+ * a CSV or a binary feed wants the bytes and re-encoding them would be lossy.
31
+ */
32
+ ingestRequest<Out = unknown>(tokenId: string, request: Request): Promise<Out>;
33
+ /**
34
+ * Stop every worker this ingress spawned. Idempotent. A host calls it on
35
+ * shutdown; forgetting to is a thread leak, which is why the ingress owns the
36
+ * threads rather than each route.
37
+ */
38
+ dispose(): Promise<void>;
39
+ }
40
+ /**
41
+ * Build the ingress. One per app — it caches a worker per `workerEntry`, and two
42
+ * ingresses over the same registry would each spawn their own set.
43
+ */
44
+ export declare const createMarshalledIngress: (options: MarshalledIngressOptions) => MarshalledIngress;
@@ -0,0 +1,134 @@
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 } from './marshalWorker';
55
+ import { marshalInbound, } from './marshalling';
56
+ /** No marshaller is registered for the token an ingress was asked to feed. */
57
+ export const INGRESS_TOKEN_UNKNOWN = 'INGRESS_TOKEN_UNKNOWN';
58
+ /**
59
+ * Build the ingress. One per app — it caches a worker per `workerEntry`, and two
60
+ * ingresses over the same registry would each spawn their own set.
61
+ */
62
+ export const createMarshalledIngress = (options) => {
63
+ /** entry → client. The cache IS the point: see the header, step 3. */
64
+ const workers = new Map();
65
+ let disposed = false;
66
+ const resolveWorker = (marshaller) => {
67
+ const entry = marshaller.workerEntry;
68
+ if (entry === undefined)
69
+ return undefined;
70
+ if (disposed)
71
+ throw new AppError('This ingress has been disposed and cannot spawn a marshalling worker.', {
72
+ code: 'INGRESS_DISPOSED',
73
+ status: 503,
74
+ isOperational: false,
75
+ context: { marshaller: marshaller.id },
76
+ });
77
+ const existing = workers.get(entry);
78
+ if (existing)
79
+ return existing;
80
+ const client = createMarshalWorkerClient({
81
+ entry,
82
+ ...(options.workerTimeoutMs === undefined ? {} : { timeoutMs: options.workerTimeoutMs }),
83
+ ...(options.spawn === undefined ? {} : { spawn: options.spawn }),
84
+ });
85
+ workers.set(entry, client);
86
+ return client;
87
+ };
88
+ const marshallerFor = (tokenId) => {
89
+ const marshaller = options.registry.forToken(tokenId);
90
+ if (!marshaller)
91
+ // 404, not 400: from outside, a token with no marshaller and a token that
92
+ // does not exist are the same fact, and distinguishing them would make
93
+ // this endpoint an enumeration oracle for the app's wire surface.
94
+ throw new AppError(`No marshaller is registered for wire token '${tokenId}'.`, {
95
+ code: INGRESS_TOKEN_UNKNOWN,
96
+ status: 404,
97
+ context: { tokenId },
98
+ });
99
+ return marshaller;
100
+ };
101
+ return {
102
+ async ingest(tokenId, payload) {
103
+ return (await marshalInbound(marshallerFor(tokenId), payload, {
104
+ resolveWorker,
105
+ }));
106
+ },
107
+ async ingestRequest(tokenId, request) {
108
+ const contentType = request.headers.get('content-type') ?? '';
109
+ // JSON is parsed here so the marshaller sees a value rather than a string
110
+ // it would have to parse itself (and parse DIFFERENTLY per marshaller).
111
+ // A malformed body is the CALLER's error — 400 — and must not read as a
112
+ // validation failure of a payload we never managed to decode.
113
+ const payload = contentType.includes('application/json')
114
+ ? await request.json().catch(() => {
115
+ throw new AppError('Request body is not valid JSON.', {
116
+ code: 'INGRESS_BODY_MALFORMED',
117
+ status: 400,
118
+ context: { tokenId, contentType },
119
+ });
120
+ })
121
+ : new Uint8Array(await request.arrayBuffer());
122
+ return await this.ingest(tokenId, payload);
123
+ },
124
+ async dispose() {
125
+ disposed = true;
126
+ const clients = [...workers.values()];
127
+ workers.clear();
128
+ // `allSettled`: one thread that refuses to die must not prevent the rest
129
+ // from being reaped, and a shutdown path that throws leaves the others
130
+ // running forever.
131
+ await Promise.allSettled(clients.map((client) => client.dispose()));
132
+ },
133
+ };
134
+ };
@@ -0,0 +1,70 @@
1
+ import { type CCWireMarshaller, MARSHALLER_SCHEMA_VIOLATION, MARSHALLER_VALIDATION_FAILED, type MarshalWorker } from './marshalling';
2
+ export declare const MARSHALLER_WORKER_FAILED = "MARSHALLER_WORKER_FAILED";
3
+ export declare const MARSHALLER_WORKER_TIMEOUT = "MARSHALLER_WORKER_TIMEOUT";
4
+ /** How long one payload may occupy the worker before the host gives up. */
5
+ export declare const MARSHAL_WORKER_DEFAULT_TIMEOUT_MS = 30000;
6
+ export interface MarshalWorkerRequest {
7
+ /** Correlates the reply — several payloads are in flight at once. */
8
+ id: number;
9
+ payload: unknown;
10
+ }
11
+ export type MarshalWorkerResponse = {
12
+ id: number;
13
+ ok: true;
14
+ value: unknown;
15
+ } | {
16
+ id: number;
17
+ ok: false;
18
+ code: string;
19
+ message: string;
20
+ };
21
+ /**
22
+ * Run `marshaller` inside a worker, answering one payload per message.
23
+ *
24
+ * A worker entry is three lines:
25
+ *
26
+ * ```ts
27
+ * import { serveMarshalWorker } from 'cursedbelt-core/wire';
28
+ * import { sheetRowsMarshaller } from './sheetRows';
29
+ * await serveMarshalWorker(sheetRowsMarshaller);
30
+ * ```
31
+ *
32
+ * **The schema check is deliberately NOT run here.** `marshalInbound` re-checks
33
+ * every worker result against the token's canonical schema on the host side,
34
+ * because a worker is a thread boundary, not a trust boundary — a result that
35
+ * validated itself is exactly the kind of assurance that stops being true the
36
+ * day someone edits the worker entry. Doing it in both places would also mean
37
+ * two implementations of one law, and the worker's copy is the one no test
38
+ * would cover.
39
+ */
40
+ export declare function serveMarshalWorker<Inbound, Out>(marshaller: Pick<CCWireMarshaller<Inbound, Out>, 'id' | 'validate' | 'transform'>): Promise<void>;
41
+ /** The subset of both runtimes' Worker the client below needs. */
42
+ interface HostWorker {
43
+ post(data: unknown): void;
44
+ onMessage(handler: (data: unknown) => void): void;
45
+ onError(handler: (error: unknown) => void): void;
46
+ terminate(): void;
47
+ }
48
+ export interface MarshalWorkerClientOptions {
49
+ /** Module specifier / URL of the entry that calls {@link serveMarshalWorker}. */
50
+ entry: string | URL;
51
+ /** Per-payload ceiling. Defaults to {@link MARSHAL_WORKER_DEFAULT_TIMEOUT_MS}. */
52
+ timeoutMs?: number;
53
+ /** Spawn override — the seam tests use to run without a real thread. */
54
+ spawn?: (entry: string | URL) => HostWorker;
55
+ }
56
+ export interface MarshalWorkerClient extends MarshalWorker {
57
+ /** Stop the thread. Pending payloads reject; further calls are refused. */
58
+ dispose(): Promise<void>;
59
+ }
60
+ /**
61
+ * A long-lived worker for ONE marshaller, multiplexing concurrent payloads.
62
+ *
63
+ * Long-lived rather than one-worker-per-payload deliberately: thread startup
64
+ * plus module load is tens of milliseconds, which on a burst of imports would
65
+ * cost more than the parse it is avoiding, and a spawn-per-payload design lets
66
+ * an import storm fork unbounded threads.
67
+ */
68
+ export declare function createMarshalWorkerClient(options: MarshalWorkerClientOptions): MarshalWorkerClient;
69
+ /** Re-exported so a caller catching worker failures needs one import. */
70
+ export { MARSHALLER_SCHEMA_VIOLATION, MARSHALLER_VALIDATION_FAILED };
@@ -0,0 +1,273 @@
1
+ /**
2
+ * The marshalling Worker (05 §B1, second half) — `validate` + `transform` for a
3
+ * large inbound payload, off the thread that has to answer requests.
4
+ *
5
+ * `marshalling.ts` has always had the SIZE BOUNDARY and the injection seam; what
6
+ * it never had was anything to hand a large payload to, so a >512KB webhook body
7
+ * got a 503 with no documentation anywhere that a limit existed. This module is
8
+ * the missing half: a real, long-lived worker with a request/response protocol,
9
+ * on both runtimes cursedbelt ships to.
10
+ *
11
+ * ## Why the marshaller names its own worker entry
12
+ *
13
+ * A marshaller is mostly FUNCTIONS (`validate`, `transform`, a Zod schema), and
14
+ * functions do not survive structured clone — a worker cannot be handed one, it
15
+ * has to `import` it. The obvious design (one shared worker that resolves any
16
+ * marshaller by id from a registry module) needs the host to declare, register
17
+ * and keep in sync a second module listing every marshaller, and it gives that
18
+ * one worker the authority to run all of them.
19
+ *
20
+ * Instead each marshaller declares `workerEntry`: the module that constructs
21
+ * exactly that marshaller and calls {@link serveMarshalWorker}. The worker can
22
+ * therefore only ever run the one marshaller it was built for, the host wires
23
+ * nothing, and — the part that matters for the gate — "can this marshaller
24
+ * offload?" stops being a boolean somebody remembered to set and becomes a
25
+ * question about an artifact that either exists or does not. V3 checks it
26
+ * statically (`marshaller-worker-offload-missing`), so the 503 path below is
27
+ * unreachable in any app that passed verification.
28
+ *
29
+ * ## The two runtimes
30
+ *
31
+ * Server (Bun/Node) uses `node:worker_threads`; the browser uses a module
32
+ * `Worker`. The import of `node:worker_threads` is deliberately DYNAMIC and
33
+ * inside the server branch — a static import would put a Node builtin in the
34
+ * module graph of every client bundle that touches wire code.
35
+ */
36
+ import { AppError } from '../../shared/errors';
37
+ import { MARSHALLER_SCHEMA_VIOLATION, MARSHALLER_VALIDATION_FAILED, } from './marshalling';
38
+ export const MARSHALLER_WORKER_FAILED = 'MARSHALLER_WORKER_FAILED';
39
+ export const MARSHALLER_WORKER_TIMEOUT = 'MARSHALLER_WORKER_TIMEOUT';
40
+ /** How long one payload may occupy the worker before the host gives up. */
41
+ export const MARSHAL_WORKER_DEFAULT_TIMEOUT_MS = 30_000;
42
+ async function resolveWorkerChannel() {
43
+ // Browser/Web Worker: `self` has postMessage and no `window`.
44
+ const maybeSelf = globalThis;
45
+ if (maybeSelf.window === undefined &&
46
+ typeof maybeSelf.postMessage === 'function' &&
47
+ typeof maybeSelf.addEventListener === 'function') {
48
+ const post = maybeSelf.postMessage.bind(globalThis);
49
+ const listen = maybeSelf.addEventListener.bind(globalThis);
50
+ return {
51
+ onMessage: (handler) => listen('message', (event) => handler(event.data)),
52
+ post,
53
+ };
54
+ }
55
+ const { parentPort } = await import('node:worker_threads');
56
+ if (!parentPort)
57
+ throw new AppError('serveMarshalWorker was called outside a worker thread.', {
58
+ code: MARSHALLER_WORKER_FAILED,
59
+ status: 500,
60
+ });
61
+ return {
62
+ onMessage: (handler) => parentPort.on('message', handler),
63
+ post: (data) => parentPort.postMessage(data),
64
+ };
65
+ }
66
+ /**
67
+ * Run `marshaller` inside a worker, answering one payload per message.
68
+ *
69
+ * A worker entry is three lines:
70
+ *
71
+ * ```ts
72
+ * import { serveMarshalWorker } from 'cursedbelt-core/wire';
73
+ * import { sheetRowsMarshaller } from './sheetRows';
74
+ * await serveMarshalWorker(sheetRowsMarshaller);
75
+ * ```
76
+ *
77
+ * **The schema check is deliberately NOT run here.** `marshalInbound` re-checks
78
+ * every worker result against the token's canonical schema on the host side,
79
+ * because a worker is a thread boundary, not a trust boundary — a result that
80
+ * validated itself is exactly the kind of assurance that stops being true the
81
+ * day someone edits the worker entry. Doing it in both places would also mean
82
+ * two implementations of one law, and the worker's copy is the one no test
83
+ * would cover.
84
+ */
85
+ export async function serveMarshalWorker(marshaller) {
86
+ const channel = await resolveWorkerChannel();
87
+ channel.onMessage((data) => {
88
+ const request = data;
89
+ if (typeof request?.id !== 'number')
90
+ return; // not ours; ignore rather than crash the worker
91
+ try {
92
+ if (!marshaller.validate(request.payload)) {
93
+ channel.post({
94
+ id: request.id,
95
+ ok: false,
96
+ code: MARSHALLER_VALIDATION_FAILED,
97
+ message: `Inbound payload rejected by marshaller '${marshaller.id}'.`,
98
+ });
99
+ return;
100
+ }
101
+ const value = marshaller.transform(request.payload);
102
+ channel.post({ id: request.id, ok: true, value });
103
+ }
104
+ catch (error) {
105
+ // A throwing transform must not take the worker down with it: the next
106
+ // payload is a different payload, and a dead worker turns one bad webhook
107
+ // into an outage.
108
+ channel.post({
109
+ id: request.id,
110
+ ok: false,
111
+ code: MARSHALLER_WORKER_FAILED,
112
+ message: error instanceof Error ? error.message : String(error),
113
+ });
114
+ }
115
+ });
116
+ }
117
+ function spawnHostWorker(entry) {
118
+ const WebWorker = globalThis
119
+ .Worker;
120
+ // A browser (or Bun's web-Worker shim) — `type: 'module'` so the entry may use
121
+ // static imports, which every entry will.
122
+ if (typeof WebWorker === 'function') {
123
+ const worker = new WebWorker(entry, { type: 'module' });
124
+ return {
125
+ post: (data) => worker.postMessage(data),
126
+ onMessage: (handler) => worker.addEventListener('message', (event) => handler(event.data)),
127
+ onError: (handler) => worker.addEventListener('error', (event) => handler(event.message ?? event)),
128
+ terminate: () => worker.terminate(),
129
+ };
130
+ }
131
+ // Node/Bun worker_threads. Constructed lazily and synchronously-wrapped: the
132
+ // client below queues sends until the thread exists, so callers never see the
133
+ // construction race.
134
+ let pending = [];
135
+ let realPost = null;
136
+ let terminated = false;
137
+ const messageHandlers = [];
138
+ const errorHandlers = [];
139
+ let terminateReal = null;
140
+ void (async () => {
141
+ try {
142
+ const { Worker } = await import('node:worker_threads');
143
+ if (terminated)
144
+ return;
145
+ const worker = new Worker(entry);
146
+ worker.on('message', (data) => {
147
+ for (const handler of messageHandlers)
148
+ handler(data);
149
+ });
150
+ worker.on('error', (error) => {
151
+ for (const handler of errorHandlers)
152
+ handler(error);
153
+ });
154
+ // ANY exit is a failure for this client: the worker is long-lived by
155
+ // design, so it should outlive every payload. Gating this on `code !== 0`
156
+ // left a cleanly-exited thread invisible — payloads in flight waited out
157
+ // their full timeout instead of failing at once, and every later `run()`
158
+ // posted into a dead thread and did the same.
159
+ worker.on('exit', (code) => {
160
+ const how = code === 0 ? 'exited cleanly while still in use' : `exited with code ${code}`;
161
+ for (const handler of errorHandlers)
162
+ handler(new Error(`marshal worker ${how}`));
163
+ });
164
+ // `unref` would let the process exit with payloads in flight; the client's
165
+ // own timeout is the liveness backstop instead.
166
+ realPost = (data) => worker.postMessage(data);
167
+ terminateReal = () => void worker.terminate();
168
+ for (const queued of pending)
169
+ realPost(queued);
170
+ pending = [];
171
+ }
172
+ catch (error) {
173
+ for (const handler of errorHandlers)
174
+ handler(error);
175
+ }
176
+ })();
177
+ return {
178
+ post: (data) => (realPost ? realPost(data) : pending.push(data)),
179
+ onMessage: (handler) => messageHandlers.push(handler),
180
+ onError: (handler) => errorHandlers.push(handler),
181
+ terminate: () => {
182
+ terminated = true;
183
+ terminateReal?.();
184
+ },
185
+ };
186
+ }
187
+ /**
188
+ * A long-lived worker for ONE marshaller, multiplexing concurrent payloads.
189
+ *
190
+ * Long-lived rather than one-worker-per-payload deliberately: thread startup
191
+ * plus module load is tens of milliseconds, which on a burst of imports would
192
+ * cost more than the parse it is avoiding, and a spawn-per-payload design lets
193
+ * an import storm fork unbounded threads.
194
+ */
195
+ export function createMarshalWorkerClient(options) {
196
+ const timeoutMs = options.timeoutMs ?? MARSHAL_WORKER_DEFAULT_TIMEOUT_MS;
197
+ const worker = (options.spawn ?? spawnHostWorker)(options.entry);
198
+ const pending = new Map();
199
+ let nextId = 1;
200
+ let disposed = false;
201
+ const settle = (id, apply) => {
202
+ const entry = pending.get(id);
203
+ if (!entry)
204
+ return; // already timed out — the late reply is discarded
205
+ pending.delete(id);
206
+ clearTimeout(entry.timer);
207
+ apply(entry);
208
+ };
209
+ worker.onMessage((data) => {
210
+ const response = data;
211
+ if (typeof response?.id !== 'number')
212
+ return;
213
+ settle(response.id, (p) => {
214
+ if (response.ok) {
215
+ p.resolve(response.value);
216
+ return;
217
+ }
218
+ p.reject(new AppError(response.message, {
219
+ // A validation failure is the CALLER's fault (400) and must keep its
220
+ // own code; anything else the worker reports is ours (500).
221
+ code: response.code,
222
+ status: response.code === MARSHALLER_VALIDATION_FAILED ? 400 : 500,
223
+ context: { worker: String(options.entry) },
224
+ }));
225
+ });
226
+ });
227
+ // A worker that dies takes every payload in flight with it — failing them all
228
+ // now beats each one waiting out its own timeout.
229
+ worker.onError((error) => {
230
+ const message = error instanceof Error ? error.message : String(error);
231
+ for (const id of [...pending.keys()])
232
+ settle(id, (p) => p.reject(new AppError(`Marshalling worker failed: ${message}`, {
233
+ code: MARSHALLER_WORKER_FAILED,
234
+ status: 500,
235
+ context: { worker: String(options.entry) },
236
+ })));
237
+ });
238
+ return {
239
+ run(_marshallerId, payload) {
240
+ if (disposed)
241
+ return Promise.reject(new AppError('Marshalling worker has been disposed.', {
242
+ code: MARSHALLER_WORKER_FAILED,
243
+ status: 500,
244
+ context: { worker: String(options.entry) },
245
+ }));
246
+ const id = nextId++;
247
+ return new Promise((resolve, reject) => {
248
+ const timer = setTimeout(() => {
249
+ pending.delete(id);
250
+ reject(new AppError(`Marshalling worker did not answer within ${timeoutMs}ms.`, {
251
+ code: MARSHALLER_WORKER_TIMEOUT,
252
+ status: 504,
253
+ context: { worker: String(options.entry), timeoutMs },
254
+ }));
255
+ }, timeoutMs);
256
+ pending.set(id, { resolve: resolve, reject, timer });
257
+ worker.post({ id, payload });
258
+ });
259
+ },
260
+ async dispose() {
261
+ disposed = true;
262
+ for (const id of [...pending.keys()])
263
+ settle(id, (p) => p.reject(new AppError('Marshalling worker was disposed while a payload was in flight.', {
264
+ code: MARSHALLER_WORKER_FAILED,
265
+ status: 503,
266
+ context: { worker: String(options.entry) },
267
+ })));
268
+ worker.terminate();
269
+ },
270
+ };
271
+ }
272
+ /** Re-exported so a caller catching worker failures needs one import. */
273
+ export { MARSHALLER_SCHEMA_VIOLATION, MARSHALLER_VALIDATION_FAILED };