@typfolio/workers 0.1.0 → 0.2.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/dist/batch.js CHANGED
@@ -2,6 +2,16 @@ import * as Effect from 'effect/Effect';
2
2
  import * as Stream from 'effect/Stream';
3
3
  import { snapshotRequest } from './snapshot.js';
4
4
  import { TypfolioWorkerError, } from './types.js';
5
+ function capture(job, index) {
6
+ try {
7
+ return { id: job.id, index, request: snapshotRequest(job.request), timeoutMs: job.timeoutMs };
8
+ }
9
+ catch (error) {
10
+ if (error instanceof TypfolioWorkerError && error.code === 'INVALID_INPUT')
11
+ return { id: job.id, index, rejected: error };
12
+ throw error;
13
+ }
14
+ }
5
15
  function cancelled(job) {
6
16
  return { id: job.id, index: job.index, status: 'cancelled' };
7
17
  }
@@ -13,6 +23,9 @@ function normalizeError(error) {
13
23
  const run = Effect.fnUntraced(function* (client, job, batchSignal) {
14
24
  if (batchSignal?.aborted)
15
25
  return cancelled(job);
26
+ if (job.request === undefined)
27
+ return { id: job.id, index: job.index, status: 'error', error: job.rejected };
28
+ const request = job.request;
16
29
  return yield* Effect.tryPromise({
17
30
  try: effectSignal => {
18
31
  const controller = new AbortController();
@@ -24,7 +37,7 @@ const run = Effect.fnUntraced(function* (client, job, batchSignal) {
24
37
  abortFromEffect();
25
38
  if (batchSignal?.aborted)
26
39
  abortFromBatch();
27
- return client.compile(job.request, {
40
+ return client.compile(request, {
28
41
  signal: controller.signal,
29
42
  timeoutMs: job.timeoutMs,
30
43
  }).finally(() => {
@@ -63,12 +76,7 @@ function advance(progress, outcome) {
63
76
  * Events are yielded in completion order and results are not retained here.
64
77
  */
65
78
  export function compileBatch(client, jobs, options = {}) {
66
- const captured = jobs.map((job, index) => ({
67
- id: job.id,
68
- index,
69
- request: snapshotRequest(job.request),
70
- timeoutMs: job.timeoutMs,
71
- }));
79
+ const captured = jobs.map(capture);
72
80
  return (async function* stream() {
73
81
  let progress = initialProgress(captured.length);
74
82
  options.onProgress?.(progress);
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import * as Effect from 'effect/Effect';
2
2
  import * as Layer from 'effect/Layer';
3
3
  import * as ManagedRuntime from 'effect/ManagedRuntime';
4
4
  import { decodeRuntimeMessage, protocolVersion } from './protocol.js';
5
- import { isSnapshot, snapshotRequest, transferableBuffers } from './snapshot.js';
5
+ import { isSnapshot, snapshotRequest, snapshotValidation, transferableBuffers } from './snapshot.js';
6
6
  import { TypfolioWorkerError, } from './types.js';
7
7
  export { TypfolioWorkerError, } from './types.js';
8
8
  export { collectBatch, compileBatch } from './batch.js';
@@ -283,6 +283,11 @@ export function createWorkerClient(options) {
283
283
  return Promise.reject(error);
284
284
  }
285
285
  let program = compileJob(captured);
286
+ const validation = snapshotValidation(captured);
287
+ if (validation) {
288
+ const checked = captured;
289
+ program = Effect.flatMap(Effect.tryPromise({ try: () => validation, catch: error => error }), () => compileJob(checked));
290
+ }
286
291
  if (duration !== undefined) {
287
292
  program = Effect.timeoutOrElse(program, {
288
293
  duration,
package/dist/protocol.js CHANGED
@@ -44,6 +44,9 @@ function diagnostic(value) {
44
44
  path: typeof item.path === 'string' ? item.path : undefined,
45
45
  range: typeof item.range === 'string' ? item.range : undefined,
46
46
  package: typeof item.package === 'string' ? item.package : undefined,
47
+ ...(typeof item.code === 'string' ? { code: item.code } : {}),
48
+ ...(typeof item.file === 'string' ? { file: item.file } : {}),
49
+ ...(typeof item.text === 'string' ? { text: item.text } : {}),
47
50
  };
48
51
  }
49
52
  function serializedError(value) {
@@ -52,7 +55,7 @@ function serializedError(value) {
52
55
  throw new TypeError('Invalid worker error');
53
56
  }
54
57
  const codes = [
55
- 'INITIALIZATION', 'INVALID_REQUEST', 'UNSUPPORTED_OPTION', 'BUSY', 'DISPOSED',
58
+ 'INITIALIZATION', 'INVALID_REQUEST', 'INVALID_INPUT', 'UNSUPPORTED_OPTION', 'BUSY', 'DISPOSED',
56
59
  'ABORTED', 'COMPILATION', 'ENGINE', 'WORKER', 'TIMEOUT', 'QUEUE_FULL',
57
60
  ];
58
61
  const code = codes.find(candidate => candidate === error.code);
@@ -1,10 +1,16 @@
1
- import type { PdfRequest } from '@typfolio/core';
1
+ import { type AnyPdfRequest, type DocumentsRequest, type PdfRequest } from '@typfolio/core';
2
+ /** The pending schema check of a snapshot, if its template's schemas validate asynchronously. */
3
+ export declare function snapshotValidation(snapshot: object): Promise<void> | undefined;
2
4
  export declare function isSnapshot(request: unknown): boolean;
3
- export declare function snapshotRequest(request: PdfRequest<unknown>): PdfRequest<unknown>;
5
+ /**
6
+ * A copy of the request the client owns. A request for a defined template is first checked against its
7
+ * schemas and turned into its low-level form (schemas are functions and cannot cross the boundary).
8
+ */
9
+ export declare function snapshotRequest(request: AnyPdfRequest): PdfRequest<unknown> | DocumentsRequest;
4
10
  /**
5
11
  * The buffers of a snapshot's assets that `postMessage` can transfer, each once. Buffers that cannot be
6
12
  * transferred (a `SharedArrayBuffer`, which structured clone shares instead of copying, or one that is already
7
13
  * detached) are left out and travel by the ordinary clone. Only call this with a snapshot: transferring detaches
8
14
  * the buffers, which would break a caller's request.
9
15
  */
10
- export declare function transferableBuffers(snapshot: PdfRequest<unknown>): ArrayBuffer[];
16
+ export declare function transferableBuffers(snapshot: PdfRequest<unknown> | DocumentsRequest): ArrayBuffer[];
package/dist/snapshot.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { TypstPdfError } from '@typfolio/core';
2
+ import { resolveRequest } from '@typfolio/core/internal';
1
3
  import { TypfolioWorkerError } from './types.js';
2
4
  function inspectCloneable(value, ancestors, at) {
3
5
  if (typeof value === 'function' || typeof value === 'symbol') {
@@ -28,19 +30,49 @@ function inspectCloneable(value, ancestors, at) {
28
30
  // Every request returned by snapshotRequest. The client owns these (nothing else holds their buffers), so it
29
31
  // neither snapshots them again nor hesitates to transfer them.
30
32
  const snapshots = new WeakSet();
33
+ // A schema that validates asynchronously: its check, which the client awaits before dispatching.
34
+ const validations = new WeakMap();
35
+ // Errors from core (an input that fails its schema, a missing partial) as the client's own error type.
36
+ function asWorkerError(error) {
37
+ return error instanceof TypstPdfError
38
+ ? new TypfolioWorkerError(error.code, error.message, error.diagnostics, { cause: error })
39
+ : error;
40
+ }
41
+ /** The pending schema check of a snapshot, if its template's schemas validate asynchronously. */
42
+ export function snapshotValidation(snapshot) {
43
+ return validations.get(snapshot);
44
+ }
31
45
  export function isSnapshot(request) {
32
46
  return typeof request === 'object' && request !== null && snapshots.has(request);
33
47
  }
48
+ /**
49
+ * A copy of the request the client owns. A request for a defined template is first checked against its
50
+ * schemas and turned into its low-level form (schemas are functions and cannot cross the boundary).
51
+ */
34
52
  export function snapshotRequest(request) {
53
+ let resolved;
35
54
  try {
36
- inspectCloneable(request, new Set(), 'request');
37
- const snapshot = structuredClone(request);
55
+ resolved = resolveRequest(request);
56
+ }
57
+ catch (error) {
58
+ throw asWorkerError(error);
59
+ }
60
+ let snapshot;
61
+ try {
62
+ inspectCloneable(resolved.request, new Set(), 'request');
63
+ snapshot = structuredClone(resolved.request);
38
64
  snapshots.add(snapshot);
39
- return snapshot;
40
65
  }
41
66
  catch (cause) {
42
67
  throw new TypfolioWorkerError('INVALID_REQUEST', 'The PDF request cannot cross the worker boundary', [], { cause });
43
68
  }
69
+ if (resolved.validated) {
70
+ const validation = resolved.validated.catch(error => { throw asWorkerError(error); });
71
+ // A batch job may wait its turn before the client awaits this: a failure is not unhandled meanwhile.
72
+ validation.catch(() => { });
73
+ validations.set(snapshot, validation);
74
+ }
75
+ return snapshot;
44
76
  }
45
77
  /**
46
78
  * The buffers of a snapshot's assets that `postMessage` can transfer, each once. Buffers that cannot be
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Diagnostic, ErrorCode, PdfClient, PdfRequest, PdfResult } from '@typfolio/core';
1
+ import type { AnyPdfRequest, CheckedRequest, Diagnostic, ErrorCode, PdfClient, PdfResult } from '@typfolio/core';
2
2
  /** WASM sources that the structured-clone worker protocol can carry reliably. */
3
3
  export type WorkerWasmSource = string | ArrayBuffer | Uint8Array | WebAssembly.Module;
4
4
  /**
@@ -22,7 +22,8 @@ export interface WorkerCompileOptions {
22
22
  }
23
23
  export interface PdfWorkerClient extends PdfClient {
24
24
  readonly workerCount: number;
25
- compile(request: PdfRequest<unknown>, options?: WorkerCompileOptions): Promise<PdfResult>;
25
+ /** A plain request, or one for a defined template: its inputs are checked against their schemas on this thread. */
26
+ compile<R extends AnyPdfRequest>(request: R & CheckedRequest<R>, options?: WorkerCompileOptions): Promise<PdfResult>;
26
27
  /** Rejects queued/active jobs and terminates all owned workers. Idempotent. */
27
28
  dispose(): Promise<void>;
28
29
  }
@@ -41,7 +42,7 @@ export interface CreateWorkerClientOptions {
41
42
  }
42
43
  export interface PdfBatchJob<Id> {
43
44
  readonly id: Id;
44
- readonly request: PdfRequest<unknown>;
45
+ readonly request: AnyPdfRequest;
45
46
  readonly timeoutMs?: number;
46
47
  }
47
48
  export type PdfBatchOutcome<Id> = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typfolio/workers",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-independent Web Worker execution for Typfolio PDF compilation",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -16,6 +16,7 @@
16
16
  "dist",
17
17
  "README.md"
18
18
  ],
19
+ "types": "./dist/index.d.ts",
19
20
  "exports": {
20
21
  ".": {
21
22
  "types": "./dist/index.d.ts",
@@ -24,7 +25,7 @@
24
25
  "./runtime": "./dist/runtime.js"
25
26
  },
26
27
  "dependencies": {
27
- "@typfolio/core": "^0.1.0",
28
+ "@typfolio/core": "^0.2.0",
28
29
  "effect": "4.0.0-rc.118"
29
30
  },
30
31
  "publishConfig": {