@tasker-systems/tasker 0.1.7 → 0.1.9

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/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { EventEmitter } from 'eventemitter3';
2
- import pino from 'pino';
3
1
  import { existsSync } from 'fs';
4
2
  import { dirname, join } from 'path';
5
3
  import { fileURLToPath } from 'url';
4
+ import { EventEmitter } from 'eventemitter3';
5
+ import pino from 'pino';
6
6
  import { readdir } from 'fs/promises';
7
7
 
8
8
  var __defProp = Object.defineProperty;
@@ -378,6 +378,131 @@ function healthCheck(module) {
378
378
  return false;
379
379
  }
380
380
  }
381
+ var FfiLayer = class _FfiLayer {
382
+ module = null;
383
+ modulePath = null;
384
+ configuredModulePath;
385
+ constructor(config = {}) {
386
+ this.configuredModulePath = config.modulePath;
387
+ }
388
+ /**
389
+ * Load the napi-rs native module.
390
+ *
391
+ * @param customPath - Optional override for module path
392
+ * @throws Error if module not found or failed to load
393
+ */
394
+ async load(customPath) {
395
+ if (this.module) {
396
+ return;
397
+ }
398
+ const path = customPath ?? this.configuredModulePath ?? this.discoverModulePath();
399
+ if (!path) {
400
+ throw new Error(
401
+ `napi-rs native module not found. No bundled .node file matches this platform, and TASKER_FFI_MODULE_PATH is not set.
402
+ Current platform: ${process.platform}-${process.arch}
403
+ Supported: linux-x64, darwin-arm64
404
+ Override: export TASKER_FFI_MODULE_PATH=/path/to/tasker_ts.linux-x64-gnu.node`
405
+ );
406
+ }
407
+ const nativeModule = __require(path);
408
+ this.module = nativeModule;
409
+ this.modulePath = path;
410
+ }
411
+ /**
412
+ * Unload the native module and release resources.
413
+ */
414
+ async unload() {
415
+ this.module = null;
416
+ this.modulePath = null;
417
+ }
418
+ /**
419
+ * Check if the native module is loaded.
420
+ */
421
+ isLoaded() {
422
+ return this.module !== null;
423
+ }
424
+ /**
425
+ * Get the loaded napi-rs module.
426
+ *
427
+ * @throws Error if module is not loaded
428
+ */
429
+ getModule() {
430
+ if (!this.module) {
431
+ throw new Error("FFI not loaded. Call load() first.");
432
+ }
433
+ return this.module;
434
+ }
435
+ /**
436
+ * Backward-compatible alias for getModule().
437
+ *
438
+ * @deprecated Use getModule() instead
439
+ */
440
+ getRuntime() {
441
+ return this.getModule();
442
+ }
443
+ /**
444
+ * Get the path to the loaded module.
445
+ */
446
+ getModulePath() {
447
+ return this.modulePath;
448
+ }
449
+ /**
450
+ * Find the napi-rs module path.
451
+ *
452
+ * Resolution order:
453
+ * 1. TASKER_FFI_MODULE_PATH environment variable (explicit override, for unusual setups)
454
+ * 2. Bundled .node file in package directory (standard path — `napi build --platform` places it here)
455
+ */
456
+ static findModulePath() {
457
+ const envPath = process.env.TASKER_FFI_MODULE_PATH;
458
+ if (envPath) {
459
+ if (!existsSync(envPath)) {
460
+ console.warn(`TASKER_FFI_MODULE_PATH is set to "${envPath}" but the file does not exist`);
461
+ return null;
462
+ }
463
+ return envPath;
464
+ }
465
+ const bundledPath = findBundledNodeModule();
466
+ if (bundledPath && existsSync(bundledPath)) {
467
+ return bundledPath;
468
+ }
469
+ return null;
470
+ }
471
+ /**
472
+ * Backward-compatible alias for findModulePath().
473
+ *
474
+ * @deprecated Use findModulePath() instead
475
+ */
476
+ static findLibraryPath(_callerDir) {
477
+ return _FfiLayer.findModulePath();
478
+ }
479
+ discoverModulePath() {
480
+ return _FfiLayer.findModulePath();
481
+ }
482
+ };
483
+ var BUNDLED_NODE_MODULES = {
484
+ "linux-x64": "tasker_ts.linux-x64-gnu.node",
485
+ "darwin-arm64": "tasker_ts.darwin-arm64.node",
486
+ "darwin-x64": "tasker_ts.darwin-x64.node"
487
+ };
488
+ function findBundledNodeModule() {
489
+ const key = `${process.platform}-${process.arch}`;
490
+ const filename = BUNDLED_NODE_MODULES[key];
491
+ if (!filename) {
492
+ return null;
493
+ }
494
+ let dir = dirname(fileURLToPath(import.meta.url));
495
+ for (let i = 0; i < 5; i++) {
496
+ const candidate = join(dir, filename);
497
+ if (existsSync(candidate)) return candidate;
498
+ const nativeCandidate = join(dir, "native", filename);
499
+ if (existsSync(nativeCandidate)) return nativeCandidate;
500
+ const parent = dirname(dir);
501
+ if (parent === dir) break;
502
+ dir = parent;
503
+ }
504
+ return null;
505
+ }
381
506
 
382
507
  // src/client/index.ts
383
508
  var TaskerClientError = class extends Error {
@@ -398,7 +523,7 @@ var TaskerClient = class {
398
523
  * Create a task via the orchestration API.
399
524
  *
400
525
  * @param options - Task creation options (only `name` is required)
401
- * @returns Typed task response data
526
+ * @returns Task response with execution context and step readiness
402
527
  * @throws TaskerClientError if the operation fails
403
528
  */
404
529
  createTask(options) {
@@ -425,7 +550,7 @@ var TaskerClient = class {
425
550
  * Get a task by UUID.
426
551
  *
427
552
  * @param taskUuid - The task UUID
428
- * @returns Typed task response data
553
+ * @returns Task response with execution context and step readiness
429
554
  * @throws TaskerClientError if the operation fails
430
555
  */
431
556
  getTask(taskUuid) {
@@ -436,7 +561,7 @@ var TaskerClient = class {
436
561
  * List tasks with optional filtering and pagination.
437
562
  *
438
563
  * @param options - Filtering and pagination options
439
- * @returns Typed task list response data
564
+ * @returns Task list with pagination metadata
440
565
  * @throws TaskerClientError if the operation fails
441
566
  */
442
567
  listTasks(options = {}) {
@@ -463,7 +588,7 @@ var TaskerClient = class {
463
588
  * List workflow steps for a task.
464
589
  *
465
590
  * @param taskUuid - The task UUID
466
- * @returns Array of step data
591
+ * @returns Array of step responses with readiness information
467
592
  * @throws TaskerClientError if the operation fails
468
593
  */
469
594
  listTaskSteps(taskUuid) {
@@ -475,7 +600,7 @@ var TaskerClient = class {
475
600
  *
476
601
  * @param taskUuid - The task UUID
477
602
  * @param stepUuid - The step UUID
478
- * @returns Typed step response data
603
+ * @returns Step response with readiness information
479
604
  * @throws TaskerClientError if the operation fails
480
605
  */
481
606
  getStep(taskUuid, stepUuid) {
@@ -487,7 +612,7 @@ var TaskerClient = class {
487
612
  *
488
613
  * @param taskUuid - The task UUID
489
614
  * @param stepUuid - The step UUID
490
- * @returns Array of audit history entries
615
+ * @returns Array of audit history entries with attribution context
491
616
  * @throws TaskerClientError if the operation fails
492
617
  */
493
618
  getStepAuditHistory(taskUuid, stepUuid) {
@@ -497,7 +622,7 @@ var TaskerClient = class {
497
622
  /**
498
623
  * Check orchestration API health.
499
624
  *
500
- * @returns Typed health response data
625
+ * @returns Health status from the orchestration API
501
626
  * @throws TaskerClientError if the operation fails
502
627
  */
503
628
  healthCheck() {
@@ -506,6 +631,11 @@ var TaskerClient = class {
506
631
  }
507
632
  /**
508
633
  * Unwrap a NapiClientResult envelope, throwing on error.
634
+ *
635
+ * The type parameter allows callers to assert the expected response shape.
636
+ * The actual data comes from Rust via serde_json serialization, so the
637
+ * runtime shape is guaranteed by the Rust type system — the cast here
638
+ * is safe at the FFI boundary.
509
639
  */
510
640
  unwrap(result) {
511
641
  if (!result.success) {
@@ -523,6 +653,26 @@ var TaskerClient = class {
523
653
  return this.ffiLayer.getModule();
524
654
  }
525
655
  };
656
+ var sharedFfiLayer = null;
657
+ var loadPromise = null;
658
+ async function getFfiLayer() {
659
+ if (sharedFfiLayer) {
660
+ return sharedFfiLayer;
661
+ }
662
+ if (!loadPromise) {
663
+ loadPromise = (async () => {
664
+ const layer = new FfiLayer();
665
+ await layer.load();
666
+ sharedFfiLayer = layer;
667
+ return layer;
668
+ })();
669
+ }
670
+ return loadPromise;
671
+ }
672
+ async function getTaskerClient() {
673
+ const ffiLayer = await getFfiLayer();
674
+ return new TaskerClient(ffiLayer);
675
+ }
526
676
 
527
677
  // src/events/event-names.ts
528
678
  var StepEventNames = {
@@ -2093,6 +2243,10 @@ var StepExecutionSubscriber = class {
2093
2243
  /**
2094
2244
  * Send a completion result to Rust via FFI and handle the response.
2095
2245
  *
2246
+ * If the primary FFI call fails (e.g. serialization error, unexpected
2247
+ * type mismatch), we construct a guaranteed-safe failure result so the
2248
+ * step is marked as permanently failed rather than silently lost.
2249
+ *
2096
2250
  * @returns true if the completion was accepted by Rust, false otherwise
2097
2251
  */
2098
2252
  async sendCompletionViaFfi(event, napiResult, isSuccess) {
@@ -2116,8 +2270,61 @@ var StepExecutionSubscriber = class {
2116
2270
  return false;
2117
2271
  } catch (error) {
2118
2272
  this.handleFfiError(event, error);
2119
- return false;
2273
+ return this.submitFallbackFailure(event, error);
2274
+ }
2275
+ }
2276
+ /**
2277
+ * Submit a minimal safe-failure result after the primary FFI call fails.
2278
+ *
2279
+ * Builds a primitive-only result guaranteed to serialize, then attempts
2280
+ * to submit it. Throws if the fallback is also rejected or fails,
2281
+ * since the step would otherwise be silently lost.
2282
+ */
2283
+ submitFallbackFailure(event, originalError) {
2284
+ const fallback = this.buildFfiSafeFailure(event, originalError);
2285
+ try {
2286
+ const accepted = this.module.completeStepEvent(event.eventId, fallback);
2287
+ if (accepted) {
2288
+ pinoLog.warn(
2289
+ { component: "subscriber", eventId: event.eventId },
2290
+ "FFI fallback failure submitted successfully"
2291
+ );
2292
+ return true;
2293
+ }
2294
+ this.throwOrphanedError(event, "rejected", originalError);
2295
+ } catch (fallbackError) {
2296
+ this.throwOrphanedError(event, "failed", originalError, fallbackError);
2297
+ }
2298
+ return false;
2299
+ }
2300
+ /**
2301
+ * Throw an error indicating a step has been orphaned (both primary and fallback failed).
2302
+ */
2303
+ throwOrphanedError(event, reason, originalError, fallbackError) {
2304
+ const originalMsg = originalError instanceof Error ? originalError.message : String(originalError);
2305
+ if (reason === "rejected") {
2306
+ pinoLog.error(
2307
+ { component: "subscriber", eventId: event.eventId, stepUuid: event.stepUuid },
2308
+ "FFI fallback submission also rejected - step is orphaned"
2309
+ );
2310
+ throw new Error(
2311
+ `Both primary and fallback FFI submissions rejected for event ${event.eventId}. Step ${event.stepUuid} is orphaned.`
2312
+ );
2120
2313
  }
2314
+ const fallbackMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
2315
+ pinoLog.error(
2316
+ {
2317
+ component: "subscriber",
2318
+ eventId: event.eventId,
2319
+ stepUuid: event.stepUuid,
2320
+ error: fallbackMsg,
2321
+ originalError: originalMsg
2322
+ },
2323
+ "FFI fallback also failed - step is orphaned"
2324
+ );
2325
+ throw new Error(
2326
+ `Both primary and fallback FFI submissions failed for event ${event.eventId}. Step ${event.stepUuid} is orphaned. Fallback: ${fallbackMsg}, Original: ${originalMsg}`
2327
+ );
2121
2328
  }
2122
2329
  /**
2123
2330
  * Handle successful FFI completion submission.
@@ -2177,6 +2384,36 @@ var StepExecutionSubscriber = class {
2177
2384
  error_message: error instanceof Error ? error.message : String(error)
2178
2385
  });
2179
2386
  }
2387
+ /**
2388
+ * Build a minimal NapiStepExecutionResult guaranteed to serialize through napi-rs.
2389
+ *
2390
+ * Uses only primitive values so there is zero chance of a secondary
2391
+ * serialization failure. The step will be marked as permanently failed
2392
+ * rather than silently lost.
2393
+ */
2394
+ buildFfiSafeFailure(event, error) {
2395
+ const errorMessage = error instanceof Error ? error.message : String(error);
2396
+ return {
2397
+ stepUuid: event.stepUuid,
2398
+ success: false,
2399
+ result: {},
2400
+ status: "error",
2401
+ metadata: {
2402
+ executionTimeMs: 0,
2403
+ retryable: false,
2404
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
2405
+ ...this.workerId != null && { workerId: this.workerId },
2406
+ custom: {
2407
+ ffi_serialization_error: errorMessage.substring(0, 500)
2408
+ }
2409
+ },
2410
+ error: {
2411
+ message: `FFI serialization failed: ${errorMessage}`.substring(0, 500),
2412
+ errorType: "FFI_SERIALIZATION_ERROR",
2413
+ retryable: false
2414
+ }
2415
+ };
2416
+ }
2180
2417
  };
2181
2418
 
2182
2419
  // src/events/event-system.ts
@@ -2322,131 +2559,6 @@ var EventSystem = class {
2322
2559
  };
2323
2560
  }
2324
2561
  };
2325
- var FfiLayer = class _FfiLayer {
2326
- module = null;
2327
- modulePath = null;
2328
- configuredModulePath;
2329
- constructor(config = {}) {
2330
- this.configuredModulePath = config.modulePath;
2331
- }
2332
- /**
2333
- * Load the napi-rs native module.
2334
- *
2335
- * @param customPath - Optional override for module path
2336
- * @throws Error if module not found or failed to load
2337
- */
2338
- async load(customPath) {
2339
- if (this.module) {
2340
- return;
2341
- }
2342
- const path = customPath ?? this.configuredModulePath ?? this.discoverModulePath();
2343
- if (!path) {
2344
- throw new Error(
2345
- `napi-rs native module not found. No bundled .node file matches this platform, and TASKER_FFI_MODULE_PATH is not set.
2346
- Current platform: ${process.platform}-${process.arch}
2347
- Supported: linux-x64, darwin-arm64
2348
- Override: export TASKER_FFI_MODULE_PATH=/path/to/tasker_ts.linux-x64-gnu.node`
2349
- );
2350
- }
2351
- const nativeModule = __require(path);
2352
- this.module = nativeModule;
2353
- this.modulePath = path;
2354
- }
2355
- /**
2356
- * Unload the native module and release resources.
2357
- */
2358
- async unload() {
2359
- this.module = null;
2360
- this.modulePath = null;
2361
- }
2362
- /**
2363
- * Check if the native module is loaded.
2364
- */
2365
- isLoaded() {
2366
- return this.module !== null;
2367
- }
2368
- /**
2369
- * Get the loaded napi-rs module.
2370
- *
2371
- * @throws Error if module is not loaded
2372
- */
2373
- getModule() {
2374
- if (!this.module) {
2375
- throw new Error("FFI not loaded. Call load() first.");
2376
- }
2377
- return this.module;
2378
- }
2379
- /**
2380
- * Backward-compatible alias for getModule().
2381
- *
2382
- * @deprecated Use getModule() instead
2383
- */
2384
- getRuntime() {
2385
- return this.getModule();
2386
- }
2387
- /**
2388
- * Get the path to the loaded module.
2389
- */
2390
- getModulePath() {
2391
- return this.modulePath;
2392
- }
2393
- /**
2394
- * Find the napi-rs module path.
2395
- *
2396
- * Resolution order:
2397
- * 1. TASKER_FFI_MODULE_PATH environment variable (explicit override, for unusual setups)
2398
- * 2. Bundled .node file in package directory (standard path — `napi build --platform` places it here)
2399
- */
2400
- static findModulePath() {
2401
- const envPath = process.env.TASKER_FFI_MODULE_PATH;
2402
- if (envPath) {
2403
- if (!existsSync(envPath)) {
2404
- console.warn(`TASKER_FFI_MODULE_PATH is set to "${envPath}" but the file does not exist`);
2405
- return null;
2406
- }
2407
- return envPath;
2408
- }
2409
- const bundledPath = findBundledNodeModule();
2410
- if (bundledPath && existsSync(bundledPath)) {
2411
- return bundledPath;
2412
- }
2413
- return null;
2414
- }
2415
- /**
2416
- * Backward-compatible alias for findModulePath().
2417
- *
2418
- * @deprecated Use findModulePath() instead
2419
- */
2420
- static findLibraryPath(_callerDir) {
2421
- return _FfiLayer.findModulePath();
2422
- }
2423
- discoverModulePath() {
2424
- return _FfiLayer.findModulePath();
2425
- }
2426
- };
2427
- var BUNDLED_NODE_MODULES = {
2428
- "linux-x64": "tasker_ts.linux-x64-gnu.node",
2429
- "darwin-arm64": "tasker_ts.darwin-arm64.node",
2430
- "darwin-x64": "tasker_ts.darwin-x64.node"
2431
- };
2432
- function findBundledNodeModule() {
2433
- const key = `${process.platform}-${process.arch}`;
2434
- const filename = BUNDLED_NODE_MODULES[key];
2435
- if (!filename) {
2436
- return null;
2437
- }
2438
- let dir = dirname(fileURLToPath(import.meta.url));
2439
- for (let i = 0; i < 5; i++) {
2440
- const candidate = join(dir, filename);
2441
- if (existsSync(candidate)) return candidate;
2442
- const nativeCandidate = join(dir, "native", filename);
2443
- if (existsSync(nativeCandidate)) return nativeCandidate;
2444
- const parent = dirname(dir);
2445
- if (parent === dir) break;
2446
- dir = parent;
2447
- }
2448
- return null;
2449
- }
2450
2562
 
2451
2563
  // src/handler/base.ts
2452
2564
  var StepHandler = class {
@@ -3355,6 +3467,22 @@ var BatchableMixin = class {
3355
3467
  * @returns BatchWorkerContext or null if not found
3356
3468
  */
3357
3469
  getBatchContext(context) {
3470
+ if (context.stepInputs && "cursor" in context.stepInputs) {
3471
+ const cursorData = context.stepInputs.cursor ?? {};
3472
+ const batchData2 = {
3473
+ batch_id: cursorData.batch_id,
3474
+ cursor_config: {
3475
+ start_cursor: cursorData.start_cursor,
3476
+ end_cursor: cursorData.end_cursor,
3477
+ step_size: cursorData.step_size ?? cursorData.batch_size ?? 1,
3478
+ metadata: cursorData.metadata ?? {}
3479
+ },
3480
+ batch_index: 0,
3481
+ total_batches: 1,
3482
+ batch_metadata: context.stepInputs.batch_metadata ?? {}
3483
+ };
3484
+ return createBatchWorkerContext(batchData2);
3485
+ }
3358
3486
  let batchData;
3359
3487
  if (context.stepConfig) {
3360
3488
  batchData = context.stepConfig.batch_context;
@@ -6458,6 +6586,6 @@ var WorkerServer = class {
6458
6586
  }
6459
6587
  };
6460
6588
 
6461
- export { APIMixin, ApiHandler, ApiResponse, BasePublisher, BaseSubscriber, BatchableMixin, ClassLookupResolver, Decision, DecisionHandler, DecisionMixin, DecisionType, DefaultPublisher, DuplicatePublisherError, ErrorType, EventNames, EventPoller, EventSystem, ExplicitMappingResolver, FfiLayer, HandlerRegistry, HandlerSystem, InProcessDomainEventPoller, MethodDispatchError, MethodDispatchWrapper, MetricsEventNames, NoResolverMatchError, PermanentError, PollerEventNames, PublisherNotFoundError, PublisherRegistry, PublisherValidationError, RegistryFrozenError, RegistryResolver, ResolutionError, ResolverChain, ResolverNotFoundError, RetryableError, ShutdownController, StepContext, StepEventNames, StepExecutionSubscriber, StepHandler, StepHandlerResult, SubscriberRegistry, TaskerClient, TaskerClientError, TaskerEventEmitter, WorkerEventNames, WorkerServer, aggregateBatchResults, applyAPI, applyBatchable, applyDecision, bootstrapWorker, createBatchWorkerContext, createBatches, createDomainEvent, createEventPoller, createFfiPollAdapter, createLogger, createStepEventContext, defineApiHandler, defineBatchAnalyzer, defineBatchWorker, defineDecisionHandler, defineHandler, effectiveMethod, ffiEventToDomainEvent, fromCallable, fromDto, getRustVersion, getVersion, getWorkerStatus, hasResolverHint, healthCheck, isCreateBatches, isNoBatches, isStandardErrorType, isTypicallyRetryable, isWorkerRunning, logDebug, logError, logInfo, logTrace, logWarn, noBatches, normalizeToDefinition, stopWorker, transitionToGracefulShutdown, usesMethodDispatch };
6589
+ export { APIMixin, ApiHandler, ApiResponse, BasePublisher, BaseSubscriber, BatchableMixin, ClassLookupResolver, Decision, DecisionHandler, DecisionMixin, DecisionType, DefaultPublisher, DuplicatePublisherError, ErrorType, EventNames, EventPoller, EventSystem, ExplicitMappingResolver, FfiLayer, HandlerRegistry, HandlerSystem, InProcessDomainEventPoller, MethodDispatchError, MethodDispatchWrapper, MetricsEventNames, NoResolverMatchError, PermanentError, PollerEventNames, PublisherNotFoundError, PublisherRegistry, PublisherValidationError, RegistryFrozenError, RegistryResolver, ResolutionError, ResolverChain, ResolverNotFoundError, RetryableError, ShutdownController, StepContext, StepEventNames, StepExecutionSubscriber, StepHandler, StepHandlerResult, SubscriberRegistry, TaskerClient, TaskerClientError, TaskerEventEmitter, WorkerEventNames, WorkerServer, aggregateBatchResults, applyAPI, applyBatchable, applyDecision, bootstrapWorker, createBatchWorkerContext, createBatches, createDomainEvent, createEventPoller, createFfiPollAdapter, createLogger, createStepEventContext, defineApiHandler, defineBatchAnalyzer, defineBatchWorker, defineDecisionHandler, defineHandler, effectiveMethod, ffiEventToDomainEvent, fromCallable, fromDto, getFfiLayer, getRustVersion, getTaskerClient, getVersion, getWorkerStatus, hasResolverHint, healthCheck, isCreateBatches, isNoBatches, isStandardErrorType, isTypicallyRetryable, isWorkerRunning, logDebug, logError, logInfo, logTrace, logWarn, noBatches, normalizeToDefinition, stopWorker, transitionToGracefulShutdown, usesMethodDispatch };
6462
6590
  //# sourceMappingURL=index.js.map
6463
6591
  //# sourceMappingURL=index.js.map