@fin.cx/einvoice 6.2.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.js +3 -2
  3. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +7 -0
  4. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +19 -3
  5. package/dist_ts/formats/validation/conformance.harness.js +1 -1
  6. package/dist_ts/formats/validation/schematron.downloader.d.ts +43 -15
  7. package/dist_ts/formats/validation/schematron.downloader.js +122 -77
  8. package/dist_ts/formats/validation/schematron.integration.d.ts +5 -1
  9. package/dist_ts/formats/validation/schematron.integration.js +16 -40
  10. package/dist_ts/formats/validation/schematron.validator.d.ts +35 -15
  11. package/dist_ts/formats/validation/schematron.validator.js +136 -138
  12. package/dist_ts/formats/validation/schematron.worker.d.ts +12 -9
  13. package/dist_ts/formats/validation/schematron.worker.js +148 -108
  14. package/dist_ts/plugins.d.ts +3 -1
  15. package/dist_ts/plugins.js +7 -2
  16. package/dist_ts_install/download-schematron.js +6 -3
  17. package/dist_ts_install/index.js +15 -11
  18. package/package.json +6 -8
  19. package/ts/00_commitinfo_data.ts +1 -1
  20. package/ts/einvoice.ts +2 -1
  21. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +19 -2
  22. package/ts/formats/validation/conformance.harness.ts +1 -1
  23. package/ts/formats/validation/schematron.downloader.ts +146 -97
  24. package/ts/formats/validation/schematron.integration.ts +18 -44
  25. package/ts/formats/validation/schematron.validator.ts +181 -181
  26. package/ts/formats/validation/schematron.worker.ts +191 -135
  27. package/ts/plugins.ts +8 -0
  28. package/ts/vendor/modules.d.ts +0 -19
  29. package/dist_ts/vendor/saxonjs.d.ts +0 -23
  30. package/dist_ts/vendor/saxonjs.js +0 -9
  31. package/readme.hints.md +0 -1134
  32. package/readme.howtofixtests.md +0 -38
  33. package/readme.literature.md +0 -1
  34. package/readme.plan.md +0 -497
  35. package/ts/vendor/saxonjs.ts +0 -36
@@ -1,11 +1,69 @@
1
1
  import { Worker } from 'worker_threads';
2
+ import { promises as fs } from 'fs';
2
3
  import type { ValidationResult } from './validation.types.js';
3
- import type { SchematronOptions } from './schematron.validator.js';
4
+ import {
5
+ applySchematronOptions,
6
+ schematronReportToValidationResults,
7
+ type SchematronOptions,
8
+ } from './schematron.validator.js';
4
9
 
5
10
  export interface ISchematronWorkerPoolInitializeOptions {
6
11
  isFilePath?: boolean;
7
12
  }
8
13
 
14
+ /**
15
+ * The worker body. It compiles the schema once and then answers validation
16
+ * messages with the report, which the pool maps exactly like the in-process
17
+ * validator does.
18
+ *
19
+ * The engine is imported from the URL the pool resolves, because an eval'd worker
20
+ * resolves a bare specifier against the working directory rather than this package.
21
+ */
22
+ const WORKER_SOURCE = `
23
+ const { parentPort } = require('worker_threads');
24
+ const { readFileSync } = require('fs');
25
+ const { dirname, resolve } = require('path');
26
+
27
+ let source = null;
28
+ const schemasByPhase = new Map();
29
+
30
+ const compile = async (phase) => {
31
+ const key = phase || '';
32
+ if (!schemasByPhase.has(key)) {
33
+ const { Schematron } = await import(source.engineUrl);
34
+ schemasByPhase.set(key, Schematron.fromString(source.schematronSource, {
35
+ phase: phase || undefined,
36
+ baseUri: source.baseUri || undefined,
37
+ resolveInclude: (href, base) =>
38
+ readFileSync(resolve(dirname(base || source.baseUri || '.'), href), 'utf-8'),
39
+ }));
40
+ }
41
+ return schemasByPhase.get(key);
42
+ };
43
+
44
+ parentPort.on('message', async (msg) => {
45
+ try {
46
+ if (msg.type === 'init') {
47
+ source = {
48
+ schematronSource: msg.schematronSource,
49
+ baseUri: msg.baseUri,
50
+ engineUrl: msg.engineUrl,
51
+ };
52
+ await compile(undefined);
53
+ parentPort.postMessage({ type: 'ready' });
54
+ } else if (msg.type === 'validate') {
55
+ if (!source) {
56
+ throw new Error('Worker not initialized');
57
+ }
58
+ const schema = await compile(msg.options.phase);
59
+ parentPort.postMessage({ type: 'result', report: schema.validate(msg.xmlContent) });
60
+ }
61
+ } catch (error) {
62
+ parentPort.postMessage({ type: 'error', error: error.message });
63
+ }
64
+ });
65
+ `;
66
+
9
67
  /**
10
68
  * Worker pool for Schematron validation
11
69
  * Provides non-blocking validation in worker threads
@@ -20,134 +78,125 @@ export class SchematronWorkerPool {
20
78
  reject: (error: Error) => void;
21
79
  }> = [];
22
80
  private maxWorkers: number;
23
- private stylesheetFileName?: string;
24
- private stylesheetInternal?: unknown;
25
-
81
+ private schematronSource?: string;
82
+ private baseUri?: string;
83
+ /** the task each busy worker is holding, so a thread that dies fails its task */
84
+ private tasksByWorker = new Map<Worker, { reject: (error: Error) => void }>();
85
+
26
86
  constructor(maxWorkers: number = 4) {
27
87
  this.maxWorkers = maxWorkers;
28
88
  }
29
-
89
+
30
90
  /**
31
91
  * Initialize worker pool
32
92
  */
33
93
  public async initialize(
34
- stylesheetSource: string,
35
- options: ISchematronWorkerPoolInitializeOptions = {}
94
+ schematronSource: string,
95
+ options: ISchematronWorkerPoolInitializeOptions = {},
36
96
  ): Promise<void> {
37
- this.loadStylesheet(stylesheetSource, options);
38
-
39
- // Create workers
40
- for (let i = 0; i < this.maxWorkers; i++) {
41
- await this.createWorker();
97
+ await this.loadSchematron(schematronSource, options);
98
+
99
+ try {
100
+ // Create workers
101
+ for (let i = 0; i < this.maxWorkers; i++) {
102
+ await this.createWorker();
103
+ }
104
+ } catch (error) {
105
+ // a pool that did not come up completely must not leave threads behind
106
+ await this.terminate();
107
+ throw error;
42
108
  }
43
109
  }
44
110
 
45
- private loadStylesheet(
46
- stylesheetSource: string,
47
- options: ISchematronWorkerPoolInitializeOptions
48
- ): void {
49
- const source = stylesheetSource.trim();
50
- const isFilePath = options.isFilePath ?? /\.sef\.json$/i.test(stylesheetSource);
51
-
52
- this.stylesheetFileName = undefined;
53
- this.stylesheetInternal = undefined;
111
+ private async loadSchematron(
112
+ schematronSource: string,
113
+ options: ISchematronWorkerPoolInitializeOptions,
114
+ ): Promise<void> {
115
+ const isFilePath = options.isFilePath ?? /\.sch$/i.test(schematronSource);
54
116
 
55
117
  if (isFilePath) {
56
- if (!/\.sef\.json$/i.test(stylesheetSource)) {
57
- throw new Error('Schematron worker validation requires a precompiled Saxon-JS SEF JSON stylesheet file.');
58
- }
59
- this.stylesheetFileName = stylesheetSource;
118
+ this.schematronSource = await fs.readFile(schematronSource, 'utf-8');
119
+ this.baseUri = schematronSource;
60
120
  return;
61
121
  }
62
122
 
63
- if (!source.startsWith('{')) {
64
- throw new Error('Schematron worker validation requires a precompiled Saxon-JS SEF JSON stylesheet. Raw .sch files are not executed.');
123
+ if (schematronSource.trim().startsWith('{')) {
124
+ throw new Error(
125
+ 'Schematron worker validation takes a Schematron schema, not a compiled XSLT stylesheet (SEF).',
126
+ );
65
127
  }
66
128
 
67
- this.stylesheetInternal = JSON.parse(source);
129
+ this.schematronSource = schematronSource;
130
+ this.baseUri = undefined;
68
131
  }
69
-
132
+
70
133
  /**
71
134
  * Create a new worker
72
135
  */
73
136
  private async createWorker(): Promise<void> {
74
- const worker = new Worker(`
75
- const { parentPort } = require('worker_threads');
76
- const SaxonJS = require('saxon-js');
77
-
78
- let stylesheetFileName = null;
79
- let stylesheetInternal = null;
80
-
81
- parentPort.on('message', async (msg) => {
82
- try {
83
- if (msg.type === 'init') {
84
- stylesheetFileName = msg.stylesheetFileName || null;
85
- stylesheetInternal = msg.stylesheetInternal || null;
86
- if (!stylesheetFileName && !stylesheetInternal) {
87
- throw new Error('Worker not initialized with a SEF stylesheet');
88
- }
89
- parentPort.postMessage({ type: 'ready' });
90
- } else if (msg.type === 'validate') {
91
- if (!stylesheetFileName && !stylesheetInternal) {
92
- throw new Error('Worker not initialized');
93
- }
94
-
95
- const transformOptions = {
96
- sourceText: msg.xmlContent,
97
- destination: 'serialized',
98
- stylesheetParams: msg.options.parameters || {}
99
- };
100
-
101
- if (stylesheetFileName) {
102
- transformOptions.stylesheetFileName = stylesheetFileName;
103
- } else {
104
- transformOptions.stylesheetInternal = stylesheetInternal;
105
- }
106
-
107
- const result = await SaxonJS.transform(transformOptions, 'async');
108
-
109
- parentPort.postMessage({
110
- type: 'result',
111
- svrl: result.principalResult
112
- });
137
+ const worker = new Worker(WORKER_SOURCE, { eval: true });
138
+
139
+ // Compile the schema inside the worker before it joins the pool.
140
+ try {
141
+ await new Promise<void>((resolve, reject) => {
142
+ worker.once('error', reject);
143
+ worker.once('exit', (code) =>
144
+ reject(new Error(`Schematron worker exited with code ${code} before it was ready`)),
145
+ );
146
+ worker.once('message', (msg) => {
147
+ if (msg.type === 'ready') {
148
+ resolve();
149
+ } else if (msg.type === 'error') {
150
+ reject(new Error(msg.error));
113
151
  }
114
- } catch (error) {
115
- parentPort.postMessage({
116
- type: 'error',
117
- error: error.message
118
- });
119
- }
120
- });
121
- `, { eval: true });
122
-
123
- // Initialize worker with a precompiled SEF stylesheet.
124
- await new Promise<void>((resolve, reject) => {
125
- worker.once('message', (msg) => {
126
- if (msg.type === 'ready') {
127
- resolve();
128
- } else if (msg.type === 'error') {
129
- reject(new Error(msg.error));
130
- }
131
- });
132
-
133
- // Send initialization message
134
- worker.postMessage({
135
- type: 'init',
136
- stylesheetFileName: this.stylesheetFileName,
137
- stylesheetInternal: this.stylesheetInternal
152
+ });
153
+
154
+ worker.postMessage({
155
+ type: 'init',
156
+ schematronSource: this.schematronSource,
157
+ baseUri: this.baseUri,
158
+ engineUrl: import.meta.resolve('@push.rocks/smartxml/schematron'),
159
+ });
138
160
  });
139
- });
140
-
161
+ } catch (error) {
162
+ await worker.terminate();
163
+ throw error;
164
+ }
165
+
166
+ worker.removeAllListeners('exit');
167
+ // a thread that dies later must not take its task down with it
168
+ worker.on('error', (error: unknown) =>
169
+ this.retireWorker(worker, error instanceof Error ? error : new Error(String(error))),
170
+ );
171
+ worker.on('exit', () =>
172
+ this.retireWorker(worker, new Error('Schematron worker exited unexpectedly')),
173
+ );
174
+
141
175
  this.workers.push(worker);
142
176
  this.availableWorkers.push(worker);
143
177
  }
144
-
178
+
179
+ /**
180
+ * Drops a thread that died, failing the task it was holding so no caller waits
181
+ * for an answer that can never come.
182
+ */
183
+ private retireWorker(worker: Worker, error: Error): void {
184
+ this.workers = this.workers.filter((candidate) => candidate !== worker);
185
+ this.availableWorkers = this.availableWorkers.filter((candidate) => candidate !== worker);
186
+ const holder = this.tasksByWorker.get(worker);
187
+ if (holder) {
188
+ this.tasksByWorker.delete(worker);
189
+ holder.reject(error);
190
+ }
191
+ this.processTasks();
192
+ }
193
+
145
194
  /**
146
195
  * Validate XML using worker pool
147
196
  */
148
197
  public async validate(
149
198
  xmlContent: string,
150
- options: SchematronOptions = {}
199
+ options: SchematronOptions = {},
151
200
  ): Promise<ValidationResult[]> {
152
201
  return new Promise((resolve, reject) => {
153
202
  // Add task to queue
@@ -155,7 +204,7 @@ export class SchematronWorkerPool {
155
204
  this.processTasks();
156
205
  });
157
206
  }
158
-
207
+
159
208
  /**
160
209
  * Process queued validation tasks
161
210
  */
@@ -163,65 +212,72 @@ export class SchematronWorkerPool {
163
212
  while (this.taskQueue.length > 0 && this.availableWorkers.length > 0) {
164
213
  const task = this.taskQueue.shift()!;
165
214
  const worker = this.availableWorkers.shift()!;
166
-
215
+
216
+ this.tasksByWorker.set(worker, task);
217
+
167
218
  // Set up one-time listeners
219
+ const finish = (settle: () => void) => {
220
+ this.tasksByWorker.delete(worker);
221
+ worker.removeListener('message', messageHandler);
222
+ settle();
223
+
224
+ // Return worker to pool
225
+ this.availableWorkers.push(worker);
226
+
227
+ // Process next task
228
+ this.processTasks();
229
+ };
230
+
168
231
  const messageHandler = (msg: any) => {
169
232
  if (msg.type === 'result') {
170
- // Parse SVRL and return results
171
- const results = this.parseSVRL(msg.svrl);
172
- task.resolve(results);
173
-
174
- // Return worker to pool
175
- this.availableWorkers.push(worker);
176
- worker.removeListener('message', messageHandler);
177
-
178
- // Process next task
179
- this.processTasks();
233
+ finish(() =>
234
+ task.resolve(
235
+ applySchematronOptions(schematronReportToValidationResults(msg.report), task.options),
236
+ ),
237
+ );
180
238
  } else if (msg.type === 'error') {
181
- task.reject(new Error(msg.error));
182
-
183
- // Return worker to pool
184
- this.availableWorkers.push(worker);
185
- worker.removeListener('message', messageHandler);
186
-
187
- // Process next task
188
- this.processTasks();
239
+ finish(() => task.reject(new Error(msg.error)));
189
240
  }
190
241
  };
191
-
242
+
192
243
  worker.on('message', messageHandler);
193
-
244
+
194
245
  // Send validation task
195
246
  worker.postMessage({
196
247
  type: 'validate',
197
248
  xmlContent: task.xmlContent,
198
- options: task.options
249
+ options: task.options,
199
250
  });
200
251
  }
201
252
  }
202
-
203
- /**
204
- * Parse SVRL output
205
- */
206
- private parseSVRL(svrlXml: string): ValidationResult[] {
207
- const results: ValidationResult[] = [];
208
-
209
- // This would use the same parsing logic as SchematronValidator
210
- // Simplified for brevity
211
-
212
- return results;
213
- }
214
-
253
+
215
254
  /**
216
255
  * Terminate all workers
217
256
  */
218
257
  public async terminate(): Promise<void> {
219
- await Promise.all(this.workers.map(w => w.terminate()));
258
+ const workers = this.workers;
220
259
  this.workers = [];
221
260
  this.availableWorkers = [];
261
+
262
+ // nobody may keep waiting for a pool that is shutting down
263
+ for (const holder of this.tasksByWorker.values()) {
264
+ holder.reject(new Error('Schematron worker pool was terminated'));
265
+ }
266
+ this.tasksByWorker.clear();
267
+ for (const task of this.taskQueue) {
268
+ task.reject(new Error('Schematron worker pool was terminated'));
269
+ }
222
270
  this.taskQueue = [];
271
+
272
+ await Promise.all(
273
+ workers.map((worker) => {
274
+ worker.removeAllListeners('exit');
275
+ worker.removeAllListeners('error');
276
+ return worker.terminate();
277
+ }),
278
+ );
223
279
  }
224
-
280
+
225
281
  /**
226
282
  * Get pool statistics
227
283
  */
@@ -233,7 +289,7 @@ export class SchematronWorkerPool {
233
289
  return {
234
290
  totalWorkers: this.workers.length,
235
291
  availableWorkers: this.availableWorkers.length,
236
- queuedTasks: this.taskQueue.length
292
+ queuedTasks: this.taskQueue.length,
237
293
  };
238
294
  }
239
295
  }
package/ts/plugins.ts CHANGED
@@ -10,9 +10,13 @@ import * as path from 'path';
10
10
  import * as crypto from 'crypto';
11
11
 
12
12
  // XML-related imports
13
+ import { readFileSync } from 'fs';
13
14
  import { DOMParser, XMLSerializer, xmldom } from './vendor/xmldom.js';
14
15
  import * as xpath from 'xpath';
15
16
 
17
+ // Standards-related imports
18
+ import * as smartxmlSchematron from '@push.rocks/smartxml/schematron';
19
+
16
20
  // Compression-related imports
17
21
  import { pako } from './vendor/pako.js';
18
22
 
@@ -27,10 +31,14 @@ export {
27
31
  crypto,
28
32
 
29
33
  // XML-related exports
34
+ readFileSync,
30
35
  DOMParser,
31
36
  XMLSerializer,
32
37
  xmldom,
33
38
  xpath,
39
+
40
+ // Standards-related exports
41
+ smartxmlSchematron,
34
42
 
35
43
  // Compression-related exports
36
44
  pako,
@@ -11,22 +11,3 @@ declare module 'xmldom' {
11
11
  declare module 'pako' {
12
12
  export function inflate(input: Uint8Array | ArrayBuffer | string, options?: unknown): Uint8Array;
13
13
  }
14
-
15
- declare module 'saxon-js' {
16
- export function compile(options: {
17
- stylesheetText: string;
18
- warnings?: string;
19
- [key: string]: unknown;
20
- }): Promise<unknown>;
21
-
22
- export function transform(options: {
23
- stylesheetInternal?: unknown;
24
- sourceText: string;
25
- destination?: string;
26
- stylesheetParams?: Record<string, unknown>;
27
- [key: string]: unknown;
28
- }): Promise<{
29
- principalResult: string;
30
- [key: string]: unknown;
31
- }>;
32
- }
@@ -1,23 +0,0 @@
1
- export interface ISaxonJSCompileOptions {
2
- stylesheetText: string;
3
- warnings?: string;
4
- [key: string]: unknown;
5
- }
6
- export interface ISaxonJSTransformOptions {
7
- stylesheetInternal?: unknown;
8
- stylesheetFileName?: string;
9
- sourceText: string;
10
- destination?: string;
11
- stylesheetParams?: Record<string, unknown>;
12
- [key: string]: unknown;
13
- }
14
- export interface ISaxonJSTransformResult {
15
- principalResult: string;
16
- [key: string]: unknown;
17
- }
18
- export interface ISaxonJSModule {
19
- compile?: (options: ISaxonJSCompileOptions) => Promise<unknown>;
20
- transform(options: ISaxonJSTransformOptions, executionMode?: 'async' | 'sync'): Promise<ISaxonJSTransformResult> | ISaxonJSTransformResult;
21
- [key: string]: unknown;
22
- }
23
- export declare const loadSaxonJS: () => Promise<ISaxonJSModule>;
@@ -1,9 +0,0 @@
1
- let cachedSaxonJS;
2
- export const loadSaxonJS = async () => {
3
- cachedSaxonJS ||= import('saxon-js').then((saxonJSRuntime) => {
4
- const runtime = saxonJSRuntime.default ?? saxonJSRuntime;
5
- return runtime;
6
- });
7
- return cachedSaxonJS;
8
- };
9
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2F4b25qcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3ZlbmRvci9zYXhvbmpzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQTBCQSxJQUFJLGFBQWtELENBQUM7QUFFdkQsTUFBTSxDQUFDLE1BQU0sV0FBVyxHQUFHLEtBQUssSUFBNkIsRUFBRTtJQUM3RCxhQUFhLEtBQUssTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLGNBQWMsRUFBRSxFQUFFO1FBQzNELE1BQU0sT0FBTyxHQUFJLGNBQXNCLENBQUMsT0FBTyxJQUFJLGNBQWMsQ0FBQztRQUNsRSxPQUFPLE9BQXlCLENBQUM7SUFDbkMsQ0FBQyxDQUFDLENBQUM7SUFFSCxPQUFPLGFBQWEsQ0FBQztBQUN2QixDQUFDLENBQUMifQ==