cursedbelt-core 2.13.1 → 2.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/analytics/range.d.ts +2 -2
- package/dist/core/analytics/range.js +2 -2
- package/dist/core/analytics/schedule.d.ts +1 -1
- package/dist/core/analytics/schedule.js +3 -3
- package/dist/core/duplicates/index.d.ts +2 -2
- package/dist/core/duplicates/index.js +1 -1
- package/dist/core/file-tree/fileTreeModel.d.ts +3 -3
- package/dist/core/file-tree/fileTreeModel.js +2 -2
- package/dist/core/folder-tree/actions.d.ts +1 -1
- package/dist/core/folder-tree/actions.js +1 -1
- package/dist/core/master-lock/kdf.js +1 -1
- package/dist/core/master-lock/presence.d.ts +1 -1
- package/dist/core/master-lock/presence.js +1 -1
- package/dist/core/master-lock/wire.d.ts +1 -1
- package/dist/core/media/mediaItem.js +1 -1
- package/dist/core/media/types.d.ts +4 -4
- package/dist/core/media/videoCuts.d.ts +1 -1
- package/dist/core/media/videoCuts.js +2 -2
- package/dist/core/navigation/index.js +1 -1
- package/dist/core/navigation/navigationModel.d.ts +1 -1
- package/dist/core/navigation/navigationModel.js +1 -1
- package/dist/core/navigation/navigationSelectors.d.ts +2 -2
- package/dist/core/navigation/navigationSelectors.js +2 -2
- package/dist/core/navigation/searchCatalogue.d.ts +2 -2
- package/dist/core/navigation/searchCatalogue.js +4 -4
- package/dist/core/palette/fuzzyRank.d.ts +1 -1
- package/dist/core/palette/fuzzyRank.js +3 -3
- package/dist/core/tokens/registry.d.ts +1 -1
- package/dist/core/tokens/registry.js +1 -1
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +2 -2
- package/dist/core/wire/index.d.ts +1 -1
- package/dist/core/wire/index.js +1 -1
- package/dist/core/wire/ingress.d.ts +4 -4
- package/dist/core/wire/ingress.js +12 -12
- package/dist/core/wire/marshalWorker.d.ts +1 -1
- package/dist/core/wire/marshalWorker.js +15 -15
- package/dist/core/wire/marshalling.d.ts +16 -16
- package/dist/core/wire/marshalling.js +13 -13
- package/dist/ctgrClient/convertRequest.js +21 -5
- package/dist/testing/bytes.d.ts +1 -1
- package/dist/testing/bytes.js +1 -1
- package/package.json +1 -1
- package/src/core/analytics/range.spec.ts +1 -1
- package/src/core/analytics/range.ts +2 -2
- package/src/core/analytics/schedule.spec.ts +3 -3
- package/src/core/analytics/schedule.ts +3 -3
- package/src/core/duplicates/index.ts +2 -2
- package/src/core/file-tree/fileTreeModel.ts +4 -4
- package/src/core/folder-tree/actions.ts +1 -1
- package/src/core/master-lock/kdf.ts +1 -1
- package/src/core/master-lock/presence.ts +1 -1
- package/src/core/master-lock/wire.ts +1 -1
- package/src/core/media/mediaItem.ts +1 -1
- package/src/core/media/types.ts +4 -4
- package/src/core/media/videoCuts.ts +2 -2
- package/src/core/navigation/index.ts +1 -1
- package/src/core/navigation/navigation.spec.ts +3 -3
- package/src/core/navigation/navigationModel.ts +2 -2
- package/src/core/navigation/navigationSelectors.ts +3 -3
- package/src/core/navigation/searchCatalogue.ts +4 -4
- package/src/core/palette/fuzzyRank.ts +3 -3
- package/src/core/tokens/registry.ts +2 -2
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +2 -2
- package/src/core/wire/index.ts +1 -1
- package/src/core/wire/ingress.spec.ts +12 -12
- package/src/core/wire/ingress.ts +15 -15
- package/src/core/wire/marshalWorker.spec.ts +7 -7
- package/src/core/wire/marshalWorker.ts +15 -15
- package/src/core/wire/marshalling.spec.ts +3 -3
- package/src/core/wire/marshalling.ts +25 -25
- package/src/ctgrClient/convertRequest.ts +21 -4
- package/src/ctgrClient/ctgrClient.spec.ts +32 -0
- package/src/namedSubpathsResolve.spec.ts +1 -1
- package/src/publishShape.spec.ts +1 -1
- package/src/shippedFilesAreTracked.spec.ts +2 -2
- package/src/testing/bytes.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The marshaling Worker (05 §B1, second half) — `validate` + `transform` for a
|
|
3
3
|
* large inbound payload, off the thread that has to answer requests.
|
|
4
4
|
*
|
|
5
5
|
* `marshalling.ts` has always had the SIZE BOUNDARY and the injection seam; what
|
|
@@ -8,19 +8,19 @@
|
|
|
8
8
|
* the missing half: a real, long-lived worker with a request/response protocol,
|
|
9
9
|
* on both runtimes cursedbelt ships to.
|
|
10
10
|
*
|
|
11
|
-
* ## Why the
|
|
11
|
+
* ## Why the marshaler names its own worker entry
|
|
12
12
|
*
|
|
13
|
-
* A
|
|
13
|
+
* A marshaler is mostly FUNCTIONS (`validate`, `transform`, a Zod schema), and
|
|
14
14
|
* functions do not survive structured clone — a worker cannot be handed one, it
|
|
15
15
|
* has to `import` it. The obvious design (one shared worker that resolves any
|
|
16
|
-
*
|
|
17
|
-
* and keep in sync a second module listing every
|
|
16
|
+
* marshaler by id from a registry module) needs the host to declare, register
|
|
17
|
+
* and keep in sync a second module listing every marshaler, and it gives that
|
|
18
18
|
* one worker the authority to run all of them.
|
|
19
19
|
*
|
|
20
|
-
* Instead each
|
|
21
|
-
* exactly that
|
|
22
|
-
* therefore only ever run the one
|
|
23
|
-
* nothing, and — the part that matters for the gate — "can this
|
|
20
|
+
* Instead each marshaler declares `workerEntry`: the module that constructs
|
|
21
|
+
* exactly that marshaler and calls {@link serveMarshalWorker}. The worker can
|
|
22
|
+
* therefore only ever run the one marshaler it was built for, the host wires
|
|
23
|
+
* nothing, and — the part that matters for the gate — "can this marshaler
|
|
24
24
|
* offload?" stops being a boolean somebody remembered to set and becomes a
|
|
25
25
|
* question about an artifact that either exists or does not. V3 checks it
|
|
26
26
|
* statically (`marshaller-worker-offload-missing`), so the 503 path below is
|
|
@@ -94,7 +94,7 @@ export async function serveMarshalWorker(marshaller) {
|
|
|
94
94
|
id: request.id,
|
|
95
95
|
ok: false,
|
|
96
96
|
code: MARSHALLER_VALIDATION_FAILED,
|
|
97
|
-
message: `Inbound payload rejected by
|
|
97
|
+
message: `Inbound payload rejected by marshaler '${marshaller.id}'.`,
|
|
98
98
|
});
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
@@ -185,7 +185,7 @@ function spawnHostWorker(entry) {
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
|
-
* A long-lived worker for ONE
|
|
188
|
+
* A long-lived worker for ONE marshaler, multiplexing concurrent payloads.
|
|
189
189
|
*
|
|
190
190
|
* Long-lived rather than one-worker-per-payload deliberately: thread startup
|
|
191
191
|
* plus module load is tens of milliseconds, which on a burst of imports would
|
|
@@ -229,7 +229,7 @@ export function createMarshalWorkerClient(options) {
|
|
|
229
229
|
worker.onError((error) => {
|
|
230
230
|
const message = error instanceof Error ? error.message : String(error);
|
|
231
231
|
for (const id of [...pending.keys()])
|
|
232
|
-
settle(id, (p) => p.reject(new AppError(`
|
|
232
|
+
settle(id, (p) => p.reject(new AppError(`Marshaling worker failed: ${message}`, {
|
|
233
233
|
code: MARSHALLER_WORKER_FAILED,
|
|
234
234
|
status: 500,
|
|
235
235
|
context: { worker: String(options.entry) },
|
|
@@ -238,7 +238,7 @@ export function createMarshalWorkerClient(options) {
|
|
|
238
238
|
return {
|
|
239
239
|
run(_marshallerId, payload) {
|
|
240
240
|
if (disposed)
|
|
241
|
-
return Promise.reject(new AppError('
|
|
241
|
+
return Promise.reject(new AppError('Marshaling worker has been disposed.', {
|
|
242
242
|
code: MARSHALLER_WORKER_FAILED,
|
|
243
243
|
status: 500,
|
|
244
244
|
context: { worker: String(options.entry) },
|
|
@@ -247,7 +247,7 @@ export function createMarshalWorkerClient(options) {
|
|
|
247
247
|
return new Promise((resolve, reject) => {
|
|
248
248
|
const timer = setTimeout(() => {
|
|
249
249
|
pending.delete(id);
|
|
250
|
-
reject(new AppError(`
|
|
250
|
+
reject(new AppError(`Marshaling worker did not answer within ${timeoutMs}ms.`, {
|
|
251
251
|
code: MARSHALLER_WORKER_TIMEOUT,
|
|
252
252
|
status: 504,
|
|
253
253
|
context: { worker: String(options.entry), timeoutMs },
|
|
@@ -260,7 +260,7 @@ export function createMarshalWorkerClient(options) {
|
|
|
260
260
|
async dispose() {
|
|
261
261
|
disposed = true;
|
|
262
262
|
for (const id of [...pending.keys()])
|
|
263
|
-
settle(id, (p) => p.reject(new AppError('
|
|
263
|
+
settle(id, (p) => p.reject(new AppError('Marshaling worker was disposed while a payload was in flight.', {
|
|
264
264
|
code: MARSHALLER_WORKER_FAILED,
|
|
265
265
|
status: 503,
|
|
266
266
|
context: { worker: String(options.entry) },
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Port
|
|
2
|
+
* Port marshaling + the 512KB worker gate (02 §7.1).
|
|
3
3
|
*
|
|
4
4
|
* External payloads — webhooks, third-party feeds, spreadsheet imports — never
|
|
5
|
-
* touch component code. A
|
|
5
|
+
* touch component code. A marshaler registered on the token is the only entry,
|
|
6
6
|
* and its `transform` output is checked against the token's canonical Zod schema
|
|
7
7
|
* (V3 asserts this statically; here it is enforced at runtime too, because a
|
|
8
8
|
* provider can change its shape without anyone recompiling).
|
|
@@ -18,7 +18,7 @@ import type { z } from 'zod';
|
|
|
18
18
|
/** Above this, `validate` + `transform` are offloaded to a Worker (02 §7.1). */
|
|
19
19
|
export declare const MARSHALLER_WORKER_OFFLOAD_BYTES: number;
|
|
20
20
|
/**
|
|
21
|
-
* The ONE
|
|
21
|
+
* The ONE marshaler contract (05 §B1, unified 2026-07-26).
|
|
22
22
|
*
|
|
23
23
|
* There were TWO interfaces of this name in this package: this one, and a
|
|
24
24
|
* divergent copy in `core/tokens/registry.ts` that additionally required
|
|
@@ -32,24 +32,24 @@ export declare const MARSHALLER_WORKER_OFFLOAD_BYTES: number;
|
|
|
32
32
|
*/
|
|
33
33
|
export interface CCWireMarshaller<Inbound = unknown, Out = unknown> {
|
|
34
34
|
id: string;
|
|
35
|
-
/** The wire token this
|
|
35
|
+
/** The wire token this marshaler feeds — V3 resolves it against the registry. */
|
|
36
36
|
tokenId: string;
|
|
37
37
|
/** REQUIRED — V3 `marshaller-payload-unbounded` when absent or non-positive. */
|
|
38
38
|
maxPayloadBytes: number;
|
|
39
39
|
/**
|
|
40
|
-
* The module that runs this
|
|
40
|
+
* The module that runs this marshaler's `validate`+`transform` off the main
|
|
41
41
|
* thread — an entry calling `serveMarshalWorker` (`core/wire/marshalWorker.ts`).
|
|
42
42
|
*
|
|
43
43
|
* **This replaced a `workerOffload: boolean` in the Worker commit (05 §B1).**
|
|
44
44
|
* That flag was authored by hand and asserted a capability the codebase did
|
|
45
|
-
* not have: every
|
|
45
|
+
* not have: every marshaler in the repo set it `true` while no worker
|
|
46
46
|
* existed anywhere, so the V3 gate it fed was checking that someone had
|
|
47
|
-
* remembered to type `true`. Whether a
|
|
47
|
+
* remembered to type `true`. Whether a marshaler can offload is now a
|
|
48
48
|
* question about an ARTIFACT — this entry either exists or it does not — and
|
|
49
49
|
* whether it NEEDS to is {@link requiresWorkerOffload}, derived from
|
|
50
50
|
* `maxPayloadBytes`. Neither is a promise anyone can make by hand.
|
|
51
51
|
*
|
|
52
|
-
* Optional because a
|
|
52
|
+
* Optional because a marshaler whose `maxPayloadBytes` sits under the
|
|
53
53
|
* main-thread limit can never reach the offload path; V3 requires it of
|
|
54
54
|
* exactly those that can.
|
|
55
55
|
*/
|
|
@@ -60,9 +60,9 @@ export interface CCWireMarshaller<Inbound = unknown, Out = unknown> {
|
|
|
60
60
|
targetZodSchema: z.ZodType<Out>;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Can this
|
|
63
|
+
* Can this marshaler ever reach the offload path? A DERIVED fact (05 §B1).
|
|
64
64
|
*
|
|
65
|
-
* A
|
|
65
|
+
* A marshaler that refuses anything over 256KB can never produce a payload
|
|
66
66
|
* above the 512KB main-thread limit, so it needs no worker and demanding one
|
|
67
67
|
* would be noise. One whose ceiling is 2MB certainly can, and without a
|
|
68
68
|
* `workerEntry` its large payloads are a guaranteed 503 — which is why V3
|
|
@@ -87,13 +87,13 @@ export interface MarshalWorker {
|
|
|
87
87
|
}
|
|
88
88
|
export interface MarshalOptions {
|
|
89
89
|
/**
|
|
90
|
-
* Use THIS worker instead of the one the
|
|
90
|
+
* Use THIS worker instead of the one the marshaler's `workerEntry` names.
|
|
91
91
|
* Left unset in production: offload is the default path, resolved from the
|
|
92
|
-
*
|
|
92
|
+
* marshaler itself, so an ingress route needs no worker wiring of its own.
|
|
93
93
|
*/
|
|
94
94
|
worker?: MarshalWorker;
|
|
95
95
|
/**
|
|
96
|
-
* Resolves a worker for a
|
|
96
|
+
* Resolves a worker for a marshaler that declares a `workerEntry`. The host
|
|
97
97
|
* supplies this once (`createMarshalWorkerClient`, cached per entry) — it is a
|
|
98
98
|
* parameter rather than a module-level singleton because a worker is a live
|
|
99
99
|
* OS thread, and something has to own disposing of it.
|
|
@@ -103,14 +103,14 @@ export interface MarshalOptions {
|
|
|
103
103
|
offloadThresholdBytes?: number;
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
* Run a payload through its
|
|
106
|
+
* Run a payload through its marshaler, offloading past the size gate.
|
|
107
107
|
*
|
|
108
|
-
* Order matters: the
|
|
108
|
+
* Order matters: the marshaler's OWN `maxPayloadBytes` is checked first, so an
|
|
109
109
|
* oversized payload is refused with a typed diagnostic before any work — worker
|
|
110
110
|
* or otherwise — is scheduled for it.
|
|
111
111
|
*/
|
|
112
112
|
export declare function marshalInbound<Inbound, Out>(marshaller: CCWireMarshaller<Inbound, Out>, payload: Inbound, options?: MarshalOptions): Promise<Out>;
|
|
113
|
-
/** The registry of
|
|
113
|
+
/** The registry of marshalers, keyed by token id (02 §7.1: registered ON the token). */
|
|
114
114
|
export interface MarshallerRegistry {
|
|
115
115
|
register(tokenId: string, marshaller: CCWireMarshaller): void;
|
|
116
116
|
forToken(tokenId: string): CCWireMarshaller | undefined;
|
|
@@ -2,9 +2,9 @@ import { AppError } from '../../shared/errors.js';
|
|
|
2
2
|
/** Above this, `validate` + `transform` are offloaded to a Worker (02 §7.1). */
|
|
3
3
|
export const MARSHALLER_WORKER_OFFLOAD_BYTES = 512 * 1024;
|
|
4
4
|
/**
|
|
5
|
-
* Can this
|
|
5
|
+
* Can this marshaler ever reach the offload path? A DERIVED fact (05 §B1).
|
|
6
6
|
*
|
|
7
|
-
* A
|
|
7
|
+
* A marshaler that refuses anything over 256KB can never produce a payload
|
|
8
8
|
* above the 512KB main-thread limit, so it needs no worker and demanding one
|
|
9
9
|
* would be noise. One whose ceiling is 2MB certainly can, and without a
|
|
10
10
|
* `workerEntry` its large payloads are a guaranteed 503 — which is why V3
|
|
@@ -41,16 +41,16 @@ export function payloadBytes(payload) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* Run a payload through its
|
|
44
|
+
* Run a payload through its marshaler, offloading past the size gate.
|
|
45
45
|
*
|
|
46
|
-
* Order matters: the
|
|
46
|
+
* Order matters: the marshaler's OWN `maxPayloadBytes` is checked first, so an
|
|
47
47
|
* oversized payload is refused with a typed diagnostic before any work — worker
|
|
48
48
|
* or otherwise — is scheduled for it.
|
|
49
49
|
*/
|
|
50
50
|
export async function marshalInbound(marshaller, payload, options = {}) {
|
|
51
51
|
const bytes = payloadBytes(payload);
|
|
52
52
|
if (bytes > marshaller.maxPayloadBytes)
|
|
53
|
-
throw new AppError(`Payload of ${bytes} bytes exceeds
|
|
53
|
+
throw new AppError(`Payload of ${bytes} bytes exceeds marshaler '${marshaller.id}' limit of ${marshaller.maxPayloadBytes}.`, {
|
|
54
54
|
code: MARSHALLER_PAYLOAD_TOO_LARGE,
|
|
55
55
|
status: 413,
|
|
56
56
|
context: { marshaller: marshaller.id, bytes, maxPayloadBytes: marshaller.maxPayloadBytes },
|
|
@@ -58,13 +58,13 @@ export async function marshalInbound(marshaller, payload, options = {}) {
|
|
|
58
58
|
const threshold = options.offloadThresholdBytes ?? MARSHALLER_WORKER_OFFLOAD_BYTES;
|
|
59
59
|
if (bytes > threshold) {
|
|
60
60
|
// Offload is the DEFAULT path (05 §B1): an explicitly injected worker wins,
|
|
61
|
-
// otherwise the
|
|
62
|
-
// resolver. Neither present means this
|
|
61
|
+
// otherwise the marshaler's own `workerEntry` is resolved through the host's
|
|
62
|
+
// resolver. Neither present means this marshaler has no offload path at all
|
|
63
63
|
// — which V3 refuses to verify, so a verified app never reaches this throw.
|
|
64
64
|
const worker = options.worker ??
|
|
65
65
|
(marshaller.workerEntry ? options.resolveWorker?.(marshaller) : undefined);
|
|
66
66
|
if (!worker)
|
|
67
|
-
throw new AppError(`Payload of ${bytes} bytes exceeds the ${threshold}-byte main-thread limit and
|
|
67
|
+
throw new AppError(`Payload of ${bytes} bytes exceeds the ${threshold}-byte main-thread limit and marshaler '${marshaller.id}' has no marshaling worker.`, {
|
|
68
68
|
code: MARSHALLER_PAYLOAD_TOO_LARGE,
|
|
69
69
|
status: 503,
|
|
70
70
|
context: {
|
|
@@ -73,8 +73,8 @@ export async function marshalInbound(marshaller, payload, options = {}) {
|
|
|
73
73
|
threshold,
|
|
74
74
|
workerEntry: marshaller.workerEntry ?? null,
|
|
75
75
|
hint: marshaller.workerEntry
|
|
76
|
-
? 'The
|
|
77
|
-
: 'Give the
|
|
76
|
+
? 'The marshaler declares a workerEntry but no resolveWorker was supplied — pass createMarshalWorkerClient through MarshalOptions.resolveWorker.'
|
|
77
|
+
: 'Give the marshaler a workerEntry (a module calling serveMarshalWorker), or lower maxPayloadBytes below the main-thread limit.',
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
80
|
// The worker validates AND transforms; the result still gets schema-checked
|
|
@@ -83,7 +83,7 @@ export async function marshalInbound(marshaller, payload, options = {}) {
|
|
|
83
83
|
return assertSchema(marshaller, result);
|
|
84
84
|
}
|
|
85
85
|
if (!marshaller.validate(payload))
|
|
86
|
-
throw new AppError(`Inbound payload rejected by
|
|
86
|
+
throw new AppError(`Inbound payload rejected by marshaler '${marshaller.id}'.`, {
|
|
87
87
|
code: MARSHALLER_VALIDATION_FAILED,
|
|
88
88
|
status: 400,
|
|
89
89
|
context: { marshaller: marshaller.id },
|
|
@@ -98,7 +98,7 @@ export async function marshalInbound(marshaller, payload, options = {}) {
|
|
|
98
98
|
function assertSchema(marshaller, value) {
|
|
99
99
|
const parsed = marshaller.targetZodSchema.safeParse(value);
|
|
100
100
|
if (!parsed.success)
|
|
101
|
-
throw new AppError(`
|
|
101
|
+
throw new AppError(`Marshaler '${marshaller.id}' produced output that violates its token's schema.`, {
|
|
102
102
|
code: MARSHALLER_SCHEMA_VIOLATION,
|
|
103
103
|
status: 502,
|
|
104
104
|
context: {
|
|
@@ -113,7 +113,7 @@ export function createMarshallerRegistry() {
|
|
|
113
113
|
return {
|
|
114
114
|
register(tokenId, marshaller) {
|
|
115
115
|
if (marshaller.maxPayloadBytes === undefined || marshaller.maxPayloadBytes <= 0)
|
|
116
|
-
throw new AppError(`
|
|
116
|
+
throw new AppError(`Marshaler '${marshaller.id}' must declare a positive maxPayloadBytes.`, {
|
|
117
117
|
code: 'marshaller-payload-unbounded',
|
|
118
118
|
status: 500,
|
|
119
119
|
context: { marshaller: marshaller.id, tokenId },
|
|
@@ -33,7 +33,7 @@ export async function convertIfOnlyGETRequests(url, init = { method: 'GET' }, fo
|
|
|
33
33
|
if (!METHOD_TO_ABBREV[upper]) {
|
|
34
34
|
throw new Error(`ctgr: "${method}" is not a tunnelable method`);
|
|
35
35
|
}
|
|
36
|
-
const body = form ? await formDataToBody(form) : bodyFromInit(init.body);
|
|
36
|
+
const body = form ? await formDataToBody(form) : bodyFromInit(init.body, upper, url);
|
|
37
37
|
const getInit = toGetInit(init);
|
|
38
38
|
const { chunks } = await encodeRequest(upper, url, body);
|
|
39
39
|
return { requests: chunks.map((chunk) => ({ url: chunk.url, init: getInit })) };
|
|
@@ -62,10 +62,26 @@ async function formDataToBody(formData) {
|
|
|
62
62
|
}
|
|
63
63
|
return { __files: files, ...fields };
|
|
64
64
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The request body as a JS value: a JSON string is parsed, a plain string rides as itself.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 Anything else is REFUSED. The tunnel carries a JSON value or a form's files; a `File`, a
|
|
69
|
+
* `Blob`, bytes, a stream or `URLSearchParams` handed through here was JSON-serialized as `{}` —
|
|
70
|
+
* desk's import preview posted a 3.4 MB workbook and the server described "a JSON file" with `{}`
|
|
71
|
+
* as its sample (forge task 3443). A loud error at the call is the bug; a silent `{}` is data loss.
|
|
72
|
+
* Send an upload as `FormData`, which is carried whole.
|
|
73
|
+
*/
|
|
74
|
+
function bodyFromInit(body, method, url) {
|
|
75
|
+
if (body === null || body === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (typeof body !== 'string') {
|
|
78
|
+
// A plain object or array a caller cast to `BodyInit` serializes as itself — carried as before.
|
|
79
|
+
const proto = Object.getPrototypeOf(body);
|
|
80
|
+
if (Array.isArray(body) || proto === Object.prototype || proto === null)
|
|
81
|
+
return body;
|
|
82
|
+
const shape = Object.prototype.toString.call(body).slice(8, -1);
|
|
83
|
+
throw new Error(`ctgr: a ${shape} body cannot ride the GET-only tunnel (${method} ${url}) — send it as FormData or a JSON string`);
|
|
84
|
+
}
|
|
69
85
|
try {
|
|
70
86
|
return JSON.parse(body);
|
|
71
87
|
}
|
package/dist/testing/bytes.d.ts
CHANGED
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
export declare const asBody: (bytes: Uint8Array) => Uint8Array<ArrayBuffer>;
|
|
29
29
|
/** `new Response(bytes)` with the assertion applied — the common case. */
|
|
30
30
|
export declare const bytesResponse: (bytes: Uint8Array, init?: ResponseInit) => Response;
|
|
31
|
-
/** A readable stream of `bytes` — what the media
|
|
31
|
+
/** A readable stream of `bytes` — what the media catalog's ingest paths take. */
|
|
32
32
|
export declare const bytesStream: (bytes: Uint8Array | string) => ReadableStream<Uint8Array>;
|
package/dist/testing/bytes.js
CHANGED
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
export const asBody = (bytes) => bytes;
|
|
29
29
|
/** `new Response(bytes)` with the assertion applied — the common case. */
|
|
30
30
|
export const bytesResponse = (bytes, init) => new Response(asBody(bytes), init);
|
|
31
|
-
/** A readable stream of `bytes` — what the media
|
|
31
|
+
/** A readable stream of `bytes` — what the media catalog's ingest paths take. */
|
|
32
32
|
export const bytesStream = (bytes) => new Response(typeof bytes === 'string' ? asBody(new TextEncoder().encode(bytes)) : asBody(bytes))
|
|
33
33
|
.body;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-core",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.3",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
unitForSpan,
|
|
19
19
|
} from './range.js';
|
|
20
20
|
|
|
21
|
-
/** 2026-08-13 14:30 LOCAL — the tests are about local calendar
|
|
21
|
+
/** 2026-08-13 14:30 LOCAL — the tests are about local calendar behavior. */
|
|
22
22
|
const NOW = new Date(2026, 7, 13, 14, 30, 0).getTime();
|
|
23
23
|
|
|
24
24
|
describe('the presets', () => {
|
|
@@ -158,8 +158,8 @@ export const startOfBucket = (t: number, unit: BucketUnit): number =>
|
|
|
158
158
|
* duplicated or missing bar exactly once every spring and autumn — the kind of
|
|
159
159
|
* bug nobody reproduces.
|
|
160
160
|
*
|
|
161
|
-
* The time-of-day is carried through rather than
|
|
162
|
-
* caller here passes an already-aligned value, so
|
|
161
|
+
* The time-of-day is carried through rather than normalized to midnight: every
|
|
162
|
+
* caller here passes an already-aligned value, so normalizing would be
|
|
163
163
|
* invisible in this file and a trap for the first caller that does not.
|
|
164
164
|
*/
|
|
165
165
|
export const addDays = (t: number, n: number): number => {
|
|
@@ -25,7 +25,7 @@ describe('simulateSchedule', () => {
|
|
|
25
25
|
expect(simulateSchedule(items, { workers: 1 }).makespanMs).toBe(40 * MIN);
|
|
26
26
|
expect(simulateSchedule(items, { workers: 2 }).makespanMs).toBe(20 * MIN);
|
|
27
27
|
expect(simulateSchedule(items, { workers: 4 }).makespanMs).toBe(10 * MIN);
|
|
28
|
-
// Nothing left to
|
|
28
|
+
// Nothing left to parallelize — the fifth worker is inert.
|
|
29
29
|
expect(simulateSchedule(items, { workers: 5 }).makespanMs).toBe(10 * MIN);
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -50,12 +50,12 @@ describe('simulateSchedule', () => {
|
|
|
50
50
|
expect(simulateSchedule(items, { workers: 2 }).makespanMs).toBe(10 * MIN);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('
|
|
53
|
+
it('serializes a named group without needing a shared resource', () => {
|
|
54
54
|
const items = [item('a', 5, { group: 'gpu' }), item('b', 5, { group: 'gpu' })];
|
|
55
55
|
expect(simulateSchedule(items, { workers: 4 }).makespanMs).toBe(10 * MIN);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it('
|
|
58
|
+
it('honors dependencies', () => {
|
|
59
59
|
const items = [item('second', 10, { dependsOn: ['first'] }), item('first', 10)];
|
|
60
60
|
const r = simulateSchedule(items, { workers: 4 });
|
|
61
61
|
expect(r.makespanMs).toBe(20 * MIN);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Three separate claims hide in that, and a `remaining / rate` division answers
|
|
12
12
|
* none of them:
|
|
13
13
|
*
|
|
14
|
-
* 1. **Work does not
|
|
14
|
+
* 1. **Work does not parallelize freely.** Two tasks that write the same files
|
|
15
15
|
* may not run together whatever the fleet size — that is the orchestrator's
|
|
16
16
|
* own scope rule, and it is why a queue of ten tasks all scoped to one app
|
|
17
17
|
* drains at exactly the same speed with eight workers as with one.
|
|
@@ -312,10 +312,10 @@ function blocks(candidate: ScheduleItem, other: ScheduleItem): boolean {
|
|
|
312
312
|
/**
|
|
313
313
|
* A best-effort reason an item started late — for the table behind the chart.
|
|
314
314
|
*
|
|
315
|
-
* Deliberately approximate and
|
|
315
|
+
* Deliberately approximate and labeled as such: the precise answer would need
|
|
316
316
|
* the admission loop to record every refusal at every event, which is a lot of
|
|
317
317
|
* bookkeeping for a column. "Was it waiting on a dependency, on a conflicting
|
|
318
|
-
*
|
|
318
|
+
* neighbor, or simply for a free worker" is the distinction a reader acts on,
|
|
319
319
|
* and that is decidable from the item alone.
|
|
320
320
|
*/
|
|
321
321
|
function whyWaited(
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ── What is NOT here, deliberately ──────────────────────────────────────────────────────
|
|
19
19
|
* Anything that needs a database, a filesystem, a fetch or a picture decoder. This module is
|
|
20
|
-
* pure so both consumers can prove their
|
|
20
|
+
* pure so both consumers can prove their behavior with `bun test` instead of against a live
|
|
21
21
|
* store, and so a browser bundle may import it (music renders the pair side by side).
|
|
22
22
|
*/
|
|
23
23
|
|
|
@@ -158,7 +158,7 @@ export function durationsClose(
|
|
|
158
158
|
/** One file, reduced to what this module can reason about. Units are the caller's — pass
|
|
159
159
|
* both durations in the same unit or not at all. */
|
|
160
160
|
export interface NamedFileFacts {
|
|
161
|
-
/** The name a person would
|
|
161
|
+
/** The name a person would recognize: a display title, else the last path segment. */
|
|
162
162
|
name: string | null | undefined;
|
|
163
163
|
sizeBytes: number;
|
|
164
164
|
/** Playing time, in whatever unit the caller uses consistently. Null = never measured. */
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
export type FileTreeNodeKind = 'folder' | 'file';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* How visible a node is,
|
|
27
|
+
* How visible a node is, generalized from collections' private/locked folders.
|
|
28
28
|
*
|
|
29
29
|
* 🔴 It is a LABEL, never an enforcement. The server decides what a session may see, and a
|
|
30
30
|
* tree that hides a row the server sent (or shows one it did not) is a second, weaker
|
|
@@ -117,7 +117,7 @@ export interface FileTreeNode {
|
|
|
117
117
|
*
|
|
118
118
|
* For a consumer that narrows the tree itself (by content, not only by name) and keeps
|
|
119
119
|
* the folders on the way to each hit: the hits are what the eye is scanning for, and the
|
|
120
|
-
* folders kept around them are context. Omit it and nothing is
|
|
120
|
+
* folders kept around them are context. Omit it and nothing is emphasized.
|
|
121
121
|
*/
|
|
122
122
|
matched?: boolean;
|
|
123
123
|
/**
|
|
@@ -230,7 +230,7 @@ export function flattenFileTree(
|
|
|
230
230
|
* "A row that claims to open onto nothing is its own small lie" was the old reasoning
|
|
231
231
|
* and it is answered by what VS Code does: the chevron says *folder*, not *occupied*,
|
|
232
232
|
* and opening an empty one onto nothing is the honest answer, not a broken promise.
|
|
233
|
-
* `node.expandable === false` is still
|
|
233
|
+
* `node.expandable === false` is still honored for a row that can never hold anything.
|
|
234
234
|
*/
|
|
235
235
|
const expandable = node.kind === 'folder' && (node.expandable ?? true);
|
|
236
236
|
const expanded = expandable && !shut.has(node.id);
|
|
@@ -447,7 +447,7 @@ export interface KeyboardMoveOptions {
|
|
|
447
447
|
* without asking which of several folders was meant — and **← promotes out to the folder its
|
|
448
448
|
* own folder is in**, which is `root` at depth 1.
|
|
449
449
|
*
|
|
450
|
-
* 🔴 Every refusal the DRAG
|
|
450
|
+
* 🔴 Every refusal the DRAG honors is honored here, from the same fields: `acceptsDrop`,
|
|
451
451
|
* the consumer's `canDrop`, and the descendant rule. A keyboard route that can reach a
|
|
452
452
|
* destination the pointer refuses is a second, weaker opinion about what is legal — and it
|
|
453
453
|
* is the one nobody looks at, because the pointer is what gets tested by hand.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* applies, with nothing destroyed. Deleting the CONTENTS destroys items, and this module
|
|
26
26
|
* has never seen an item; it only ever sees the paths they carry. So `purge` is a mode the
|
|
27
27
|
* menu offers and the CONSUMER implements against its own store, which is also the only
|
|
28
|
-
* place that can count what is about to be lost or refuse the operation.
|
|
28
|
+
* place that can count what is about to be lost or refuse the operation. Modeling it here
|
|
29
29
|
* would mean inventing an item, and an item this model cannot see is one it cannot protect.
|
|
30
30
|
*/
|
|
31
31
|
|
|
@@ -89,7 +89,7 @@ export async function deriveMasterLockVerifier(
|
|
|
89
89
|
}
|
|
90
90
|
const salt = unb64u(params.salt);
|
|
91
91
|
const kek = await pbkdf2(utf8(masterPassword), salt, params.iter);
|
|
92
|
-
// Same salt as the KEK, by design: it
|
|
92
|
+
// Same salt as the KEK, by design: it individualizes the verifier without a second
|
|
93
93
|
// stored value. One round, over KEK ‖ password.
|
|
94
94
|
const verifier = await pbkdf2(concat(kek, utf8(masterPassword)), salt, 1);
|
|
95
95
|
return b64u(verifier);
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ── 🔴 Why a decorative loop is NOT presence ────────────────────────────────
|
|
24
24
|
* The failure mode that would make this a security hole is an ambient background clip
|
|
25
25
|
* — muted, looping, autoplaying, running forever with nobody in the building. That is
|
|
26
|
-
* a
|
|
26
|
+
* a recognizable signature (`muted` AND `loop`) and it is refused. A film the owner is
|
|
27
27
|
* actually watching is not both at once; a muted film that loops is not something this
|
|
28
28
|
* fleet renders. Where a component knows better than the heuristic, it says so:
|
|
29
29
|
* `data-master-lock-presence="watch"` counts unconditionally, `"ignore"` never does.
|
|
@@ -123,7 +123,7 @@ export const MASTER_LOCK_STATE_HEADER = "x-master-lock-state";
|
|
|
123
123
|
* nothing in the fleet constructs one. So adding a required field to it is
|
|
124
124
|
* additive, and without this annotation it is the residual false MAJOR that
|
|
125
125
|
* Phase −1's M-B measured and had no cheap fix for (F4). `release/surface.ts`
|
|
126
|
-
* records the annotation; `release/classify.ts`
|
|
126
|
+
* records the annotation; `release/classify.ts` honors it. */
|
|
127
127
|
export interface MasterLockStatus {
|
|
128
128
|
/** Is a master password set at all? False only on an app that was never seeded,
|
|
129
129
|
* or — in per-person mode — for a person who has not chosen one yet. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure MediaItem helpers — kind predicates + dimension/aspect resolution. The React
|
|
2
|
-
//
|
|
2
|
+
// catalog maps items to layout photos through these, so the "what size is this
|
|
3
3
|
// thumbnail" logic is tested here, not buried in a component.
|
|
4
4
|
|
|
5
5
|
import type { MediaItem } from './types.js';
|
package/src/core/media/types.ts
CHANGED
|
@@ -66,11 +66,11 @@ export type VideoProgressEvent = {
|
|
|
66
66
|
error?: string;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
/** The three media kinds the
|
|
69
|
+
/** The three media kinds the catalog + lightbox understand. */
|
|
70
70
|
export type MediaKind = 'image' | 'video' | 'audio';
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* A URL-only
|
|
73
|
+
* A URL-only catalog item — the unit the `<MediaCatalogueViewer>` renders. It
|
|
74
74
|
* holds NO storage identity (no FsNode, no binary id): the app maps its own records
|
|
75
75
|
* to this and pre-resolves `url`/`thumbnailUrl` (signing private files as needed).
|
|
76
76
|
*/
|
|
@@ -92,7 +92,7 @@ export type MediaItem = {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* A gallery photo — a {@link MediaItem} specialized to images, plus the
|
|
95
|
+
* A gallery photo — a {@link MediaItem} specialized to images, plus the catalog affordances a
|
|
96
96
|
* photo grid renders (tags for the filter strip, createdAt for ordering, favorite for the ♥).
|
|
97
97
|
* Like every media-core unit it carries only resolved URLs — no storage identity.
|
|
98
98
|
*/
|
|
@@ -123,7 +123,7 @@ export type ProcessingStatus = 'pending' | 'processing' | 'ready' | 'failed';
|
|
|
123
123
|
/**
|
|
124
124
|
* A surface-agnostic description of a playable video with every URL already resolved —
|
|
125
125
|
* the unit the `<VideoPlayer>` primitive consumes (distinct from {@link MediaItem}, which
|
|
126
|
-
* is the lightbox
|
|
126
|
+
* is the lightbox catalog unit). It carries NO storage identity: a source pre-resolves
|
|
127
127
|
* the streaming URLs (signing/tokenising private ones), so the player couples to nothing.
|
|
128
128
|
* Richer than `MediaItem`: an HLS master + scrub-preview track for a real player HUD.
|
|
129
129
|
*/
|
|
@@ -74,7 +74,7 @@ export interface VideoCut {
|
|
|
74
74
|
* The shortest span worth cutting, in seconds.
|
|
75
75
|
*
|
|
76
76
|
* 🔴 Not zero, and not "whatever the person dragged". Below roughly a frame the request is
|
|
77
|
-
* indistinguishable from a mis-click, and it is a request ffmpeg cannot
|
|
77
|
+
* indistinguishable from a mis-click, and it is a request ffmpeg cannot honor anyway — a
|
|
78
78
|
* cut has to land on a frame boundary, so a 4 ms span asks the encoder to remove nothing and
|
|
79
79
|
* charges a full re-encode of the file for it. 40 ms is one frame at 25fps, the slowest rate
|
|
80
80
|
* anything in this library was shot at.
|
|
@@ -320,7 +320,7 @@ export function toggleSegment(plan: CutPlan, segment: VideoCut, duration: number
|
|
|
320
320
|
splits: plan.splits,
|
|
321
321
|
// Drop every removal this segment sits inside, then put back the parts of a WIDER
|
|
322
322
|
// removal that are still meant to go — un-marking one piece of a span that was marked
|
|
323
|
-
// before it was split must not resurrect its
|
|
323
|
+
// before it was split must not resurrect its neighbors.
|
|
324
324
|
removed: plan.removed.flatMap((cut) => {
|
|
325
325
|
if (!(cut.start <= current.start + 1e-6 && cut.end >= current.end - 1e-6)) return [cut];
|
|
326
326
|
const rest: VideoCut[] = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `cursedbelt-core/navigation` — the navigation contract + pure selectors. React-free and
|
|
2
2
|
// routing-free: the single source of truth (the IA tree) plus every derivation off
|
|
3
|
-
// it (side nav, hub tiles, breadcrumbs, search
|
|
3
|
+
// it (side nav, hub tiles, breadcrumbs, search catalog, coverage test, prefs
|
|
4
4
|
// migration). The React components in `cursedbelt/react` consume exactly this.
|
|
5
5
|
|
|
6
6
|
export {
|
|
@@ -245,7 +245,7 @@ describe('deriveBreadcrumbTrail', () => {
|
|
|
245
245
|
describe('deriveSearchCatalogue + rankCatalogue', () => {
|
|
246
246
|
const catalogue = deriveSearchCatalogue(model, allowAll);
|
|
247
247
|
|
|
248
|
-
test('every visible domain + item is
|
|
248
|
+
test('every visible domain + item is cataloged; deepLinkOnly is excluded', () => {
|
|
249
249
|
const ids = catalogue.map((e) => e.id);
|
|
250
250
|
expect(ids).toContain('fractal');
|
|
251
251
|
expect(ids).toContain('env-sets');
|
|
@@ -280,7 +280,7 @@ describe('deriveSearchCatalogue + rankCatalogue', () => {
|
|
|
280
280
|
expect(userCat.map((e) => e.id)).not.toContain('admin-users');
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
-
test("a DOMAIN's description + keywords reach the
|
|
283
|
+
test("a DOMAIN's description + keywords reach the catalog, so a leaf-shaped rail is findable by synonym", () => {
|
|
284
284
|
// The asymmetry this closes: an app whose places are leaf domains (no
|
|
285
285
|
// sub-items) used to be searchable by exact label only, while an identical
|
|
286
286
|
// rail expressed as items matched synonyms — a data-model gap, not a ranking one.
|
|
@@ -305,7 +305,7 @@ describe('deriveSearchCatalogue + rankCatalogue', () => {
|
|
|
305
305
|
expect(rankCatalogue(leafRail, 'review')[0]?.id).toBe('changes');
|
|
306
306
|
});
|
|
307
307
|
|
|
308
|
-
test('a domain without them still
|
|
308
|
+
test('a domain without them still catalogs cleanly (both stay optional)', () => {
|
|
309
309
|
const bare = deriveSearchCatalogue(
|
|
310
310
|
{ domains: [{ id: 'x', label: 'X', icon: 'a', path: '/x', groups: [] }] },
|
|
311
311
|
allowAll,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The single navigation source of truth — a declarative Information-Architecture
|
|
2
2
|
// tree (Domain → Group → Item) every nav surface derives from: the side nav, hub
|
|
3
|
-
// landing tiles, breadcrumbs, the universal-search
|
|
3
|
+
// landing tiles, breadcrumbs, the universal-search catalog, and the command
|
|
4
4
|
// palette. Authoring a feature into the model is the *only* way to surface it, so
|
|
5
5
|
// the historical "four hand-maintained lists drift out of sync → features get
|
|
6
6
|
// buried" bug is structurally impossible (the coverage test enforces it in CI).
|
|
@@ -135,7 +135,7 @@ export interface NavDomain {
|
|
|
135
135
|
* again the same as on a {@link NavItem}.
|
|
136
136
|
*
|
|
137
137
|
* Both of these exist because a domain IS a destination: `deriveSearchCatalogue`
|
|
138
|
-
* emits one
|
|
138
|
+
* emits one catalog entry per domain, so a rail of leaf-shaped domains (an app
|
|
139
139
|
* whose places have no sub-items) had no way to be found by synonym while an
|
|
140
140
|
* otherwise identical rail of items did. That asymmetry was in the data model,
|
|
141
141
|
* not in the ranker.
|