@fullstackhouse/open-mercato-data-sync-durable 0.1.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 +86 -0
- package/dist/engine/durable-run.js +130 -0
- package/dist/engine/durable-run.js.map +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/kinds/data-sync-run.js +57 -0
- package/dist/kinds/data-sync-run.js.map +7 -0
- package/dist/mirror-version.js +5 -0
- package/dist/mirror-version.js.map +7 -0
- package/dist/mirror.manifest.json +65 -0
- package/dist/modules/data_sync/acl.js +8 -0
- package/dist/modules/data_sync/acl.js.map +7 -0
- package/dist/modules/data_sync/api/mappings/[id]/route.js +17 -0
- package/dist/modules/data_sync/api/mappings/[id]/route.js.map +7 -0
- package/dist/modules/data_sync/api/mappings/route.js +15 -0
- package/dist/modules/data_sync/api/mappings/route.js.map +7 -0
- package/dist/modules/data_sync/api/options.js +13 -0
- package/dist/modules/data_sync/api/options.js.map +7 -0
- package/dist/modules/data_sync/api/run.js +59 -0
- package/dist/modules/data_sync/api/run.js.map +7 -0
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +13 -0
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +7 -0
- package/dist/modules/data_sync/api/runs/[id]/retry.js +13 -0
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +7 -0
- package/dist/modules/data_sync/api/runs/[id]/route.js +13 -0
- package/dist/modules/data_sync/api/runs/[id]/route.js.map +7 -0
- package/dist/modules/data_sync/api/runs.js +13 -0
- package/dist/modules/data_sync/api/runs.js.map +7 -0
- package/dist/modules/data_sync/api/schedules/[id]/route.js +17 -0
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +7 -0
- package/dist/modules/data_sync/api/schedules/route.js +15 -0
- package/dist/modules/data_sync/api/schedules/route.js.map +7 -0
- package/dist/modules/data_sync/api/schedules/serialize.js +6 -0
- package/dist/modules/data_sync/api/schedules/serialize.js.map +7 -0
- package/dist/modules/data_sync/api/validate.js +13 -0
- package/dist/modules/data_sync/api/validate.js.map +7 -0
- package/dist/modules/data_sync/backend/data-sync/page.js +7 -0
- package/dist/modules/data_sync/backend/data-sync/page.js.map +7 -0
- package/dist/modules/data_sync/backend/data-sync/page.meta.js +6 -0
- package/dist/modules/data_sync/backend/data-sync/page.meta.js.map +7 -0
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +7 -0
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +7 -0
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js +6 -0
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js.map +7 -0
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +9 -0
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +7 -0
- package/dist/modules/data_sync/components/RunParameterFields.js +13 -0
- package/dist/modules/data_sync/components/RunParameterFields.js.map +7 -0
- package/dist/modules/data_sync/data/entities.js +9 -0
- package/dist/modules/data_sync/data/entities.js.map +7 -0
- package/dist/modules/data_sync/data/validators.js +12 -0
- package/dist/modules/data_sync/data/validators.js.map +7 -0
- package/dist/modules/data_sync/di.js +33 -0
- package/dist/modules/data_sync/di.js.map +7 -0
- package/dist/modules/data_sync/events.js +9 -0
- package/dist/modules/data_sync/events.js.map +7 -0
- package/dist/modules/data_sync/extension-points.js +8 -0
- package/dist/modules/data_sync/extension-points.js.map +7 -0
- package/dist/modules/data_sync/i18n/de.json +135 -0
- package/dist/modules/data_sync/i18n/en.json +135 -0
- package/dist/modules/data_sync/i18n/es.json +135 -0
- package/dist/modules/data_sync/i18n/ko.json +135 -0
- package/dist/modules/data_sync/i18n/pl.json +135 -0
- package/dist/modules/data_sync/index.js +6 -0
- package/dist/modules/data_sync/index.js.map +7 -0
- package/dist/modules/data_sync/lib/abandoned-run.js +6 -0
- package/dist/modules/data_sync/lib/abandoned-run.js.map +7 -0
- package/dist/modules/data_sync/lib/adapter-registry.js +10 -0
- package/dist/modules/data_sync/lib/adapter-registry.js.map +7 -0
- package/dist/modules/data_sync/lib/adapter.js +2 -0
- package/dist/modules/data_sync/lib/adapter.js.map +7 -0
- package/dist/modules/data_sync/lib/adopt-on-delivery.js +35 -0
- package/dist/modules/data_sync/lib/adopt-on-delivery.js.map +7 -0
- package/dist/modules/data_sync/lib/batch-stream.js +6 -0
- package/dist/modules/data_sync/lib/batch-stream.js.map +7 -0
- package/dist/modules/data_sync/lib/id-mapping.js +6 -0
- package/dist/modules/data_sync/lib/id-mapping.js.map +7 -0
- package/dist/modules/data_sync/lib/queue-policy.js +11 -0
- package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
- package/dist/modules/data_sync/lib/queue.js +6 -0
- package/dist/modules/data_sync/lib/queue.js.map +7 -0
- package/dist/modules/data_sync/lib/run-parameters.js +8 -0
- package/dist/modules/data_sync/lib/run-parameters.js.map +7 -0
- package/dist/modules/data_sync/lib/start-cursor.js +8 -0
- package/dist/modules/data_sync/lib/start-cursor.js.map +7 -0
- package/dist/modules/data_sync/lib/start-run.js +51 -0
- package/dist/modules/data_sync/lib/start-run.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +6 -0
- package/dist/modules/data_sync/lib/sync-engine.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-run-service.js +7 -0
- package/dist/modules/data_sync/lib/sync-run-service.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-schedule-service.js +6 -0
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +7 -0
- package/dist/modules/data_sync/lib/syncRunStatus.js +9 -0
- package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
- package/dist/modules/data_sync/lib/version-guard.js +14 -0
- package/dist/modules/data_sync/lib/version-guard.js.map +7 -0
- package/dist/modules/data_sync/migrations/Migration20260304113737.js +6 -0
- package/dist/modules/data_sync/migrations/Migration20260304113737.js.map +7 -0
- package/dist/modules/data_sync/migrations/Migration20260810120000.js +6 -0
- package/dist/modules/data_sync/migrations/Migration20260810120000.js.map +7 -0
- package/dist/modules/data_sync/setup.js +8 -0
- package/dist/modules/data_sync/setup.js.map +7 -0
- package/dist/modules/data_sync/workers/sync-export.js +19 -0
- package/dist/modules/data_sync/workers/sync-export.js.map +7 -0
- package/dist/modules/data_sync/workers/sync-import.js +19 -0
- package/dist/modules/data_sync/workers/sync-import.js.map +7 -0
- package/dist/modules/data_sync/workers/sync-scheduled.js +8 -0
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +7 -0
- package/generated/entities/sync_cursor/index.ts +8 -0
- package/generated/entities/sync_mapping/index.ts +8 -0
- package/generated/entities/sync_run/index.ts +22 -0
- package/generated/entities/sync_schedule/index.ts +16 -0
- package/generated/entities.ids.generated.ts +13 -0
- package/package.json +135 -0
- package/src/compat/mirror-shape.test.ts +106 -0
- package/src/compat/seams.test.ts +97 -0
- package/src/engine/durable-run.ts +231 -0
- package/src/index.ts +4 -0
- package/src/kinds/data-sync-run.ts +104 -0
- package/src/mirror-version.ts +3 -0
- package/src/mirror.manifest.json +65 -0
- package/src/modules/data_sync/__integration__/TC-DSD-001.spec.ts +80 -0
- package/src/modules/data_sync/acl.ts +5 -0
- package/src/modules/data_sync/api/mappings/[id]/route.ts +12 -0
- package/src/modules/data_sync/api/mappings/route.ts +11 -0
- package/src/modules/data_sync/api/options.ts +10 -0
- package/src/modules/data_sync/api/run.ts +103 -0
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +10 -0
- package/src/modules/data_sync/api/runs/[id]/retry.ts +10 -0
- package/src/modules/data_sync/api/runs/[id]/route.ts +10 -0
- package/src/modules/data_sync/api/runs.ts +10 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +12 -0
- package/src/modules/data_sync/api/schedules/route.ts +11 -0
- package/src/modules/data_sync/api/schedules/serialize.ts +4 -0
- package/src/modules/data_sync/api/validate.ts +10 -0
- package/src/modules/data_sync/backend/data-sync/page.meta.ts +4 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +5 -0
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.meta.ts +4 -0
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +5 -0
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +6 -0
- package/src/modules/data_sync/components/RunParameterFields.tsx +5 -0
- package/src/modules/data_sync/data/entities.ts +4 -0
- package/src/modules/data_sync/data/validators.ts +4 -0
- package/src/modules/data_sync/di.ts +57 -0
- package/src/modules/data_sync/events.ts +5 -0
- package/src/modules/data_sync/extension-points.ts +5 -0
- package/src/modules/data_sync/i18n/de.json +135 -0
- package/src/modules/data_sync/i18n/en.json +135 -0
- package/src/modules/data_sync/i18n/es.json +135 -0
- package/src/modules/data_sync/i18n/ko.json +135 -0
- package/src/modules/data_sync/i18n/pl.json +135 -0
- package/src/modules/data_sync/index.ts +4 -0
- package/src/modules/data_sync/lib/adapter-registry.ts +4 -0
- package/src/modules/data_sync/lib/adapter.ts +3 -0
- package/src/modules/data_sync/lib/adopt-on-delivery.ts +69 -0
- package/src/modules/data_sync/lib/batch-stream.ts +4 -0
- package/src/modules/data_sync/lib/id-mapping.ts +4 -0
- package/src/modules/data_sync/lib/queue-policy.ts +4 -0
- package/src/modules/data_sync/lib/queue.ts +4 -0
- package/src/modules/data_sync/lib/run-parameters.ts +4 -0
- package/src/modules/data_sync/lib/start-cursor.ts +4 -0
- package/src/modules/data_sync/lib/start-run.ts +106 -0
- package/src/modules/data_sync/lib/sync-engine.ts +4 -0
- package/src/modules/data_sync/lib/sync-run-service.ts +4 -0
- package/src/modules/data_sync/lib/sync-schedule-service.ts +4 -0
- package/src/modules/data_sync/lib/syncRunStatus.ts +4 -0
- package/src/modules/data_sync/lib/version-guard.ts +27 -0
- package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +1013 -0
- package/src/modules/data_sync/migrations/Migration20260304113737.ts +4 -0
- package/src/modules/data_sync/migrations/Migration20260810120000.ts +4 -0
- package/src/modules/data_sync/setup.ts +5 -0
- package/src/modules/data_sync/workers/sync-export.ts +20 -0
- package/src/modules/data_sync/workers/sync-import.ts +20 -0
- package/src/modules/data_sync/workers/sync-scheduled.ts +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @fullstackhouse/open-mercato-data-sync-durable
|
|
2
|
+
|
|
3
|
+
A drop-in replacement for Open Mercato's core `data_sync` module that runs sync runs as durable
|
|
4
|
+
work. Same tables, same REST API, same adapter contract, same events, same UI.
|
|
5
|
+
|
|
6
|
+
## The problem
|
|
7
|
+
|
|
8
|
+
A `data_sync` run is one long stream over a cursor. On core's engine:
|
|
9
|
+
|
|
10
|
+
- a deploy in the middle of a multi-day backfill kills the run
|
|
11
|
+
- a worker that dies leaves the run `running` forever
|
|
12
|
+
- one transient error at hour nine fails the whole run, throwing away hour one through eight
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
The packages are not on npm yet, so install them from this repository. Both build on pack, so a
|
|
17
|
+
git dependency delivers a built package, and yarn pins the resolved commit in your lockfile.
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
# .yarnrc.yml
|
|
21
|
+
approvedGitRepositories:
|
|
22
|
+
- "https://github.com/fullstackhouse/*"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
REPO="git+https://github.com/fullstackhouse/open-mercato-durable.git"
|
|
27
|
+
yarn add \
|
|
28
|
+
"@fullstackhouse/open-mercato-durable-work@$REPO#workspace=@fullstackhouse/open-mercato-durable-work" \
|
|
29
|
+
"@fullstackhouse/open-mercato-data-sync-durable@$REPO#workspace=@fullstackhouse/open-mercato-data-sync-durable"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
HTTPS needs no credentials, which is what makes this work in CI without a deploy key. Add
|
|
33
|
+
`&commit=<sha>` to pin explicitly; otherwise yarn records the resolved commit in the lockfile
|
|
34
|
+
and re-resolves only when you ask it to.
|
|
35
|
+
|
|
36
|
+
**Add both, always.** `durable-work` is a *peer* of the adopter, and installing exactly one copy
|
|
37
|
+
is not tidiness: it exports a process-wide registry, and a second copy would mean job kinds
|
|
38
|
+
register into one while the worker reads the other — silently.
|
|
39
|
+
|
|
40
|
+
Once the packages are published this becomes `yarn mercato module add @fullstackhouse/…`.
|
|
41
|
+
|
|
42
|
+
`src/modules.ts` — the whole change is one `from`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
{ id: 'durable_work', from: '@fullstackhouse/open-mercato-durable-work' },
|
|
46
|
+
{ id: 'data_sync', from: '@fullstackhouse/open-mercato-data-sync-durable' }, // was '@open-mercato/core'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then `yarn generate && yarn db:migrate` — a no-op for existing `data_sync` history, because the
|
|
50
|
+
migration classes keep core's names — and run `yarn mercato durable_work worker`.
|
|
51
|
+
|
|
52
|
+
Reverting is the same line.
|
|
53
|
+
|
|
54
|
+
## What changes
|
|
55
|
+
|
|
56
|
+
A run gets a lease. It commits its cursor under that lease, hands back at a batch boundary when
|
|
57
|
+
its slice budget is spent or the process is stopping, and resumes from the committed cursor. A
|
|
58
|
+
transient error retries the slice instead of failing the run. A worker that dies is noticed by
|
|
59
|
+
the reconciler within about a minute. One live run per integration, entity and direction.
|
|
60
|
+
|
|
61
|
+
## What does not change
|
|
62
|
+
|
|
63
|
+
Your adapters. They import core's deep paths, which this package re-exports, so there is one
|
|
64
|
+
process-wide adapter registry and one entity class. `sync_excel`, `sync_akeneo` and anything you
|
|
65
|
+
wrote keep working untouched — including runs they start themselves, which are adopted when the
|
|
66
|
+
queue delivers them.
|
|
67
|
+
|
|
68
|
+
The UI, the REST API, the events and the ACL features are core's, unchanged.
|
|
69
|
+
|
|
70
|
+
## How it works
|
|
71
|
+
|
|
72
|
+
It does **not** fork core's engine. Core's engine already survives being displaced by another
|
|
73
|
+
worker: it asks a cancellation question at every batch boundary, and it stays silent when a
|
|
74
|
+
terminal write is refused. This package answers those two questions differently and routes the
|
|
75
|
+
cursor commit through the lease fence. See
|
|
76
|
+
[docs/adr/0004](https://github.com/fullstackhouse/open-mercato-durable/blob/main/docs/adr/0004-fork-slice-engine.md).
|
|
77
|
+
|
|
78
|
+
Every seam it relies on is asserted against the installed core in CI, on both the released and
|
|
79
|
+
the development channel, so an upstream change fails there rather than in your app.
|
|
80
|
+
|
|
81
|
+
## Versioning
|
|
82
|
+
|
|
83
|
+
One release line per core minor: `data-sync-durable 0.7.x` mirrors `@open-mercato/core 0.7.x`.
|
|
84
|
+
Bump both together. Hosts that have ejected `data_sync` into `@app` are out of scope.
|
|
85
|
+
|
|
86
|
+
MIT. Part of [open-mercato-durable](https://github.com/fullstackhouse/open-mercato-durable).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { SeamBrokenError } from "../modules/data_sync/lib/version-guard.js";
|
|
2
|
+
function recordSlice(ctx, runService, progressService) {
|
|
3
|
+
let captured = null;
|
|
4
|
+
let stopReason = null;
|
|
5
|
+
let committed = 0;
|
|
6
|
+
return {
|
|
7
|
+
captured: () => captured,
|
|
8
|
+
stopReason: () => stopReason,
|
|
9
|
+
committedBatches: () => committed,
|
|
10
|
+
// Spread, not rebuilt.
|
|
11
|
+
//
|
|
12
|
+
// Core's engine calls far more than the three methods decorated here — `startJob`,
|
|
13
|
+
// `updateProgress`, `touchJobHeartbeat`, `markCancelled` and others on the progress
|
|
14
|
+
// service, and more of the run service besides. An object carrying only the overrides
|
|
15
|
+
// looks fine to TypeScript through a structural type and then fails at the first
|
|
16
|
+
// undecorated call, mid-run.
|
|
17
|
+
runService: {
|
|
18
|
+
...runService,
|
|
19
|
+
/**
|
|
20
|
+
* Records a terminal transition instead of performing it, and answers with the run
|
|
21
|
+
* unchanged.
|
|
22
|
+
*
|
|
23
|
+
* Core's `finalizeRun` compares what comes back to what it asked for, and stays silent
|
|
24
|
+
* when they differ — the branch it has for "another worker already finalized this". So
|
|
25
|
+
* this both captures the outcome and suppresses the progress write, the operational log
|
|
26
|
+
* and the lifecycle event, leaving all three to the durable terminal transition, which
|
|
27
|
+
* writes them in the same transaction as the job's own terminal state.
|
|
28
|
+
*/
|
|
29
|
+
async markStatus(runId, status, scope, error) {
|
|
30
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
31
|
+
captured = { status, error };
|
|
32
|
+
return runService.getRun(runId, scope);
|
|
33
|
+
}
|
|
34
|
+
return runService.markStatus(runId, status, scope, error);
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* Commits the batch cursor under the lease.
|
|
38
|
+
*
|
|
39
|
+
* This is the write that must not outlive the right to make it: a worker whose lease
|
|
40
|
+
* expired mid-batch would otherwise advance the cursor of a run another worker is now
|
|
41
|
+
* driving, and the two would interleave over one stream.
|
|
42
|
+
*/
|
|
43
|
+
async commitBatchProgress(runId, delta, cursor, scope, options) {
|
|
44
|
+
const result = await ctx.fencedWrite(async () => runService.commitBatchProgress(runId, delta, cursor, scope, options));
|
|
45
|
+
committed += 1;
|
|
46
|
+
await ctx.heartbeat({ committed: true });
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
progressService: {
|
|
51
|
+
...progressService,
|
|
52
|
+
/**
|
|
53
|
+
* Mirrors the run's counters onto the durable job as they move.
|
|
54
|
+
*
|
|
55
|
+
* Core reports progress here after every committed batch. Without this the operator API
|
|
56
|
+
* shows a job that is plainly running with `0 of null` processed, and the one place
|
|
57
|
+
* somebody looks to see whether a multi-day backfill is advancing tells them nothing.
|
|
58
|
+
*/
|
|
59
|
+
async updateProgress(progressJobId, patch, scope) {
|
|
60
|
+
await ctx.heartbeat({ processedCount: patch?.processedCount, totalCount: patch?.totalCount ?? null }).catch(() => void 0);
|
|
61
|
+
const inner = progressService.updateProgress;
|
|
62
|
+
return inner?.call(progressService, progressJobId, patch, scope);
|
|
63
|
+
},
|
|
64
|
+
/**
|
|
65
|
+
* Core asks this once per batch and stops the stream cleanly when it is true. That makes
|
|
66
|
+
* it the slice's hand-back point as well as its cancellation point — the two need the
|
|
67
|
+
* same clean stop, and differ only in what happens afterwards.
|
|
68
|
+
*/
|
|
69
|
+
async isCancellationRequested(progressJobId, tenantId, organizationId) {
|
|
70
|
+
if (await progressService.isCancellationRequested(progressJobId, tenantId, organizationId)) {
|
|
71
|
+
stopReason = "cancelled";
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (ctx.signal.aborted || ctx.shouldYield()) {
|
|
75
|
+
stopReason = "budget";
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
class SyncRunFailedError extends Error {
|
|
84
|
+
constructor(runId, message) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.runId = runId;
|
|
87
|
+
this.durableErrorClass = "transient";
|
|
88
|
+
this.name = "SyncRunFailedError";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function outcomeOf(recorder, runId, runStatus) {
|
|
92
|
+
const captured = recorder.captured();
|
|
93
|
+
if (recorder.stopReason() === "budget") return "budget";
|
|
94
|
+
if (recorder.stopReason() === "cancelled") return "cancelled";
|
|
95
|
+
if (!captured) {
|
|
96
|
+
if (recorder.committedBatches() > 0 && runStatus && runStatus !== "running" && runStatus !== "pending") {
|
|
97
|
+
throw new SeamBrokenError(runId, `the run reached "${runStatus}" without the adopter recording it`);
|
|
98
|
+
}
|
|
99
|
+
return "drained";
|
|
100
|
+
}
|
|
101
|
+
if (captured.status === "failed") throw new SyncRunFailedError(runId, captured.error ?? "Sync run failed");
|
|
102
|
+
if (captured.status === "cancelled") return "cancelled";
|
|
103
|
+
return "drained";
|
|
104
|
+
}
|
|
105
|
+
async function mirrorRunStatus(tx, runId, status, errorMessage) {
|
|
106
|
+
const result = await tx.query(
|
|
107
|
+
`update sync_runs
|
|
108
|
+
set status = $2, last_error = $3, updated_at = now()
|
|
109
|
+
where id = $1 and deleted_at is null and status in ('pending','running')`,
|
|
110
|
+
[runId, status, errorMessage]
|
|
111
|
+
);
|
|
112
|
+
return { matched: result.rowCount };
|
|
113
|
+
}
|
|
114
|
+
async function reopenRun(tx, runId) {
|
|
115
|
+
const result = await tx.query(
|
|
116
|
+
`update sync_runs
|
|
117
|
+
set status = 'running', last_error = null, updated_at = now()
|
|
118
|
+
where id = $1 and deleted_at is null and status in ('failed','pending','running')`,
|
|
119
|
+
[runId]
|
|
120
|
+
);
|
|
121
|
+
return { matched: result.rowCount };
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
SyncRunFailedError,
|
|
125
|
+
mirrorRunStatus,
|
|
126
|
+
outcomeOf,
|
|
127
|
+
recordSlice,
|
|
128
|
+
reopenRun
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=durable-run.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/engine/durable-run.ts"],
|
|
4
|
+
"sourcesContent": ["// How a `data_sync` run becomes durable work without core's engine being changed or copied.\n//\n// Core's engine already survives being displaced by another worker: it asks a cancellation\n// question at every batch boundary, and it stays silent when a terminal write is refused. A\n// durable adopter is exactly a worker in that position, so all this does is answer those two\n// questions differently \u2014 and route the cursor commit through the lease fence.\n//\n// See docs/adr/0004: the original plan was to fork the batch loop. Reading the engine showed\n// the seams were already there.\n\nimport type { SliceContext, SliceOutcome, SqlExecutor } from '@fullstackhouse/open-mercato-durable-work'\n\nimport { SeamBrokenError } from '../modules/data_sync/lib/version-guard'\n\nexport type SyncScope = { tenantId: string; organizationId: string | null; userId?: string | null }\nexport type SyncTerminalStatus = 'completed' | 'failed' | 'cancelled'\n\n/** The methods this decorator *wraps*, not the whole service: core's engine calls plenty more,\n * and they are passed through untouched. Anything not listed here is delegated. */\nexport type SyncRunServiceLike = {\n [method: string]: unknown\n getRun(runId: string, scope: SyncScope): Promise<{ status: string; progressJobId?: string | null } | null>\n markStatus(runId: string, status: string, scope: SyncScope, error?: string): Promise<unknown>\n commitBatchProgress(\n runId: string,\n delta: Record<string, unknown>,\n cursor: unknown,\n scope: SyncScope,\n options?: Record<string, unknown>,\n ): Promise<unknown>\n}\n\nexport type ProgressServiceLike = {\n [method: string]: unknown\n isCancellationRequested(progressJobId: string, tenantId: string, organizationId: string | null): Promise<boolean>\n}\n\n/** What core's engine tried to do, captured rather than applied. */\nexport type CapturedOutcome = { status: SyncTerminalStatus; error?: string } | null\n\nexport type SliceRecorder = {\n runService: SyncRunServiceLike\n progressService: ProgressServiceLike\n /** What the engine tried to finalize the run as, if anything. */\n captured(): CapturedOutcome\n /** Why the slice stopped early, when it did. */\n stopReason(): 'budget' | 'cancelled' | null\n /** Batches whose cursor this slice committed. Zero means the slice made no progress. */\n committedBatches(): number\n}\n\n/**\n * Wraps core's two services for the duration of one slice.\n *\n * @param ctx the slice this recorder belongs to; its lease is what fences the cursor commits\n */\nexport function recordSlice(\n ctx: SliceContext,\n runService: SyncRunServiceLike,\n progressService: ProgressServiceLike,\n): SliceRecorder {\n let captured: CapturedOutcome = null\n let stopReason: 'budget' | 'cancelled' | null = null\n let committed = 0\n\n return {\n captured: () => captured,\n stopReason: () => stopReason,\n committedBatches: () => committed,\n\n // Spread, not rebuilt.\n //\n // Core's engine calls far more than the three methods decorated here \u2014 `startJob`,\n // `updateProgress`, `touchJobHeartbeat`, `markCancelled` and others on the progress\n // service, and more of the run service besides. An object carrying only the overrides\n // looks fine to TypeScript through a structural type and then fails at the first\n // undecorated call, mid-run.\n runService: {\n ...runService,\n\n /**\n * Records a terminal transition instead of performing it, and answers with the run\n * unchanged.\n *\n * Core's `finalizeRun` compares what comes back to what it asked for, and stays silent\n * when they differ \u2014 the branch it has for \"another worker already finalized this\". So\n * this both captures the outcome and suppresses the progress write, the operational log\n * and the lifecycle event, leaving all three to the durable terminal transition, which\n * writes them in the same transaction as the job's own terminal state.\n */\n async markStatus(runId, status, scope, error) {\n if (status === 'completed' || status === 'failed' || status === 'cancelled') {\n captured = { status, error }\n return runService.getRun(runId, scope)\n }\n return runService.markStatus(runId, status, scope, error)\n },\n\n /**\n * Commits the batch cursor under the lease.\n *\n * This is the write that must not outlive the right to make it: a worker whose lease\n * expired mid-batch would otherwise advance the cursor of a run another worker is now\n * driving, and the two would interleave over one stream.\n */\n async commitBatchProgress(runId, delta, cursor, scope, options) {\n const result = await ctx.fencedWrite(async () => runService.commitBatchProgress(runId, delta, cursor, scope, options))\n committed += 1\n // A committed unit of work resets the failure and orphan budgets: a run that is making\n // progress has not earned the suspicion those counters represent, however many times\n // it was interrupted getting there.\n await ctx.heartbeat({ committed: true })\n return result\n },\n },\n\n progressService: {\n ...progressService,\n\n /**\n * Mirrors the run's counters onto the durable job as they move.\n *\n * Core reports progress here after every committed batch. Without this the operator API\n * shows a job that is plainly running with `0 of null` processed, and the one place\n * somebody looks to see whether a multi-day backfill is advancing tells them nothing.\n */\n async updateProgress(progressJobId: string, patch: { processedCount?: number; totalCount?: number | null }, scope: unknown) {\n await ctx\n .heartbeat({ processedCount: patch?.processedCount, totalCount: patch?.totalCount ?? null })\n .catch(() => undefined)\n const inner = progressService.updateProgress as\n | ((id: string, patch: unknown, scope: unknown) => Promise<unknown>)\n | undefined\n return inner?.call(progressService, progressJobId, patch, scope)\n },\n\n /**\n * Core asks this once per batch and stops the stream cleanly when it is true. That makes\n * it the slice's hand-back point as well as its cancellation point \u2014 the two need the\n * same clean stop, and differ only in what happens afterwards.\n */\n async isCancellationRequested(progressJobId, tenantId, organizationId) {\n if (await progressService.isCancellationRequested(progressJobId, tenantId, organizationId)) {\n stopReason = 'cancelled'\n return true\n }\n if (ctx.signal.aborted || ctx.shouldYield()) {\n stopReason = 'budget'\n return true\n }\n return false\n },\n },\n }\n}\n\n/** A run that ended `failed` inside core's engine. Thrown so the durable error taxonomy and the\n * retry budget apply to it, instead of the run being thrown away at the first blip. */\nexport class SyncRunFailedError extends Error {\n readonly durableErrorClass = 'transient' as const\n constructor(\n readonly runId: string,\n message: string,\n ) {\n super(message)\n this.name = 'SyncRunFailedError'\n }\n}\n\n/**\n * Turns what the recorder saw into the outcome the mechanism understands.\n *\n * @param runStatus the run's status after the slice, used only to tell \"already finished\" from\n * \"core finalized this itself\" \u2014 see the seam check below\n */\nexport function outcomeOf(recorder: SliceRecorder, runId: string, runStatus?: string): SliceOutcome {\n const captured = recorder.captured()\n\n if (recorder.stopReason() === 'budget') return 'budget'\n if (recorder.stopReason() === 'cancelled') return 'cancelled'\n\n if (!captured) {\n // Nothing was recorded. Two very different situations look the same from here, and telling\n // them apart is the whole point:\n //\n // - the run was already terminal, or gone, before this slice started \u2014 nothing to do\n // - core finalized the run itself, without going through the decorated `markStatus`\n //\n // The second means the seam this package rests on has moved (ADR 0004), and reporting it\n // as success would leave a job that says `completed` beside a run that says `failed`.\n // Committed batches are what distinguishes them: a slice that did real work and then found\n // the run terminal without recording anything did not simply arrive late.\n if (recorder.committedBatches() > 0 && runStatus && runStatus !== 'running' && runStatus !== 'pending') {\n throw new SeamBrokenError(runId, `the run reached \"${runStatus}\" without the adopter recording it`)\n }\n return 'drained'\n }\n if (captured.status === 'failed') throw new SyncRunFailedError(runId, captured.error ?? 'Sync run failed')\n if (captured.status === 'cancelled') return 'cancelled'\n return 'drained'\n}\n\n/** Maps a durable terminal state onto the run's own, inside the terminal transaction. */\nexport async function mirrorRunStatus(\n tx: SqlExecutor,\n runId: string,\n status: SyncTerminalStatus,\n errorMessage: string | null,\n): Promise<{ matched: number }> {\n // Fenced on the run still being open, so a run finished by another path is not overwritten \u2014\n // \"mirrored\" means the domain row agrees, and a row that already disagrees for a good reason\n // must not be forced.\n const result = await tx.query(\n `update sync_runs\n set status = $2, last_error = $3, updated_at = now()\n where id = $1 and deleted_at is null and status in ('pending','running')`,\n [runId, status, errorMessage],\n )\n return { matched: result.rowCount }\n}\n\n/** Re-opens a run when an operator re-drives its job. The mirror image of the above. */\nexport async function reopenRun(tx: SqlExecutor, runId: string): Promise<{ matched: number }> {\n const result = await tx.query(\n `update sync_runs\n set status = 'running', last_error = null, updated_at = now()\n where id = $1 and deleted_at is null and status in ('failed','pending','running')`,\n [runId],\n )\n return { matched: result.rowCount }\n}\n"],
|
|
5
|
+
"mappings": "AAYA,SAAS,uBAAuB;AA4CzB,SAAS,YACd,KACA,YACA,iBACe;AACf,MAAI,WAA4B;AAChC,MAAI,aAA4C;AAChD,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL,UAAU,MAAM;AAAA,IAChB,YAAY,MAAM;AAAA,IAClB,kBAAkB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASxB,YAAY;AAAA,MACV,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAYH,MAAM,WAAW,OAAO,QAAQ,OAAO,OAAO;AAC5C,YAAI,WAAW,eAAe,WAAW,YAAY,WAAW,aAAa;AAC3E,qBAAW,EAAE,QAAQ,MAAM;AAC3B,iBAAO,WAAW,OAAO,OAAO,KAAK;AAAA,QACvC;AACA,eAAO,WAAW,WAAW,OAAO,QAAQ,OAAO,KAAK;AAAA,MAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASA,MAAM,oBAAoB,OAAO,OAAO,QAAQ,OAAO,SAAS;AAC9D,cAAM,SAAS,MAAM,IAAI,YAAY,YAAY,WAAW,oBAAoB,OAAO,OAAO,QAAQ,OAAO,OAAO,CAAC;AACrH,qBAAa;AAIb,cAAM,IAAI,UAAU,EAAE,WAAW,KAAK,CAAC;AACvC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,iBAAiB;AAAA,MACf,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASH,MAAM,eAAe,eAAuB,OAAgE,OAAgB;AAC1H,cAAM,IACH,UAAU,EAAE,gBAAgB,OAAO,gBAAgB,YAAY,OAAO,cAAc,KAAK,CAAC,EAC1F,MAAM,MAAM,MAAS;AACxB,cAAM,QAAQ,gBAAgB;AAG9B,eAAO,OAAO,KAAK,iBAAiB,eAAe,OAAO,KAAK;AAAA,MACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,MAAM,wBAAwB,eAAe,UAAU,gBAAgB;AACrE,YAAI,MAAM,gBAAgB,wBAAwB,eAAe,UAAU,cAAc,GAAG;AAC1F,uBAAa;AACb,iBAAO;AAAA,QACT;AACA,YAAI,IAAI,OAAO,WAAW,IAAI,YAAY,GAAG;AAC3C,uBAAa;AACb,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAIO,MAAM,2BAA2B,MAAM;AAAA,EAE5C,YACW,OACT,SACA;AACA,UAAM,OAAO;AAHJ;AAFX,SAAS,oBAAoB;AAM3B,SAAK,OAAO;AAAA,EACd;AACF;AAQO,SAAS,UAAU,UAAyB,OAAe,WAAkC;AAClG,QAAM,WAAW,SAAS,SAAS;AAEnC,MAAI,SAAS,WAAW,MAAM,SAAU,QAAO;AAC/C,MAAI,SAAS,WAAW,MAAM,YAAa,QAAO;AAElD,MAAI,CAAC,UAAU;AAWb,QAAI,SAAS,iBAAiB,IAAI,KAAK,aAAa,cAAc,aAAa,cAAc,WAAW;AACtG,YAAM,IAAI,gBAAgB,OAAO,oBAAoB,SAAS,oCAAoC;AAAA,IACpG;AACA,WAAO;AAAA,EACT;AACA,MAAI,SAAS,WAAW,SAAU,OAAM,IAAI,mBAAmB,OAAO,SAAS,SAAS,iBAAiB;AACzG,MAAI,SAAS,WAAW,YAAa,QAAO;AAC5C,SAAO;AACT;AAGA,eAAsB,gBACpB,IACA,OACA,QACA,cAC8B;AAI9B,QAAM,SAAS,MAAM,GAAG;AAAA,IACtB;AAAA;AAAA;AAAA,IAGA,CAAC,OAAO,QAAQ,YAAY;AAAA,EAC9B;AACA,SAAO,EAAE,SAAS,OAAO,SAAS;AACpC;AAGA,eAAsB,UAAU,IAAiB,OAA6C;AAC5F,QAAM,SAAS,MAAM,GAAG;AAAA,IACtB;AAAA;AAAA;AAAA,IAGA,CAAC,KAAK;AAAA,EACR;AACA,SAAO,EAAE,SAAS,OAAO,SAAS;AACpC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["// Public API of @fullstackhouse/open-mercato-data-sync-durable.\n// The host loads ./modules/data_sync as module id `data_sync` in place of core's:\n// { id: 'data_sync', from: '@fullstackhouse/open-mercato-data-sync-durable' }\nexport { metadata } from './modules/data_sync/index'\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,gBAAgB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mirrorRunStatus,
|
|
3
|
+
outcomeOf,
|
|
4
|
+
recordSlice,
|
|
5
|
+
reopenRun
|
|
6
|
+
} from "../engine/durable-run.js";
|
|
7
|
+
const IMPORT_KIND = "data_sync.import";
|
|
8
|
+
const EXPORT_KIND = "data_sync.export";
|
|
9
|
+
const DATA_SYNC_QUEUE = "durable-work.data-sync";
|
|
10
|
+
async function runSlice(ctx, deps) {
|
|
11
|
+
const input = ctx.job.input;
|
|
12
|
+
const scope = { tenantId: ctx.scope.tenantId, organizationId: ctx.scope.organizationId };
|
|
13
|
+
const { engine, runService, progressService } = await deps.resolve();
|
|
14
|
+
const recorder = recordSlice(ctx, runService, progressService);
|
|
15
|
+
const decorated = engine({ runService: recorder.runService, progressService: recorder.progressService });
|
|
16
|
+
if (input.direction === "export") await decorated.runExport(input.runId, input.batchSize, scope);
|
|
17
|
+
else await decorated.runImport(input.runId, input.batchSize, scope);
|
|
18
|
+
const after = await runService.getRun(input.runId, scope);
|
|
19
|
+
return outcomeOf(recorder, input.runId, after?.status);
|
|
20
|
+
}
|
|
21
|
+
function dataSyncKinds(deps) {
|
|
22
|
+
const shared = {
|
|
23
|
+
queue: DATA_SYNC_QUEUE,
|
|
24
|
+
requiredFeatures: ["data_sync.run"],
|
|
25
|
+
orphanPolicy: "redrive",
|
|
26
|
+
// Long enough that a slow adapter page does not look like a dead worker, short enough that
|
|
27
|
+
// a dead worker is noticed in about a minute.
|
|
28
|
+
lease: { ttlMs: 6e4, sliceBudgetMs: 3e5, pendingTtlMs: 9e5 },
|
|
29
|
+
async onTransition(job, _scope, tx) {
|
|
30
|
+
const { runId } = job.input;
|
|
31
|
+
const status = job.status === "completed" ? "completed" : job.status === "cancelled" ? "cancelled" : "failed";
|
|
32
|
+
return mirrorRunStatus(tx, runId, status, job.errorMessage);
|
|
33
|
+
},
|
|
34
|
+
async onRedrive(job, _scope, tx) {
|
|
35
|
+
return reopenRun(tx, job.input.runId);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return [
|
|
39
|
+
{ ...shared, kind: IMPORT_KIND, step: (ctx) => runSlice(ctx, deps) },
|
|
40
|
+
{ ...shared, kind: EXPORT_KIND, step: (ctx) => runSlice(ctx, deps) }
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
function syncLockKey(integrationId, entityType, direction) {
|
|
44
|
+
return `data_sync:${integrationId}:${entityType}:${direction}`;
|
|
45
|
+
}
|
|
46
|
+
function syncIdempotencyKey(runId) {
|
|
47
|
+
return `data_sync.run:${runId}`;
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
DATA_SYNC_QUEUE,
|
|
51
|
+
EXPORT_KIND,
|
|
52
|
+
IMPORT_KIND,
|
|
53
|
+
dataSyncKinds,
|
|
54
|
+
syncIdempotencyKey,
|
|
55
|
+
syncLockKey
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=data-sync-run.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/kinds/data-sync-run.ts"],
|
|
4
|
+
"sourcesContent": ["// The job kinds a `data_sync` run becomes.\n//\n// A slice is one call into core's own engine, with the run service and the progress service\n// decorated for its duration. The engine does all the work it always did; what changes is where\n// its cursor commits go (through the lease fence), when it stops (at a batch boundary when the\n// slice budget is spent), and who writes the terminal state (the durable transition, in the\n// same transaction as the job's own).\n\nimport type { KindDefinition, SliceContext, SliceOutcome, SqlExecutor } from '@fullstackhouse/open-mercato-durable-work'\n\nimport {\n mirrorRunStatus,\n outcomeOf,\n recordSlice,\n reopenRun,\n type ProgressServiceLike,\n type SyncRunServiceLike,\n type SyncScope,\n} from '../engine/durable-run'\n\nexport const IMPORT_KIND = 'data_sync.import'\nexport const EXPORT_KIND = 'data_sync.export'\nexport const DATA_SYNC_QUEUE = 'durable-work.data-sync'\n\nexport type SyncRunInput = {\n runId: string\n batchSize: number\n direction: 'import' | 'export'\n}\n\n/** What a slice needs from the host, resolved per slice from the container. */\nexport type SyncEngineLike = {\n runImport(runId: string, batchSize: number, scope: SyncScope): Promise<void>\n runExport(runId: string, batchSize: number, scope: SyncScope): Promise<void>\n}\n\nexport type DataSyncKindDeps = {\n /** Resolved per slice, because each slice runs on its own EntityManager. */\n resolve(): Promise<{\n engine: (services: { runService: SyncRunServiceLike; progressService: ProgressServiceLike }) => SyncEngineLike\n runService: SyncRunServiceLike\n progressService: ProgressServiceLike\n }>\n}\n\nasync function runSlice(ctx: SliceContext<SyncRunInput>, deps: DataSyncKindDeps): Promise<SliceOutcome> {\n const input = ctx.job.input as SyncRunInput\n const scope: SyncScope = { tenantId: ctx.scope.tenantId, organizationId: ctx.scope.organizationId }\n const { engine, runService, progressService } = await deps.resolve()\n\n const recorder = recordSlice(ctx, runService, progressService)\n const decorated = engine({ runService: recorder.runService, progressService: recorder.progressService })\n\n if (input.direction === 'export') await decorated.runExport(input.runId, input.batchSize, scope)\n else await decorated.runImport(input.runId, input.batchSize, scope)\n\n const after = await runService.getRun(input.runId, scope)\n return outcomeOf(recorder, input.runId, after?.status)\n}\n\n/**\n * Builds both kinds.\n *\n * `orphanPolicy: 'redrive'` is deliberate and is the one place the adopter asserts something\n * about the work rather than the mechanism: a sync run keeps a committed cursor, so re-running\n * it after a worker died resumes rather than repeats. That is what makes automatic recovery\n * safe here when the mechanism's default is to park and wait for a human.\n */\nexport function dataSyncKinds(deps: DataSyncKindDeps): KindDefinition<SyncRunInput, never>[] {\n const shared = {\n queue: DATA_SYNC_QUEUE,\n requiredFeatures: ['data_sync.run'],\n orphanPolicy: 'redrive' as const,\n // Long enough that a slow adapter page does not look like a dead worker, short enough that\n // a dead worker is noticed in about a minute.\n lease: { ttlMs: 60_000, sliceBudgetMs: 300_000, pendingTtlMs: 900_000 },\n\n async onTransition(job: { id: string; input: unknown; status: string; errorMessage: string | null }, _scope: unknown, tx: SqlExecutor) {\n const { runId } = job.input as SyncRunInput\n const status = job.status === 'completed' ? 'completed' : job.status === 'cancelled' ? 'cancelled' : 'failed'\n return mirrorRunStatus(tx, runId, status, job.errorMessage)\n },\n\n async onRedrive(job: { input: unknown }, _scope: unknown, tx: SqlExecutor) {\n return reopenRun(tx, (job.input as SyncRunInput).runId)\n },\n }\n\n return [\n { ...shared, kind: IMPORT_KIND, step: (ctx) => runSlice(ctx, deps) } as KindDefinition<SyncRunInput, never>,\n { ...shared, kind: EXPORT_KIND, step: (ctx) => runSlice(ctx, deps) } as KindDefinition<SyncRunInput, never>,\n ]\n}\n\n/** The single-runner key: one live run per integration, entity and direction. Two imports of\n * the same entity would interleave over one cursor. */\nexport function syncLockKey(integrationId: string, entityType: string, direction: string): string {\n return `data_sync:${integrationId}:${entityType}:${direction}`\n}\n\n/** Makes starting a run twice for the same run id return the first job rather than a second. */\nexport function syncIdempotencyKey(runId: string): string {\n return `data_sync.run:${runId}`\n}\n"],
|
|
5
|
+
"mappings": "AAUA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAEA,MAAM,cAAc;AACpB,MAAM,cAAc;AACpB,MAAM,kBAAkB;AAuB/B,eAAe,SAAS,KAAiC,MAA+C;AACtG,QAAM,QAAQ,IAAI,IAAI;AACtB,QAAM,QAAmB,EAAE,UAAU,IAAI,MAAM,UAAU,gBAAgB,IAAI,MAAM,eAAe;AAClG,QAAM,EAAE,QAAQ,YAAY,gBAAgB,IAAI,MAAM,KAAK,QAAQ;AAEnE,QAAM,WAAW,YAAY,KAAK,YAAY,eAAe;AAC7D,QAAM,YAAY,OAAO,EAAE,YAAY,SAAS,YAAY,iBAAiB,SAAS,gBAAgB,CAAC;AAEvG,MAAI,MAAM,cAAc,SAAU,OAAM,UAAU,UAAU,MAAM,OAAO,MAAM,WAAW,KAAK;AAAA,MAC1F,OAAM,UAAU,UAAU,MAAM,OAAO,MAAM,WAAW,KAAK;AAElE,QAAM,QAAQ,MAAM,WAAW,OAAO,MAAM,OAAO,KAAK;AACxD,SAAO,UAAU,UAAU,MAAM,OAAO,OAAO,MAAM;AACvD;AAUO,SAAS,cAAc,MAA+D;AAC3F,QAAM,SAAS;AAAA,IACb,OAAO;AAAA,IACP,kBAAkB,CAAC,eAAe;AAAA,IAClC,cAAc;AAAA;AAAA;AAAA,IAGd,OAAO,EAAE,OAAO,KAAQ,eAAe,KAAS,cAAc,IAAQ;AAAA,IAEtE,MAAM,aAAa,KAAkF,QAAiB,IAAiB;AACrI,YAAM,EAAE,MAAM,IAAI,IAAI;AACtB,YAAM,SAAS,IAAI,WAAW,cAAc,cAAc,IAAI,WAAW,cAAc,cAAc;AACrG,aAAO,gBAAgB,IAAI,OAAO,QAAQ,IAAI,YAAY;AAAA,IAC5D;AAAA,IAEA,MAAM,UAAU,KAAyB,QAAiB,IAAiB;AACzE,aAAO,UAAU,IAAK,IAAI,MAAuB,KAAK;AAAA,IACxD;AAAA,EACF;AAEA,SAAO;AAAA,IACL,EAAE,GAAG,QAAQ,MAAM,aAAa,MAAM,CAAC,QAAQ,SAAS,KAAK,IAAI,EAAE;AAAA,IACnE,EAAE,GAAG,QAAQ,MAAM,aAAa,MAAM,CAAC,QAAQ,SAAS,KAAK,IAAI,EAAE;AAAA,EACrE;AACF;AAIO,SAAS,YAAY,eAAuB,YAAoB,WAA2B;AAChG,SAAO,aAAa,aAAa,IAAI,UAAU,IAAI,SAAS;AAC9D;AAGO,SAAS,mBAAmB,OAAuB;AACxD,SAAO,iBAAiB,KAAK;AAC/B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/mirror-version.ts"],
|
|
4
|
+
"sourcesContent": ["// GENERATED by scripts/gen-mirror.mjs. Do not edit.\n/** The @open-mercato/core version this package's mirror and decorators were built against. */\nexport const MIRRORED_CORE_VERSION = \"0.7.0\"\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,wBAAwB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coreVersion": "0.7.0",
|
|
3
|
+
"moduleId": "data_sync",
|
|
4
|
+
"generated": {
|
|
5
|
+
"acl.ts": "3b30f591eae78964",
|
|
6
|
+
"api/mappings/[id]/route.ts": "7b3fb917db7bd831",
|
|
7
|
+
"api/mappings/route.ts": "5551bc5ff61dfebb",
|
|
8
|
+
"api/options.ts": "bff527b4061b6c2b",
|
|
9
|
+
"api/runs.ts": "a2024a96d896c814",
|
|
10
|
+
"api/runs/[id]/cancel.ts": "1c99c66183d994ae",
|
|
11
|
+
"api/runs/[id]/retry.ts": "cf6ea67efcebdeb9",
|
|
12
|
+
"api/runs/[id]/route.ts": "2d0e39daef513f92",
|
|
13
|
+
"api/schedules/[id]/route.ts": "af2b16bd45fd02b5",
|
|
14
|
+
"api/schedules/route.ts": "2dbf0d2f04cdc100",
|
|
15
|
+
"api/schedules/serialize.ts": "44664c58abafd26f",
|
|
16
|
+
"api/validate.ts": "9fd095700abd39cb",
|
|
17
|
+
"backend/data-sync/page.meta.ts": "0dd9b6b406fa2699",
|
|
18
|
+
"backend/data-sync/page.tsx": "e68b00a4242abbbb",
|
|
19
|
+
"backend/data-sync/runs/[id]/page.meta.ts": "a1946eb5885a2109",
|
|
20
|
+
"backend/data-sync/runs/[id]/page.tsx": "963778ff29f7d96f",
|
|
21
|
+
"components/IntegrationScheduleTab.tsx": "185732c76a2f953a",
|
|
22
|
+
"components/RunParameterFields.tsx": "3f49886ef0a55193",
|
|
23
|
+
"data/entities.ts": "16476c912a6c7d40",
|
|
24
|
+
"data/validators.ts": "6c238076541373b0",
|
|
25
|
+
"events.ts": "8eea801045ff9789",
|
|
26
|
+
"extension-points.ts": "903cae9d931e0fb3",
|
|
27
|
+
"i18n/de.json": "943e5875db1ab98b",
|
|
28
|
+
"i18n/en.json": "ae6c5edcba6bb7f7",
|
|
29
|
+
"i18n/es.json": "de135edcf1917269",
|
|
30
|
+
"i18n/ko.json": "cb75626e56a0d267",
|
|
31
|
+
"i18n/pl.json": "6096d5c85f98ba8a",
|
|
32
|
+
"index.ts": "c15d4c24db598205",
|
|
33
|
+
"lib/adapter-registry.ts": "9ddb1b6cbfa911d7",
|
|
34
|
+
"lib/adapter.ts": "3902953e7bdc6021",
|
|
35
|
+
"lib/batch-stream.ts": "db921759b23aef76",
|
|
36
|
+
"lib/id-mapping.ts": "08628d924244bfc8",
|
|
37
|
+
"lib/queue-policy.ts": "7c75ee82eca0ca70",
|
|
38
|
+
"lib/queue.ts": "0fb590d7695c3837",
|
|
39
|
+
"lib/run-parameters.ts": "145b0a8697d34c82",
|
|
40
|
+
"lib/start-cursor.ts": "2620c39153035fb5",
|
|
41
|
+
"lib/sync-engine.ts": "bf3dbfdb1676190c",
|
|
42
|
+
"lib/sync-run-service.ts": "f55c4ec8c547cbd9",
|
|
43
|
+
"lib/sync-schedule-service.ts": "fe2e6bafc37ed319",
|
|
44
|
+
"lib/syncRunStatus.ts": "c30a457245c9cd11",
|
|
45
|
+
"migrations/.snapshot-open-mercato.json": "662107af1e52e5d3",
|
|
46
|
+
"migrations/Migration20260304113737.ts": "e3f0edec96d4a4d8",
|
|
47
|
+
"migrations/Migration20260810120000.ts": "29a2154fcf03c5ce",
|
|
48
|
+
"setup.ts": "f33921e5cb6aafa3",
|
|
49
|
+
"workers/sync-scheduled.ts": "bd1c585595bc7148"
|
|
50
|
+
},
|
|
51
|
+
"replaced": {
|
|
52
|
+
"api/run.ts": "0eda5b6869a0b95d",
|
|
53
|
+
"di.ts": "d351534bd20b2b29",
|
|
54
|
+
"lib/start-run.ts": "74081590458b202b",
|
|
55
|
+
"workers/sync-export.ts": "a23cad4d03dd42da",
|
|
56
|
+
"workers/sync-import.ts": "21e5356d2d394b6d"
|
|
57
|
+
},
|
|
58
|
+
"entityDescriptor": [
|
|
59
|
+
"entities.ids.generated.ts",
|
|
60
|
+
"entities/sync_run/index.ts",
|
|
61
|
+
"entities/sync_cursor/index.ts",
|
|
62
|
+
"entities/sync_mapping/index.ts",
|
|
63
|
+
"entities/sync_schedule/index.ts"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "@open-mercato/core/modules/data_sync/acl";
|
|
2
|
+
import { default as default2 } from "@open-mercato/core/modules/data_sync/acl";
|
|
3
|
+
import { features } from "@open-mercato/core/modules/data_sync/acl";
|
|
4
|
+
export {
|
|
5
|
+
default2 as default,
|
|
6
|
+
features
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=acl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/data_sync/acl.ts"],
|
|
4
|
+
"sourcesContent": ["// GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (acl.ts). Do not edit.\n// This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.\nexport * from '@open-mercato/core/modules/data_sync/acl'\nexport { default } from '@open-mercato/core/modules/data_sync/acl'\nexport { features } from '@open-mercato/core/modules/data_sync/acl'\n"],
|
|
5
|
+
"mappings": "AAEA,cAAc;AACd,SAAS,WAAAA,gBAAe;AACxB,SAAS,gBAAgB;",
|
|
6
|
+
"names": ["default"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "@open-mercato/core/modules/data_sync/api/mappings/[id]/route";
|
|
2
|
+
import { GET, PUT, DELETE } from "@open-mercato/core/modules/data_sync/api/mappings/[id]/route";
|
|
3
|
+
import { openApi as coreOpenApi } from "@open-mercato/core/modules/data_sync/api/mappings/[id]/route";
|
|
4
|
+
const openApi = coreOpenApi;
|
|
5
|
+
const metadata = {
|
|
6
|
+
GET: { requireAuth: true, requireFeatures: ["data_sync.configure"] },
|
|
7
|
+
PUT: { requireAuth: true, requireFeatures: ["data_sync.configure"] },
|
|
8
|
+
DELETE: { requireAuth: true, requireFeatures: ["data_sync.configure"] }
|
|
9
|
+
};
|
|
10
|
+
export {
|
|
11
|
+
DELETE,
|
|
12
|
+
GET,
|
|
13
|
+
PUT,
|
|
14
|
+
metadata,
|
|
15
|
+
openApi
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/data_sync/api/mappings/%5Bid%5D/route.ts"],
|
|
4
|
+
"sourcesContent": ["// GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/mappings/[id]/route.ts). Do not edit.\n// This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.\nexport * from '@open-mercato/core/modules/data_sync/api/mappings/[id]/route'\nexport { GET, PUT, DELETE } from '@open-mercato/core/modules/data_sync/api/mappings/[id]/route'\nimport { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/mappings/[id]/route'\nexport const openApi = coreOpenApi\n// Copied from core: the generator reads this by AST and cannot follow a re-export.\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },\n PUT: { requireAuth: true, requireFeatures: ['data_sync.configure'] },\n DELETE: { requireAuth: true, requireFeatures: ['data_sync.configure'] },\n}\n"],
|
|
5
|
+
"mappings": "AAEA,cAAc;AACd,SAAS,KAAK,KAAK,cAAc;AACjC,SAAS,WAAW,mBAAmB;AAChC,MAAM,UAAU;AAEhB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AAAA,EACnE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AAAA,EACnE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACxE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "@open-mercato/core/modules/data_sync/api/mappings/route";
|
|
2
|
+
import { GET, POST } from "@open-mercato/core/modules/data_sync/api/mappings/route";
|
|
3
|
+
import { openApi as coreOpenApi } from "@open-mercato/core/modules/data_sync/api/mappings/route";
|
|
4
|
+
const openApi = coreOpenApi;
|
|
5
|
+
const metadata = {
|
|
6
|
+
GET: { requireAuth: true, requireFeatures: ["data_sync.configure"] },
|
|
7
|
+
POST: { requireAuth: true, requireFeatures: ["data_sync.configure"] }
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
GET,
|
|
11
|
+
POST,
|
|
12
|
+
metadata,
|
|
13
|
+
openApi
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/data_sync/api/mappings/route.ts"],
|
|
4
|
+
"sourcesContent": ["// GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/mappings/route.ts). Do not edit.\n// This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.\nexport * from '@open-mercato/core/modules/data_sync/api/mappings/route'\nexport { GET, POST } from '@open-mercato/core/modules/data_sync/api/mappings/route'\nimport { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/mappings/route'\nexport const openApi = coreOpenApi\n// Copied from core: the generator reads this by AST and cannot follow a re-export.\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },\n POST: { requireAuth: true, requireFeatures: ['data_sync.configure'] },\n}\n"],
|
|
5
|
+
"mappings": "AAEA,cAAc;AACd,SAAS,KAAK,YAAY;AAC1B,SAAS,WAAW,mBAAmB;AAChC,MAAM,UAAU;AAEhB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AAAA,EACnE,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "@open-mercato/core/modules/data_sync/api/options";
|
|
2
|
+
import { GET } from "@open-mercato/core/modules/data_sync/api/options";
|
|
3
|
+
import { openApi as coreOpenApi } from "@open-mercato/core/modules/data_sync/api/options";
|
|
4
|
+
const openApi = coreOpenApi;
|
|
5
|
+
const metadata = {
|
|
6
|
+
GET: { requireAuth: true, requireFeatures: ["data_sync.view"] }
|
|
7
|
+
};
|
|
8
|
+
export {
|
|
9
|
+
GET,
|
|
10
|
+
metadata,
|
|
11
|
+
openApi
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/data_sync/api/options.ts"],
|
|
4
|
+
"sourcesContent": ["// GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/options.ts). Do not edit.\n// This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.\nexport * from '@open-mercato/core/modules/data_sync/api/options'\nexport { GET } from '@open-mercato/core/modules/data_sync/api/options'\nimport { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/options'\nexport const openApi = coreOpenApi\n// Copied from core: the generator reads this by AST and cannot follow a re-export.\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['data_sync.view'] },\n}\n"],
|
|
5
|
+
"mappings": "AAEA,cAAc;AACd,SAAS,WAAW;AACpB,SAAS,WAAW,mBAAmB;AAChC,MAAM,UAAU;AAEhB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAChE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
4
|
+
import { POST as corePost, openApi as coreOpenApi } from "@open-mercato/core/modules/data_sync/api/run";
|
|
5
|
+
import { EXPORT_KIND, IMPORT_KIND, syncIdempotencyKey, syncLockKey } from "../../../kinds/data-sync-run.js";
|
|
6
|
+
const logger = createLogger("data_sync").child({ component: "durable-run-route" });
|
|
7
|
+
const metadata = {
|
|
8
|
+
POST: { requireAuth: true, requireFeatures: ["data_sync.run"] }
|
|
9
|
+
};
|
|
10
|
+
const openApi = coreOpenApi;
|
|
11
|
+
const DURABLE_HEADER = "x-durable-work";
|
|
12
|
+
async function POST(req) {
|
|
13
|
+
const response = await corePost(req);
|
|
14
|
+
if (response.status !== 201) return response;
|
|
15
|
+
const started = await response.clone().json();
|
|
16
|
+
try {
|
|
17
|
+
const auth = await getAuthFromRequest(req);
|
|
18
|
+
if (!auth?.tenantId) {
|
|
19
|
+
response.headers.set(DURABLE_HEADER, "deferred");
|
|
20
|
+
return response;
|
|
21
|
+
}
|
|
22
|
+
const container = await createRequestContainer();
|
|
23
|
+
const durable = container.resolve("durableWorkService");
|
|
24
|
+
const runService = container.resolve("dataSyncRunService");
|
|
25
|
+
const scope = { tenantId: auth.tenantId, organizationId: auth.orgId ?? null };
|
|
26
|
+
const run = await runService.getRun(started.id, { ...scope, organizationId: auth.orgId });
|
|
27
|
+
if (!run) {
|
|
28
|
+
response.headers.set(DURABLE_HEADER, "deferred");
|
|
29
|
+
return response;
|
|
30
|
+
}
|
|
31
|
+
await durable.startAndEnqueue(
|
|
32
|
+
{
|
|
33
|
+
kind: run.direction === "export" ? EXPORT_KIND : IMPORT_KIND,
|
|
34
|
+
input: { runId: run.id, batchSize: 100, direction: run.direction === "export" ? "export" : "import" },
|
|
35
|
+
lockKey: syncLockKey(run.integrationId, run.entityType, run.direction),
|
|
36
|
+
idempotencyKey: syncIdempotencyKey(run.id),
|
|
37
|
+
subject: { type: "data_sync.run", id: run.id },
|
|
38
|
+
progressJobId: run.progressJobId ?? started.progressJobId ?? null,
|
|
39
|
+
createdBy: auth.sub ?? null
|
|
40
|
+
},
|
|
41
|
+
scope
|
|
42
|
+
);
|
|
43
|
+
response.headers.set(DURABLE_HEADER, "created");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.warn("Could not create the durable job for a started run; the queue delivery will adopt it", {
|
|
46
|
+
runId: started.id,
|
|
47
|
+
err: error
|
|
48
|
+
});
|
|
49
|
+
response.headers.set(DURABLE_HEADER, "deferred");
|
|
50
|
+
response.headers.set(`${DURABLE_HEADER}-reason`, error instanceof Error ? error.message.slice(0, 200) : "unknown");
|
|
51
|
+
}
|
|
52
|
+
return response;
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
POST,
|
|
56
|
+
metadata,
|
|
57
|
+
openApi
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/data_sync/api/run.ts"],
|
|
4
|
+
"sourcesContent": ["// Core's run route, with the durable job created alongside the run.\n//\n// Wrapped rather than copied. Core's route is 180 lines of validation, ACL guards, cursor\n// resolution and parameter normalisation, and duplicating that is precisely the liability\n// ADR 0004 removed for the engine \u2014 the same argument applies here.\n//\n// Why the wrapper is needed at all: replacing `lib/start-run.ts` redirects only callers that\n// import *our* deep path. Core's own route imports its own sibling, so a run started through\n// it never reaches our start path. This is where that is fixed.\n//\n// The durable job is created after core's transaction rather than inside it, which is weaker\n// than the transactional start our own path gives. It is not a hole: core also enqueues onto\n// its queue, our adopt-on-delivery worker is registered under core's worker id, and both paths\n// carry the same idempotency key \u2014 so a crash between the two ends with the delivery adopting\n// the run instead. What the wrapper buys is that the job normally exists immediately, rather\n// than whenever the queue gets to it.\n\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { POST as corePost, openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/run'\nimport type { DurableWorkService } from '@fullstackhouse/open-mercato-durable-work'\n\nimport { EXPORT_KIND, IMPORT_KIND, syncIdempotencyKey, syncLockKey } from '../../../kinds/data-sync-run'\n\nconst logger = createLogger('data_sync').child({ component: 'durable-run-route' })\n\n// Copied from core, because the generator reads it by AST and cannot follow a re-export.\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = coreOpenApi\n\ntype StartedRun = { id: string; progressJobId: string | null }\ntype RunRow = { id: string; integrationId: string; entityType: string; direction: string; progressJobId?: string | null }\n\n/**\n * Says what happened to the durable job, without changing core's response body.\n *\n * created \u2014 the job exists now\n * deferred \u2014 it does not, and the queue delivery will adopt the run instead\n *\n * A header rather than a log line because the difference matters to whoever called: `created`\n * means the run is already under a lease, `deferred` means it is not yet. Buried in a log,\n * that distinction is unavailable to exactly the people who need it, including a test.\n */\nconst DURABLE_HEADER = 'x-durable-work'\n\nexport async function POST(req: Request) {\n const response = await corePost(req)\n if (response.status !== 201) return response\n\n // Cloned: the caller still gets core's response untouched, whatever happens below.\n const started = (await response.clone().json()) as StartedRun\n\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n response.headers.set(DURABLE_HEADER, 'deferred')\n return response\n }\n\n const container = await createRequestContainer()\n const durable = container.resolve('durableWorkService') as DurableWorkService\n const runService = container.resolve('dataSyncRunService') as {\n getRun(id: string, scope: unknown): Promise<RunRow | null>\n }\n const scope = { tenantId: auth.tenantId, organizationId: auth.orgId ?? null }\n\n const run = await runService.getRun(started.id, { ...scope, organizationId: auth.orgId })\n if (!run) {\n response.headers.set(DURABLE_HEADER, 'deferred')\n return response\n }\n\n await durable.startAndEnqueue(\n {\n kind: run.direction === 'export' ? EXPORT_KIND : IMPORT_KIND,\n input: { runId: run.id, batchSize: 100, direction: run.direction === 'export' ? 'export' : 'import' },\n lockKey: syncLockKey(run.integrationId, run.entityType, run.direction),\n idempotencyKey: syncIdempotencyKey(run.id),\n subject: { type: 'data_sync.run', id: run.id },\n progressJobId: run.progressJobId ?? started.progressJobId ?? null,\n createdBy: auth.sub ?? null,\n },\n scope,\n )\n response.headers.set(DURABLE_HEADER, 'created')\n } catch (error) {\n // Never fails the request. The run exists and core has already enqueued it, so the\n // adopt-on-delivery worker will make it durable; turning that into a 500 would refuse a\n // run that is going to happen anyway.\n logger.warn('Could not create the durable job for a started run; the queue delivery will adopt it', {\n runId: started.id,\n err: error,\n })\n response.headers.set(DURABLE_HEADER, 'deferred')\n response.headers.set(`${DURABLE_HEADER}-reason`, error instanceof Error ? error.message.slice(0, 200) : 'unknown')\n }\n\n return response\n}\n"],
|
|
5
|
+
"mappings": "AAiBA,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,QAAQ,UAAU,WAAW,mBAAmB;AAGzD,SAAS,aAAa,aAAa,oBAAoB,mBAAmB;AAE1E,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,oBAAoB,CAAC;AAG1E,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAChE;AAEO,MAAM,UAAU;AAevB,MAAM,iBAAiB;AAEvB,eAAsB,KAAK,KAAc;AACvC,QAAM,WAAW,MAAM,SAAS,GAAG;AACnC,MAAI,SAAS,WAAW,IAAK,QAAO;AAGpC,QAAM,UAAW,MAAM,SAAS,MAAM,EAAE,KAAK;AAE7C,MAAI;AACF,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,MAAM,UAAU;AACnB,eAAS,QAAQ,IAAI,gBAAgB,UAAU;AAC/C,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,oBAAoB;AACtD,UAAM,aAAa,UAAU,QAAQ,oBAAoB;AAGzD,UAAM,QAAQ,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,SAAS,KAAK;AAE5E,UAAM,MAAM,MAAM,WAAW,OAAO,QAAQ,IAAI,EAAE,GAAG,OAAO,gBAAgB,KAAK,MAAM,CAAC;AACxF,QAAI,CAAC,KAAK;AACR,eAAS,QAAQ,IAAI,gBAAgB,UAAU;AAC/C,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ;AAAA,MACZ;AAAA,QACE,MAAM,IAAI,cAAc,WAAW,cAAc;AAAA,QACjD,OAAO,EAAE,OAAO,IAAI,IAAI,WAAW,KAAK,WAAW,IAAI,cAAc,WAAW,WAAW,SAAS;AAAA,QACpG,SAAS,YAAY,IAAI,eAAe,IAAI,YAAY,IAAI,SAAS;AAAA,QACrE,gBAAgB,mBAAmB,IAAI,EAAE;AAAA,QACzC,SAAS,EAAE,MAAM,iBAAiB,IAAI,IAAI,GAAG;AAAA,QAC7C,eAAe,IAAI,iBAAiB,QAAQ,iBAAiB;AAAA,QAC7D,WAAW,KAAK,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,IACF;AACA,aAAS,QAAQ,IAAI,gBAAgB,SAAS;AAAA,EAChD,SAAS,OAAO;AAId,WAAO,KAAK,wFAAwF;AAAA,MAClG,OAAO,QAAQ;AAAA,MACf,KAAK;AAAA,IACP,CAAC;AACD,aAAS,QAAQ,IAAI,gBAAgB,UAAU;AAC/C,aAAS,QAAQ,IAAI,GAAG,cAAc,WAAW,iBAAiB,QAAQ,MAAM,QAAQ,MAAM,GAAG,GAAG,IAAI,SAAS;AAAA,EACnH;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "@open-mercato/core/modules/data_sync/api/runs/[id]/cancel";
|
|
2
|
+
import { POST } from "@open-mercato/core/modules/data_sync/api/runs/[id]/cancel";
|
|
3
|
+
import { openApi as coreOpenApi } from "@open-mercato/core/modules/data_sync/api/runs/[id]/cancel";
|
|
4
|
+
const openApi = coreOpenApi;
|
|
5
|
+
const metadata = {
|
|
6
|
+
POST: { requireAuth: true, requireFeatures: ["data_sync.run"] }
|
|
7
|
+
};
|
|
8
|
+
export {
|
|
9
|
+
POST,
|
|
10
|
+
metadata,
|
|
11
|
+
openApi
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=cancel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/data_sync/api/runs/%5Bid%5D/cancel.ts"],
|
|
4
|
+
"sourcesContent": ["// GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/runs/[id]/cancel.ts). Do not edit.\n// This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.\nexport * from '@open-mercato/core/modules/data_sync/api/runs/[id]/cancel'\nexport { POST } from '@open-mercato/core/modules/data_sync/api/runs/[id]/cancel'\nimport { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/runs/[id]/cancel'\nexport const openApi = coreOpenApi\n// Copied from core: the generator reads this by AST and cannot follow a re-export.\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n"],
|
|
5
|
+
"mappings": "AAEA,cAAc;AACd,SAAS,YAAY;AACrB,SAAS,WAAW,mBAAmB;AAChC,MAAM,UAAU;AAEhB,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,eAAe,EAAE;AAChE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|