@fullstackhouse/open-mercato-durable-work 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.
Files changed (105) hide show
  1. package/README.md +136 -0
  2. package/dist/core/errors.js +81 -0
  3. package/dist/core/errors.js.map +7 -0
  4. package/dist/core/ids.js +30 -0
  5. package/dist/core/ids.js.map +7 -0
  6. package/dist/core/reconciler.js +149 -0
  7. package/dist/core/reconciler.js.map +7 -0
  8. package/dist/core/registry.js +72 -0
  9. package/dist/core/registry.js.map +7 -0
  10. package/dist/core/run-slice.js +210 -0
  11. package/dist/core/run-slice.js.map +7 -0
  12. package/dist/core/schema.js +100 -0
  13. package/dist/core/schema.js.map +7 -0
  14. package/dist/core/service.js +161 -0
  15. package/dist/core/service.js.map +7 -0
  16. package/dist/core/store.js +516 -0
  17. package/dist/core/store.js.map +7 -0
  18. package/dist/core/terminal.js +53 -0
  19. package/dist/core/terminal.js.map +7 -0
  20. package/dist/core/types.js +1 -0
  21. package/dist/core/types.js.map +7 -0
  22. package/dist/core/worker.js +111 -0
  23. package/dist/core/worker.js.map +7 -0
  24. package/dist/index.js +99 -0
  25. package/dist/index.js.map +7 -0
  26. package/dist/modules/durable_work/acl.js +10 -0
  27. package/dist/modules/durable_work/acl.js.map +7 -0
  28. package/dist/modules/durable_work/api/jobs/[id]/redrive.js +20 -0
  29. package/dist/modules/durable_work/api/jobs/[id]/redrive.js.map +7 -0
  30. package/dist/modules/durable_work/api/jobs/[id]/route.js +26 -0
  31. package/dist/modules/durable_work/api/jobs/[id]/route.js.map +7 -0
  32. package/dist/modules/durable_work/api/jobs/route.js +24 -0
  33. package/dist/modules/durable_work/api/jobs/route.js.map +7 -0
  34. package/dist/modules/durable_work/cli.js +72 -0
  35. package/dist/modules/durable_work/cli.js.map +7 -0
  36. package/dist/modules/durable_work/data/entities.js +163 -0
  37. package/dist/modules/durable_work/data/entities.js.map +7 -0
  38. package/dist/modules/durable_work/di.js +34 -0
  39. package/dist/modules/durable_work/di.js.map +7 -0
  40. package/dist/modules/durable_work/events.js +26 -0
  41. package/dist/modules/durable_work/events.js.map +7 -0
  42. package/dist/modules/durable_work/index.js +17 -0
  43. package/dist/modules/durable_work/index.js.map +7 -0
  44. package/dist/modules/durable_work/lib/route-helpers.js +66 -0
  45. package/dist/modules/durable_work/lib/route-helpers.js.map +7 -0
  46. package/dist/modules/durable_work/migrations/Migration20260908120000.js +17 -0
  47. package/dist/modules/durable_work/migrations/Migration20260908120000.js.map +7 -0
  48. package/dist/modules/durable_work/setup.js +12 -0
  49. package/dist/modules/durable_work/setup.js.map +7 -0
  50. package/dist/om/config.js +49 -0
  51. package/dist/om/config.js.map +7 -0
  52. package/dist/om/progress-mirror.js +49 -0
  53. package/dist/om/progress-mirror.js.map +7 -0
  54. package/dist/om/sql-executor-mikro.js +48 -0
  55. package/dist/om/sql-executor-mikro.js.map +7 -0
  56. package/dist/transport/bullmq.js +144 -0
  57. package/dist/transport/bullmq.js.map +7 -0
  58. package/dist/transport/conformance.js +177 -0
  59. package/dist/transport/conformance.js.map +7 -0
  60. package/dist/transport/memory.js +139 -0
  61. package/dist/transport/memory.js.map +7 -0
  62. package/dist/transport/pgboss.js +176 -0
  63. package/dist/transport/pgboss.js.map +7 -0
  64. package/dist/transport/types.js +1 -0
  65. package/dist/transport/types.js.map +7 -0
  66. package/generated/entities/durable_work_job/index.ts +42 -0
  67. package/generated/entities.ids.generated.ts +9 -0
  68. package/package.json +145 -0
  69. package/src/core/__tests__/registry.test.ts +43 -0
  70. package/src/core/errors.ts +104 -0
  71. package/src/core/ids.ts +58 -0
  72. package/src/core/reconciler.ts +242 -0
  73. package/src/core/registry.ts +199 -0
  74. package/src/core/run-slice.ts +343 -0
  75. package/src/core/schema.ts +114 -0
  76. package/src/core/service.ts +222 -0
  77. package/src/core/store.ts +786 -0
  78. package/src/core/terminal.ts +107 -0
  79. package/src/core/types.ts +120 -0
  80. package/src/core/worker.ts +169 -0
  81. package/src/index.ts +100 -0
  82. package/src/modules/durable_work/__integration__/TC-DW-001.spec.ts +51 -0
  83. package/src/modules/durable_work/__tests__/metadata.test.ts +13 -0
  84. package/src/modules/durable_work/__tests__/schema-agreement.test.ts +52 -0
  85. package/src/modules/durable_work/acl.ts +6 -0
  86. package/src/modules/durable_work/api/jobs/[id]/redrive.ts +27 -0
  87. package/src/modules/durable_work/api/jobs/[id]/route.ts +27 -0
  88. package/src/modules/durable_work/api/jobs/route.ts +26 -0
  89. package/src/modules/durable_work/cli.ts +91 -0
  90. package/src/modules/durable_work/data/entities.ts +158 -0
  91. package/src/modules/durable_work/di.ts +41 -0
  92. package/src/modules/durable_work/events.ts +30 -0
  93. package/src/modules/durable_work/index.ts +16 -0
  94. package/src/modules/durable_work/lib/route-helpers.ts +83 -0
  95. package/src/modules/durable_work/migrations/Migration20260908120000.ts +24 -0
  96. package/src/modules/durable_work/setup.ts +10 -0
  97. package/src/om/__tests__/sql-executor-mikro.test.ts +83 -0
  98. package/src/om/config.ts +65 -0
  99. package/src/om/progress-mirror.ts +80 -0
  100. package/src/om/sql-executor-mikro.ts +104 -0
  101. package/src/transport/bullmq.ts +213 -0
  102. package/src/transport/conformance.ts +218 -0
  103. package/src/transport/memory.ts +191 -0
  104. package/src/transport/pgboss.ts +250 -0
  105. package/src/transport/types.ts +81 -0
@@ -0,0 +1,163 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ import { OptionalProps } from "@mikro-orm/core";
12
+ import { Entity, Index, PrimaryKey, Property } from "@mikro-orm/decorators/legacy";
13
+ OptionalProps;
14
+ let DurableWorkJob = class {
15
+ constructor() {
16
+ this.status = "pending";
17
+ this.createdAt = /* @__PURE__ */ new Date();
18
+ this.updatedAt = /* @__PURE__ */ new Date();
19
+ this.leaseEpoch = 0;
20
+ this.continuationSeq = 0;
21
+ this.redrives = 0;
22
+ this.redrivesSinceCommit = 0;
23
+ this.consecutiveFailures = 0;
24
+ this.interruptions = 0;
25
+ this.mirrorAttempts = 0;
26
+ this.processedCount = 0;
27
+ }
28
+ };
29
+ __decorateClass([
30
+ PrimaryKey({ type: "uuid", defaultRaw: "gen_random_uuid()" })
31
+ ], DurableWorkJob.prototype, "id", 2);
32
+ __decorateClass([
33
+ Property({ name: "tenant_id", type: "uuid" })
34
+ ], DurableWorkJob.prototype, "tenantId", 2);
35
+ __decorateClass([
36
+ Property({ name: "organization_id", type: "uuid", nullable: true })
37
+ ], DurableWorkJob.prototype, "organizationId", 2);
38
+ __decorateClass([
39
+ Property({ name: "kind", type: "text" })
40
+ ], DurableWorkJob.prototype, "kind", 2);
41
+ __decorateClass([
42
+ Property({ name: "status", type: "text" })
43
+ ], DurableWorkJob.prototype, "status", 2);
44
+ __decorateClass([
45
+ Property({ name: "created_by", type: "uuid", nullable: true })
46
+ ], DurableWorkJob.prototype, "createdBy", 2);
47
+ __decorateClass([
48
+ Property({ name: "created_at", type: "timestamptz" })
49
+ ], DurableWorkJob.prototype, "createdAt", 2);
50
+ __decorateClass([
51
+ Property({ name: "updated_at", type: "timestamptz", onUpdate: () => /* @__PURE__ */ new Date() })
52
+ ], DurableWorkJob.prototype, "updatedAt", 2);
53
+ __decorateClass([
54
+ Property({ name: "input", type: "jsonb", nullable: true })
55
+ ], DurableWorkJob.prototype, "input", 2);
56
+ __decorateClass([
57
+ Property({ name: "checkpoint", type: "jsonb", nullable: true })
58
+ ], DurableWorkJob.prototype, "checkpoint", 2);
59
+ __decorateClass([
60
+ Property({ name: "meta", type: "jsonb", nullable: true })
61
+ ], DurableWorkJob.prototype, "meta", 2);
62
+ __decorateClass([
63
+ Property({ name: "idempotency_key", type: "text", nullable: true })
64
+ ], DurableWorkJob.prototype, "idempotencyKey", 2);
65
+ __decorateClass([
66
+ Property({ name: "lock_key", type: "text", nullable: true })
67
+ ], DurableWorkJob.prototype, "lockKey", 2);
68
+ __decorateClass([
69
+ Property({ name: "subject_type", type: "text", nullable: true })
70
+ ], DurableWorkJob.prototype, "subjectType", 2);
71
+ __decorateClass([
72
+ Property({ name: "subject_id", type: "text", nullable: true })
73
+ ], DurableWorkJob.prototype, "subjectId", 2);
74
+ __decorateClass([
75
+ Property({ name: "progress_job_id", type: "uuid", nullable: true })
76
+ ], DurableWorkJob.prototype, "progressJobId", 2);
77
+ __decorateClass([
78
+ Property({ name: "lease_owner", type: "text", nullable: true })
79
+ ], DurableWorkJob.prototype, "leaseOwner", 2);
80
+ __decorateClass([
81
+ Property({ name: "lease_epoch", type: "bigint" })
82
+ ], DurableWorkJob.prototype, "leaseEpoch", 2);
83
+ __decorateClass([
84
+ Property({ name: "lease_expires_at", type: "timestamptz", nullable: true })
85
+ ], DurableWorkJob.prototype, "leaseExpiresAt", 2);
86
+ __decorateClass([
87
+ Property({ name: "heartbeat_at", type: "timestamptz", nullable: true })
88
+ ], DurableWorkJob.prototype, "heartbeatAt", 2);
89
+ __decorateClass([
90
+ Property({ name: "queue_name", type: "text", nullable: true })
91
+ ], DurableWorkJob.prototype, "queueName", 2);
92
+ __decorateClass([
93
+ Property({ name: "queue_job_id", type: "text", nullable: true })
94
+ ], DurableWorkJob.prototype, "queueJobId", 2);
95
+ __decorateClass([
96
+ Property({ name: "continuation_seq", type: "int" })
97
+ ], DurableWorkJob.prototype, "continuationSeq", 2);
98
+ __decorateClass([
99
+ Property({ name: "redrives", type: "int" })
100
+ ], DurableWorkJob.prototype, "redrives", 2);
101
+ __decorateClass([
102
+ Property({ name: "next_run_at", type: "timestamptz", nullable: true })
103
+ ], DurableWorkJob.prototype, "nextRunAt", 2);
104
+ __decorateClass([
105
+ Property({ name: "pending_since", type: "timestamptz", nullable: true })
106
+ ], DurableWorkJob.prototype, "pendingSince", 2);
107
+ __decorateClass([
108
+ Property({ name: "redrives_since_commit", type: "int" })
109
+ ], DurableWorkJob.prototype, "redrivesSinceCommit", 2);
110
+ __decorateClass([
111
+ Property({ name: "consecutive_failures", type: "int" })
112
+ ], DurableWorkJob.prototype, "consecutiveFailures", 2);
113
+ __decorateClass([
114
+ Property({ name: "interruptions", type: "int" })
115
+ ], DurableWorkJob.prototype, "interruptions", 2);
116
+ __decorateClass([
117
+ Property({ name: "mirror_attempts", type: "int" })
118
+ ], DurableWorkJob.prototype, "mirrorAttempts", 2);
119
+ __decorateClass([
120
+ Property({ name: "last_committed_at", type: "timestamptz", nullable: true })
121
+ ], DurableWorkJob.prototype, "lastCommittedAt", 2);
122
+ __decorateClass([
123
+ Property({ name: "started_at", type: "timestamptz", nullable: true })
124
+ ], DurableWorkJob.prototype, "startedAt", 2);
125
+ __decorateClass([
126
+ Property({ name: "finished_at", type: "timestamptz", nullable: true })
127
+ ], DurableWorkJob.prototype, "finishedAt", 2);
128
+ __decorateClass([
129
+ Property({ name: "parked_at", type: "timestamptz", nullable: true })
130
+ ], DurableWorkJob.prototype, "parkedAt", 2);
131
+ __decorateClass([
132
+ Property({ name: "cancel_requested_at", type: "timestamptz", nullable: true })
133
+ ], DurableWorkJob.prototype, "cancelRequestedAt", 2);
134
+ __decorateClass([
135
+ Property({ name: "cancelled_by", type: "uuid", nullable: true })
136
+ ], DurableWorkJob.prototype, "cancelledBy", 2);
137
+ __decorateClass([
138
+ Property({ name: "error_class", type: "text", nullable: true })
139
+ ], DurableWorkJob.prototype, "errorClass", 2);
140
+ __decorateClass([
141
+ Property({ name: "error_code", type: "text", nullable: true })
142
+ ], DurableWorkJob.prototype, "errorCode", 2);
143
+ __decorateClass([
144
+ Property({ name: "error_message", type: "text", nullable: true })
145
+ ], DurableWorkJob.prototype, "errorMessage", 2);
146
+ __decorateClass([
147
+ Property({ name: "domain_mirrored_at", type: "timestamptz", nullable: true })
148
+ ], DurableWorkJob.prototype, "domainMirroredAt", 2);
149
+ __decorateClass([
150
+ Property({ name: "processed_count", type: "int" })
151
+ ], DurableWorkJob.prototype, "processedCount", 2);
152
+ __decorateClass([
153
+ Property({ name: "total_count", type: "int", nullable: true })
154
+ ], DurableWorkJob.prototype, "totalCount", 2);
155
+ DurableWorkJob = __decorateClass([
156
+ Entity({ tableName: "durable_work_jobs" }),
157
+ Index({ name: "durable_work_jobs_running_idx", properties: ["tenantId"] }),
158
+ Index({ name: "durable_work_jobs_subject_idx", properties: ["subjectType", "subjectId"] })
159
+ ], DurableWorkJob);
160
+ export {
161
+ DurableWorkJob
162
+ };
163
+ //# sourceMappingURL=entities.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/durable_work/data/entities.ts"],
4
+ "sourcesContent": ["// The MikroORM view of the job table.\n//\n// The mechanism itself never goes through this entity \u2014 every statement in `core/store.ts` is\n// hand-written SQL, because each is a compare-and-set whose predicate is the guarantee. The\n// entity exists so the table is discoverable to the host: migrations, the entity registry,\n// query tooling and anything an app wants to join against.\n//\n// It must therefore stay in step with `core/schema.ts`, which is the definition. A test\n// asserts they agree column for column.\n\nimport { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property } from '@mikro-orm/decorators/legacy'\n\nexport type DurableWorkJobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n\n@Entity({ tableName: 'durable_work_jobs' })\n@Index({ name: 'durable_work_jobs_running_idx', properties: ['tenantId'] })\n@Index({ name: 'durable_work_jobs_subject_idx', properties: ['subjectType', 'subjectId'] })\nexport class DurableWorkJob {\n [OptionalProps]?:\n | 'status'\n | 'leaseEpoch'\n | 'continuationSeq'\n | 'redrives'\n | 'redrivesSinceCommit'\n | 'consecutiveFailures'\n | 'interruptions'\n | 'mirrorAttempts'\n | 'processedCount'\n | 'createdAt'\n | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'kind', type: 'text' })\n kind!: string\n\n @Property({ name: 'status', type: 'text' })\n status: DurableWorkJobStatus = 'pending'\n\n @Property({ name: 'created_by', type: 'uuid', nullable: true })\n createdBy?: string | null\n\n @Property({ name: 'created_at', type: 'timestamptz' })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: 'timestamptz', onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'input', type: 'jsonb', nullable: true })\n input?: unknown\n\n @Property({ name: 'checkpoint', type: 'jsonb', nullable: true })\n checkpoint?: unknown\n\n @Property({ name: 'meta', type: 'jsonb', nullable: true })\n meta?: Record<string, unknown> | null\n\n @Property({ name: 'idempotency_key', type: 'text', nullable: true })\n idempotencyKey?: string | null\n\n @Property({ name: 'lock_key', type: 'text', nullable: true })\n lockKey?: string | null\n\n @Property({ name: 'subject_type', type: 'text', nullable: true })\n subjectType?: string | null\n\n @Property({ name: 'subject_id', type: 'text', nullable: true })\n subjectId?: string | null\n\n @Property({ name: 'progress_job_id', type: 'uuid', nullable: true })\n progressJobId?: string | null\n\n @Property({ name: 'lease_owner', type: 'text', nullable: true })\n leaseOwner?: string | null\n\n @Property({ name: 'lease_epoch', type: 'bigint' })\n leaseEpoch: number = 0\n\n @Property({ name: 'lease_expires_at', type: 'timestamptz', nullable: true })\n leaseExpiresAt?: Date | null\n\n @Property({ name: 'heartbeat_at', type: 'timestamptz', nullable: true })\n heartbeatAt?: Date | null\n\n @Property({ name: 'queue_name', type: 'text', nullable: true })\n queueName?: string | null\n\n @Property({ name: 'queue_job_id', type: 'text', nullable: true })\n queueJobId?: string | null\n\n @Property({ name: 'continuation_seq', type: 'int' })\n continuationSeq: number = 0\n\n @Property({ name: 'redrives', type: 'int' })\n redrives: number = 0\n\n @Property({ name: 'next_run_at', type: 'timestamptz', nullable: true })\n nextRunAt?: Date | null\n\n @Property({ name: 'pending_since', type: 'timestamptz', nullable: true })\n pendingSince?: Date | null\n\n @Property({ name: 'redrives_since_commit', type: 'int' })\n redrivesSinceCommit: number = 0\n\n @Property({ name: 'consecutive_failures', type: 'int' })\n consecutiveFailures: number = 0\n\n @Property({ name: 'interruptions', type: 'int' })\n interruptions: number = 0\n\n @Property({ name: 'mirror_attempts', type: 'int' })\n mirrorAttempts: number = 0\n\n @Property({ name: 'last_committed_at', type: 'timestamptz', nullable: true })\n lastCommittedAt?: Date | null\n\n @Property({ name: 'started_at', type: 'timestamptz', nullable: true })\n startedAt?: Date | null\n\n @Property({ name: 'finished_at', type: 'timestamptz', nullable: true })\n finishedAt?: Date | null\n\n @Property({ name: 'parked_at', type: 'timestamptz', nullable: true })\n parkedAt?: Date | null\n\n @Property({ name: 'cancel_requested_at', type: 'timestamptz', nullable: true })\n cancelRequestedAt?: Date | null\n\n @Property({ name: 'cancelled_by', type: 'uuid', nullable: true })\n cancelledBy?: string | null\n\n @Property({ name: 'error_class', type: 'text', nullable: true })\n errorClass?: string | null\n\n @Property({ name: 'error_code', type: 'text', nullable: true })\n errorCode?: string | null\n\n @Property({ name: 'error_message', type: 'text', nullable: true })\n errorMessage?: string | null\n\n @Property({ name: 'domain_mirrored_at', type: 'timestamptz', nullable: true })\n domainMirroredAt?: Date | null\n\n @Property({ name: 'processed_count', type: 'int' })\n processedCount: number = 0\n\n @Property({ name: 'total_count', type: 'int', nullable: true })\n totalCount?: number | null\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,34 @@
1
+ import { DurableWorkService } from "../../core/service.js";
2
+ import { registry } from "../../core/registry.js";
3
+ import { createTransport, readConfig } from "../../om/config.js";
4
+ import { mikroExecutor } from "../../om/sql-executor-mikro.js";
5
+ let transport = null;
6
+ function sharedTransport() {
7
+ if (!transport) transport = createTransport(readConfig());
8
+ return transport;
9
+ }
10
+ function register(container) {
11
+ container.register({
12
+ durableWorkService: {
13
+ resolve: (c) => {
14
+ const em = c.resolve("em");
15
+ const config = readConfig();
16
+ return new DurableWorkService({
17
+ sql: mikroExecutor(em),
18
+ transport: sharedTransport(),
19
+ registry,
20
+ graceMs: config.reconcilerGraceMs
21
+ });
22
+ }
23
+ },
24
+ // The raw executor, for the worker command: it drives the mechanism directly rather than
25
+ // through the service, and needs to open its own transactions.
26
+ durableWorkSql: { resolve: (c) => mikroExecutor(c.resolve("em")) },
27
+ durableWorkRegistry: { resolve: () => registry },
28
+ durableWorkTransport: { resolve: () => sharedTransport() }
29
+ });
30
+ }
31
+ export {
32
+ register
33
+ };
34
+ //# sourceMappingURL=di.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/durable_work/di.ts"],
4
+ "sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nimport { DurableWorkService } from '../../core/service'\nimport { registry } from '../../core/registry'\nimport { createTransport, readConfig } from '../../om/config'\nimport { mikroExecutor } from '../../om/sql-executor-mikro'\nimport type { TransportAdapter } from '../../transport/types'\n\n// One transport per process, not per request.\n//\n// A container is built per request, and a transport owns broker connections and bound workers.\n// Building one per request would open a Redis connection per HTTP call \u2014 the kind of leak that\n// looks like a memory problem for a week before anyone finds it.\nlet transport: TransportAdapter | null = null\nfunction sharedTransport(): TransportAdapter {\n if (!transport) transport = createTransport(readConfig())\n return transport\n}\n\nexport function register(container: AppContainer) {\n container.register({\n durableWorkService: {\n resolve: (c) => {\n const em = c.resolve<EntityManager>('em')\n const config = readConfig()\n return new DurableWorkService({\n sql: mikroExecutor(em),\n transport: sharedTransport(),\n registry,\n graceMs: config.reconcilerGraceMs,\n })\n },\n },\n // The raw executor, for the worker command: it drives the mechanism directly rather than\n // through the service, and needs to open its own transactions.\n durableWorkSql: { resolve: (c) => mikroExecutor(c.resolve<EntityManager>('em')) },\n durableWorkRegistry: { resolve: () => registry },\n durableWorkTransport: { resolve: () => sharedTransport() },\n })\n}\n"],
5
+ "mappings": "AAGA,SAAS,0BAA0B;AACnC,SAAS,gBAAgB;AACzB,SAAS,iBAAiB,kBAAkB;AAC5C,SAAS,qBAAqB;AAQ9B,IAAI,YAAqC;AACzC,SAAS,kBAAoC;AAC3C,MAAI,CAAC,UAAW,aAAY,gBAAgB,WAAW,CAAC;AACxD,SAAO;AACT;AAEO,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,oBAAoB;AAAA,MAClB,SAAS,CAAC,MAAM;AACd,cAAM,KAAK,EAAE,QAAuB,IAAI;AACxC,cAAM,SAAS,WAAW;AAC1B,eAAO,IAAI,mBAAmB;AAAA,UAC5B,KAAK,cAAc,EAAE;AAAA,UACrB,WAAW,gBAAgB;AAAA,UAC3B;AAAA,UACA,SAAS,OAAO;AAAA,QAClB,CAAC;AAAA,MACH;AAAA,IACF;AAAA;AAAA;AAAA,IAGA,gBAAgB,EAAE,SAAS,CAAC,MAAM,cAAc,EAAE,QAAuB,IAAI,CAAC,EAAE;AAAA,IAChF,qBAAqB,EAAE,SAAS,MAAM,SAAS;AAAA,IAC/C,sBAAsB,EAAE,SAAS,MAAM,gBAAgB,EAAE;AAAA,EAC3D,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,26 @@
1
+ import { createModuleEvents } from "@open-mercato/shared/modules/events";
2
+ const events = [
3
+ { id: "durable_work.job.created", label: "Job created", entity: "job", category: "crud", clientBroadcast: true },
4
+ { id: "durable_work.job.started", label: "Job started", entity: "job", category: "lifecycle", clientBroadcast: true },
5
+ { id: "durable_work.job.yielded", label: "Slice handed back", entity: "job", category: "lifecycle", clientBroadcast: false },
6
+ { id: "durable_work.job.completed", label: "Job completed", entity: "job", category: "lifecycle", clientBroadcast: true },
7
+ { id: "durable_work.job.failed", label: "Job failed", entity: "job", category: "lifecycle", clientBroadcast: true },
8
+ { id: "durable_work.job.parked", label: "Job parked for an operator", entity: "job", category: "lifecycle", clientBroadcast: true },
9
+ { id: "durable_work.job.cancelled", label: "Job cancelled", entity: "job", category: "lifecycle", clientBroadcast: true },
10
+ { id: "durable_work.job.redriven", label: "Job re-driven", entity: "job", category: "lifecycle", clientBroadcast: true },
11
+ { id: "durable_work.job.lease_lost", label: "Lease lost", entity: "job", category: "lifecycle", clientBroadcast: false },
12
+ { id: "durable_work.job.mirror_stuck", label: "Domain mirror stuck", entity: "job", category: "lifecycle", clientBroadcast: true }
13
+ ];
14
+ const eventsConfig = createModuleEvents({
15
+ moduleId: "durable_work",
16
+ events
17
+ });
18
+ const emitDurableWorkEvent = eventsConfig.emit;
19
+ var events_default = eventsConfig;
20
+ export {
21
+ events_default as default,
22
+ emitDurableWorkEvent,
23
+ events,
24
+ eventsConfig
25
+ };
26
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/durable_work/events.ts"],
4
+ "sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\n// The lifecycle a host can subscribe to.\n//\n// Every one is emitted after its transaction commits, and never inside it: a subscriber that\n// throws must not be able to roll back the fact the event describes, and a slow subscriber\n// must not hold a row lock for the length of its work.\nexport const events = [\n { id: 'durable_work.job.created', label: 'Job created', entity: 'job', category: 'crud', clientBroadcast: true },\n { id: 'durable_work.job.started', label: 'Job started', entity: 'job', category: 'lifecycle', clientBroadcast: true },\n { id: 'durable_work.job.yielded', label: 'Slice handed back', entity: 'job', category: 'lifecycle', clientBroadcast: false },\n { id: 'durable_work.job.completed', label: 'Job completed', entity: 'job', category: 'lifecycle', clientBroadcast: true },\n { id: 'durable_work.job.failed', label: 'Job failed', entity: 'job', category: 'lifecycle', clientBroadcast: true },\n { id: 'durable_work.job.parked', label: 'Job parked for an operator', entity: 'job', category: 'lifecycle', clientBroadcast: true },\n { id: 'durable_work.job.cancelled', label: 'Job cancelled', entity: 'job', category: 'lifecycle', clientBroadcast: true },\n { id: 'durable_work.job.redriven', label: 'Job re-driven', entity: 'job', category: 'lifecycle', clientBroadcast: true },\n { id: 'durable_work.job.lease_lost', label: 'Lease lost', entity: 'job', category: 'lifecycle', clientBroadcast: false },\n { id: 'durable_work.job.mirror_stuck', label: 'Domain mirror stuck', entity: 'job', category: 'lifecycle', clientBroadcast: true },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'durable_work',\n events,\n})\n\nexport const emitDurableWorkEvent = eventsConfig.emit\n\nexport type DurableWorkEventId = (typeof events)[number]['id']\n\nexport default eventsConfig\n"],
5
+ "mappings": "AAAA,SAAS,0BAA0B;AAO5B,MAAM,SAAS;AAAA,EACpB,EAAE,IAAI,4BAA4B,OAAO,eAAe,QAAQ,OAAO,UAAU,QAAQ,iBAAiB,KAAK;AAAA,EAC/G,EAAE,IAAI,4BAA4B,OAAO,eAAe,QAAQ,OAAO,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACpH,EAAE,IAAI,4BAA4B,OAAO,qBAAqB,QAAQ,OAAO,UAAU,aAAa,iBAAiB,MAAM;AAAA,EAC3H,EAAE,IAAI,8BAA8B,OAAO,iBAAiB,QAAQ,OAAO,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACxH,EAAE,IAAI,2BAA2B,OAAO,cAAc,QAAQ,OAAO,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAClH,EAAE,IAAI,2BAA2B,OAAO,8BAA8B,QAAQ,OAAO,UAAU,aAAa,iBAAiB,KAAK;AAAA,EAClI,EAAE,IAAI,8BAA8B,OAAO,iBAAiB,QAAQ,OAAO,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACxH,EAAE,IAAI,6BAA6B,OAAO,iBAAiB,QAAQ,OAAO,UAAU,aAAa,iBAAiB,KAAK;AAAA,EACvH,EAAE,IAAI,+BAA+B,OAAO,cAAc,QAAQ,OAAO,UAAU,aAAa,iBAAiB,MAAM;AAAA,EACvH,EAAE,IAAI,iCAAiC,OAAO,uBAAuB,QAAQ,OAAO,UAAU,aAAa,iBAAiB,KAAK;AACnI;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,uBAAuB,aAAa;AAIjD,IAAO,iBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,17 @@
1
+ const metadata = {
2
+ name: "durable_work",
3
+ title: "Durable Work",
4
+ version: "0.0.1",
5
+ description: "Durable at-least-once background work: a leased job record with epoch fencing, bounded resumable slices, a server-side reconciler, fenced cancel and an operator API. Other modules register job kinds; this module runs them.",
6
+ author: "Full Stack House",
7
+ license: "MIT",
8
+ ejectable: true
9
+ };
10
+ import { features } from "./acl.js";
11
+ var durable_work_default = metadata;
12
+ export {
13
+ durable_work_default as default,
14
+ features,
15
+ metadata
16
+ };
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/durable_work/index.ts"],
4
+ "sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: ModuleInfo = {\n name: 'durable_work',\n title: 'Durable Work',\n version: '0.0.1',\n description:\n 'Durable at-least-once background work: a leased job record with epoch fencing, bounded resumable slices, a server-side reconciler, fenced cancel and an operator API. Other modules register job kinds; this module runs them.',\n author: 'Full Stack House',\n license: 'MIT',\n ejectable: true,\n}\n\nexport { features } from './acl'\n\nexport default metadata\n"],
5
+ "mappings": "AAEO,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aACE;AAAA,EACF,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AACb;AAEA,SAAS,gBAAgB;AAEzB,IAAO,uBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,66 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
3
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
+ async function routeContext(req) {
5
+ const auth = await getAuthFromRequest(req);
6
+ if (!auth || !auth.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
7
+ const container = await createRequestContainer();
8
+ return {
9
+ service: container.resolve("durableWorkService"),
10
+ scope: { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },
11
+ userId: auth.sub ?? null
12
+ };
13
+ }
14
+ function toDto(job) {
15
+ return {
16
+ id: job.id,
17
+ kind: job.kind,
18
+ status: job.status,
19
+ input: job.input,
20
+ checkpoint: job.checkpoint,
21
+ subject: job.subjectType ? { type: job.subjectType, id: job.subjectId } : null,
22
+ lockKey: job.lockKey,
23
+ idempotencyKey: job.idempotencyKey,
24
+ processedCount: job.processedCount,
25
+ totalCount: job.totalCount,
26
+ redrives: job.redrives,
27
+ interruptions: job.interruptions,
28
+ consecutiveFailures: job.consecutiveFailures,
29
+ mirrorAttempts: job.mirrorAttempts,
30
+ leaseOwner: job.leaseOwner,
31
+ leaseEpoch: job.leaseEpoch,
32
+ leaseExpiresAt: iso(job.leaseExpiresAt),
33
+ heartbeatAt: iso(job.heartbeatAt),
34
+ nextRunAt: iso(job.nextRunAt),
35
+ startedAt: iso(job.startedAt),
36
+ finishedAt: iso(job.finishedAt),
37
+ parkedAt: iso(job.parkedAt),
38
+ cancelRequestedAt: iso(job.cancelRequestedAt),
39
+ errorClass: job.errorClass,
40
+ errorCode: job.errorCode,
41
+ errorMessage: job.errorMessage,
42
+ createdAt: iso(job.createdAt),
43
+ updatedAt: iso(job.updatedAt),
44
+ // Derived on the server because the client cannot see the kind's grace, its pending TTL
45
+ // or its mirror budget — and a UI that guessed would offer buttons that then 409.
46
+ redrivable: isRedrivable(job),
47
+ stuck: isStuck(job)
48
+ };
49
+ }
50
+ const iso = (value) => value ? value.toISOString() : null;
51
+ function isRedrivable(job) {
52
+ if (job.status === "completed" || job.status === "cancelled") return false;
53
+ if (job.status === "failed") return true;
54
+ return job.status === "running" && job.leaseExpiresAt != null && job.leaseExpiresAt.getTime() < Date.now();
55
+ }
56
+ function isStuck(job) {
57
+ if (job.status !== "running") return false;
58
+ const expired = job.leaseExpiresAt != null && job.leaseExpiresAt.getTime() < Date.now() - 2e4;
59
+ const nothingScheduled = job.nextRunAt == null || job.nextRunAt.getTime() < Date.now() - 9e5;
60
+ return expired && nothingScheduled;
61
+ }
62
+ export {
63
+ routeContext,
64
+ toDto
65
+ };
66
+ //# sourceMappingURL=route-helpers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/durable_work/lib/route-helpers.ts"],
4
+ "sourcesContent": ["// Shared plumbing for the operator routes.\n\nimport { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\n\nimport type { DurableWorkService } from '../../../core/service'\nimport type { DurableJob, Scope } from '../../../core/types'\n\nexport type RouteContext = { service: DurableWorkService; scope: Scope; userId: string | null }\n\n/**\n * Resolves the caller's scope and the service, or the response to return instead.\n *\n * The scope comes from the session, never from the request body or the path. An operator API\n * that let a caller name a tenant would be a way to re-drive somebody else's work.\n */\nexport async function routeContext(req: Request): Promise<RouteContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const container = await createRequestContainer()\n return {\n service: container.resolve('durableWorkService') as DurableWorkService,\n scope: { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n userId: auth.sub ?? null,\n }\n}\n\n/** The wire shape. Timestamps as ISO strings, plus two things only the server can decide. */\nexport function toDto(job: DurableJob) {\n return {\n id: job.id,\n kind: job.kind,\n status: job.status,\n input: job.input,\n checkpoint: job.checkpoint,\n subject: job.subjectType ? { type: job.subjectType, id: job.subjectId } : null,\n lockKey: job.lockKey,\n idempotencyKey: job.idempotencyKey,\n processedCount: job.processedCount,\n totalCount: job.totalCount,\n redrives: job.redrives,\n interruptions: job.interruptions,\n consecutiveFailures: job.consecutiveFailures,\n mirrorAttempts: job.mirrorAttempts,\n leaseOwner: job.leaseOwner,\n leaseEpoch: job.leaseEpoch,\n leaseExpiresAt: iso(job.leaseExpiresAt),\n heartbeatAt: iso(job.heartbeatAt),\n nextRunAt: iso(job.nextRunAt),\n startedAt: iso(job.startedAt),\n finishedAt: iso(job.finishedAt),\n parkedAt: iso(job.parkedAt),\n cancelRequestedAt: iso(job.cancelRequestedAt),\n errorClass: job.errorClass,\n errorCode: job.errorCode,\n errorMessage: job.errorMessage,\n createdAt: iso(job.createdAt),\n updatedAt: iso(job.updatedAt),\n // Derived on the server because the client cannot see the kind's grace, its pending TTL\n // or its mirror budget \u2014 and a UI that guessed would offer buttons that then 409.\n redrivable: isRedrivable(job),\n stuck: isStuck(job),\n }\n}\n\nconst iso = (value: Date | null): string | null => (value ? value.toISOString() : null)\n\n/** `completed` and `cancelled` are done on purpose: the first succeeded, the second was asked\n * for. Everything else that has stopped can be re-driven. */\nfunction isRedrivable(job: DurableJob): boolean {\n if (job.status === 'completed' || job.status === 'cancelled') return false\n if (job.status === 'failed') return true\n return job.status === 'running' && job.leaseExpiresAt != null && job.leaseExpiresAt.getTime() < Date.now()\n}\n\n/** \"Nobody is driving this and nothing is scheduled\" \u2014 the state an operator needs to see. */\nfunction isStuck(job: DurableJob): boolean {\n if (job.status !== 'running') return false\n const expired = job.leaseExpiresAt != null && job.leaseExpiresAt.getTime() < Date.now() - 20_000\n const nothingScheduled = job.nextRunAt == null || job.nextRunAt.getTime() < Date.now() - 900_000\n return expired && nothingScheduled\n}\n"],
5
+ "mappings": "AAEA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAavC,eAAsB,aAAa,KAAoD;AACrF,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,SAAU,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAChG,QAAM,YAAY,MAAM,uBAAuB;AAC/C,SAAO;AAAA,IACL,SAAS,UAAU,QAAQ,oBAAoB;AAAA,IAC/C,OAAO,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,SAAS,KAAK;AAAA,IACrE,QAAQ,KAAK,OAAO;AAAA,EACtB;AACF;AAGO,SAAS,MAAM,KAAiB;AACrC,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,MAAM,IAAI;AAAA,IACV,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,YAAY,IAAI;AAAA,IAChB,SAAS,IAAI,cAAc,EAAE,MAAM,IAAI,aAAa,IAAI,IAAI,UAAU,IAAI;AAAA,IAC1E,SAAS,IAAI;AAAA,IACb,gBAAgB,IAAI;AAAA,IACpB,gBAAgB,IAAI;AAAA,IACpB,YAAY,IAAI;AAAA,IAChB,UAAU,IAAI;AAAA,IACd,eAAe,IAAI;AAAA,IACnB,qBAAqB,IAAI;AAAA,IACzB,gBAAgB,IAAI;AAAA,IACpB,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,gBAAgB,IAAI,IAAI,cAAc;AAAA,IACtC,aAAa,IAAI,IAAI,WAAW;AAAA,IAChC,WAAW,IAAI,IAAI,SAAS;AAAA,IAC5B,WAAW,IAAI,IAAI,SAAS;AAAA,IAC5B,YAAY,IAAI,IAAI,UAAU;AAAA,IAC9B,UAAU,IAAI,IAAI,QAAQ;AAAA,IAC1B,mBAAmB,IAAI,IAAI,iBAAiB;AAAA,IAC5C,YAAY,IAAI;AAAA,IAChB,WAAW,IAAI;AAAA,IACf,cAAc,IAAI;AAAA,IAClB,WAAW,IAAI,IAAI,SAAS;AAAA,IAC5B,WAAW,IAAI,IAAI,SAAS;AAAA;AAAA;AAAA,IAG5B,YAAY,aAAa,GAAG;AAAA,IAC5B,OAAO,QAAQ,GAAG;AAAA,EACpB;AACF;AAEA,MAAM,MAAM,CAAC,UAAuC,QAAQ,MAAM,YAAY,IAAI;AAIlF,SAAS,aAAa,KAA0B;AAC9C,MAAI,IAAI,WAAW,eAAe,IAAI,WAAW,YAAa,QAAO;AACrE,MAAI,IAAI,WAAW,SAAU,QAAO;AACpC,SAAO,IAAI,WAAW,aAAa,IAAI,kBAAkB,QAAQ,IAAI,eAAe,QAAQ,IAAI,KAAK,IAAI;AAC3G;AAGA,SAAS,QAAQ,KAA0B;AACzC,MAAI,IAAI,WAAW,UAAW,QAAO;AACrC,QAAM,UAAU,IAAI,kBAAkB,QAAQ,IAAI,eAAe,QAAQ,IAAI,KAAK,IAAI,IAAI;AAC1F,QAAM,mBAAmB,IAAI,aAAa,QAAQ,IAAI,UAAU,QAAQ,IAAI,KAAK,IAAI,IAAI;AACzF,SAAO,WAAW;AACpB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,17 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ import { CREATE_INDEXES, CREATE_TABLE, DROP_INDEXES, DROP_TABLE, SET_FILLFACTOR } from "../../../core/schema.js";
3
+ class Migration20260908120000 extends Migration {
4
+ async up() {
5
+ this.addSql(CREATE_TABLE);
6
+ this.addSql(SET_FILLFACTOR);
7
+ for (const statement of CREATE_INDEXES) this.addSql(statement);
8
+ }
9
+ async down() {
10
+ for (const statement of DROP_INDEXES) this.addSql(statement);
11
+ this.addSql(DROP_TABLE);
12
+ }
13
+ }
14
+ export {
15
+ Migration20260908120000
16
+ };
17
+ //# sourceMappingURL=Migration20260908120000.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/durable_work/migrations/Migration20260908120000.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\nimport { CREATE_INDEXES, CREATE_TABLE, DROP_INDEXES, DROP_TABLE, SET_FILLFACTOR } from '../../../core/schema'\n\n/**\n * Creates `durable_work_jobs`.\n *\n * The statements come from `core/schema.ts` rather than being written out again here, so what\n * a host migrates and what the failure harness exercises are the same DDL. Two copies of a\n * schema drift, and the way that drift surfaces is a predicate silently not being enforced in\n * production while every test still passes.\n */\nexport class Migration20260908120000 extends Migration {\n override async up(): Promise<void> {\n this.addSql(CREATE_TABLE)\n this.addSql(SET_FILLFACTOR)\n for (const statement of CREATE_INDEXES) this.addSql(statement)\n }\n\n override async down(): Promise<void> {\n for (const statement of DROP_INDEXES) this.addSql(statement)\n this.addSql(DROP_TABLE)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAE1B,SAAS,gBAAgB,cAAc,cAAc,YAAY,sBAAsB;AAUhF,MAAM,gCAAgC,UAAU;AAAA,EACrD,MAAe,KAAoB;AACjC,SAAK,OAAO,YAAY;AACxB,SAAK,OAAO,cAAc;AAC1B,eAAW,aAAa,eAAgB,MAAK,OAAO,SAAS;AAAA,EAC/D;AAAA,EAEA,MAAe,OAAsB;AACnC,eAAW,aAAa,aAAc,MAAK,OAAO,SAAS;AAC3D,SAAK,OAAO,UAAU;AAAA,EACxB;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ const setup = {
2
+ defaultRoleFeatures: {
3
+ superadmin: ["durable_work.view", "durable_work.operate"],
4
+ admin: ["durable_work.view", "durable_work.operate"]
5
+ }
6
+ };
7
+ var setup_default = setup;
8
+ export {
9
+ setup_default as default,
10
+ setup
11
+ };
12
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/durable_work/setup.ts"],
4
+ "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['durable_work.view', 'durable_work.operate'],\n admin: ['durable_work.view', 'durable_work.operate'],\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,qBAAqB,sBAAsB;AAAA,IACxD,OAAO,CAAC,qBAAqB,sBAAsB;AAAA,EACrD;AACF;AAEA,IAAO,gBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ import { BullMQTransport } from "../transport/bullmq.js";
2
+ import { MemoryTransport } from "../transport/memory.js";
3
+ import { PgBossTransport } from "../transport/pgboss.js";
4
+ const num = (value, fallback) => {
5
+ const parsed = Number(value);
6
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
7
+ };
8
+ const bool = (value) => value === "1" || value?.toLowerCase() === "true";
9
+ function readConfig(env = process.env) {
10
+ const raw = (env.DURABLE_WORK_TRANSPORT ?? "pgboss").trim().toLowerCase();
11
+ if (raw !== "memory" && raw !== "bullmq" && raw !== "pgboss") {
12
+ throw new Error(`DURABLE_WORK_TRANSPORT must be memory | bullmq | pgboss, got ${JSON.stringify(raw)}`);
13
+ }
14
+ return {
15
+ transport: raw,
16
+ // Falls back to the queue module's Redis, because an app that already runs one should not
17
+ // have to configure a second.
18
+ redisUrl: env.DURABLE_WORK_REDIS_URL ?? env.QUEUE_REDIS_URL ?? env.REDIS_URL ?? null,
19
+ databaseUrl: env.DATABASE_URL ?? null,
20
+ pgBossSchema: env.DURABLE_WORK_PGBOSS_SCHEMA ?? "durable_work_boss",
21
+ tickMs: num(env.DURABLE_WORK_TICK_MS, 15e3),
22
+ drainTimeoutMs: num(env.DURABLE_WORK_DRAIN_TIMEOUT_MS, 3e4),
23
+ reconcilerGraceMs: num(env.DURABLE_WORK_GRACE_MS, 2e4),
24
+ inProcessWorker: bool(env.DURABLE_WORK_INPROCESS_WORKER)
25
+ };
26
+ }
27
+ function createTransport(config, deps = {}) {
28
+ switch (config.transport) {
29
+ case "memory":
30
+ if (process.env.NODE_ENV === "production") {
31
+ throw new Error("DURABLE_WORK_TRANSPORT=memory keeps jobs in process memory and cannot be used in production.");
32
+ }
33
+ return new MemoryTransport();
34
+ case "bullmq": {
35
+ const connection = deps.redisConnection ?? config.redisUrl;
36
+ if (!connection) throw new Error("DURABLE_WORK_TRANSPORT=bullmq requires DURABLE_WORK_REDIS_URL (or QUEUE_REDIS_URL).");
37
+ return new BullMQTransport({ connection });
38
+ }
39
+ case "pgboss": {
40
+ if (!config.databaseUrl) throw new Error("DURABLE_WORK_TRANSPORT=pgboss requires DATABASE_URL.");
41
+ return new PgBossTransport({ connectionString: config.databaseUrl, schema: config.pgBossSchema });
42
+ }
43
+ }
44
+ }
45
+ export {
46
+ createTransport,
47
+ readConfig
48
+ };
49
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/om/config.ts"],
4
+ "sourcesContent": ["// How a host configures the mechanism: environment variables in, a transport out.\n\nimport { BullMQTransport } from '../transport/bullmq'\nimport { MemoryTransport } from '../transport/memory'\nimport { PgBossTransport } from '../transport/pgboss'\nimport type { TransportAdapter, TransportName } from '../transport/types'\n\nexport type DurableWorkConfig = {\n transport: TransportName\n redisUrl: string | null\n databaseUrl: string | null\n pgBossSchema: string\n tickMs: number\n drainTimeoutMs: number\n reconcilerGraceMs: number\n /** Hosts the worker inside the app process. Dev and ephemeral tests only. */\n inProcessWorker: boolean\n}\n\nconst num = (value: string | undefined, fallback: number): number => {\n const parsed = Number(value)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback\n}\n\nconst bool = (value: string | undefined): boolean => value === '1' || value?.toLowerCase() === 'true'\n\nexport function readConfig(env: NodeJS.ProcessEnv = process.env): DurableWorkConfig {\n const raw = (env.DURABLE_WORK_TRANSPORT ?? 'pgboss').trim().toLowerCase()\n if (raw !== 'memory' && raw !== 'bullmq' && raw !== 'pgboss') {\n throw new Error(`DURABLE_WORK_TRANSPORT must be memory | bullmq | pgboss, got ${JSON.stringify(raw)}`)\n }\n return {\n transport: raw,\n // Falls back to the queue module's Redis, because an app that already runs one should not\n // have to configure a second.\n redisUrl: env.DURABLE_WORK_REDIS_URL ?? env.QUEUE_REDIS_URL ?? env.REDIS_URL ?? null,\n databaseUrl: env.DATABASE_URL ?? null,\n pgBossSchema: env.DURABLE_WORK_PGBOSS_SCHEMA ?? 'durable_work_boss',\n tickMs: num(env.DURABLE_WORK_TICK_MS, 15_000),\n drainTimeoutMs: num(env.DURABLE_WORK_DRAIN_TIMEOUT_MS, 30_000),\n reconcilerGraceMs: num(env.DURABLE_WORK_GRACE_MS, 20_000),\n inProcessWorker: bool(env.DURABLE_WORK_INPROCESS_WORKER),\n }\n}\n\nexport function createTransport(config: DurableWorkConfig, deps: { redisConnection?: unknown } = {}): TransportAdapter {\n switch (config.transport) {\n case 'memory':\n // Nothing survives the process, so this is a development convenience and is refused in\n // production rather than quietly losing every job on the next deploy.\n if (process.env.NODE_ENV === 'production') {\n throw new Error('DURABLE_WORK_TRANSPORT=memory keeps jobs in process memory and cannot be used in production.')\n }\n return new MemoryTransport()\n case 'bullmq': {\n const connection = deps.redisConnection ?? config.redisUrl\n if (!connection) throw new Error('DURABLE_WORK_TRANSPORT=bullmq requires DURABLE_WORK_REDIS_URL (or QUEUE_REDIS_URL).')\n return new BullMQTransport({ connection })\n }\n case 'pgboss': {\n if (!config.databaseUrl) throw new Error('DURABLE_WORK_TRANSPORT=pgboss requires DATABASE_URL.')\n return new PgBossTransport({ connectionString: config.databaseUrl, schema: config.pgBossSchema })\n }\n }\n}\n"],
5
+ "mappings": "AAEA,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAehC,MAAM,MAAM,CAAC,OAA2B,aAA6B;AACnE,QAAM,SAAS,OAAO,KAAK;AAC3B,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,MAAM,OAAO,CAAC,UAAuC,UAAU,OAAO,OAAO,YAAY,MAAM;AAExF,SAAS,WAAW,MAAyB,QAAQ,KAAwB;AAClF,QAAM,OAAO,IAAI,0BAA0B,UAAU,KAAK,EAAE,YAAY;AACxE,MAAI,QAAQ,YAAY,QAAQ,YAAY,QAAQ,UAAU;AAC5D,UAAM,IAAI,MAAM,gEAAgE,KAAK,UAAU,GAAG,CAAC,EAAE;AAAA,EACvG;AACA,SAAO;AAAA,IACL,WAAW;AAAA;AAAA;AAAA,IAGX,UAAU,IAAI,0BAA0B,IAAI,mBAAmB,IAAI,aAAa;AAAA,IAChF,aAAa,IAAI,gBAAgB;AAAA,IACjC,cAAc,IAAI,8BAA8B;AAAA,IAChD,QAAQ,IAAI,IAAI,sBAAsB,IAAM;AAAA,IAC5C,gBAAgB,IAAI,IAAI,+BAA+B,GAAM;AAAA,IAC7D,mBAAmB,IAAI,IAAI,uBAAuB,GAAM;AAAA,IACxD,iBAAiB,KAAK,IAAI,6BAA6B;AAAA,EACzD;AACF;AAEO,SAAS,gBAAgB,QAA2B,OAAsC,CAAC,GAAqB;AACrH,UAAQ,OAAO,WAAW;AAAA,IACxB,KAAK;AAGH,UAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,cAAM,IAAI,MAAM,8FAA8F;AAAA,MAChH;AACA,aAAO,IAAI,gBAAgB;AAAA,IAC7B,KAAK,UAAU;AACb,YAAM,aAAa,KAAK,mBAAmB,OAAO;AAClD,UAAI,CAAC,WAAY,OAAM,IAAI,MAAM,qFAAqF;AACtH,aAAO,IAAI,gBAAgB,EAAE,WAAW,CAAC;AAAA,IAC3C;AAAA,IACA,KAAK,UAAU;AACb,UAAI,CAAC,OAAO,YAAa,OAAM,IAAI,MAAM,sDAAsD;AAC/F,aAAO,IAAI,gBAAgB,EAAE,kBAAkB,OAAO,aAAa,QAAQ,OAAO,aAAa,CAAC;AAAA,IAClG;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ function createProgressMirror(progress) {
2
+ const quietly = async (fn) => {
3
+ try {
4
+ await fn();
5
+ } catch {
6
+ }
7
+ };
8
+ return {
9
+ async onStarted(job, scope) {
10
+ if (!job.progressJobId) return;
11
+ await quietly(() => progress.startJob?.(job.progressJobId, scope));
12
+ },
13
+ async onProgress(job, scope) {
14
+ if (!job.progressJobId) return;
15
+ await quietly(
16
+ () => progress.updateProgress?.(
17
+ job.progressJobId,
18
+ {
19
+ processedCount: job.processedCount,
20
+ totalCount: job.totalCount ?? void 0,
21
+ // Says why a healthy job looks idle. Core's read path fails a progress row whose
22
+ // heartbeat is older than a minute, and a durable job waiting out a retry backoff
23
+ // legitimately trips that; without this the UI's only story is "it broke".
24
+ message: job.nextRunAt && job.nextRunAt.getTime() > Date.now() ? "waiting for redelivery" : void 0
25
+ },
26
+ scope
27
+ )
28
+ );
29
+ },
30
+ async onTerminal(job, scope) {
31
+ if (!job.progressJobId) return;
32
+ if (job.status === "completed") {
33
+ await quietly(() => progress.completeJob?.(job.progressJobId, { processedCount: job.processedCount }, scope));
34
+ return;
35
+ }
36
+ if (job.status === "cancelled") {
37
+ await quietly(() => progress.markCancelled?.(job.progressJobId, scope));
38
+ return;
39
+ }
40
+ await quietly(
41
+ () => progress.failJob?.(job.progressJobId, { errorMessage: job.errorMessage ?? job.errorCode ?? "failed" }, scope)
42
+ );
43
+ }
44
+ };
45
+ }
46
+ export {
47
+ createProgressMirror
48
+ };
49
+ //# sourceMappingURL=progress-mirror.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/om/progress-mirror.ts"],
4
+ "sourcesContent": ["// One-way mirror onto core's `progress_jobs`, so the existing progress UI keeps working.\n//\n// The direction matters and is the whole design (ADR 0002): the durable job row is the\n// authority for liveness, and `progress_jobs` is presentation. Nothing here is ever read back\n// to make a decision. A progress row can lag, or be briefly wrong, without any consequence\n// beyond what a user sees for a moment.\n\nimport type { DurableJob, Scope } from '../core/types'\n\n/** The subset of core's ProgressService this bridge uses. Structural rather than imported so\n * the package does not take a hard dependency on a service it only writes to. */\nexport type ProgressServiceLike = {\n startJob?(id: string, ctx: unknown): Promise<unknown>\n updateProgress?(id: string, patch: Record<string, unknown>, ctx: unknown): Promise<unknown>\n touchJobHeartbeat?(id: string, ctx: unknown): Promise<unknown>\n completeJob?(id: string, input: Record<string, unknown>, ctx: unknown): Promise<unknown>\n failJob?(id: string, input: Record<string, unknown>, ctx: unknown): Promise<unknown>\n markCancelled?(id: string, ctx: unknown): Promise<unknown>\n}\n\nexport type ProgressMirror = {\n onStarted(job: DurableJob, scope: Scope): Promise<void>\n onProgress(job: DurableJob, scope: Scope): Promise<void>\n onTerminal(job: DurableJob, scope: Scope): Promise<void>\n}\n\n/**\n * Every call is best-effort.\n *\n * A mirror that could fail a job would make the presentation layer able to stop the work,\n * which is exactly backwards. The cost of swallowing is a stale progress card; the cost of\n * not swallowing is a sync run failed by a UI table.\n */\nexport function createProgressMirror(progress: ProgressServiceLike): ProgressMirror {\n const quietly = async (fn: () => Promise<unknown> | undefined) => {\n try {\n await fn()\n } catch {\n /* presentation only \u2014 never allowed to affect the job */\n }\n }\n\n return {\n async onStarted(job, scope) {\n if (!job.progressJobId) return\n await quietly(() => progress.startJob?.(job.progressJobId!, scope))\n },\n async onProgress(job, scope) {\n if (!job.progressJobId) return\n await quietly(() =>\n progress.updateProgress?.(\n job.progressJobId!,\n {\n processedCount: job.processedCount,\n totalCount: job.totalCount ?? undefined,\n // Says why a healthy job looks idle. Core's read path fails a progress row whose\n // heartbeat is older than a minute, and a durable job waiting out a retry backoff\n // legitimately trips that; without this the UI's only story is \"it broke\".\n message: job.nextRunAt && job.nextRunAt.getTime() > Date.now() ? 'waiting for redelivery' : undefined,\n },\n scope,\n ),\n )\n },\n async onTerminal(job, scope) {\n if (!job.progressJobId) return\n if (job.status === 'completed') {\n await quietly(() => progress.completeJob?.(job.progressJobId!, { processedCount: job.processedCount }, scope))\n return\n }\n if (job.status === 'cancelled') {\n await quietly(() => progress.markCancelled?.(job.progressJobId!, scope))\n return\n }\n await quietly(() =>\n progress.failJob?.(job.progressJobId!, { errorMessage: job.errorMessage ?? job.errorCode ?? 'failed' }, scope),\n )\n },\n }\n}\n"],
5
+ "mappings": "AAiCO,SAAS,qBAAqB,UAA+C;AAClF,QAAM,UAAU,OAAO,OAA2C;AAChE,QAAI;AACF,YAAM,GAAG;AAAA,IACX,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,UAAU,KAAK,OAAO;AAC1B,UAAI,CAAC,IAAI,cAAe;AACxB,YAAM,QAAQ,MAAM,SAAS,WAAW,IAAI,eAAgB,KAAK,CAAC;AAAA,IACpE;AAAA,IACA,MAAM,WAAW,KAAK,OAAO;AAC3B,UAAI,CAAC,IAAI,cAAe;AACxB,YAAM;AAAA,QAAQ,MACZ,SAAS;AAAA,UACP,IAAI;AAAA,UACJ;AAAA,YACE,gBAAgB,IAAI;AAAA,YACpB,YAAY,IAAI,cAAc;AAAA;AAAA;AAAA;AAAA,YAI9B,SAAS,IAAI,aAAa,IAAI,UAAU,QAAQ,IAAI,KAAK,IAAI,IAAI,2BAA2B;AAAA,UAC9F;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,WAAW,KAAK,OAAO;AAC3B,UAAI,CAAC,IAAI,cAAe;AACxB,UAAI,IAAI,WAAW,aAAa;AAC9B,cAAM,QAAQ,MAAM,SAAS,cAAc,IAAI,eAAgB,EAAE,gBAAgB,IAAI,eAAe,GAAG,KAAK,CAAC;AAC7G;AAAA,MACF;AACA,UAAI,IAAI,WAAW,aAAa;AAC9B,cAAM,QAAQ,MAAM,SAAS,gBAAgB,IAAI,eAAgB,KAAK,CAAC;AACvE;AAAA,MACF;AACA,YAAM;AAAA,QAAQ,MACZ,SAAS,UAAU,IAAI,eAAgB,EAAE,cAAc,IAAI,gBAAgB,IAAI,aAAa,SAAS,GAAG,KAAK;AAAA,MAC/G;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,48 @@
1
+ const RETURNS_ROWS = /\breturning\b|^\s*(select|with)\b/i;
2
+ function toPositional(text, params) {
3
+ const ordered = [];
4
+ const rewritten = text.replace(/\$(\d+)/g, (_match, index) => {
5
+ const position = Number(index);
6
+ if (position < 1 || position > params.length) {
7
+ throw new Error(`SQL references $${position} but ${params.length} parameter(s) were supplied`);
8
+ }
9
+ ordered.push(params[position - 1]);
10
+ return "?";
11
+ });
12
+ return { text: rewritten, params: ordered };
13
+ }
14
+ function executorFor(em) {
15
+ return {
16
+ async query(text, params = []) {
17
+ const manager = em;
18
+ const connection = manager.getConnection();
19
+ const bound = toPositional(text, params);
20
+ const ctx = manager.getTransactionContext?.();
21
+ if (RETURNS_ROWS.test(bound.text)) {
22
+ const rows = await connection.execute(bound.text, bound.params, "all", ctx);
23
+ return { rows, rowCount: rows.length };
24
+ }
25
+ const result = await connection.execute(bound.text, bound.params, "run", ctx);
26
+ return { rows: [], rowCount: result?.affectedRows ?? result?.rowCount ?? 0 };
27
+ }
28
+ };
29
+ }
30
+ function mikroExecutor(em) {
31
+ const base = executorFor(em);
32
+ return {
33
+ query: base.query,
34
+ async transaction(fn) {
35
+ const forked = em.fork();
36
+ return forked.transactional(async (trx) => fn(executorFor(trx)));
37
+ }
38
+ };
39
+ }
40
+ function mikroTx(em) {
41
+ return executorFor(em);
42
+ }
43
+ export {
44
+ mikroExecutor,
45
+ mikroTx,
46
+ toPositional
47
+ };
48
+ //# sourceMappingURL=sql-executor-mikro.js.map