@xeplr/actions 1.1.14 → 1.1.15

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 CHANGED
@@ -184,13 +184,16 @@ await actions.uploader.upload({
184
184
  columns, // optional [{ name, type }] — declared; skips inference
185
185
  batchSize: 5000, firstBatchScanRows: 1000,
186
186
  metaStore, mtId1, mtId2, mtId3, mtId4, details,
187
- errorTable, batchDir, keepBatchFiles, onProgress
187
+ errorTable, batchDir, keepBatchFiles, onProgress,
188
+ signal // optional AbortSignal: stop on request
188
189
  })
189
190
  // → { movementId, tables: { main, errors }, columns, totalRows, totalBatches, completed, dropped, aborted, durationMs }
190
191
  ```
191
192
 
192
193
  `onProgress({ rowsRead, batches, rowsWritten, phase? })` fires after each batch is read, with `rowsWritten` from the write queue's own count, so a caller can show both and see whether writing keeps up. After the read, while the queue drains, it fires again each time the written count moves (`phase: 'writing'`), never on a bare timer, so a stuck write goes quiet.
193
194
 
195
+ `signal` (an `AbortSignal`, passed to db-move as `system.signal`) stops a running movement: the writes still queued are dropped at once (in-flight ones settle), the read stops at the next batch, and it ends with an error whose `code` is `'STOPPED'` (the runner keeps `error.code`), neither a failure nor a success. What it wrote stays, under its `movementId`, for the caller to roll back or keep.
196
+
194
197
  Rows are spooled to NDJSON files on disk between source and database. From the first batch it creates, if missing: the target table, `<targetTable>_import_errors` (`movement_id`, `row_num`, `error_description`, `underlying_sql`, `raw_row`, `recorded_at`), a `__xeplr_movement_id__` column, and a unique index on `primaryKeys`. Server dialects also add `__xeplr_id__`; DuckDB does not.
195
198
 
196
199
  Logical types and what each driver creates:
@@ -312,7 +315,7 @@ node --test test/uploader/reconcile.test.js
312
315
 
313
316
  | needs nothing | needs a database |
314
317
  |---|---|
315
- | `test/procedure-call.test.js`, `test/streaming/spool.test.js`, `test/uploader/reconcile.test.js`, `test/write-progress.test.js`, `test/formats/csv-gnarly.test.js`, `test/drivers/mysql.test.js`, `test/drivers/mssql.test.js`, `test/drivers/query-columns.test.js`, `test/builtins/email.test.js` (loads the package root, so `@xeplr/db` must be resolvable) | Postgres at `localhost:5435` (`PG_PASSWORD`, default `postgres`; database `xeplr_actions_test`): `test/drivers/postgres.test.js`, `test/builtins/db-push.test.js`, `test/builtins/file-upload.test.js`, `test/uploader/upload.test.js`; plus `@xeplr/db` for `test/uploader/meta-store-knex.test.js` |
318
+ | `test/procedure-call.test.js`, `test/streaming/spool.test.js`, `test/uploader/reconcile.test.js`, `test/write-progress.test.js`, `test/stop-movement.test.js`, `test/formats/csv-gnarly.test.js`, `test/drivers/mysql.test.js`, `test/drivers/mssql.test.js`, `test/drivers/query-columns.test.js`, `test/builtins/email.test.js` (loads the package root, so `@xeplr/db` must be resolvable) | Postgres at `localhost:5435` (`PG_PASSWORD`, default `postgres`; database `xeplr_actions_test`): `test/drivers/postgres.test.js`, `test/builtins/db-push.test.js`, `test/builtins/file-upload.test.js`, `test/uploader/upload.test.js`; plus `@xeplr/db` for `test/uploader/meta-store-knex.test.js` |
316
319
  | `test/drivers/duckdb.test.js`, `test/drivers/duckdb-values.test.js` (needs `@duckdb/node-api`) | MySQL `localhost:3306` (`MYSQL_HOST/PORT/USER/PASSWORD`): `test/drivers/mysql-integration.test.js` |
317
320
  | | SQL Server `localhost:1433` (`MSSQL_HOST/PORT/USER/PASSWORD`): `test/drivers/mssql-integration.test.js` |
318
321
  | | Postgres + MySQL + SQL Server (`PG_*`, `MYSQL_*`, `MSSQL_*`): `test/builtins/db-replication.test.js` |
@@ -304,7 +304,9 @@ module.exports = {
304
304
  // Passed through from the caller's `system`, not from `input` — a
305
305
  // progress reporter is a function, and an action's input is data that
306
306
  // has to survive being stored in a job row.
307
- onProgress: system.onProgress || null
307
+ onProgress: system.onProgress || null,
308
+ // An AbortSignal from the caller: stop reading, drop the queued writes.
309
+ signal: system.signal || null
308
310
  });
309
311
 
310
312
  // Every count upload() has, not just the headline one — "read" and
package/lib/runner.js CHANGED
@@ -109,6 +109,8 @@ function finish(start, status, output, err) {
109
109
  if (err.stack) e.stack = err.stack;
110
110
  if (err.details) e.details = err.details;
111
111
  if (err.actionName) e.actionName = err.actionName;
112
+ // Kept, so a caller can tell a movement STOPPED on request from one that failed.
113
+ if (err.code) e.code = err.code;
112
114
  }
113
115
  return { status: status, output: output, error: e, durationMs: Date.now() - start };
114
116
  }
@@ -77,6 +77,14 @@ async function upload(opts) {
77
77
  var spoolResult = null;
78
78
  var runDir = null;
79
79
 
80
+ // STOPPING ON REQUEST (opts.signal, an AbortSignal). The writes still queued
81
+ // are dropped the moment it fires — in-flight ones settle — and the read
82
+ // stops at the next batch. What was written stays, identified by
83
+ // movementId, for the caller to roll back or keep.
84
+ if (opts.signal && opts.signal.aborted) throw stoppedError();
85
+ var onStop = opts.signal ? function() { opts.queue.abort(opts.movementId, 'stopped'); } : null;
86
+ if (onStop) opts.signal.addEventListener('abort', onStop, { once: true });
87
+
80
88
  try {
81
89
  var summary = await spool(opts.source, {
82
90
  runId: opts.movementId,
@@ -131,6 +139,7 @@ async function upload(opts) {
131
139
  // in turn awaits spool's next batch — the whole pipeline throttles
132
140
  // correctly, but disk (not memory) is the buffer.
133
141
  onBatch: async function(rows, meta) {
142
+ if (opts.signal && opts.signal.aborted) throw stoppedError();
134
143
  if (!columns) throw new Error('uploader: onBatch before beforeAll — spool contract violated');
135
144
  for (var off = 0; off < rows.length; off += effectiveBatchSize) {
136
145
  var chunk = rows.slice(off, off + effectiveBatchSize);
@@ -207,6 +216,8 @@ async function upload(opts) {
207
216
  } finally {
208
217
  if (drainWatch) drainWatch.stop();
209
218
  }
219
+ // Stopped while the queue drained: not a finished movement.
220
+ if (opts.signal && opts.signal.aborted) throw stoppedError();
210
221
 
211
222
  var stats = opts.queue.stats(opts.movementId) || {};
212
223
  var status = stats.aborted ? 'aborted' : 'completed';
@@ -236,9 +247,17 @@ async function upload(opts) {
236
247
  try { await fsp.rm(runDir, { recursive: true, force: true }); } catch (_) {}
237
248
  }
238
249
  if (!poolIsExternal && pool) await driver.close(pool);
250
+ if (onStop) opts.signal.removeEventListener('abort', onStop);
239
251
  }
240
252
  }
241
253
 
254
+ /** The error a stopped movement ends with: code 'STOPPED', so a caller can tell it from a failure. */
255
+ function stoppedError() {
256
+ var err = new Error('Stopped by request');
257
+ err.code = 'STOPPED';
258
+ return err;
259
+ }
260
+
242
261
  // Rollback a movement — drop from main + error tables, clear meta row.
243
262
  // Graceful queue abort first (in-flight SQLs allowed to settle).
244
263
  //
@@ -406,6 +425,7 @@ var { inferColumnType } = require('./infer');
406
425
  module.exports = {
407
426
  upload: upload,
408
427
  watchWrites: watchWrites,
428
+ stoppedError: stoppedError,
409
429
  rollback: rollback,
410
430
  inferColumns: inferColumns,
411
431
  inferColumnType: inferColumnType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/actions",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "Named-action registry with typed inputs and safe invocation — plus built-in actions for HTTP, DB, files, email, spawn",
5
5
  "main": "index.js",
6
6
  "files": [