tywrap 0.3.0 → 0.4.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/README.md +39 -5
- package/dist/cli.js +24 -6
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +19 -13
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +0 -1
- package/dist/core/analyzer.js.map +1 -1
- package/dist/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +53 -17
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +5 -4
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +5 -4
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/worker-pool.d.ts +6 -1
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +56 -29
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +0 -7
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +10 -3
- package/dist/tywrap.js.map +1 -1
- package/package.json +17 -13
- package/src/cli.ts +27 -6
- package/src/config/index.ts +28 -15
- package/src/core/analyzer.ts +1 -2
- package/src/dev.ts +983 -0
- package/src/index.ts +1 -1
- package/src/runtime/node.ts +74 -28
- package/src/runtime/optimized-node.ts +5 -4
- package/src/runtime/worker-pool.ts +65 -30
- package/src/types/index.ts +0 -8
- package/src/tywrap.ts +17 -3
package/src/index.ts
CHANGED
|
@@ -121,7 +121,6 @@ export type {
|
|
|
121
121
|
NodeConfig,
|
|
122
122
|
HttpConfig,
|
|
123
123
|
PerformanceConfig,
|
|
124
|
-
DevelopmentConfig,
|
|
125
124
|
TypeMappingConfig,
|
|
126
125
|
TypePreset,
|
|
127
126
|
BridgeInfo,
|
|
@@ -135,6 +134,7 @@ export type {
|
|
|
135
134
|
// Main API
|
|
136
135
|
export { tywrap } from './tywrap.js';
|
|
137
136
|
export { generate } from './tywrap.js';
|
|
137
|
+
export type { GenerateFailure, GenerateResult, GenerateRunOptions } from './tywrap.js';
|
|
138
138
|
|
|
139
139
|
// Runtime detection utilities
|
|
140
140
|
export { detectRuntime, isNodejs, isDeno, isBun, isBrowser } from './utils/runtime.js';
|
package/src/runtime/node.ts
CHANGED
|
@@ -340,16 +340,14 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
340
340
|
codec: options.codec,
|
|
341
341
|
warmupCommands,
|
|
342
342
|
};
|
|
343
|
-
|
|
344
343
|
// Build environment for ProcessIO
|
|
345
344
|
const processEnv = buildProcessEnv(resolvedOptions);
|
|
346
345
|
|
|
347
|
-
// Create warmup callback for per-worker initialization
|
|
348
346
|
const userWarmup =
|
|
349
347
|
resolvedOptions.warmupCommands.length > 0
|
|
350
348
|
? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
|
|
351
349
|
: undefined;
|
|
352
|
-
const
|
|
350
|
+
const onWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
|
|
353
351
|
|
|
354
352
|
// Create pooled transport with ProcessIO workers
|
|
355
353
|
const transport = new PooledTransport({
|
|
@@ -364,8 +362,8 @@ export class NodeBridge extends BridgeProtocol {
|
|
|
364
362
|
minWorkers: resolvedOptions.minProcesses,
|
|
365
363
|
queueTimeoutMs: resolvedOptions.queueTimeoutMs,
|
|
366
364
|
maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
|
|
367
|
-
onWorkerReady
|
|
368
|
-
onReplacementWorkerReady:
|
|
365
|
+
onWorkerReady,
|
|
366
|
+
onReplacementWorkerReady: onWorkerReady,
|
|
369
367
|
});
|
|
370
368
|
|
|
371
369
|
// Initialize BridgeProtocol with pooled transport
|
|
@@ -565,6 +563,72 @@ function generateWarmupId(): number {
|
|
|
565
563
|
return ++warmupRequestId;
|
|
566
564
|
}
|
|
567
565
|
|
|
566
|
+
async function sendWarmupRequest(
|
|
567
|
+
worker: PooledWorker,
|
|
568
|
+
timeoutMs: number,
|
|
569
|
+
requestId: number,
|
|
570
|
+
label: string,
|
|
571
|
+
messagePayload: Record<string, unknown>
|
|
572
|
+
): Promise<void> {
|
|
573
|
+
let message: string;
|
|
574
|
+
try {
|
|
575
|
+
message = JSON.stringify({
|
|
576
|
+
id: requestId,
|
|
577
|
+
protocol: PROTOCOL_ID,
|
|
578
|
+
...messagePayload,
|
|
579
|
+
});
|
|
580
|
+
} catch (error) {
|
|
581
|
+
throw new BridgeExecutionError(
|
|
582
|
+
`${label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
|
|
583
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let response: string;
|
|
588
|
+
try {
|
|
589
|
+
response = await worker.transport.send(message, timeoutMs);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw new BridgeExecutionError(
|
|
592
|
+
`${label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
|
|
593
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let parsed: unknown;
|
|
598
|
+
try {
|
|
599
|
+
parsed = JSON.parse(response);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw new BridgeExecutionError(`${label} returned invalid JSON response`, {
|
|
602
|
+
cause: error instanceof Error ? error : undefined,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
607
|
+
throw new BridgeExecutionError(
|
|
608
|
+
`${label} returned malformed response envelope for request ${requestId}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const envelope = parsed as { result?: unknown; error?: unknown };
|
|
613
|
+
if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
|
|
614
|
+
const errorPayload = envelope.error;
|
|
615
|
+
if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
|
|
616
|
+
const err = errorPayload as { type?: unknown; message?: unknown };
|
|
617
|
+
const errType = typeof err.type === 'string' ? err.type : 'Error';
|
|
618
|
+
const errMessage = typeof err.message === 'string' ? err.message : 'Unknown warmup error';
|
|
619
|
+
throw new BridgeExecutionError(`${label} failed: ${errType}: ${errMessage}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
throw new BridgeExecutionError(`${label} failed with malformed error payload`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!('result' in envelope)) {
|
|
626
|
+
throw new BridgeExecutionError(
|
|
627
|
+
`${label} returned malformed response envelope for request ${requestId}`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
568
632
|
/**
|
|
569
633
|
* Create a callback that runs warmup commands on each worker.
|
|
570
634
|
*
|
|
@@ -604,29 +668,11 @@ function createWorkerReadyCallback(
|
|
|
604
668
|
extraWarmup?: (worker: PooledWorker) => Promise<void>
|
|
605
669
|
): (worker: PooledWorker) => Promise<void> {
|
|
606
670
|
return async (worker: PooledWorker) => {
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
args: [[]],
|
|
613
|
-
},
|
|
614
|
-
Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS),
|
|
615
|
-
{
|
|
616
|
-
label: 'Worker readiness probe (builtins.len)',
|
|
617
|
-
invalidJsonMessage: 'returned invalid JSON response',
|
|
618
|
-
malformedEnvelopeMessage: requestId =>
|
|
619
|
-
`returned malformed response envelope for request ${requestId}`,
|
|
620
|
-
pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
|
|
621
|
-
malformedErrorPayloadMessage: 'failed with malformed error payload',
|
|
622
|
-
}
|
|
623
|
-
);
|
|
624
|
-
|
|
625
|
-
if (result !== 0) {
|
|
626
|
-
throw new BridgeExecutionError(
|
|
627
|
-
`Worker readiness probe (builtins.len) returned unexpected result: ${JSON.stringify(result)}`
|
|
628
|
-
);
|
|
629
|
-
}
|
|
671
|
+
const readyTimeoutMs = timeoutMs > 0 ? Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS) : 0;
|
|
672
|
+
await sendWarmupRequest(worker, readyTimeoutMs, generateWarmupId(), 'Worker warmup check', {
|
|
673
|
+
method: 'meta',
|
|
674
|
+
params: {},
|
|
675
|
+
});
|
|
630
676
|
|
|
631
677
|
await extraWarmup?.(worker);
|
|
632
678
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @deprecated Import from './node.js' instead.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* now supports both single-process mode (default) and multi-process
|
|
6
|
-
* via the minProcesses/maxProcesses options.
|
|
4
|
+
* Backward-compat shim for older deep imports. NodeBridge is the public API,
|
|
5
|
+
* and it now supports both single-process mode (default) and multi-process
|
|
6
|
+
* pooling via the minProcesses/maxProcesses options.
|
|
7
7
|
*
|
|
8
8
|
* Migration:
|
|
9
9
|
* ```typescript
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* const bridge = new NodeBridge({ minProcesses: 2, maxProcesses: 4 });
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
|
-
* This file is
|
|
19
|
+
* This file is not exposed through the package exports map and is maintained
|
|
20
|
+
* for backward compatibility only.
|
|
20
21
|
*/
|
|
21
22
|
export {
|
|
22
23
|
NodeBridge as OptimizedNodeBridge,
|
|
@@ -162,13 +162,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
162
162
|
*/
|
|
163
163
|
protected async doInit(): Promise<void> {
|
|
164
164
|
// Pre-spawn minimum workers if configured
|
|
165
|
-
|
|
166
|
-
const spawns: Promise<PooledWorker>[] = [];
|
|
167
|
-
for (let i = 0; i < this.options.minWorkers; i++) {
|
|
168
|
-
spawns.push(this.createWorker());
|
|
169
|
-
}
|
|
170
|
-
await Promise.all(spawns);
|
|
171
|
-
}
|
|
165
|
+
await this.fillToMinimumWorkers();
|
|
172
166
|
}
|
|
173
167
|
|
|
174
168
|
/**
|
|
@@ -224,7 +218,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
224
218
|
*/
|
|
225
219
|
async acquire(): Promise<PooledWorker> {
|
|
226
220
|
// Check for disposed state
|
|
227
|
-
if (this.isDisposed) {
|
|
221
|
+
if (this.isDisposed || this.state === 'disposing') {
|
|
228
222
|
throw new BridgeExecutionError('Pool has been disposed');
|
|
229
223
|
}
|
|
230
224
|
|
|
@@ -243,7 +237,12 @@ export class WorkerPool extends BoundedContext {
|
|
|
243
237
|
this.pendingCreations++;
|
|
244
238
|
try {
|
|
245
239
|
const newWorker = await this.createWorker();
|
|
240
|
+
if (this.isShuttingDown()) {
|
|
241
|
+
this.removeWorker(newWorker);
|
|
242
|
+
throw new BridgeExecutionError('Pool has been disposed');
|
|
243
|
+
}
|
|
246
244
|
newWorker.inFlightCount++;
|
|
245
|
+
this.publishAvailableWorker(newWorker);
|
|
247
246
|
return newWorker;
|
|
248
247
|
} finally {
|
|
249
248
|
this.pendingCreations--;
|
|
@@ -270,16 +269,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
270
269
|
|
|
271
270
|
// Decrement in-flight count (minimum 0)
|
|
272
271
|
worker.inFlightCount = Math.max(0, worker.inFlightCount - 1);
|
|
273
|
-
|
|
274
|
-
// If there are waiters and this worker has capacity, fulfill the first waiter
|
|
275
|
-
if (this.waitQueue.length > 0 && worker.inFlightCount < this.options.maxConcurrentPerWorker) {
|
|
276
|
-
const waiter = this.waitQueue.shift();
|
|
277
|
-
if (waiter) {
|
|
278
|
-
clearTimeout(waiter.timer);
|
|
279
|
-
worker.inFlightCount++;
|
|
280
|
-
waiter.resolve(worker);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
272
|
+
this.publishAvailableWorker(worker);
|
|
283
273
|
}
|
|
284
274
|
|
|
285
275
|
/**
|
|
@@ -331,14 +321,8 @@ export class WorkerPool extends BoundedContext {
|
|
|
331
321
|
* - Process exited unexpectedly
|
|
332
322
|
* - Pipe errors (EPIPE)
|
|
333
323
|
* - Connection reset errors (ECONNRESET)
|
|
334
|
-
* - Request timeouts (the worker may still be busy with an uncancellable request)
|
|
335
324
|
*/
|
|
336
325
|
private isFatalWorkerError(error: unknown): boolean {
|
|
337
|
-
// If a request times out, the underlying transport may still be executing it.
|
|
338
|
-
// Quarantine this worker so subsequent requests don't get stuck behind it.
|
|
339
|
-
if (error instanceof BridgeTimeoutError) {
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
326
|
if (error instanceof BridgeProtocolError) {
|
|
343
327
|
const msg = error.message.toLowerCase();
|
|
344
328
|
return (
|
|
@@ -365,7 +349,9 @@ export class WorkerPool extends BoundedContext {
|
|
|
365
349
|
worker.transport.dispose().catch(() => {
|
|
366
350
|
// Ignore disposal errors for dead workers
|
|
367
351
|
});
|
|
368
|
-
this.
|
|
352
|
+
if (this.state === 'ready') {
|
|
353
|
+
this.scheduleReplacementWorker();
|
|
354
|
+
}
|
|
369
355
|
}
|
|
370
356
|
}
|
|
371
357
|
|
|
@@ -405,6 +391,30 @@ export class WorkerPool extends BoundedContext {
|
|
|
405
391
|
return this.workers.find(w => w.inFlightCount < this.options.maxConcurrentPerWorker);
|
|
406
392
|
}
|
|
407
393
|
|
|
394
|
+
private getMinimumWorkerDeficit(): number {
|
|
395
|
+
return Math.max(0, this.options.minWorkers - (this.workers.length + this.pendingCreations));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private isShuttingDown(): boolean {
|
|
399
|
+
return this.isDisposed || this.state === 'disposing';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async fillToMinimumWorkers(): Promise<void> {
|
|
403
|
+
await this.spawnWorkers(this.getMinimumWorkerDeficit());
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private async spawnWorkers(count: number): Promise<void> {
|
|
407
|
+
if (count === 0) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.pendingCreations += count;
|
|
411
|
+
try {
|
|
412
|
+
await Promise.all(Array.from({ length: count }, () => this.spawnWorkerToPool()));
|
|
413
|
+
} finally {
|
|
414
|
+
this.pendingCreations = Math.max(0, this.pendingCreations - count);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
408
418
|
/**
|
|
409
419
|
* Replace a removed worker in the background so the next caller does not pay
|
|
410
420
|
* the full worker cold-start penalty after a timeout or crash.
|
|
@@ -419,11 +429,7 @@ export class WorkerPool extends BoundedContext {
|
|
|
419
429
|
|
|
420
430
|
this.pendingCreations++;
|
|
421
431
|
const replacementReady = this.options.onReplacementWorkerReady ?? this.options.onWorkerReady;
|
|
422
|
-
this.
|
|
423
|
-
.then(worker => {
|
|
424
|
-
// Reuse release() to wake queued callers if one is waiting.
|
|
425
|
-
this.release(worker);
|
|
426
|
-
})
|
|
432
|
+
this.spawnWorkerToPool(replacementReady)
|
|
427
433
|
.catch(() => {
|
|
428
434
|
// Ignore background replacement failures. A later acquire() can retry.
|
|
429
435
|
})
|
|
@@ -432,6 +438,35 @@ export class WorkerPool extends BoundedContext {
|
|
|
432
438
|
});
|
|
433
439
|
}
|
|
434
440
|
|
|
441
|
+
private async spawnWorkerToPool(onWorkerReady = this.options.onWorkerReady): Promise<void> {
|
|
442
|
+
if (this.isShuttingDown()) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const worker = await this.createWorker(onWorkerReady);
|
|
447
|
+
if (this.isShuttingDown()) {
|
|
448
|
+
this.removeWorker(worker);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.publishAvailableWorker(worker);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private publishAvailableWorker(worker: PooledWorker): void {
|
|
456
|
+
while (
|
|
457
|
+
this.waitQueue.length > 0 &&
|
|
458
|
+
worker.inFlightCount < this.options.maxConcurrentPerWorker
|
|
459
|
+
) {
|
|
460
|
+
const waiter = this.waitQueue.shift();
|
|
461
|
+
if (!waiter) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
clearTimeout(waiter.timer);
|
|
465
|
+
worker.inFlightCount++;
|
|
466
|
+
waiter.resolve(worker);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
435
470
|
/**
|
|
436
471
|
* Create a new worker and add it to the pool.
|
|
437
472
|
*
|
package/src/types/index.ts
CHANGED
|
@@ -304,7 +304,6 @@ export interface TywrapOptions {
|
|
|
304
304
|
output: OutputConfig;
|
|
305
305
|
runtime: RuntimeConfig;
|
|
306
306
|
performance: PerformanceConfig;
|
|
307
|
-
development: DevelopmentConfig;
|
|
308
307
|
types?: TypeMappingConfig;
|
|
309
308
|
debug?: boolean;
|
|
310
309
|
}
|
|
@@ -320,7 +319,6 @@ export interface PythonModuleConfig {
|
|
|
320
319
|
excludePatterns?: string[];
|
|
321
320
|
alias?: string;
|
|
322
321
|
typeHints: 'strict' | 'loose' | 'ignore';
|
|
323
|
-
watch?: boolean;
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
export interface OutputConfig {
|
|
@@ -360,12 +358,6 @@ export interface PerformanceConfig {
|
|
|
360
358
|
compression: 'auto' | 'gzip' | 'brotli' | 'none';
|
|
361
359
|
}
|
|
362
360
|
|
|
363
|
-
export interface DevelopmentConfig {
|
|
364
|
-
hotReload: boolean;
|
|
365
|
-
sourceMap: boolean;
|
|
366
|
-
validation: 'runtime' | 'compile' | 'both' | 'none';
|
|
367
|
-
}
|
|
368
|
-
|
|
369
361
|
export type TypePreset = 'numpy' | 'pandas' | 'pydantic' | 'stdlib' | 'scipy' | 'torch' | 'sklearn';
|
|
370
362
|
|
|
371
363
|
export interface TypeMappingConfig {
|
package/src/tywrap.ts
CHANGED
|
@@ -86,9 +86,16 @@ export interface GenerateRunOptions {
|
|
|
86
86
|
check?: boolean;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export interface GenerateFailure {
|
|
90
|
+
module: string;
|
|
91
|
+
code: 'ir-unavailable';
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
export interface GenerateResult {
|
|
90
96
|
written: string[];
|
|
91
97
|
warnings: string[];
|
|
98
|
+
failures: GenerateFailure[];
|
|
92
99
|
/**
|
|
93
100
|
* Only set when `GenerateRunOptions.check === true`.
|
|
94
101
|
* Lists files that are missing or differ from what would be generated.
|
|
@@ -123,6 +130,7 @@ export async function generate(
|
|
|
123
130
|
const written: string[] = [];
|
|
124
131
|
const outOfDate: string[] = [];
|
|
125
132
|
const warnings: string[] = [];
|
|
133
|
+
const failures: GenerateFailure[] = [];
|
|
126
134
|
const outputDir = resolvedOptions.output.dir;
|
|
127
135
|
const caching = resolvedOptions.performance.caching;
|
|
128
136
|
const cacheDir = '.tywrap/cache';
|
|
@@ -173,7 +181,13 @@ export async function generate(
|
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
if (!ir) {
|
|
176
|
-
|
|
184
|
+
const message = `No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`;
|
|
185
|
+
warnings.push(message);
|
|
186
|
+
failures.push({
|
|
187
|
+
module: moduleKey,
|
|
188
|
+
code: 'ir-unavailable',
|
|
189
|
+
message,
|
|
190
|
+
});
|
|
177
191
|
continue;
|
|
178
192
|
}
|
|
179
193
|
|
|
@@ -324,10 +338,10 @@ export async function generate(
|
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
if (checkMode) {
|
|
327
|
-
return { written: [], warnings, outOfDate };
|
|
341
|
+
return { written: [], warnings, failures, outOfDate };
|
|
328
342
|
}
|
|
329
343
|
|
|
330
|
-
return { written, warnings };
|
|
344
|
+
return { written, warnings, failures };
|
|
331
345
|
}
|
|
332
346
|
|
|
333
347
|
/**
|