@reventlessdev/reventless-postgres 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +202 -0
  3. package/__mocks__/emptyModule.js +1 -0
  4. package/lib/bs/.compiler.log +2 -0
  5. package/lib/bs/ReventlessPostgres.cmi +0 -0
  6. package/lib/bs/ReventlessPostgres.cmj +0 -0
  7. package/lib/bs/ReventlessPostgres.cmt +0 -0
  8. package/lib/bs/ReventlessPostgres.mlmap +9 -0
  9. package/lib/bs/build.ninja +0 -0
  10. package/lib/bs/compiler-info.json +8 -0
  11. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  12. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  13. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  14. package/lib/bs/src/DcbEventLogStorage_Postgres.ast +0 -0
  15. package/lib/bs/src/DcbEventLogStorage_Postgres.res +352 -0
  16. package/lib/bs/src/DcbEventLogStorage_Postgres.res.mjs +385 -0
  17. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  18. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  19. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  20. package/lib/bs/src/EventLogStorage_Postgres.ast +0 -0
  21. package/lib/bs/src/EventLogStorage_Postgres.res +176 -0
  22. package/lib/bs/src/EventLogStorage_Postgres.res.mjs +207 -0
  23. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  24. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmj +0 -0
  25. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  26. package/lib/bs/src/PgChangeFeed.ast +0 -0
  27. package/lib/bs/src/PgChangeFeed.res +145 -0
  28. package/lib/bs/src/PgChangeFeed.res.mjs +90 -0
  29. package/lib/bs/src/PgDriver-ReventlessPostgres.cmi +0 -0
  30. package/lib/bs/src/PgDriver-ReventlessPostgres.cmj +0 -0
  31. package/lib/bs/src/PgDriver-ReventlessPostgres.cmt +0 -0
  32. package/lib/bs/src/PgDriver-ReventlessPostgres.cmti +0 -0
  33. package/lib/bs/src/PgDriver.ast +0 -0
  34. package/lib/bs/src/PgDriver.iast +0 -0
  35. package/lib/bs/src/PgDriver.res +111 -0
  36. package/lib/bs/src/PgDriver.res.mjs +85 -0
  37. package/lib/bs/src/PgDriver.resi +68 -0
  38. package/lib/bs/src/PgSchema-ReventlessPostgres.cmi +0 -0
  39. package/lib/bs/src/PgSchema-ReventlessPostgres.cmj +0 -0
  40. package/lib/bs/src/PgSchema-ReventlessPostgres.cmt +0 -0
  41. package/lib/bs/src/PgSchema.ast +0 -0
  42. package/lib/bs/src/PgSchema.res +256 -0
  43. package/lib/bs/src/PgSchema.res.mjs +46 -0
  44. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  45. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmj +0 -0
  46. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  47. package/lib/bs/src/QueryDbStorage_Postgres.ast +0 -0
  48. package/lib/bs/src/QueryDbStorage_Postgres.res +244 -0
  49. package/lib/bs/src/QueryDbStorage_Postgres.res.mjs +225 -0
  50. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  51. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmj +0 -0
  52. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  53. package/lib/bs/src/QueryEnginePostgres.ast +0 -0
  54. package/lib/bs/src/QueryEnginePostgres.res +127 -0
  55. package/lib/bs/src/QueryEnginePostgres.res.mjs +172 -0
  56. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
  57. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmj +0 -0
  58. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
  59. package/lib/bs/tests/PostgresIntegrationTest.ast +0 -0
  60. package/lib/bs/tests/PostgresIntegrationTest.res +236 -0
  61. package/lib/bs/tests/PostgresIntegrationTest.res.mjs +328 -0
  62. package/lib/ocaml/.compiler.log +2 -0
  63. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  64. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  65. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  66. package/lib/ocaml/DcbEventLogStorage_Postgres.ast +0 -0
  67. package/lib/ocaml/DcbEventLogStorage_Postgres.res +352 -0
  68. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  69. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  70. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  71. package/lib/ocaml/EventLogStorage_Postgres.ast +0 -0
  72. package/lib/ocaml/EventLogStorage_Postgres.res +176 -0
  73. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  74. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmj +0 -0
  75. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  76. package/lib/ocaml/PgChangeFeed.ast +0 -0
  77. package/lib/ocaml/PgChangeFeed.res +145 -0
  78. package/lib/ocaml/PgDriver-ReventlessPostgres.cmi +0 -0
  79. package/lib/ocaml/PgDriver-ReventlessPostgres.cmj +0 -0
  80. package/lib/ocaml/PgDriver-ReventlessPostgres.cmt +0 -0
  81. package/lib/ocaml/PgDriver-ReventlessPostgres.cmti +0 -0
  82. package/lib/ocaml/PgDriver.ast +0 -0
  83. package/lib/ocaml/PgDriver.iast +0 -0
  84. package/lib/ocaml/PgDriver.res +111 -0
  85. package/lib/ocaml/PgDriver.resi +68 -0
  86. package/lib/ocaml/PgSchema-ReventlessPostgres.cmi +0 -0
  87. package/lib/ocaml/PgSchema-ReventlessPostgres.cmj +0 -0
  88. package/lib/ocaml/PgSchema-ReventlessPostgres.cmt +0 -0
  89. package/lib/ocaml/PgSchema.ast +0 -0
  90. package/lib/ocaml/PgSchema.res +256 -0
  91. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
  92. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmj +0 -0
  93. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
  94. package/lib/ocaml/PostgresIntegrationTest.ast +0 -0
  95. package/lib/ocaml/PostgresIntegrationTest.res +236 -0
  96. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  97. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmj +0 -0
  98. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  99. package/lib/ocaml/QueryDbStorage_Postgres.ast +0 -0
  100. package/lib/ocaml/QueryDbStorage_Postgres.res +244 -0
  101. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  102. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmj +0 -0
  103. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  104. package/lib/ocaml/QueryEnginePostgres.ast +0 -0
  105. package/lib/ocaml/QueryEnginePostgres.res +127 -0
  106. package/lib/ocaml/ReventlessPostgres.cmi +0 -0
  107. package/lib/ocaml/ReventlessPostgres.cmj +0 -0
  108. package/lib/ocaml/ReventlessPostgres.cmt +0 -0
  109. package/lib/ocaml/ReventlessPostgres.mlmap +9 -0
  110. package/package.json +56 -0
  111. package/rescript.json +36 -0
  112. package/src/DcbEventLogStorage_Postgres.res +352 -0
  113. package/src/DcbEventLogStorage_Postgres.res.mjs +385 -0
  114. package/src/EventLogStorage_Postgres.res +176 -0
  115. package/src/EventLogStorage_Postgres.res.mjs +207 -0
  116. package/src/PgChangeFeed.res +145 -0
  117. package/src/PgChangeFeed.res.mjs +90 -0
  118. package/src/PgDriver.res +111 -0
  119. package/src/PgDriver.res.mjs +85 -0
  120. package/src/PgDriver.resi +68 -0
  121. package/src/PgSchema.res +256 -0
  122. package/src/PgSchema.res.mjs +46 -0
  123. package/src/QueryDbStorage_Postgres.res +244 -0
  124. package/src/QueryDbStorage_Postgres.res.mjs +225 -0
  125. package/src/QueryEnginePostgres.res +127 -0
  126. package/src/QueryEnginePostgres.res.mjs +172 -0
  127. package/tests/PostgresIntegrationTest.res +236 -0
  128. package/tests/PostgresIntegrationTest.res.mjs +328 -0
@@ -0,0 +1,236 @@
1
+ // Live-Postgres integration + concurrency suite (Phase F2).
2
+ //
3
+ // Skipped unless PG_URL is set, so the default `pnpm test` stays dependency-free.
4
+ // Run against a real database with e.g.:
5
+ // PG_URL=postgres://postgres:postgres@localhost:5432/postgres pnpm test
6
+ //
7
+ // Covers the concurrency-critical core that nothing else in the repo has:
8
+ // - classic OCC append (success, gap/stale → Conflict, replay)
9
+ // - DCB append + xmin-fenced read + conditional-append conflict
10
+ // - write-skew: two genuinely concurrent conditional appends on one boundary —
11
+ // exactly one commits, the other conflicts cleanly (the anomaly the advisory
12
+ // locks close)
13
+ // - both #AdvisoryLocks and #RowLocks strategies
14
+
15
+ open JestGlobals
16
+ open ReventlessCore
17
+ open Reventless
18
+
19
+ @val external processEnv: dict<string> = "process.env"
20
+ let opts: Pulumi.CustomResourceOptions.t = {}
21
+
22
+ let jsonObj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
23
+
24
+ let stored = (eventType, tags, data): DcbEventLog_Adapter.rawStoredEvent => {
25
+ eventType,
26
+ data,
27
+ tags,
28
+ meta: ReventlessCore.Message.generateMeta(~service="pg-test"),
29
+ }
30
+
31
+ let evJson = (msgId, v) =>
32
+ jsonObj([("msgId", JSON.Encode.string(msgId)), ("v", JSON.Encode.int(v))])
33
+
34
+ switch processEnv->Dict.get("PG_URL") {
35
+ | None =>
36
+ testSync("Postgres integration (skipped — set PG_URL to run)", () => expect(true)->toBe(true))
37
+ | Some(url) =>
38
+ let pool = PgDriver.makePool({connectionString: url})
39
+
40
+ beforeAllAsync(async () => {
41
+ await PgSchema.ensureSchema(pool)
42
+ await PgSchema.truncateAll(pool)
43
+ })
44
+ afterAll(() => {
45
+ let _ = pool->PgDriver.endPool
46
+ })
47
+
48
+ describe("classic OCC event log", () => {
49
+ testPromise("append, replay, and conflict on wrong seq", async () => {
50
+ let (_n, ops, _s) = EventLogStorage_Postgres.makeStorage(~pool, ~name="ct-orders", ~opts)
51
+
52
+ let r0 = await ops.append(0, "agg-1", [evJson("m0", 1), evJson("m1", 2)])
53
+ switch r0 {
54
+ | Ok() => expect(true)->toBe(true)
55
+ | Error(_) => expect("append")->toBe("ok")
56
+ }
57
+
58
+ // Stale seq (0 again) → Conflict.
59
+ let rStale = await ops.append(0, "agg-1", [evJson("m2", 3)])
60
+ switch rStale {
61
+ | Error(EventLog.Conflict) => expect(true)->toBe(true)
62
+ | _ => expect("stale")->toBe("conflict")
63
+ }
64
+
65
+ // Gap seq (5) → Conflict.
66
+ let rGap = await ops.append(5, "agg-1", [evJson("m3", 4)])
67
+ switch rGap {
68
+ | Error(EventLog.Conflict) => expect(true)->toBe(true)
69
+ | _ => expect("gap")->toBe("conflict")
70
+ }
71
+
72
+ // Correct next seq (2) → Ok.
73
+ let rNext = await ops.append(2, "agg-1", [evJson("m4", 5)])
74
+ switch rNext {
75
+ | Ok() => expect(true)->toBe(true)
76
+ | Error(_) => expect("next")->toBe("ok")
77
+ }
78
+
79
+ let evs = await ops.replay("agg-1")
80
+ expect(evs->Array.length)->toBe(3)
81
+
82
+ // Empty append is a no-op, never a conflict.
83
+ let rEmpty = await ops.append(3, "agg-1", [])
84
+ switch rEmpty {
85
+ | Ok() => expect(true)->toBe(true)
86
+ | Error(_) => expect("empty")->toBe("ok")
87
+ }
88
+ })
89
+ })
90
+
91
+ describe("DCB event log", () => {
92
+ let makeLog = name =>
93
+ DcbEventLogStorage_Postgres.makeStorage(
94
+ ~pool,
95
+ ~name,
96
+ ~indexes=[],
97
+ ~partitionTag=DcbTag.Simple({key: "k"}),
98
+ ~opts,
99
+ )
100
+
101
+ testPromise("append + read round-trips events with tags", async () => {
102
+ let (_n, ops, _s) = makeLog("dcb-rt")
103
+ let e1 = stored("ItemAdded", [{key: "itemId", value: "x1"}], jsonObj([("n", JSON.Encode.string("w"))]))
104
+ let r = await ops.append([e1])
105
+ switch r {
106
+ | Ok(_) => expect(true)->toBe(true)
107
+ | Error(m) => expect(m)->toBe("ok")
108
+ }
109
+ let read = await ops.read(~query=[])
110
+ expect(read.events->Array.length)->toBe(1)
111
+ let first = read.events->Array.getUnsafe(0)
112
+ expect(first.eventType)->toBe("ItemAdded")
113
+ expect((first.tags->Array.getUnsafe(0)).key)->toBe("itemId")
114
+ expect((first.tags->Array.getUnsafe(0)).value)->toBe("x1")
115
+ })
116
+
117
+ testPromise("conditional append conflicts when the boundary already has an event", async () => {
118
+ let (_n, ops, _s) = makeLog("dcb-cond")
119
+ let mk = who => stored("Reserved", [{key: "seatId", value: "s1"}], jsonObj([("who", JSON.Encode.string(who))]))
120
+ let r1 = await ops.append([mk("a")])
121
+ switch r1 {
122
+ | Ok(_) => expect(true)->toBe(true)
123
+ | Error(_) => expect("first")->toBe("ok")
124
+ }
125
+ // Assert "no Reserved for s1 yet" — must now conflict.
126
+ let cond: DcbTag.appendCondition = {
127
+ query: [{eventTypes: ["Reserved"], tags: [{key: "seatId", value: "s1"}]}],
128
+ }
129
+ let r2 = await ops.append([mk("b")], ~condition=cond)
130
+ switch r2 {
131
+ | Error(ReventlessInfra.DcbEventLog.Conflict) => expect(true)->toBe(true)
132
+ | _ => expect("second")->toBe("conflict")
133
+ }
134
+ })
135
+
136
+ let writeSkew = (strategy, logName) =>
137
+ testPromise(`write-skew: exactly one of two concurrent appends wins (${logName})`, async () => {
138
+ let (_n, ops, _s) = DcbEventLogStorage_Postgres.makeStorage(
139
+ ~pool,
140
+ ~name=logName,
141
+ ~indexes=[],
142
+ ~partitionTag=DcbTag.Simple({key: "k"}),
143
+ ~opts,
144
+ ~lockStrategy=strategy,
145
+ )
146
+ let cond: DcbTag.appendCondition = {
147
+ query: [{eventTypes: ["Reserved"], tags: [{key: "seatId", value: "hot"}]}],
148
+ }
149
+ let mk = who => stored("Reserved", [{key: "seatId", value: "hot"}], jsonObj([("who", JSON.Encode.string(who))]))
150
+ // Dispatch BOTH before awaiting either — genuinely concurrent on the pool.
151
+ let p1 = ops.append([mk("a")], ~condition=cond)
152
+ let p2 = ops.append([mk("b")], ~condition=cond)
153
+ let r1 = await p1
154
+ let r2 = await p2
155
+ let isOk = r => switch r {
156
+ | Ok(_) => true
157
+ | _ => false
158
+ }
159
+ let isConflict = r => r == Error(ReventlessInfra.DcbEventLog.Conflict)
160
+ let oks = [r1, r2]->Array.filter(isOk)->Array.length
161
+ let conflicts = [r1, r2]->Array.filter(isConflict)->Array.length
162
+ expect(oks)->toBe(1)
163
+ expect(conflicts)->toBe(1)
164
+ // And only one event actually landed on the boundary.
165
+ let read = await ops.read(~query=[{tags: [{key: "seatId", value: "hot"}]}])
166
+ expect(read.events->Array.length)->toBe(1)
167
+ })
168
+
169
+ writeSkew(#AdvisoryLocks, "dcb-skew-advisory")
170
+ writeSkew(#RowLocks, "dcb-skew-rows")
171
+
172
+ testPromise("hot tag: N concurrent conditional appends, exactly one wins", async () => {
173
+ let (_n, ops, _s) = makeLog("dcb-hot")
174
+ let cond: DcbTag.appendCondition = {
175
+ query: [{eventTypes: ["Claimed"], tags: [{key: "prizeId", value: "p1"}]}],
176
+ }
177
+ let n = 12
178
+ // Dispatch all N before awaiting any — genuinely concurrent on the pool.
179
+ let promises = Array.make(~length=n, 0)->Array.mapWithIndex((_, i) =>
180
+ ops.append(
181
+ [stored("Claimed", [{key: "prizeId", value: "p1"}], jsonObj([("who", JSON.Encode.int(i))]))],
182
+ ~condition=cond,
183
+ )
184
+ )
185
+ let results = await Promise.all(promises)
186
+ let oks = results->Array.filter(r => switch r {
187
+ | Ok(_) => true
188
+ | _ => false
189
+ })->Array.length
190
+ expect(oks)->toBe(1)
191
+ // No lost updates: exactly one Claimed landed on the boundary.
192
+ let read = await ops.read(~query=[{tags: [{key: "prizeId", value: "p1"}]}])
193
+ expect(read.events->Array.length)->toBe(1)
194
+ })
195
+
196
+ testPromise("cursors are monotonic and usable as `after`", async () => {
197
+ let (_n, ops, _s) = makeLog("dcb-cursor")
198
+ let mk = i => stored("Tick", [{key: "streamId", value: "c1"}], jsonObj([("i", JSON.Encode.int(i))]))
199
+ let _ = await ops.append([mk(0)])
200
+ let mid = switch await ops.append([mk(1)]) {
201
+ | Ok(pos) => pos
202
+ | Error(_) => ""
203
+ }
204
+ let _ = await ops.append([mk(2)])
205
+ // Reading after `mid` returns only the events strictly after it.
206
+ let read = await ops.read(~query=[{tags: [{key: "streamId", value: "c1"}]}], ~after=mid)
207
+ expect(read.events->Array.length)->toBe(1)
208
+ expect((read.events->Array.getUnsafe(0)).eventType)->toBe("Tick")
209
+ })
210
+ })
211
+
212
+ describe("change feed", () => {
213
+ testPromise("drains all events and checkpoints", async () => {
214
+ let (_n, ops, _s) = DcbEventLogStorage_Postgres.makeStorage(
215
+ ~pool,
216
+ ~name="feed-log",
217
+ ~indexes=[],
218
+ ~partitionTag=DcbTag.Simple({key: "k"}),
219
+ ~opts,
220
+ )
221
+ let mk = i => stored("Fed", [{key: "id", value: "f"}], jsonObj([("i", JSON.Encode.int(i))]))
222
+ let _ = await ops.append([mk(0), mk(1), mk(2)])
223
+
224
+ let collected = ref(0)
225
+ let processed = await PgChangeFeed.drain(pool, ~logName="feed-log", ~subscriber="sub-1", ~handle=async evs => {
226
+ collected := collected.contents + evs->Array.length
227
+ })
228
+ expect(processed)->toBe(3)
229
+ expect(collected.contents)->toBe(3)
230
+
231
+ // A second drain from the saved checkpoint sees nothing new.
232
+ let again = await PgChangeFeed.drain(pool, ~logName="feed-log", ~subscriber="sub-1", ~handle=async _ => ())
233
+ expect(again)->toBe(0)
234
+ })
235
+ })
236
+ }
@@ -0,0 +1,328 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
5
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
6
+ import * as PgDriver$ReventlessPostgres from "../src/PgDriver.res.mjs";
7
+ import * as PgSchema$ReventlessPostgres from "../src/PgSchema.res.mjs";
8
+ import * as PgChangeFeed$ReventlessPostgres from "../src/PgChangeFeed.res.mjs";
9
+ import * as EventLogStorage_Postgres$ReventlessPostgres from "../src/EventLogStorage_Postgres.res.mjs";
10
+ import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "../src/DcbEventLogStorage_Postgres.res.mjs";
11
+
12
+ let opts = {};
13
+
14
+ function jsonObj(pairs) {
15
+ return Object.fromEntries(pairs);
16
+ }
17
+
18
+ function stored(eventType, tags, data) {
19
+ return {
20
+ eventType: eventType,
21
+ data: data,
22
+ tags: tags,
23
+ meta: Message$ReventlessCore.generateMeta("pg-test", undefined, undefined, undefined, undefined, undefined, undefined, undefined)
24
+ };
25
+ }
26
+
27
+ function evJson(msgId, v) {
28
+ return Object.fromEntries([
29
+ [
30
+ "msgId",
31
+ msgId
32
+ ],
33
+ [
34
+ "v",
35
+ v
36
+ ]
37
+ ]);
38
+ }
39
+
40
+ let url = process.env["PG_URL"];
41
+
42
+ if (url !== undefined) {
43
+ let pool = PgDriver$ReventlessPostgres.makePool({
44
+ connectionString: url
45
+ });
46
+ globalThis.beforeAll(async () => {
47
+ await PgSchema$ReventlessPostgres.ensureSchema(pool);
48
+ return await PgSchema$ReventlessPostgres.truncateAll(pool);
49
+ });
50
+ globalThis.afterAll(() => {
51
+ PgDriver$ReventlessPostgres.endPool(pool);
52
+ });
53
+ globalThis.describe("classic OCC event log", () => {
54
+ globalThis.test("append, replay, and conflict on wrong seq", async () => {
55
+ let match = EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "ct-orders", opts, undefined);
56
+ let ops = match[1];
57
+ let r0 = await ops.append(0, "agg-1", [
58
+ evJson("m0", 1),
59
+ evJson("m1", 2)
60
+ ]);
61
+ if (r0.TAG === "Ok") {
62
+ globalThis.expect(true).toBe(true);
63
+ } else {
64
+ globalThis.expect("append").toBe("ok");
65
+ }
66
+ let rStale = await ops.append(0, "agg-1", [evJson("m2", 3)]);
67
+ if (rStale.TAG === "Ok") {
68
+ globalThis.expect("stale").toBe("conflict");
69
+ } else {
70
+ let tmp = rStale._0;
71
+ if (typeof tmp !== "object") {
72
+ globalThis.expect(true).toBe(true);
73
+ } else {
74
+ globalThis.expect("stale").toBe("conflict");
75
+ }
76
+ }
77
+ let rGap = await ops.append(5, "agg-1", [evJson("m3", 4)]);
78
+ if (rGap.TAG === "Ok") {
79
+ globalThis.expect("gap").toBe("conflict");
80
+ } else {
81
+ let tmp$1 = rGap._0;
82
+ if (typeof tmp$1 !== "object") {
83
+ globalThis.expect(true).toBe(true);
84
+ } else {
85
+ globalThis.expect("gap").toBe("conflict");
86
+ }
87
+ }
88
+ let rNext = await ops.append(2, "agg-1", [evJson("m4", 5)]);
89
+ if (rNext.TAG === "Ok") {
90
+ globalThis.expect(true).toBe(true);
91
+ } else {
92
+ globalThis.expect("next").toBe("ok");
93
+ }
94
+ let evs = await ops.replay("agg-1");
95
+ globalThis.expect(evs.length).toBe(3);
96
+ let rEmpty = await ops.append(3, "agg-1", []);
97
+ if (rEmpty.TAG === "Ok") {
98
+ globalThis.expect(true).toBe(true);
99
+ return;
100
+ }
101
+ globalThis.expect("empty").toBe("ok");
102
+ });
103
+ });
104
+ globalThis.describe("DCB event log", () => {
105
+ let makeLog = name => DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, [], {
106
+ TAG: "Simple",
107
+ _0: {
108
+ key: "k"
109
+ }
110
+ }, undefined, opts, undefined, undefined);
111
+ globalThis.test("append + read round-trips events with tags", async () => {
112
+ let match = makeLog("dcb-rt");
113
+ let ops = match[1];
114
+ let e1 = stored("ItemAdded", [{
115
+ key: "itemId",
116
+ value: "x1"
117
+ }], Object.fromEntries([[
118
+ "n",
119
+ "w"
120
+ ]]));
121
+ let r = await ops.append([e1], undefined);
122
+ if (r.TAG === "Ok") {
123
+ globalThis.expect(true).toBe(true);
124
+ } else {
125
+ globalThis.expect(r._0).toBe("ok");
126
+ }
127
+ let read = await ops.read([], undefined);
128
+ globalThis.expect(read.events.length).toBe(1);
129
+ let first = read.events[0];
130
+ globalThis.expect(first.eventType).toBe("ItemAdded");
131
+ globalThis.expect(first.tags[0].key).toBe("itemId");
132
+ globalThis.expect(first.tags[0].value).toBe("x1");
133
+ });
134
+ globalThis.test("conditional append conflicts when the boundary already has an event", async () => {
135
+ let match = makeLog("dcb-cond");
136
+ let ops = match[1];
137
+ let mk = who => stored("Reserved", [{
138
+ key: "seatId",
139
+ value: "s1"
140
+ }], Object.fromEntries([[
141
+ "who",
142
+ who
143
+ ]]));
144
+ let r1 = await ops.append([mk("a")], undefined);
145
+ if (r1.TAG === "Ok") {
146
+ globalThis.expect(true).toBe(true);
147
+ } else {
148
+ globalThis.expect("first").toBe("ok");
149
+ }
150
+ let cond_query = [{
151
+ eventTypes: ["Reserved"],
152
+ tags: [{
153
+ key: "seatId",
154
+ value: "s1"
155
+ }]
156
+ }];
157
+ let cond = {
158
+ query: cond_query
159
+ };
160
+ let r2 = await ops.append([mk("b")], cond);
161
+ if (r2.TAG === "Ok") {
162
+ globalThis.expect("second").toBe("conflict");
163
+ return;
164
+ }
165
+ let tmp = r2._0;
166
+ if (typeof tmp !== "object") {
167
+ globalThis.expect(true).toBe(true);
168
+ return;
169
+ }
170
+ globalThis.expect("second").toBe("conflict");
171
+ });
172
+ let writeSkew = (strategy, logName) => {
173
+ globalThis.test(`write-skew: exactly one of two concurrent appends wins (` + logName + `)`, async () => {
174
+ let match = DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, logName, [], {
175
+ TAG: "Simple",
176
+ _0: {
177
+ key: "k"
178
+ }
179
+ }, undefined, opts, strategy, undefined);
180
+ let ops = match[1];
181
+ let cond_query = [{
182
+ eventTypes: ["Reserved"],
183
+ tags: [{
184
+ key: "seatId",
185
+ value: "hot"
186
+ }]
187
+ }];
188
+ let cond = {
189
+ query: cond_query
190
+ };
191
+ let mk = who => stored("Reserved", [{
192
+ key: "seatId",
193
+ value: "hot"
194
+ }], Object.fromEntries([[
195
+ "who",
196
+ who
197
+ ]]));
198
+ let p1 = ops.append([mk("a")], cond);
199
+ let p2 = ops.append([mk("b")], cond);
200
+ let r1 = await p1;
201
+ let r2 = await p2;
202
+ let isOk = r => r.TAG === "Ok";
203
+ let isConflict = r => Primitive_object.equal(r, {
204
+ TAG: "Error",
205
+ _0: "Conflict"
206
+ });
207
+ let oks = [
208
+ r1,
209
+ r2
210
+ ].filter(isOk).length;
211
+ let conflicts = [
212
+ r1,
213
+ r2
214
+ ].filter(isConflict).length;
215
+ globalThis.expect(oks).toBe(1);
216
+ globalThis.expect(conflicts).toBe(1);
217
+ let read = await ops.read([{
218
+ tags: [{
219
+ key: "seatId",
220
+ value: "hot"
221
+ }]
222
+ }], undefined);
223
+ globalThis.expect(read.events.length).toBe(1);
224
+ });
225
+ };
226
+ writeSkew("AdvisoryLocks", "dcb-skew-advisory");
227
+ writeSkew("RowLocks", "dcb-skew-rows");
228
+ globalThis.test("hot tag: N concurrent conditional appends, exactly one wins", async () => {
229
+ let match = makeLog("dcb-hot");
230
+ let ops = match[1];
231
+ let cond_query = [{
232
+ eventTypes: ["Claimed"],
233
+ tags: [{
234
+ key: "prizeId",
235
+ value: "p1"
236
+ }]
237
+ }];
238
+ let cond = {
239
+ query: cond_query
240
+ };
241
+ let promises = Stdlib_Array.make(12, 0).map((param, i) => ops.append([stored("Claimed", [{
242
+ key: "prizeId",
243
+ value: "p1"
244
+ }], Object.fromEntries([[
245
+ "who",
246
+ i
247
+ ]]))], cond));
248
+ let results = await Promise.all(promises);
249
+ let oks = results.filter(r => r.TAG === "Ok").length;
250
+ globalThis.expect(oks).toBe(1);
251
+ let read = await ops.read([{
252
+ tags: [{
253
+ key: "prizeId",
254
+ value: "p1"
255
+ }]
256
+ }], undefined);
257
+ globalThis.expect(read.events.length).toBe(1);
258
+ });
259
+ globalThis.test("cursors are monotonic and usable as `after`", async () => {
260
+ let match = makeLog("dcb-cursor");
261
+ let ops = match[1];
262
+ let mk = i => stored("Tick", [{
263
+ key: "streamId",
264
+ value: "c1"
265
+ }], Object.fromEntries([[
266
+ "i",
267
+ i
268
+ ]]));
269
+ await ops.append([mk(0)], undefined);
270
+ let pos = await ops.append([mk(1)], undefined);
271
+ let mid;
272
+ mid = pos.TAG === "Ok" ? pos._0 : "";
273
+ await ops.append([mk(2)], undefined);
274
+ let read = await ops.read([{
275
+ tags: [{
276
+ key: "streamId",
277
+ value: "c1"
278
+ }]
279
+ }], mid);
280
+ globalThis.expect(read.events.length).toBe(1);
281
+ globalThis.expect(read.events[0].eventType).toBe("Tick");
282
+ });
283
+ });
284
+ globalThis.describe("change feed", () => {
285
+ globalThis.test("drains all events and checkpoints", async () => {
286
+ let match = DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "feed-log", [], {
287
+ TAG: "Simple",
288
+ _0: {
289
+ key: "k"
290
+ }
291
+ }, undefined, opts, undefined, undefined);
292
+ let mk = i => stored("Fed", [{
293
+ key: "id",
294
+ value: "f"
295
+ }], Object.fromEntries([[
296
+ "i",
297
+ i
298
+ ]]));
299
+ await match[1].append([
300
+ mk(0),
301
+ mk(1),
302
+ mk(2)
303
+ ], undefined);
304
+ let collected = {
305
+ contents: 0
306
+ };
307
+ let processed = await PgChangeFeed$ReventlessPostgres.drain(pool, "feed-log", "sub-1", undefined, async evs => {
308
+ collected.contents = collected.contents + evs.length | 0;
309
+ });
310
+ globalThis.expect(processed).toBe(3);
311
+ globalThis.expect(collected.contents).toBe(3);
312
+ let again = await PgChangeFeed$ReventlessPostgres.drain(pool, "feed-log", "sub-1", undefined, async param => {});
313
+ globalThis.expect(again).toBe(0);
314
+ });
315
+ });
316
+ } else {
317
+ globalThis.test("Postgres integration (skipped — set PG_URL to run)", () => {
318
+ globalThis.expect(true).toBe(true);
319
+ });
320
+ }
321
+
322
+ export {
323
+ opts,
324
+ jsonObj,
325
+ stored,
326
+ evJson,
327
+ }
328
+ /* url Not a pure module */
@@ -0,0 +1,2 @@
1
+ #Start(1783244875589)
2
+ #Done(1783244877492)