bonescript-compiler 0.5.3 → 0.5.4

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 (194) hide show
  1. package/LICENSE +21 -21
  2. package/dist/algorithm_catalog.js +166 -166
  3. package/dist/cli.d.ts +1 -2
  4. package/dist/cli.js +543 -75
  5. package/dist/cli.js.map +1 -1
  6. package/dist/emit_capability.d.ts +0 -13
  7. package/dist/emit_capability.js +134 -296
  8. package/dist/emit_capability.js.map +1 -1
  9. package/dist/emit_composition.js +3 -37
  10. package/dist/emit_composition.js.map +1 -1
  11. package/dist/emit_deploy.js +167 -165
  12. package/dist/emit_deploy.js.map +1 -1
  13. package/dist/emit_events.d.ts +0 -1
  14. package/dist/emit_events.js +275 -325
  15. package/dist/emit_events.js.map +1 -1
  16. package/dist/emit_extras.js +5 -3
  17. package/dist/emit_extras.js.map +1 -1
  18. package/dist/emit_full.js +112 -272
  19. package/dist/emit_full.js.map +1 -1
  20. package/dist/emit_maintenance.js +249 -249
  21. package/dist/emit_runtime.d.ts +11 -17
  22. package/dist/emit_runtime.js +688 -29
  23. package/dist/emit_runtime.js.map +1 -1
  24. package/dist/emit_sourcemap.js +66 -66
  25. package/dist/emit_tests.js +12 -47
  26. package/dist/emit_tests.js.map +1 -1
  27. package/dist/emit_websocket.js +3 -0
  28. package/dist/emit_websocket.js.map +1 -1
  29. package/dist/emitter.js +49 -94
  30. package/dist/emitter.js.map +1 -1
  31. package/dist/extension_manager.d.ts +2 -2
  32. package/dist/extension_manager.js +20 -9
  33. package/dist/extension_manager.js.map +1 -1
  34. package/dist/ir.d.ts +0 -4
  35. package/dist/lowering.d.ts +14 -5
  36. package/dist/lowering.js +417 -66
  37. package/dist/lowering.js.map +1 -1
  38. package/dist/module_loader.d.ts +2 -2
  39. package/dist/module_loader.js +23 -20
  40. package/dist/module_loader.js.map +1 -1
  41. package/dist/optimizer.js +3 -6
  42. package/dist/optimizer.js.map +1 -1
  43. package/dist/scaffold.d.ts +2 -2
  44. package/dist/scaffold.js +319 -315
  45. package/dist/scaffold.js.map +1 -1
  46. package/dist/solver.js +1 -1
  47. package/dist/solver.js.map +1 -1
  48. package/dist/source_map.js.map +1 -0
  49. package/dist/test.js.map +1 -0
  50. package/dist/test_typechecker.d.ts +5 -0
  51. package/dist/test_typechecker.js +126 -0
  52. package/dist/test_typechecker.js.map +1 -0
  53. package/dist/typechecker.d.ts +0 -7
  54. package/dist/typechecker.js +16 -103
  55. package/dist/typechecker.js.map +1 -1
  56. package/dist/verifier.d.ts +1 -5
  57. package/dist/verifier.js +38 -142
  58. package/dist/verifier.js.map +1 -1
  59. package/package.json +52 -62
  60. package/src/algorithm_catalog.ts +345 -345
  61. package/src/ast.d.ts +244 -0
  62. package/src/ast.ts +334 -334
  63. package/src/cli.ts +624 -98
  64. package/src/emit_batch.ts +140 -140
  65. package/src/emit_capability.ts +436 -613
  66. package/src/emit_composition.ts +196 -229
  67. package/src/emit_deploy.ts +190 -187
  68. package/src/emit_events.ts +307 -362
  69. package/src/emit_extras.ts +240 -237
  70. package/src/emit_full.ts +309 -472
  71. package/src/emit_maintenance.ts +459 -459
  72. package/src/emit_runtime.ts +730 -17
  73. package/src/emit_sourcemap.ts +140 -140
  74. package/src/emit_tests.ts +205 -243
  75. package/src/emit_websocket.ts +229 -226
  76. package/src/emitter.ts +578 -626
  77. package/src/extension_manager.ts +187 -177
  78. package/src/formatter.ts +297 -297
  79. package/src/index.ts +88 -88
  80. package/src/ir.ts +215 -216
  81. package/src/lexer.d.ts +195 -0
  82. package/src/lexer.ts +630 -630
  83. package/src/lowering.ts +556 -168
  84. package/src/module_loader.ts +114 -112
  85. package/src/optimizer.ts +196 -199
  86. package/src/parse_decls.d.ts +13 -0
  87. package/src/parse_decls.ts +409 -409
  88. package/src/parse_decls2.d.ts +13 -0
  89. package/src/parse_decls2.ts +244 -244
  90. package/src/parse_expr.d.ts +7 -0
  91. package/src/parse_expr.ts +197 -197
  92. package/src/parse_types.d.ts +6 -0
  93. package/src/parse_types.ts +54 -54
  94. package/src/parser.d.ts +10 -0
  95. package/src/parser.ts +1 -1
  96. package/src/parser_base.d.ts +19 -0
  97. package/src/parser_base.ts +57 -57
  98. package/src/parser_recovery.ts +153 -153
  99. package/src/scaffold.ts +375 -371
  100. package/src/solver.ts +330 -330
  101. package/src/typechecker.d.ts +52 -0
  102. package/src/typechecker.ts +591 -700
  103. package/src/types.d.ts +38 -0
  104. package/src/types.ts +122 -122
  105. package/src/verifier.ts +49 -154
  106. package/README.md +0 -382
  107. package/dist/commands/check.d.ts +0 -5
  108. package/dist/commands/check.js +0 -34
  109. package/dist/commands/check.js.map +0 -1
  110. package/dist/commands/compile.d.ts +0 -5
  111. package/dist/commands/compile.js +0 -215
  112. package/dist/commands/compile.js.map +0 -1
  113. package/dist/commands/debug.d.ts +0 -5
  114. package/dist/commands/debug.js +0 -59
  115. package/dist/commands/debug.js.map +0 -1
  116. package/dist/commands/diff.d.ts +0 -5
  117. package/dist/commands/diff.js +0 -123
  118. package/dist/commands/diff.js.map +0 -1
  119. package/dist/commands/fmt.d.ts +0 -5
  120. package/dist/commands/fmt.js +0 -49
  121. package/dist/commands/fmt.js.map +0 -1
  122. package/dist/commands/init.d.ts +0 -5
  123. package/dist/commands/init.js +0 -96
  124. package/dist/commands/init.js.map +0 -1
  125. package/dist/commands/ir.d.ts +0 -5
  126. package/dist/commands/ir.js +0 -27
  127. package/dist/commands/ir.js.map +0 -1
  128. package/dist/commands/lex.d.ts +0 -5
  129. package/dist/commands/lex.js +0 -21
  130. package/dist/commands/lex.js.map +0 -1
  131. package/dist/commands/parse.d.ts +0 -5
  132. package/dist/commands/parse.js +0 -30
  133. package/dist/commands/parse.js.map +0 -1
  134. package/dist/commands/test.d.ts +0 -5
  135. package/dist/commands/test.js +0 -61
  136. package/dist/commands/test.js.map +0 -1
  137. package/dist/commands/verify_determinism.d.ts +0 -5
  138. package/dist/commands/verify_determinism.js +0 -64
  139. package/dist/commands/verify_determinism.js.map +0 -1
  140. package/dist/commands/watch.d.ts +0 -5
  141. package/dist/commands/watch.js +0 -50
  142. package/dist/commands/watch.js.map +0 -1
  143. package/dist/emit_auth.d.ts +0 -18
  144. package/dist/emit_auth.js +0 -507
  145. package/dist/emit_auth.js.map +0 -1
  146. package/dist/emit_database.d.ts +0 -7
  147. package/dist/emit_database.js +0 -72
  148. package/dist/emit_database.js.map +0 -1
  149. package/dist/emit_index.d.ts +0 -6
  150. package/dist/emit_index.js +0 -202
  151. package/dist/emit_index.js.map +0 -1
  152. package/dist/emit_models.d.ts +0 -12
  153. package/dist/emit_models.js +0 -171
  154. package/dist/emit_models.js.map +0 -1
  155. package/dist/emit_openapi.d.ts +0 -9
  156. package/dist/emit_openapi.js +0 -306
  157. package/dist/emit_openapi.js.map +0 -1
  158. package/dist/emit_package.d.ts +0 -7
  159. package/dist/emit_package.js +0 -68
  160. package/dist/emit_package.js.map +0 -1
  161. package/dist/emit_router.d.ts +0 -12
  162. package/dist/emit_router.js +0 -389
  163. package/dist/emit_router.js.map +0 -1
  164. package/dist/lowering_channels.d.ts +0 -11
  165. package/dist/lowering_channels.js +0 -103
  166. package/dist/lowering_channels.js.map +0 -1
  167. package/dist/lowering_entities.d.ts +0 -11
  168. package/dist/lowering_entities.js +0 -232
  169. package/dist/lowering_entities.js.map +0 -1
  170. package/dist/lowering_helpers.d.ts +0 -13
  171. package/dist/lowering_helpers.js +0 -76
  172. package/dist/lowering_helpers.js.map +0 -1
  173. package/src/commands/check.ts +0 -33
  174. package/src/commands/compile.ts +0 -191
  175. package/src/commands/debug.ts +0 -33
  176. package/src/commands/diff.ts +0 -105
  177. package/src/commands/fmt.ts +0 -22
  178. package/src/commands/init.ts +0 -72
  179. package/src/commands/ir.ts +0 -23
  180. package/src/commands/lex.ts +0 -17
  181. package/src/commands/parse.ts +0 -24
  182. package/src/commands/test.ts +0 -36
  183. package/src/commands/verify_determinism.ts +0 -66
  184. package/src/commands/watch.ts +0 -25
  185. package/src/emit_auth.ts +0 -513
  186. package/src/emit_database.ts +0 -72
  187. package/src/emit_index.ts +0 -210
  188. package/src/emit_models.ts +0 -176
  189. package/src/emit_openapi.ts +0 -315
  190. package/src/emit_package.ts +0 -66
  191. package/src/emit_router.ts +0 -408
  192. package/src/lowering_channels.ts +0 -108
  193. package/src/lowering_entities.ts +0 -258
  194. package/src/lowering_helpers.ts +0 -75
@@ -1,362 +1,307 @@
1
- /**
2
- * BoneScript Durable Event System Emitter
3
- *
4
- * Generates two event delivery modes:
5
- * in_process — default dev mode, in-memory bus (existing behavior)
6
- * durable — Postgres-backed transactional outbox
7
- *
8
- * Durable mode guarantees:
9
- * at_least_once — retry until acknowledged
10
- * exactly_once — deduplicated via event_id table
11
- */
12
-
13
- import * as IR from "./ir";
14
- import { toTsType } from "./emit_router";
15
-
16
- // ─── Outbox SQL Schema ────────────────────────────────────────────────────────
17
-
18
- export function emitOutboxSchema(): string {
19
- return `-- BoneScript: Transactional Outbox Schema
20
- -- Generated by BoneScript compiler. DO NOT EDIT.
21
-
22
- CREATE TABLE IF NOT EXISTS event_outbox (
23
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
24
- event_type VARCHAR NOT NULL,
25
- payload JSONB NOT NULL,
26
- source VARCHAR NOT NULL,
27
- correlation_id UUID,
28
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
29
- scheduled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
30
- delivered_at TIMESTAMPTZ,
31
- attempts INT NOT NULL DEFAULT 0,
32
- last_error TEXT,
33
- status VARCHAR NOT NULL DEFAULT 'pending'
34
- CHECK (status IN ('pending', 'delivered', 'failed', 'dead_letter'))
35
- );
36
-
37
- CREATE INDEX IF NOT EXISTS idx_event_outbox_status ON event_outbox (status, scheduled_at)
38
- WHERE status = 'pending';
39
-
40
- CREATE INDEX IF NOT EXISTS idx_event_outbox_created ON event_outbox (created_at);
41
-
42
- -- Deduplication table for exactly_once delivery
43
- CREATE TABLE IF NOT EXISTS event_processed (
44
- event_id UUID PRIMARY KEY,
45
- event_type VARCHAR NOT NULL,
46
- processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
47
- );
48
-
49
- CREATE INDEX IF NOT EXISTS idx_event_processed_type ON event_processed (event_type, processed_at);
50
- `;
51
- }
52
-
53
- // ─── Durable Event Bus ────────────────────────────────────────────────────────
54
-
55
- export function emitDurableEventBus(system: IR.IRSystem): string {
56
- const exactlyOnceEvents = system.events
57
- .filter(e => e.delivery === "exactly_once")
58
- .map(e => `"${e.name}"`);
59
-
60
- const atLeastOnceEvents = system.events
61
- .filter(e => e.delivery === "at_least_once")
62
- .map(e => `"${e.name}"`);
63
-
64
- return `// Generated by BoneScript compiler. DO NOT EDIT.
65
- // Durable event bus with transactional outbox pattern.
66
- // Set EVENT_MODE=durable in .env to enable.
67
-
68
- import { Pool, PoolClient } from "pg";
69
- import { v4 as uuid } from "uuid";
70
- import { pool } from "./db";
71
- import { logger } from "./logger";
72
- import { counter } from "./metrics";
73
-
74
- export type EventDeliveryMode = "in_process" | "durable";
75
-
76
- const MODE: EventDeliveryMode =
77
- (process.env.EVENT_MODE as EventDeliveryMode) || "in_process";
78
-
79
- // Events requiring exactly_once delivery (deduplicated)
80
- const EXACTLY_ONCE_EVENTS = new Set([${exactlyOnceEvents.join(", ")}]);
81
-
82
- // Events requiring at_least_once delivery (retried until ack)
83
- const AT_LEAST_ONCE_EVENTS = new Set([${atLeastOnceEvents.join(", ")}]);
84
-
85
- export interface EventMetadata {
86
- source: string;
87
- timestamp: Date;
88
- correlation_id: string;
89
- causation_id: string;
90
- }
91
-
92
- export interface SystemEvent {
93
- type: string;
94
- payload: Record<string, unknown>;
95
- metadata: EventMetadata;
96
- }
97
-
98
- type Handler = (event: SystemEvent) => Promise<void>;
99
-
100
- // ─── In-Process Bus ──────────────────────────────────────────────────────────
101
-
102
- class InProcessBus {
103
- private handlers: Map<string, Handler[]> = new Map();
104
-
105
- subscribe(type: string, handler: Handler): void {
106
- const existing = this.handlers.get(type) || [];
107
- existing.push(handler);
108
- this.handlers.set(type, existing);
109
- }
110
-
111
- async publish(type: string, payload: Record<string, unknown>, source: string, correlationId?: string): Promise<void> {
112
- const event: SystemEvent = {
113
- type,
114
- payload,
115
- metadata: {
116
- source,
117
- timestamp: new Date(),
118
- correlation_id: correlationId || uuid(),
119
- causation_id: uuid(),
120
- },
121
- };
122
- counter("event.published", { type, mode: "in_process" });
123
- const handlers = this.handlers.get(type) || [];
124
- for (const handler of handlers) {
125
- try {
126
- await handler(event);
127
- counter("event.delivered", { type, mode: "in_process" });
128
- } catch (e: any) {
129
- counter("event.delivery_failed", { type, mode: "in_process" });
130
- logger.error("event_handler_failed", { event: type, metadata: { error: e.message } });
131
- }
132
- }
133
- }
134
- }
135
-
136
- // ─── Durable Bus (Transactional Outbox) ──────────────────────────────────────
137
-
138
- class DurableBus {
139
- private handlers: Map<string, Handler[]> = new Map();
140
-
141
- subscribe(type: string, handler: Handler): void {
142
- const existing = this.handlers.get(type) || [];
143
- existing.push(handler);
144
- this.handlers.set(type, existing);
145
- }
146
-
147
- // Write event to outbox within the current transaction (or a new one)
148
- async publish(
149
- type: string,
150
- payload: Record<string, unknown>,
151
- source: string,
152
- correlationId?: string,
153
- client?: PoolClient
154
- ): Promise<void> {
155
- const eventId = uuid();
156
- const corrId = correlationId || uuid();
157
- const sql = \`
158
- INSERT INTO event_outbox (id, event_type, payload, source, correlation_id)
159
- VALUES ($1, $2, $3, $4, $5)
160
- \`;
161
- const params = [eventId, type, JSON.stringify({ ...payload, _event_id: eventId }), source, corrId];
162
-
163
- if (client) {
164
- // Write within caller's transaction — atomicity guaranteed
165
- await client.query(sql, params);
166
- } else {
167
- await pool.query(sql, params);
168
- }
169
- counter("event.outboxed", { type });
170
- }
171
-
172
- // Called by the background worker
173
- async flush(): Promise<void> {
174
- const client = await pool.connect();
175
- try {
176
- await client.query("BEGIN");
177
-
178
- // Fetch pending events (lock rows to prevent concurrent processing)
179
- const { rows } = await client.query(\`
180
- SELECT id, event_type, payload, source, correlation_id, attempts
181
- FROM event_outbox
182
- WHERE status = 'pending' AND scheduled_at <= NOW()
183
- ORDER BY scheduled_at ASC
184
- LIMIT 50
185
- FOR UPDATE SKIP LOCKED
186
- \`);
187
-
188
- for (const row of rows) {
189
- try {
190
- // exactly_once: check deduplication table
191
- if (EXACTLY_ONCE_EVENTS.has(row.event_type)) {
192
- const { rows: dup } = await client.query(
193
- "SELECT 1 FROM event_processed WHERE event_id = $1",
194
- [row.payload._event_id || row.id]
195
- );
196
- if (dup.length > 0) {
197
- await client.query(
198
- "UPDATE event_outbox SET status = 'delivered', delivered_at = NOW() WHERE id = $1",
199
- [row.id]
200
- );
201
- continue;
202
- }
203
- }
204
-
205
- const event: SystemEvent = {
206
- type: row.event_type,
207
- payload: row.payload,
208
- metadata: {
209
- source: row.source,
210
- timestamp: new Date(),
211
- correlation_id: row.correlation_id,
212
- causation_id: uuid(),
213
- },
214
- };
215
-
216
- const handlers = this.handlers.get(row.event_type) || [];
217
- for (const handler of handlers) {
218
- await handler(event);
219
- }
220
-
221
- // Mark delivered
222
- await client.query(
223
- "UPDATE event_outbox SET status = 'delivered', delivered_at = NOW(), attempts = attempts + 1 WHERE id = $1",
224
- [row.id]
225
- );
226
-
227
- // Record for exactly_once deduplication
228
- if (EXACTLY_ONCE_EVENTS.has(row.event_type)) {
229
- await client.query(
230
- "INSERT INTO event_processed (event_id, event_type) VALUES ($1, $2) ON CONFLICT DO NOTHING",
231
- [row.payload._event_id || row.id, row.event_type]
232
- );
233
- }
234
-
235
- counter("event.delivered", { type: row.event_type, mode: "durable" });
236
- } catch (e: any) {
237
- const maxAttempts = AT_LEAST_ONCE_EVENTS.has(row.event_type) ? 10 : 3;
238
- const newAttempts = row.attempts + 1;
239
- const status = newAttempts >= maxAttempts ? "dead_letter" : "pending";
240
- const backoffMs = Math.min(1000 * Math.pow(2, newAttempts), 300000);
241
- await client.query(
242
- \`UPDATE event_outbox
243
- SET attempts = $1, last_error = $2, status = $3,
244
- scheduled_at = NOW() + ($4 || ' milliseconds')::interval
245
- WHERE id = $5\`,
246
- [newAttempts, e.message, status, backoffMs, row.id]
247
- );
248
- counter("event.delivery_failed", { type: row.event_type, mode: "durable" });
249
- logger.error("event_delivery_failed", { event: row.event_type, metadata: { error: e.message, attempts: newAttempts } });
250
- }
251
- }
252
-
253
- await client.query("COMMIT");
254
- } catch (e) {
255
- await client.query("ROLLBACK");
256
- throw e;
257
- } finally {
258
- client.release();
259
- }
260
- }
261
-
262
- // Start background worker
263
- startWorker(intervalMs: number = 1000): NodeJS.Timeout {
264
- logger.info("event_worker_started", { event: "startup", metadata: { interval_ms: intervalMs } });
265
- return setInterval(async () => {
266
- try {
267
- await this.flush();
268
- } catch (e: any) {
269
- logger.error("event_worker_error", { event: "flush_failed", metadata: { error: e.message } });
270
- }
271
- }, intervalMs);
272
- }
273
- }
274
-
275
- // ─── Unified Interface ────────────────────────────────────────────────────────
276
-
277
- const inProcess = new InProcessBus();
278
- const durable = new DurableBus();
279
-
280
- export const eventBus = {
281
- subscribe(type: string, handler: Handler): void {
282
- inProcess.subscribe(type, handler);
283
- durable.subscribe(type, handler);
284
- },
285
-
286
- async publish(
287
- type: string,
288
- payload: Record<string, unknown>,
289
- source: string,
290
- correlationId?: string,
291
- client?: PoolClient
292
- ): Promise<void> {
293
- if (MODE === "durable") {
294
- await durable.publish(type, payload, source, correlationId, client);
295
- } else {
296
- await inProcess.publish(type, payload, source, correlationId);
297
- }
298
- },
299
-
300
- startWorker(intervalMs?: number): NodeJS.Timeout | null {
301
- if (MODE === "durable") {
302
- return durable.startWorker(intervalMs);
303
- }
304
- return null;
305
- },
306
- };
307
- `;
308
- }
309
-
310
- // ─── Typed Event Publishers ───────────────────────────────────────────────────
311
- // Generates per-event emitXxx() typed publisher functions as specified in
312
- // spec/09_CODEGEN.md §5.4. These wrap eventBus.publish with a typed payload
313
- // interface so callers get compile-time safety instead of raw Record<string,unknown>.
314
-
315
- export function emitTypedEventPublishers(system: IR.IRSystem): string {
316
- if (system.events.length === 0) return "";
317
-
318
- const lines: string[] = [];
319
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
320
- lines.push(`// Typed event publisher functions — one per declared event.`);
321
- lines.push(`// Import these instead of calling eventBus.publish() directly.`);
322
- lines.push(``);
323
- lines.push(`import { eventBus } from "./events";`);
324
- lines.push(`import type { PoolClient } from "pg";`);
325
- lines.push(``);
326
-
327
- for (const ev of system.events) {
328
- const ifaceName = `${ev.name}Payload`;
329
-
330
- // Payload interface
331
- lines.push(`export interface ${ifaceName} {`);
332
- for (const field of ev.payload) {
333
- const nullable = field.nullable ? " | null" : "";
334
- lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
335
- }
336
- lines.push(`}`);
337
- lines.push(``);
338
-
339
- // Publisher function
340
- const sourceId = ev.source && ev.source !== "unknown" ? ev.source : system.name;
341
- lines.push(`/**`);
342
- lines.push(` * Publish a ${ev.name} event.`);
343
- lines.push(` * Delivery: ${ev.delivery}${ev.ttl_ms ? ` | TTL: ${ev.ttl_ms}ms` : ""}`);
344
- lines.push(` */`);
345
- lines.push(`export async function emit${ev.name}(`);
346
- lines.push(` payload: ${ifaceName},`);
347
- lines.push(` correlationId?: string,`);
348
- lines.push(` client?: PoolClient,`);
349
- lines.push(`): Promise<void> {`);
350
- lines.push(` await eventBus.publish(`);
351
- lines.push(` "${ev.name}",`);
352
- lines.push(` payload as unknown as Record<string, unknown>,`);
353
- lines.push(` "${sourceId}",`);
354
- lines.push(` correlationId,`);
355
- lines.push(` client,`);
356
- lines.push(` );`);
357
- lines.push(`}`);
358
- lines.push(``);
359
- }
360
-
361
- return lines.join("\n");
362
- }
1
+ /**
2
+ * BoneScript Durable Event System Emitter
3
+ *
4
+ * Generates two event delivery modes:
5
+ * in_process — default dev mode, in-memory bus (existing behavior)
6
+ * durable — Postgres-backed transactional outbox
7
+ *
8
+ * Durable mode guarantees:
9
+ * at_least_once — retry until acknowledged
10
+ * exactly_once — deduplicated via event_id table
11
+ */
12
+
13
+ import * as IR from "./ir";
14
+
15
+ // ─── Outbox SQL Schema ────────────────────────────────────────────────────────
16
+
17
+ export function emitOutboxSchema(): string {
18
+ return `-- BoneScript: Transactional Outbox Schema
19
+ -- Generated by BoneScript compiler. DO NOT EDIT.
20
+
21
+ CREATE TABLE IF NOT EXISTS event_outbox (
22
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
23
+ event_type VARCHAR NOT NULL,
24
+ payload JSONB NOT NULL,
25
+ source VARCHAR NOT NULL,
26
+ correlation_id UUID,
27
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
28
+ scheduled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
29
+ delivered_at TIMESTAMPTZ,
30
+ attempts INT NOT NULL DEFAULT 0,
31
+ last_error TEXT,
32
+ status VARCHAR NOT NULL DEFAULT 'pending'
33
+ CHECK (status IN ('pending', 'delivered', 'failed', 'dead_letter'))
34
+ );
35
+
36
+ CREATE INDEX IF NOT EXISTS idx_event_outbox_status ON event_outbox (status, scheduled_at)
37
+ WHERE status = 'pending';
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_event_outbox_created ON event_outbox (created_at);
40
+
41
+ -- Deduplication table for exactly_once delivery
42
+ CREATE TABLE IF NOT EXISTS event_processed (
43
+ event_id UUID PRIMARY KEY,
44
+ event_type VARCHAR NOT NULL,
45
+ processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
46
+ );
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_event_processed_type ON event_processed (event_type, processed_at);
49
+ `;
50
+ }
51
+
52
+ // ─── Durable Event Bus ────────────────────────────────────────────────────────
53
+
54
+ export function emitDurableEventBus(system: IR.IRSystem): string {
55
+ const exactlyOnceEvents = system.events
56
+ .filter(e => e.delivery === "exactly_once")
57
+ .map(e => `"${e.name}"`);
58
+
59
+ const atLeastOnceEvents = system.events
60
+ .filter(e => e.delivery === "at_least_once")
61
+ .map(e => `"${e.name}"`);
62
+
63
+ return `// Generated by BoneScript compiler. DO NOT EDIT.
64
+ // Durable event bus with transactional outbox pattern.
65
+ // Set EVENT_MODE=durable in .env to enable.
66
+
67
+ import { Pool, PoolClient } from "pg";
68
+ import { v4 as uuid } from "uuid";
69
+ import { pool } from "./db";
70
+ import { logger } from "./logger";
71
+ import { counter } from "./metrics";
72
+
73
+ export type EventDeliveryMode = "in_process" | "durable";
74
+
75
+ const MODE: EventDeliveryMode =
76
+ (process.env.EVENT_MODE as EventDeliveryMode) || "in_process";
77
+
78
+ // Events requiring exactly_once delivery (deduplicated)
79
+ const EXACTLY_ONCE_EVENTS = new Set<string>([${exactlyOnceEvents.join(", ")}]);
80
+
81
+ // Events requiring at_least_once delivery (retried until ack)
82
+ const AT_LEAST_ONCE_EVENTS = new Set<string>([${atLeastOnceEvents.join(", ")}]);
83
+
84
+ export interface EventMetadata {
85
+ source: string;
86
+ timestamp: Date;
87
+ correlation_id: string;
88
+ causation_id: string;
89
+ }
90
+
91
+ export interface SystemEvent {
92
+ type: string;
93
+ payload: Record<string, unknown>;
94
+ metadata: EventMetadata;
95
+ }
96
+
97
+ type Handler = (event: SystemEvent) => Promise<void>;
98
+
99
+ // ─── In-Process Bus ──────────────────────────────────────────────────────────
100
+
101
+ class InProcessBus {
102
+ private handlers: Map<string, Handler[]> = new Map();
103
+
104
+ subscribe(type: string, handler: Handler): void {
105
+ const existing = this.handlers.get(type) || [];
106
+ existing.push(handler);
107
+ this.handlers.set(type, existing);
108
+ }
109
+
110
+ async publish(type: string, payload: Record<string, unknown>, source: string, correlationId?: string): Promise<void> {
111
+ const event: SystemEvent = {
112
+ type,
113
+ payload,
114
+ metadata: {
115
+ source,
116
+ timestamp: new Date(),
117
+ correlation_id: correlationId || uuid(),
118
+ causation_id: uuid(),
119
+ },
120
+ };
121
+ counter("event.published", { type, mode: "in_process" });
122
+ const handlers = this.handlers.get(type) || [];
123
+ for (const handler of handlers) {
124
+ try {
125
+ await handler(event);
126
+ counter("event.delivered", { type, mode: "in_process" });
127
+ } catch (e: any) {
128
+ counter("event.delivery_failed", { type, mode: "in_process" });
129
+ logger.error("event_handler_failed", { event: type, metadata: { error: e.message } });
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // ─── Durable Bus (Transactional Outbox) ──────────────────────────────────────
136
+
137
+ class DurableBus {
138
+ private handlers: Map<string, Handler[]> = new Map();
139
+
140
+ subscribe(type: string, handler: Handler): void {
141
+ const existing = this.handlers.get(type) || [];
142
+ existing.push(handler);
143
+ this.handlers.set(type, existing);
144
+ }
145
+
146
+ // Write event to outbox within the current transaction (or a new one)
147
+ async publish(
148
+ type: string,
149
+ payload: Record<string, unknown>,
150
+ source: string,
151
+ correlationId?: string,
152
+ client?: PoolClient
153
+ ): Promise<void> {
154
+ const eventId = uuid();
155
+ const corrId = correlationId || uuid();
156
+ const sql = \`
157
+ INSERT INTO event_outbox (id, event_type, payload, source, correlation_id)
158
+ VALUES ($1, $2, $3, $4, $5)
159
+ \`;
160
+ const params = [eventId, type, JSON.stringify({ ...payload, _event_id: eventId }), source, corrId];
161
+
162
+ if (client) {
163
+ // Write within caller's transaction — atomicity guaranteed
164
+ await client.query(sql, params);
165
+ } else {
166
+ await pool.query(sql, params);
167
+ }
168
+ counter("event.outboxed", { type });
169
+ }
170
+
171
+ // Called by the background worker
172
+ async flush(): Promise<void> {
173
+ const client = await pool.connect();
174
+ try {
175
+ await client.query("BEGIN");
176
+
177
+ // Fetch pending events (lock rows to prevent concurrent processing)
178
+ const { rows } = await client.query(\`
179
+ SELECT id, event_type, payload, source, correlation_id, attempts
180
+ FROM event_outbox
181
+ WHERE status = 'pending' AND scheduled_at <= NOW()
182
+ ORDER BY scheduled_at ASC
183
+ LIMIT 50
184
+ FOR UPDATE SKIP LOCKED
185
+ \`);
186
+
187
+ for (const row of rows) {
188
+ try {
189
+ // exactly_once: check deduplication table
190
+ if (EXACTLY_ONCE_EVENTS.has(row.event_type)) {
191
+ const { rows: dup } = await client.query(
192
+ "SELECT 1 FROM event_processed WHERE event_id = $1",
193
+ [row.payload._event_id || row.id]
194
+ );
195
+ if (dup.length > 0) {
196
+ await client.query(
197
+ "UPDATE event_outbox SET status = 'delivered', delivered_at = NOW() WHERE id = $1",
198
+ [row.id]
199
+ );
200
+ continue;
201
+ }
202
+ }
203
+
204
+ const event: SystemEvent = {
205
+ type: row.event_type,
206
+ payload: row.payload,
207
+ metadata: {
208
+ source: row.source,
209
+ timestamp: new Date(),
210
+ correlation_id: row.correlation_id,
211
+ causation_id: uuid(),
212
+ },
213
+ };
214
+
215
+ const handlers = this.handlers.get(row.event_type) || [];
216
+ for (const handler of handlers) {
217
+ await handler(event);
218
+ }
219
+
220
+ // Mark delivered
221
+ await client.query(
222
+ "UPDATE event_outbox SET status = 'delivered', delivered_at = NOW(), attempts = attempts + 1 WHERE id = $1",
223
+ [row.id]
224
+ );
225
+
226
+ // Record for exactly_once deduplication
227
+ if (EXACTLY_ONCE_EVENTS.has(row.event_type)) {
228
+ await client.query(
229
+ "INSERT INTO event_processed (event_id, event_type) VALUES ($1, $2) ON CONFLICT DO NOTHING",
230
+ [row.payload._event_id || row.id, row.event_type]
231
+ );
232
+ }
233
+
234
+ counter("event.delivered", { type: row.event_type, mode: "durable" });
235
+ } catch (e: any) {
236
+ const maxAttempts = AT_LEAST_ONCE_EVENTS.has(row.event_type) ? 10 : 3;
237
+ const newAttempts = row.attempts + 1;
238
+ const status = newAttempts >= maxAttempts ? "dead_letter" : "pending";
239
+ const backoffMs = Math.min(1000 * Math.pow(2, newAttempts), 300000);
240
+ await client.query(
241
+ \`UPDATE event_outbox
242
+ SET attempts = $1, last_error = $2, status = $3,
243
+ scheduled_at = NOW() + ($4 || ' milliseconds')::interval
244
+ WHERE id = $5\`,
245
+ [newAttempts, e.message, status, backoffMs, row.id]
246
+ );
247
+ counter("event.delivery_failed", { type: row.event_type, mode: "durable" });
248
+ logger.error("event_delivery_failed", { event: row.event_type, metadata: { error: e.message, attempts: newAttempts } });
249
+ }
250
+ }
251
+
252
+ await client.query("COMMIT");
253
+ } catch (e) {
254
+ await client.query("ROLLBACK");
255
+ throw e;
256
+ } finally {
257
+ client.release();
258
+ }
259
+ }
260
+
261
+ // Start background worker
262
+ startWorker(intervalMs: number = 1000): NodeJS.Timeout {
263
+ logger.info("event_worker_started", { event: "startup", metadata: { interval_ms: intervalMs } });
264
+ return setInterval(async () => {
265
+ try {
266
+ await this.flush();
267
+ } catch (e: any) {
268
+ logger.error("event_worker_error", { event: "flush_failed", metadata: { error: e.message } });
269
+ }
270
+ }, intervalMs);
271
+ }
272
+ }
273
+
274
+ // ─── Unified Interface ────────────────────────────────────────────────────────
275
+
276
+ const inProcess = new InProcessBus();
277
+ const durable = new DurableBus();
278
+
279
+ export const eventBus = {
280
+ subscribe(type: string, handler: Handler): void {
281
+ inProcess.subscribe(type, handler);
282
+ durable.subscribe(type, handler);
283
+ },
284
+
285
+ async publish(
286
+ type: string,
287
+ payload: Record<string, unknown>,
288
+ source: string,
289
+ correlationId?: string,
290
+ client?: PoolClient
291
+ ): Promise<void> {
292
+ if (MODE === "durable") {
293
+ await durable.publish(type, payload, source, correlationId, client);
294
+ } else {
295
+ await inProcess.publish(type, payload, source, correlationId);
296
+ }
297
+ },
298
+
299
+ startWorker(intervalMs?: number): NodeJS.Timeout | null {
300
+ if (MODE === "durable") {
301
+ return durable.startWorker(intervalMs);
302
+ }
303
+ return null;
304
+ },
305
+ };
306
+ `;
307
+ }