@decaf-ts/core 0.12.1 → 0.13.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.
- package/README.md +140 -1
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/lib/esm/index.d.ts +2 -1
- package/lib/esm/index.js +2 -2
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/migrations/Migration.d.ts +1 -1
- package/lib/esm/migrations/MigrationService.d.ts +45 -2
- package/lib/esm/migrations/MigrationService.js +436 -97
- package/lib/esm/migrations/MigrationService.js.map +1 -1
- package/lib/esm/migrations/MigrationTaskBuilder.d.ts +10 -0
- package/lib/esm/migrations/MigrationTaskBuilder.js +21 -0
- package/lib/esm/migrations/MigrationTaskBuilder.js.map +1 -0
- package/lib/esm/migrations/MigrationTasks.js +10 -4
- package/lib/esm/migrations/MigrationTasks.js.map +1 -1
- package/lib/esm/migrations/MigrationVersioning.d.ts +7 -0
- package/lib/esm/migrations/MigrationVersioning.js +2 -0
- package/lib/esm/migrations/MigrationVersioning.js.map +1 -0
- package/lib/esm/migrations/SemverMigrationVersioning.d.ts +10 -0
- package/lib/esm/migrations/SemverMigrationVersioning.js +43 -0
- package/lib/esm/migrations/SemverMigrationVersioning.js.map +1 -0
- package/lib/esm/migrations/StandardMigrationVersioning.d.ts +12 -0
- package/lib/esm/migrations/StandardMigrationVersioning.js +27 -0
- package/lib/esm/migrations/StandardMigrationVersioning.js.map +1 -0
- package/lib/esm/migrations/decorators.d.ts +5 -0
- package/lib/esm/migrations/decorators.js +42 -11
- package/lib/esm/migrations/decorators.js.map +1 -1
- package/lib/esm/migrations/index.d.ts +3 -0
- package/lib/esm/migrations/index.js +3 -0
- package/lib/esm/migrations/index.js.map +1 -1
- package/lib/esm/migrations/types.d.ts +25 -2
- package/lib/esm/persistence/Adapter.d.ts +1 -1
- package/lib/esm/persistence/constants.js +6 -1
- package/lib/esm/persistence/constants.js.map +1 -1
- package/lib/esm/query/Paginator.js +16 -5
- package/lib/esm/query/Paginator.js.map +1 -1
- package/lib/esm/query/constants.d.ts +1 -0
- package/lib/esm/query/constants.js +1 -0
- package/lib/esm/query/constants.js.map +1 -1
- package/lib/esm/query/decorators.js +1 -1
- package/lib/esm/query/decorators.js.map +1 -1
- package/lib/esm/repository/Repository.d.ts +1 -0
- package/lib/esm/repository/Repository.js +51 -0
- package/lib/esm/repository/Repository.js.map +1 -1
- package/lib/esm/services/ModelService.d.ts +1 -0
- package/lib/esm/services/ModelService.js +4 -0
- package/lib/esm/services/ModelService.js.map +1 -1
- package/lib/esm/tasks/TaskService.d.ts +1 -0
- package/lib/esm/tasks/TaskService.js +4 -0
- package/lib/esm/tasks/TaskService.js.map +1 -1
- package/lib/index.cjs +2 -2
- package/lib/index.d.ts +2 -1
- package/lib/index.js.map +1 -1
- package/lib/migrations/Migration.d.ts +1 -1
- package/lib/migrations/MigrationService.cjs +436 -97
- package/lib/migrations/MigrationService.d.ts +45 -2
- package/lib/migrations/MigrationService.js.map +1 -1
- package/lib/migrations/MigrationTaskBuilder.cjs +25 -0
- package/lib/migrations/MigrationTaskBuilder.d.ts +10 -0
- package/lib/migrations/MigrationTaskBuilder.js.map +1 -0
- package/lib/migrations/MigrationTasks.cjs +10 -4
- package/lib/migrations/MigrationTasks.js.map +1 -1
- package/lib/migrations/MigrationVersioning.cjs +3 -0
- package/lib/migrations/MigrationVersioning.d.ts +7 -0
- package/lib/migrations/MigrationVersioning.js.map +1 -0
- package/lib/migrations/SemverMigrationVersioning.cjs +80 -0
- package/lib/migrations/SemverMigrationVersioning.d.ts +10 -0
- package/lib/migrations/SemverMigrationVersioning.js.map +1 -0
- package/lib/migrations/StandardMigrationVersioning.cjs +31 -0
- package/lib/migrations/StandardMigrationVersioning.d.ts +12 -0
- package/lib/migrations/StandardMigrationVersioning.js.map +1 -0
- package/lib/migrations/decorators.cjs +42 -11
- package/lib/migrations/decorators.d.ts +5 -0
- package/lib/migrations/decorators.js.map +1 -1
- package/lib/migrations/index.cjs +3 -0
- package/lib/migrations/index.d.ts +3 -0
- package/lib/migrations/index.js.map +1 -1
- package/lib/migrations/types.d.ts +25 -2
- package/lib/persistence/Adapter.d.ts +1 -1
- package/lib/persistence/constants.cjs +6 -1
- package/lib/persistence/constants.js.map +1 -1
- package/lib/query/Paginator.cjs +16 -5
- package/lib/query/Paginator.js.map +1 -1
- package/lib/query/constants.cjs +1 -0
- package/lib/query/constants.d.ts +1 -0
- package/lib/query/constants.js.map +1 -1
- package/lib/query/decorators.cjs +1 -1
- package/lib/query/decorators.js.map +1 -1
- package/lib/repository/Repository.cjs +51 -0
- package/lib/repository/Repository.d.ts +1 -0
- package/lib/repository/Repository.js.map +1 -1
- package/lib/services/ModelService.cjs +4 -0
- package/lib/services/ModelService.d.ts +1 -0
- package/lib/services/ModelService.js.map +1 -1
- package/lib/tasks/TaskService.cjs +4 -0
- package/lib/tasks/TaskService.d.ts +1 -0
- package/lib/tasks/TaskService.js.map +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Decaf Core provides the foundational building blocks for the Decaf TypeScript ec
|
|
|
39
39
|
|
|
40
40
|
Documentation [here](https://decaf-ts.github.io/injectable-decorators/), Test results [here](https://decaf-ts.github.io/injectable-decorators/workdocs/reports/html/test-report.html) and Coverage [here](https://decaf-ts.github.io/injectable-decorators/workdocs/reports/coverage/lcov-report/index.html)
|
|
41
41
|
|
|
42
|
-
Minimal size:
|
|
42
|
+
Minimal size: 45.9 KB kb gzipped
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
# Core Package — Detailed Description
|
|
@@ -420,6 +420,145 @@ const taskEngine = new TaskEngine({
|
|
|
420
420
|
await taskEngine.start();
|
|
421
421
|
```
|
|
422
422
|
|
|
423
|
+
### Task Engine configuration reference
|
|
424
|
+
|
|
425
|
+
`TaskEngineConfig` exposes every knob used by the engine to claim, lease, and log tasks. The full set of options is:
|
|
426
|
+
|
|
427
|
+
| Option | Description |
|
|
428
|
+
| --- | --- |
|
|
429
|
+
| `adapter` | The persistence adapter where `TaskModel` rows live. When migrations run via the CLI this is a dedicated `RamAdapter`; never reuse an alias that is also a migration target. |
|
|
430
|
+
| `overrides` | Passed to `adapter.for(...)` when a task needs custom flags (for example to seed identity metadata). |
|
|
431
|
+
| `registry` | `TaskHandlerRegistry` wiring classification strings to handler instances. Only registered handlers can run. |
|
|
432
|
+
| `bus` | Optional `TaskEventBus` that receives progress/log/status events. |
|
|
433
|
+
| `workerId` | Uniquely identifies the worker claiming leases. Each engine (including CLI migrations) must use a different `workerId` so leases do not clash. |
|
|
434
|
+
| `concurrency` | Number of work units to execute in parallel (set to `1` when migration steps must stay sequential). |
|
|
435
|
+
| `leaseMs` | How long a running task can go without a heartbeat before it is re-queued. |
|
|
436
|
+
| `pollMsIdle` | Poll interval when the queue is empty. |
|
|
437
|
+
| `pollMsBusy` | Poll interval while tasks are running (shorter than `pollMsIdle`). |
|
|
438
|
+
| `logTailMax` | Maximum log entries kept in memory before flushing to the bus. |
|
|
439
|
+
| `streamBufferSize` | Byte size of the stream buffer used for large log payloads. |
|
|
440
|
+
| `maxLoggingBuffer` | Upper limit (in bytes) for buffered logs before older entries are pruned. |
|
|
441
|
+
| `loggingBufferTruncation` | Percentage of the buffer kept when `maxLoggingBuffer` is reached; the rest gets truncated. |
|
|
442
|
+
| `gracefulShutdownMsTimeout` | Time (ms) `TaskEngine.shutdown()` waits for in-flight workers before forcing a stop. |
|
|
443
|
+
| `autoShutdown` | Optional backoff configuration (`enabled`, `backoffStepMs`, `maxIdleDelayMs`) that gradually raises `pollMsIdle` until the engine stops once the queue drains. |
|
|
444
|
+
|
|
445
|
+
`TaskContext` enriches every handler callback with helpers such as:
|
|
446
|
+
|
|
447
|
+
- `progress(payload)`: emit structured progress updates (`TaskEventType.PROGRESS`).
|
|
448
|
+
- `pipe(...log)` and `flush()`: buffer logs that eventually feed into `TaskEventType.LOG`.
|
|
449
|
+
- `heartbeat()`: extend the lease before it expires (used in long-running handlers).
|
|
450
|
+
- `scheduleCompositeSteps(...)`: dynamically insert extra steps when building migration tasks.
|
|
451
|
+
|
|
452
|
+
### Task Engine migration guardrails
|
|
453
|
+
|
|
454
|
+
When migrations run through a TaskService-backed engine the adapter alias must be dedicated to the migration queue (e.g., `decaf-cli-task-engine`). `MigrationService.migrateAdapters` enforces this by comparing every adapter alias/flavour and rejecting any run that would reuse the task engine alias as a migration target. Keeping the task queue isolated prevents lease metadata from colliding with schema updates.
|
|
455
|
+
|
|
456
|
+
Tune the knobs above with migrations in mind:
|
|
457
|
+
- Keep `concurrency` at `1` so versions apply sequentially.
|
|
458
|
+
- Increase `leaseMs` slightly above your longest expected step so long-running migrations do not get re-claimed prematurely.
|
|
459
|
+
- Use `pollMsIdle`/`pollMsBusy` to control how aggressively the engine polls when the queue is empty or busy; CLI runners typically lower `pollMsBusy`.
|
|
460
|
+
- `logTailMax`, `streamBufferSize`, `maxLoggingBuffer`, and `loggingBufferTruncation` keep migration logs bounded; the CLI attaches a `TaskEventBus` so progress/state logs flush before shutdown.
|
|
461
|
+
- `autoShutdown` gradually raises `pollMsIdle` so CLI runners stop after every tracked task completes.
|
|
462
|
+
|
|
463
|
+
### Task Engine task-mode and TaskService
|
|
464
|
+
|
|
465
|
+
Migration orchestration often runs inside `TaskService`. Typical setup:
|
|
466
|
+
|
|
467
|
+
1. Create a dedicated `Adapter`, e.g. `new RamAdapter({}, "decaf-cli-task-engine")`, and boot it before starting the `TaskService`.
|
|
468
|
+
2. `await new TaskService().boot({ adapter: taskEngineAdapter })` to power the `TaskHandlerRegistry` and `TaskTracker`.
|
|
469
|
+
3. Pass the `TaskService` instance into `MigrationService.migrateAdapters(..., { taskMode: true, taskService })`.
|
|
470
|
+
|
|
471
|
+
`TaskService.boot` mirrors `TaskEngineConfig`: you can also supply `registry`, `bus`, or custom `overrides`, and the service builds the engine, event bus, and tracker registry. The CLI attaches a migration-only `TaskHandlerRegistry` so the worker never executes unrelated handlers.
|
|
472
|
+
|
|
473
|
+
The CLI already follows this pattern and explicitly prevents the task engine adapter alias from appearing inside the migrating aliases, which keeps persistence targets isolated. When `taskMode` is true, every migration version produces a `CompositeTask`; use `migration.track()` or `taskService.track(id)` to attach listeners so progress/status events flow through the command logger.
|
|
474
|
+
|
|
475
|
+
`TaskService.track(id)` wires the CLI logger to the matching `TaskTracker` so status/progress logs stream through your console before `TaskTracker.wait()` resolves. If a migration task fails, call `MigrationService.retry(taskId)`—it uses repository overrides to reset `status` to `PENDING`, clear `error`/lease metadata, and re-queue the work—then `taskService.track(id)` again so the TaskEngine reclaims it.
|
|
476
|
+
|
|
477
|
+
Composite tasks are ordered by the sequence you pass to `CompositeTaskBuilder` or by using the `dependsOn`/`dependencies` array. Each step has a `classification` (matching a handler), an optional `name`, and `lock`/`dependsOn` metadata (`TaskStepSpecModel`). Locks avoid concurrent execution, and dependencies support either `<taskId>` or `<taskId>:<stepRef>` shorthand so you can mix tasks and steps as prerequisites.
|
|
478
|
+
|
|
479
|
+
Task attempts are bounded by `maxAttempts` and `backoff` (configured via builders). The engine records each attempt and automatically escalates to `WAITING_RETRY`/`RUNNING` states; if a task exhausts retries, the service surfaces the final error via `TaskTracker.wait()` so your migration command can decide between retrying or aborting.
|
|
480
|
+
|
|
481
|
+
## Migration System
|
|
482
|
+
|
|
483
|
+
`MigrationService` is the canonical upgrade runner. Use `MigrationService.migrateAdapters(adapters, config)` or `DecafCoreModule.migrate(config)` once your persistence layer is booted, but remember that live verification expects each migration to add a required column/property and backfill existing records before moving to the next version.
|
|
484
|
+
|
|
485
|
+
### Migration configuration reference
|
|
486
|
+
|
|
487
|
+
`MigrationService` speaks the `MigrationConfig` / `PersistenceMigrationConfig` language:
|
|
488
|
+
|
|
489
|
+
- `persistMigrationSteps`: keep track of every migration run (defaults to `true`).
|
|
490
|
+
- `persistenceFlavour`: restricts the execution plan to a single adapter flavour alias.
|
|
491
|
+
- `targetVersion`: semver/string goal for this run (CLI defaults to `package.json.version`).
|
|
492
|
+
- `taskMode`: when `true`, migrations are executed through the TaskService as `CompositeTask`s built per version. When `false`, `executeMigration` runs each migration inline.
|
|
493
|
+
- `includeGenericInTaskMode`: when `false` (the default for multi-adapter runs), only flavour-scoped migrations execute inside tasks so generic migrations stay in relational mode.
|
|
494
|
+
- `retrieveLastVersion` / `setCurrentVersion`: asynchronous handlers so each adapter can persist its own migration head. `retrieveLastVersion` is called prior to building the execution plan; `setCurrentVersion` runs after every successfully completed version (per task in task mode, once at the end in normal mode).
|
|
495
|
+
- `taskService`: required when `taskMode` is enabled; the CLI boots a `TaskService` backed by a dedicated `RamAdapter` (`decaf-cli-task-engine`).
|
|
496
|
+
- `versioning`: override the default npm-semver comparator (`MigrationVersioning`) if you deploy a non-semver scheme.
|
|
497
|
+
- `handlers`: per-flavour overrides (typically wired via the CLI defaults) for `retrieveLastVersion`/`setCurrentVersion` if you need special persistence beyond the default adapter cache.
|
|
498
|
+
- `dryRun`: compatibility flag that is parsed but does not alter runtime behaviour anymore; the migrations still execute against your database.
|
|
499
|
+
|
|
500
|
+
Example handlers:
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
handlers: {
|
|
504
|
+
nano: {
|
|
505
|
+
async retrieveLastVersion(adapter) {
|
|
506
|
+
return (await new VersionRepo(adapter).read("nano"))?.version;
|
|
507
|
+
},
|
|
508
|
+
async setCurrentVersion(version, adapter) {
|
|
509
|
+
await new VersionRepo(adapter).upsert("nano", { version });
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Version gating and lifecycle progression
|
|
516
|
+
|
|
517
|
+
`MigrationService` consults `retrieveLastVersion` before building the execution plan so it always knows the persisted `currentVersion`. Only migrations whose normalized versions fall strictly greater than that value and less than or equal to the `targetVersion` (CLI `--to`) are scheduled, ensuring each run advances the system lifecycle. After every version completes successfully, `setCurrentVersion` records the new head so subsequent boots skip already applied hops; when the stored version already matches the target, the filtering logic yields an empty plan and the migration run is a no-op.
|
|
518
|
+
|
|
519
|
+
Use `MigrationService.migrateAdapters([nanoAdapter, typeormAdapter], config)` with `taskMode: true` and the appropriate handlers to queue each version with the TaskService, then call `migration.track()` to wait on each version.
|
|
520
|
+
|
|
521
|
+
### `@migration` metadata and precedence control
|
|
522
|
+
|
|
523
|
+
Each migration class must be decorated with `@migration(...)`. The decorator accepts multiple overloads, but all forms populate the metadata that `MigrationService.sort()` uses to build a deterministic plan:
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
@migration("1.1.0-add-isActive", {
|
|
527
|
+
precedence: "1.1.0",
|
|
528
|
+
flavour: "nano",
|
|
529
|
+
rules: [
|
|
530
|
+
async (_, adapter) => Boolean(await adapter.exists("user")),
|
|
531
|
+
],
|
|
532
|
+
})
|
|
533
|
+
export class AddIsActiveMigration implements Migration<any, NanoAdapter> { ... }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
- `reference`: required string used for logging, dependency hints, and version normalization (typically the semver value).
|
|
537
|
+
- `precedence`: optional hint that can be a `Migration` constructor, string token, or object referencing another migration. `MigrationService.extractPrecedenceTokens` reads it to break ties when migrations share the same version and flavour; use it to force ordering between otherwise identical migrations.
|
|
538
|
+
- `flavour`: optional adapter flavour alias (e.g., `"nano"`, `"type-orm"`). Migrations are only considered when `targetFlavour` matches or (when `includeGeneric` is `true`) when a generic migration declares `DefaultFlavour`.
|
|
539
|
+
- `rules`: optional array of async predicates `(qr, adapter, ctx)` that gate whether the migration should run. If any rule returns `false`, the migration is skipped.
|
|
540
|
+
|
|
541
|
+
`MigrationService.sort()` first compares normalized versions (`normalize()` via `MigrationVersioning`), then uses `compareByPrecedence`, and finally falls back to flavour/reference lexicographic ordering. If two migrations share version/flavour and have conflicting precedence, an explicit `InternalError` is thrown so you can clarify the ordering.
|
|
542
|
+
|
|
543
|
+
### Version tracking, task mode, and resume semantics
|
|
544
|
+
|
|
545
|
+
`MigrationService` starts by calling `retrieveLastVersion` (when provided) to determine the persisted `currentVersion`. It builds an execution plan by filtering all decorated migrations whose normalized versions fall strictly greater than `currentVersion` and less than or equal to `targetVersion`.
|
|
546
|
+
|
|
547
|
+
In **normal mode**, `migrateNormally` executes each migration with `executeMigration`. After the last migration succeeds, `setCurrentVersion` is invoked once with the last version so the next boot knows where to resume.
|
|
548
|
+
|
|
549
|
+
In **task mode**, `migrateViaTasks` uses `MigrationTaskBuilder` (a `CompositeTaskBuilder` wrapper) to queue one `TaskModel` per version. Each queued task depends on the previous one (the CLI attaches the dependency chain automatically), and `MigrationService.track()` waits for the `TaskTracker` of each version to finish. Immediately after each task resolves, `track()` calls `setCurrentVersion` for that version (using `this.queuedTaskChain` to map task IDs to versions). This per-version update ensures that, after a crash, re-running the CLI will call `retrieveLastVersion` and resume at the correct position.
|
|
550
|
+
|
|
551
|
+
By design `setCurrentVersion` executes only after a version completely finishes: inline (`taskMode: false`) runs update at the end of the migration batch, and task mode updates after every `CompositeTask`. That means the recorded `currentVersion` always equals the last fully successful hop, so `retrieveLastVersion` can skip already applied versions and start at the next semantic bump. If a version fails mid-task, the version does not advance, and rerunning `MigrationService.retry()` or re-launching the CLI will re-queue the failed version before moving on.
|
|
552
|
+
|
|
553
|
+
If a task fails or is canceled, call `MigrationService.retry(taskId)`:
|
|
554
|
+
|
|
555
|
+
1. `retry` checks for explicit IDs, pending context IDs (`Context.pending(PersistenceKeys.MIGRATION)`), or the queued chain.
|
|
556
|
+
2. It queries the TaskRepository (with `ignoreHandlers: true`) and rewrites the `TaskModel` to `status = PENDING`, clears `error`, `leaseOwner`, and timestamps so the TaskEngine can reclaim it.
|
|
557
|
+
|
|
558
|
+
If you want to rerun an entire migration from scratch, omit `taskIds` and let `retry()` call `migrateViaTasks` again.
|
|
559
|
+
|
|
560
|
+
`MigrationService` rejects any configuration where the task engine adapter alias is also part of the migrating adapters; keeping the TaskService on a separate `RamAdapter` ensures migrations can persist their schema changes without racing the tasks that perform them.
|
|
561
|
+
|
|
423
562
|
## Advanced Repository Features
|
|
424
563
|
|
|
425
564
|
The `Repository` class now includes several high-level methods for common query patterns, simplifying data access.
|