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.
- package/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- 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 };
|