@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.319

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 (64) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +10 -10
  3. package/src/LocalPlatformRegistry.res +26 -1
  4. package/src/LocalPlatformRegistry.res.mjs +25 -5
  5. package/src/Platform.res +27 -2
  6. package/src/Platform.res.mjs +24 -4
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
  11. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
  12. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
  13. package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
  14. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
  15. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
  16. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
  17. package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
  18. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
  19. package/src/adapter/LocalBus.res +32 -5
  20. package/src/adapter/LocalBus.res.mjs +27 -7
  21. package/src/adapter/LocalEventLogRead.res +477 -0
  22. package/src/adapter/LocalEventLogRead.res.mjs +507 -0
  23. package/src/adapter/LocalEventTap.res +18 -80
  24. package/src/adapter/LocalEventTap.res.mjs +19 -108
  25. package/src/adapter/LocalFeed.res +433 -0
  26. package/src/adapter/LocalFeed.res.mjs +419 -0
  27. package/src/adapter/LocalLogFeed.res +217 -0
  28. package/src/adapter/LocalLogFeed.res.mjs +295 -0
  29. package/src/adapter/LocalPgEventLogRead.res +85 -0
  30. package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
  31. package/src/adapter/LocalPlatformObserve.res +456 -0
  32. package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
  33. package/src/adapter/LocalTapLines.res +149 -0
  34. package/src/adapter/LocalTapLines.res.mjs +232 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
  38. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  39. package/src/components/LocalAutomationSchedules.res.mjs +1 -1
  40. package/src/components/LocalAutomationSweep.res.mjs +1 -1
  41. package/src/reexport/CommandTopic.res.mjs +5 -2
  42. package/tests/LocalPlatformRegistryTest.res +17 -0
  43. package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
  44. package/tests/LocalPlatformStartTest.res.mjs +1 -1
  45. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  46. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  47. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  48. package/tests/adapter/LocalFeedTest.res +314 -0
  49. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  50. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  51. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  52. package/tests/adapter/LocalTapLinesTest.res +207 -0
  53. package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
  54. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  55. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  56. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  57. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  58. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  59. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  60. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  61. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  62. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  63. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  64. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
@@ -0,0 +1,314 @@
1
+ // The feeds a watching tool reads: a listener that stops reading costs the
2
+ // platform a bounded amount and is told what it missed; a filter line narrows
3
+ // what it is sent; the log feed starts with the recent records.
4
+
5
+ @@warning("-44")
6
+
7
+ open JestGlobals
8
+
9
+ module Logger = ReventlessCore.Logger
10
+
11
+ let sleep = (ms: int): promise<unit> =>
12
+ Promise.make((resolve, _) => {
13
+ let _ = setTimeout(() => resolve(), ms)
14
+ })
15
+
16
+ // Polls until `cond` holds, for at most `ms`.
17
+ let until = async (~ms=2000, cond: unit => bool): bool => {
18
+ let waited = ref(0)
19
+ while !cond() && waited.contents < ms {
20
+ await sleep(5)
21
+ waited := waited.contents + 5
22
+ }
23
+ cond()
24
+ }
25
+
26
+ let bind = (feed: LocalFeed.t): promise<int> =>
27
+ Promise.make((resolve, _) => feed->LocalFeed.start(~setting=Ephemeral, ~onBound=resolve, ()))
28
+
29
+ type client = {socket: NodeNet.socket, mutable received: string}
30
+
31
+ let connect = (port: int): promise<client> =>
32
+ Promise.make((resolve, _) => {
33
+ let c = ref(None)
34
+ let socket = NodeNet.connect(port, "127.0.0.1", () =>
35
+ c.contents->Option.forEach(client => resolve(client))
36
+ )
37
+ socket->NodeNet.setEncoding("utf8")
38
+ socket->NodeNet.onSocketError(_ => ())
39
+ let client = {socket, received: ""}
40
+ socket->NodeNet.onSocketData(chunk => client.received = client.received ++ chunk)
41
+ c := Some(client)
42
+ })
43
+
44
+ let lines = (c: client): array<string> => c.received->String.split("\n")->Array.filter(l => l != "")
45
+
46
+ let skippedCounts = (c: client): array<int> =>
47
+ c
48
+ ->lines
49
+ ->Array.filterMap(l =>
50
+ switch l->JSON.parseOrThrow->JSON.Decode.object {
51
+ | Some(d) if d->Dict.get("kind") == Some(JSON.String("skipped")) =>
52
+ d->Dict.get("count")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
53
+ | _ => None
54
+ | exception _ => None
55
+ }
56
+ )
57
+
58
+ describe("LocalFeed: a listener that never reads", () => {
59
+ test(
60
+ "costs a bounded amount, and is told how many lines it missed when it reads again",
61
+ async () => {
62
+ let maxBytes = 64 * 1024
63
+ let feed = LocalFeed.make(
64
+ ~name="TestFeed",
65
+ ~honours=[],
66
+ ~maxLines=200,
67
+ ~maxBytes,
68
+ ~filterWaitMs=10,
69
+ (),
70
+ )
71
+ let port = await bind(feed)
72
+ let client = await connect(port)
73
+ expect(await until(() => feed->LocalFeed.hasReaders))->toBe(true)
74
+ client.socket->NodeNet.pause
75
+
76
+ // 20,000 lines of about 1 KB: twenty megabytes the listener does not read.
77
+ let pad = String.repeat("x", 1000)
78
+ let total = 20_000
79
+ let held = ref(0)
80
+ for i in 1 to total {
81
+ feed->LocalFeed.broadcast(`{"n":${i->Int.toString},"pad":"${pad}"}`)
82
+ held := Math.Int.max(held.contents, feed->LocalFeed.heldBytes)
83
+ }
84
+ // What the platform holds for it never exceeds the queue's bound plus what
85
+ // one socket buffers before it reports backpressure.
86
+ expect(feed->LocalFeed.queuedBytes <= maxBytes)->toBe(true)
87
+ expect(held.contents < maxBytes + 4 * 1024 * 1024)->toBe(true)
88
+
89
+ client.socket->NodeNet.resume
90
+ let last = `{"n":${total->Int.toString},`
91
+ expect(await until(~ms=5000, () => client.received->String.includes(last)))->toBe(true)
92
+ let skipped = skippedCounts(client)->Array.reduce(0, (a, b) => a + b)
93
+ expect(skipped > 0)->toBe(true)
94
+ // Every line is either received or counted as skipped.
95
+ let received = lines(client)->Array.filter(l => l->String.startsWith(`{"n":`))->Array.length
96
+ expect(received + skipped)->toBe(total)
97
+ NodeNet.destroySocket(client.socket)
98
+ await feed->LocalFeed.stopForTests
99
+ },
100
+ )
101
+ })
102
+
103
+ describe("LocalFeed: the filter line", () => {
104
+ testSync("is read leniently and ignores what it does not know", () => {
105
+ expect(
106
+ LocalFeed.parseFilter(`{"plugins":["Ordering"],"level":"WARN","eventTypes":["Placed"],"x":1}`),
107
+ )->toEqual(Some({LocalFeed.plugins: ["Ordering"], level: Warn, eventTypes: ["Placed"]}))
108
+ expect(LocalFeed.parseFilter("not json"))->toEqual(None)
109
+ })
110
+
111
+ testSync("narrows only by the fields the feed honours", () => {
112
+ let filter: LocalFeed.filter = {plugins: ["Ordering"], level: Warn}
113
+ let warnOrdering: LocalFeed.facets = {plugin: "Ordering", level: Warn}
114
+ let infoOrdering: LocalFeed.facets = {plugin: "Ordering", level: Info}
115
+ let catalog: LocalFeed.facets = {plugin: "Catalog", level: Error}
116
+ let logs: array<LocalFeed.filterField> = [Plugins, Level]
117
+ expect(LocalFeed.matches(~honours=logs, filter, warnOrdering))->toBe(true)
118
+ expect(LocalFeed.matches(~honours=logs, filter, infoOrdering))->toBe(false)
119
+ expect(LocalFeed.matches(~honours=logs, filter, catalog))->toBe(false)
120
+ // An event has no level; the tap ignores the field.
121
+ expect(LocalFeed.matches(~honours=[Plugins], filter, {plugin: "Ordering"}))->toBe(true)
122
+ })
123
+
124
+ test("narrows the event tap and the log feed alike", async () => {
125
+ LocalEventTap.resetForTests()
126
+ LocalLogFeed.resetForTests()
127
+ LocalLogFeed.install(~env=Dict.make())
128
+ // Logged before anyone connects: sent first, filtered, by the log feed.
129
+ Logger.emit(~level=Warn, ~comp="Projection", ~plugin="Ordering", "before connect")
130
+ Logger.emit(~level=Info, ~comp="Projection", ~plugin="Ordering", "before connect, quiet")
131
+
132
+ let tapPort = await Promise.make(
133
+ (resolve, _) => LocalEventTap.start(~env=Dict.make(), ~onBound=resolve, ()),
134
+ )
135
+ let logPort = await Promise.make(
136
+ (resolve, _) => LocalLogFeed.start(~env=Dict.make(), ~onBound=resolve, ()),
137
+ )
138
+ let tap = await connect(tapPort)
139
+ let logFeed = await connect(logPort)
140
+ let filterLine = `{"plugins":["Ordering"],"level":"warn","eventTypes":["Placed"],"correlationId":"c-1"}\n`
141
+ let _ = tap.socket->NodeNet.write(filterLine)
142
+ let _ = logFeed.socket->NodeNet.write(filterLine)
143
+ expect(await until(() => LocalEventTap.hasReaders() && LocalLogFeed.hasReaders()))->toBe(true)
144
+
145
+ let event = (~plugin, ~eventType, ~correlationId, name) =>
146
+ LocalEventTap.publish({
147
+ line: `@@RVLESS_EVT@@ {"name":"${name}"}`,
148
+ facets: {plugin, eventType, correlationId},
149
+ })
150
+ event(~plugin="Ordering", ~eventType="Placed", ~correlationId="c-1", "match")
151
+ event(~plugin="Catalog", ~eventType="Placed", ~correlationId="c-1", "other plugin")
152
+ event(~plugin="Ordering", ~eventType="Shipped", ~correlationId="c-1", "other type")
153
+ event(~plugin="Ordering", ~eventType="Placed", ~correlationId="c-2", "other flow")
154
+
155
+ let annotations = Dict.fromArray([("correlationId", "c-1")])
156
+ Logger.emit(~level=Error, ~comp="Projection", ~plugin="Ordering", ~annotations, "live match")
157
+ Logger.emit(~level=Info, ~comp="Projection", ~plugin="Ordering", ~annotations, "too quiet")
158
+ Logger.emit(~level=Error, ~comp="Projection", ~plugin="Catalog", ~annotations, "other plugin")
159
+
160
+ expect(await until(() => logFeed.received->String.includes("live match")))->toBe(true)
161
+ expect(await until(() => tap.received->String.includes("match")))->toBe(true)
162
+ await sleep(30)
163
+
164
+ expect(tap->lines)->toEqual([`@@RVLESS_EVT@@ {"name":"match"}`])
165
+ let messages =
166
+ logFeed
167
+ ->lines
168
+ ->Array.filterMap(
169
+ l =>
170
+ l
171
+ ->JSON.parseOrThrow
172
+ ->JSON.Decode.object
173
+ ->Option.flatMap(d => d->Dict.get("message"))
174
+ ->Option.flatMap(JSON.Decode.string),
175
+ )
176
+ // The backlog line has no correlation id, so the filter's flow leaves it out.
177
+ expect(messages)->toEqual(["live match"])
178
+
179
+ NodeNet.destroySocket(tap.socket)
180
+ NodeNet.destroySocket(logFeed.socket)
181
+ await LocalEventTap.stopForTests()
182
+ await LocalLogFeed.stopForTests()
183
+ LocalLogFeed.resetForTests()
184
+ })
185
+
186
+ test("a listener that sends no filter gets everything, the recent records first", async () => {
187
+ LocalLogFeed.resetForTests()
188
+ LocalLogFeed.install(~env=Dict.make())
189
+ Logger.emit(~level=Debug, ~comp="Projection", "earlier")
190
+ let logPort = await Promise.make(
191
+ (resolve, _) => LocalLogFeed.start(~env=Dict.make(), ~onBound=resolve, ()),
192
+ )
193
+ let client = await connect(logPort)
194
+ expect(await until(() => LocalLogFeed.hasReaders()))->toBe(true)
195
+ Logger.emit(~level=Debug, ~comp="Projection", "later")
196
+ expect(await until(() => client.received->String.includes("later")))->toBe(true)
197
+ let messages =
198
+ client
199
+ ->lines
200
+ ->Array.filterMap(
201
+ l =>
202
+ l
203
+ ->JSON.parseOrThrow
204
+ ->JSON.Decode.object
205
+ ->Option.flatMap(d => d->Dict.get("message"))
206
+ ->Option.flatMap(JSON.Decode.string),
207
+ )
208
+ // Only this test's own lines: the platform's may be logged too.
209
+ expect(messages->Array.filter(m => m == "earlier" || m == "later"))->toEqual([
210
+ "earlier",
211
+ "later",
212
+ ])
213
+ NodeNet.destroySocket(client.socket)
214
+ await LocalLogFeed.stopForTests()
215
+ LocalLogFeed.resetForTests()
216
+ })
217
+ })
218
+
219
+ type consoleObj = {mutable log: string => unit}
220
+ @val external console: consoleObj = "console"
221
+
222
+ describe("LocalLogFeed: the rings", () => {
223
+ afterEach(() => {
224
+ LocalLogFeed.resetForTests()
225
+ NodeProcess.env->Dict.set("REVENTLESS_LOG_FORMAT", "text")
226
+ Reventless.AnsiStyle.reload()
227
+ })
228
+
229
+ testSync("a record in the ring equals the JSON line's fields", () => {
230
+ LocalLogFeed.install(~env=Dict.make())
231
+ NodeProcess.env->Dict.set("REVENTLESS_LOG_FORMAT", "json")
232
+ Reventless.AnsiStyle.reload()
233
+ let printed = []
234
+ let original = console.log
235
+ console.log = s => printed->Array.push(s)
236
+ Logger.emit(
237
+ ~level=Warn,
238
+ ~comp="Aggregate(Order)",
239
+ ~plugin="Ordering",
240
+ ~annotations=Dict.fromArray([("correlationId", "c-9")]),
241
+ ~detail=Dict.fromArray([("orderId", JSON.Encode.string("o-1"))])->JSON.Encode.object,
242
+ "refused",
243
+ )
244
+ console.log = original
245
+ // Only this test's own line: the platform's may be logged too.
246
+ let entry =
247
+ LocalLogFeed.snapshot()->Array.find(e => e.record.message == "refused")->Option.getOrThrow
248
+ let line = printed->Array.find(l => l->String.includes(`"refused"`))->Option.getOrThrow
249
+ expect(entry.item.line)->toEqual(line)
250
+ expect(entry.record->Logger.recordToJson)->toEqual(line->JSON.parseOrThrow)
251
+ })
252
+
253
+ testSync("detail is redacted in the ring", () => {
254
+ LocalLogFeed.install(~env=Dict.make())
255
+ Logger.emit(
256
+ ~level=Info,
257
+ ~comp="Projection",
258
+ ~detail=Dict.fromArray([
259
+ ("name", Dict.fromArray([("$pii", JSON.Encode.string("Ada Secret"))])->JSON.Encode.object),
260
+ ])->JSON.Encode.object,
261
+ "joined",
262
+ )
263
+ let line = (LocalLogFeed.snapshot()->Array.getUnsafe(0)).item.line
264
+ expect(line->String.includes("joined"))->toBe(true)
265
+ expect(line->String.includes("Ada Secret"))->toBe(false)
266
+ })
267
+
268
+ testSync("the count bound holds, and warnings outlive it in their own ring", () => {
269
+ LocalLogFeed.install(
270
+ ~env=Dict.fromArray([(LocalLogFeed.ringEnvVar, "3"), (LocalLogFeed.problemRingEnvVar, "2")]),
271
+ )
272
+ Logger.emit(~level=Warn, ~comp="C", "warn 1")
273
+ Logger.emit(~level=Error, ~comp="C", "error 2")
274
+ Logger.emit(~level=Warn, ~comp="C", "warn 3")
275
+ for i in 1 to 5 {
276
+ Logger.emit(~level=Info, ~comp="C", `info ${i->Int.toString}`)
277
+ }
278
+ expect(LocalLogFeed.recent->LocalLogFeed.toArray->Array.map(e => e.record.message))->toEqual([
279
+ "info 3",
280
+ "info 4",
281
+ "info 5",
282
+ ])
283
+ expect(LocalLogFeed.snapshot()->Array.map(e => e.record.message))->toEqual([
284
+ "error 2",
285
+ "warn 3",
286
+ "info 3",
287
+ "info 4",
288
+ "info 5",
289
+ ])
290
+ })
291
+
292
+ testSync("the byte bound holds", () => {
293
+ LocalLogFeed.install(~env=Dict.fromArray([(LocalLogFeed.ringEnvVar, "1000,600")]))
294
+ for i in 1 to 50 {
295
+ Logger.emit(~level=Info, ~comp="C", `line ${i->Int.toString}`)
296
+ }
297
+ let kept = LocalLogFeed.recent->LocalLogFeed.toArray
298
+ expect(LocalLogFeed.recent->LocalLogFeed.ringBytes <= 600)->toBe(true)
299
+ expect(kept->Array.length > 0 && kept->Array.length < 50)->toBe(true)
300
+ expect((kept->Array.at(-1)->Option.getOrThrow).record.message)->toEqual("line 50")
301
+ })
302
+
303
+ testSync("recentRecords filters like the feed and keeps the most recent", () => {
304
+ LocalLogFeed.install(~env=Dict.make())
305
+ Logger.emit(~level=Warn, ~comp="A", ~plugin="Ordering", "w1")
306
+ Logger.emit(~level=Info, ~comp="A", ~plugin="Ordering", "i1")
307
+ Logger.emit(~level=Error, ~comp="B", ~plugin="Ordering", "e1")
308
+ Logger.emit(~level=Error, ~comp="A", ~plugin="Catalog", "e2")
309
+ let messages = (~filter, ~limit=?) =>
310
+ LocalLogFeed.recentRecords(~filter, ~limit?)->Array.map(r => r.message)
311
+ expect(messages(~filter={plugins: ["Ordering"], level: Warn}))->toEqual(["w1", "e1"])
312
+ expect(messages(~filter={components: ["A"]}, ~limit=2))->toEqual(["i1", "e2"])
313
+ })
314
+ })
@@ -0,0 +1,329 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodenet from "node:net";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as AnsiStyle$Reventless from "@reventlessdev/reventless-spec/src/AnsiStyle.res.mjs";
9
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
10
+ import * as LocalFeed$ReventlessLocal from "../../src/adapter/LocalFeed.res.mjs";
11
+ import * as LocalLogFeed$ReventlessLocal from "../../src/adapter/LocalLogFeed.res.mjs";
12
+ import * as LocalEventTap$ReventlessLocal from "../../src/adapter/LocalEventTap.res.mjs";
13
+
14
+ function sleep(ms) {
15
+ return new Promise((resolve, param) => {
16
+ setTimeout(() => resolve(), ms);
17
+ });
18
+ }
19
+
20
+ async function until(msOpt, cond) {
21
+ let ms = msOpt !== undefined ? msOpt : 2000;
22
+ let waited = 0;
23
+ while (!cond() && waited < ms) {
24
+ await sleep(5);
25
+ waited = waited + 5 | 0;
26
+ };
27
+ return cond();
28
+ }
29
+
30
+ function bind(feed) {
31
+ return new Promise((resolve, param) => LocalFeed$ReventlessLocal.start(feed, "Ephemeral", resolve, undefined));
32
+ }
33
+
34
+ function connect(port) {
35
+ return new Promise((resolve, param) => {
36
+ let c = {
37
+ contents: undefined
38
+ };
39
+ let socket = Nodenet.createConnection(port, "127.0.0.1", () => Stdlib_Option.forEach(c.contents, client => resolve(client)));
40
+ socket.setEncoding("utf8");
41
+ socket.on("error", param => {});
42
+ let client = {
43
+ socket: socket,
44
+ received: ""
45
+ };
46
+ socket.on("data", chunk => {
47
+ client.received = client.received + chunk;
48
+ });
49
+ c.contents = client;
50
+ });
51
+ }
52
+
53
+ function lines(c) {
54
+ return c.received.split("\n").filter(l => l !== "");
55
+ }
56
+
57
+ function skippedCounts(c) {
58
+ return Stdlib_Array.filterMap(lines(c), l => {
59
+ let d;
60
+ try {
61
+ d = Stdlib_JSON.Decode.object(JSON.parse(l));
62
+ } catch (exn) {
63
+ return;
64
+ }
65
+ if (d !== undefined && Primitive_object.equal(d["kind"], "skipped")) {
66
+ return Stdlib_Option.map(Stdlib_Option.flatMap(d["count"], Stdlib_JSON.Decode.float), prim => prim | 0);
67
+ }
68
+ });
69
+ }
70
+
71
+ globalThis.describe("LocalFeed: a listener that never reads", () => {
72
+ globalThis.test("costs a bounded amount, and is told how many lines it missed when it reads again", async () => {
73
+ let feed = LocalFeed$ReventlessLocal.make("TestFeed", [], undefined, 200, 65536, 10, undefined);
74
+ let port = await bind(feed);
75
+ let client = await connect(port);
76
+ globalThis.expect(await until(undefined, () => LocalFeed$ReventlessLocal.hasReaders(feed))).toBe(true);
77
+ client.socket.pause();
78
+ let pad = "x".repeat(1000);
79
+ let held = 0;
80
+ for (let i = 1; i <= 20000; ++i) {
81
+ LocalFeed$ReventlessLocal.broadcast(feed, `{"n":` + i.toString() + `,"pad":"` + pad + `"}`);
82
+ held = Math.max(held, LocalFeed$ReventlessLocal.heldBytes(feed));
83
+ }
84
+ globalThis.expect(LocalFeed$ReventlessLocal.queuedBytes(feed) <= 65536).toBe(true);
85
+ globalThis.expect(held < 4259840).toBe(true);
86
+ client.socket.resume();
87
+ let last = `{"n":` + (20000).toString() + `,`;
88
+ globalThis.expect(await until(5000, () => client.received.includes(last))).toBe(true);
89
+ let skipped = Stdlib_Array.reduce(skippedCounts(client), 0, (a, b) => a + b | 0);
90
+ globalThis.expect(skipped > 0).toBe(true);
91
+ let received = lines(client).filter(l => l.startsWith(`{"n":`)).length;
92
+ globalThis.expect(received + skipped | 0).toBe(20000);
93
+ client.socket.destroy();
94
+ return await LocalFeed$ReventlessLocal.stopForTests(feed);
95
+ });
96
+ });
97
+
98
+ globalThis.describe("LocalFeed: the filter line", () => {
99
+ globalThis.test("is read leniently and ignores what it does not know", () => {
100
+ globalThis.expect(LocalFeed$ReventlessLocal.parseFilter(`{"plugins":["Ordering"],"level":"WARN","eventTypes":["Placed"],"x":1}`)).toEqual({
101
+ plugins: ["Ordering"],
102
+ level: "Warn",
103
+ eventTypes: ["Placed"]
104
+ });
105
+ globalThis.expect(LocalFeed$ReventlessLocal.parseFilter("not json")).toEqual(undefined);
106
+ });
107
+ globalThis.test("narrows only by the fields the feed honours", () => {
108
+ let filter_plugins = ["Ordering"];
109
+ let filter_level = "Warn";
110
+ let filter = {
111
+ plugins: filter_plugins,
112
+ level: filter_level
113
+ };
114
+ let logs = [
115
+ "Plugins",
116
+ "Level"
117
+ ];
118
+ globalThis.expect(LocalFeed$ReventlessLocal.matches(logs, filter, {
119
+ plugin: "Ordering",
120
+ level: "Warn"
121
+ })).toBe(true);
122
+ globalThis.expect(LocalFeed$ReventlessLocal.matches(logs, filter, {
123
+ plugin: "Ordering",
124
+ level: "Info"
125
+ })).toBe(false);
126
+ globalThis.expect(LocalFeed$ReventlessLocal.matches(logs, filter, {
127
+ plugin: "Catalog",
128
+ level: "Error"
129
+ })).toBe(false);
130
+ globalThis.expect(LocalFeed$ReventlessLocal.matches(["Plugins"], filter, {
131
+ plugin: "Ordering"
132
+ })).toBe(true);
133
+ });
134
+ globalThis.test("narrows the event tap and the log feed alike", async () => {
135
+ LocalEventTap$ReventlessLocal.resetForTests();
136
+ LocalLogFeed$ReventlessLocal.resetForTests();
137
+ LocalLogFeed$ReventlessLocal.install({});
138
+ Logger$ReventlessCore.emit("Warn", "Projection", "Ordering", undefined, undefined, "before connect");
139
+ Logger$ReventlessCore.emit("Info", "Projection", "Ordering", undefined, undefined, "before connect, quiet");
140
+ let tapPort = await new Promise((resolve, param) => LocalEventTap$ReventlessLocal.start({}, resolve, undefined));
141
+ let logPort = await new Promise((resolve, param) => LocalLogFeed$ReventlessLocal.start({}, resolve, undefined));
142
+ let tap = await connect(tapPort);
143
+ let logFeed = await connect(logPort);
144
+ let filterLine = `{"plugins":["Ordering"],"level":"warn","eventTypes":["Placed"],"correlationId":"c-1"}\n`;
145
+ tap.socket.write(filterLine);
146
+ logFeed.socket.write(filterLine);
147
+ globalThis.expect(await until(undefined, () => {
148
+ if (LocalEventTap$ReventlessLocal.hasReaders()) {
149
+ return LocalLogFeed$ReventlessLocal.hasReaders();
150
+ } else {
151
+ return false;
152
+ }
153
+ })).toBe(true);
154
+ let event = (plugin, eventType, correlationId, name) => LocalEventTap$ReventlessLocal.publish({
155
+ line: `@@RVLESS_EVT@@ {"name":"` + name + `"}`,
156
+ facets: {
157
+ plugin: plugin,
158
+ eventType: eventType,
159
+ correlationId: correlationId
160
+ }
161
+ });
162
+ event("Ordering", "Placed", "c-1", "match");
163
+ event("Catalog", "Placed", "c-1", "other plugin");
164
+ event("Ordering", "Shipped", "c-1", "other type");
165
+ event("Ordering", "Placed", "c-2", "other flow");
166
+ let annotations = Object.fromEntries([[
167
+ "correlationId",
168
+ "c-1"
169
+ ]]);
170
+ Logger$ReventlessCore.emit("Error", "Projection", "Ordering", undefined, annotations, "live match");
171
+ Logger$ReventlessCore.emit("Info", "Projection", "Ordering", undefined, annotations, "too quiet");
172
+ Logger$ReventlessCore.emit("Error", "Projection", "Catalog", undefined, annotations, "other plugin");
173
+ globalThis.expect(await until(undefined, () => logFeed.received.includes("live match"))).toBe(true);
174
+ globalThis.expect(await until(undefined, () => tap.received.includes("match"))).toBe(true);
175
+ await sleep(30);
176
+ globalThis.expect(lines(tap)).toEqual([`@@RVLESS_EVT@@ {"name":"match"}`]);
177
+ let messages = Stdlib_Array.filterMap(lines(logFeed), l => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(l)), d => d["message"]), Stdlib_JSON.Decode.string));
178
+ globalThis.expect(messages).toEqual(["live match"]);
179
+ tap.socket.destroy();
180
+ logFeed.socket.destroy();
181
+ await LocalEventTap$ReventlessLocal.stopForTests();
182
+ await LocalLogFeed$ReventlessLocal.stopForTests();
183
+ return LocalLogFeed$ReventlessLocal.resetForTests();
184
+ });
185
+ globalThis.test("a listener that sends no filter gets everything, the recent records first", async () => {
186
+ LocalLogFeed$ReventlessLocal.resetForTests();
187
+ LocalLogFeed$ReventlessLocal.install({});
188
+ Logger$ReventlessCore.emit("Debug", "Projection", undefined, undefined, undefined, "earlier");
189
+ let logPort = await new Promise((resolve, param) => LocalLogFeed$ReventlessLocal.start({}, resolve, undefined));
190
+ let client = await connect(logPort);
191
+ globalThis.expect(await until(undefined, () => LocalLogFeed$ReventlessLocal.hasReaders())).toBe(true);
192
+ Logger$ReventlessCore.emit("Debug", "Projection", undefined, undefined, undefined, "later");
193
+ globalThis.expect(await until(undefined, () => client.received.includes("later"))).toBe(true);
194
+ let messages = Stdlib_Array.filterMap(lines(client), l => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(l)), d => d["message"]), Stdlib_JSON.Decode.string));
195
+ globalThis.expect(messages.filter(m => {
196
+ if (m === "earlier") {
197
+ return true;
198
+ } else {
199
+ return m === "later";
200
+ }
201
+ })).toEqual([
202
+ "earlier",
203
+ "later"
204
+ ]);
205
+ client.socket.destroy();
206
+ await LocalLogFeed$ReventlessLocal.stopForTests();
207
+ return LocalLogFeed$ReventlessLocal.resetForTests();
208
+ });
209
+ });
210
+
211
+ globalThis.describe("LocalLogFeed: the rings", () => {
212
+ globalThis.afterEach(() => {
213
+ LocalLogFeed$ReventlessLocal.resetForTests();
214
+ process.env["REVENTLESS_LOG_FORMAT"] = "text";
215
+ AnsiStyle$Reventless.reload();
216
+ });
217
+ globalThis.test("a record in the ring equals the JSON line's fields", () => {
218
+ LocalLogFeed$ReventlessLocal.install({});
219
+ process.env["REVENTLESS_LOG_FORMAT"] = "json";
220
+ AnsiStyle$Reventless.reload();
221
+ let printed = [];
222
+ let original = console.log;
223
+ console.log = s => {
224
+ printed.push(s);
225
+ };
226
+ Logger$ReventlessCore.emit("Warn", "Aggregate(Order)", "Ordering", Object.fromEntries([[
227
+ "orderId",
228
+ "o-1"
229
+ ]]), Object.fromEntries([[
230
+ "correlationId",
231
+ "c-9"
232
+ ]]), "refused");
233
+ console.log = original;
234
+ let entry = Stdlib_Option.getOrThrow(LocalLogFeed$ReventlessLocal.snapshot().find(e => e.record.message === "refused"), undefined);
235
+ let line = Stdlib_Option.getOrThrow(printed.find(l => l.includes(`"refused"`)), undefined);
236
+ globalThis.expect(entry.item.line).toEqual(line);
237
+ globalThis.expect(Logger$ReventlessCore.recordToJson(entry.record)).toEqual(JSON.parse(line));
238
+ });
239
+ globalThis.test("detail is redacted in the ring", () => {
240
+ LocalLogFeed$ReventlessLocal.install({});
241
+ Logger$ReventlessCore.emit("Info", "Projection", undefined, Object.fromEntries([[
242
+ "name",
243
+ Object.fromEntries([[
244
+ "$pii",
245
+ "Ada Secret"
246
+ ]])
247
+ ]]), undefined, "joined");
248
+ let line = LocalLogFeed$ReventlessLocal.snapshot()[0].item.line;
249
+ globalThis.expect(line.includes("joined")).toBe(true);
250
+ globalThis.expect(line.includes("Ada Secret")).toBe(false);
251
+ });
252
+ globalThis.test("the count bound holds, and warnings outlive it in their own ring", () => {
253
+ LocalLogFeed$ReventlessLocal.install(Object.fromEntries([
254
+ [
255
+ LocalLogFeed$ReventlessLocal.ringEnvVar,
256
+ "3"
257
+ ],
258
+ [
259
+ LocalLogFeed$ReventlessLocal.problemRingEnvVar,
260
+ "2"
261
+ ]
262
+ ]));
263
+ Logger$ReventlessCore.emit("Warn", "C", undefined, undefined, undefined, "warn 1");
264
+ Logger$ReventlessCore.emit("Error", "C", undefined, undefined, undefined, "error 2");
265
+ Logger$ReventlessCore.emit("Warn", "C", undefined, undefined, undefined, "warn 3");
266
+ for (let i = 1; i <= 5; ++i) {
267
+ Logger$ReventlessCore.emit("Info", "C", undefined, undefined, undefined, `info ` + i.toString());
268
+ }
269
+ globalThis.expect(LocalLogFeed$ReventlessLocal.toArray(LocalLogFeed$ReventlessLocal.recent).map(e => e.record.message)).toEqual([
270
+ "info 3",
271
+ "info 4",
272
+ "info 5"
273
+ ]);
274
+ globalThis.expect(LocalLogFeed$ReventlessLocal.snapshot().map(e => e.record.message)).toEqual([
275
+ "error 2",
276
+ "warn 3",
277
+ "info 3",
278
+ "info 4",
279
+ "info 5"
280
+ ]);
281
+ });
282
+ globalThis.test("the byte bound holds", () => {
283
+ LocalLogFeed$ReventlessLocal.install(Object.fromEntries([[
284
+ LocalLogFeed$ReventlessLocal.ringEnvVar,
285
+ "1000,600"
286
+ ]]));
287
+ for (let i = 1; i <= 50; ++i) {
288
+ Logger$ReventlessCore.emit("Info", "C", undefined, undefined, undefined, `line ` + i.toString());
289
+ }
290
+ let kept = LocalLogFeed$ReventlessLocal.toArray(LocalLogFeed$ReventlessLocal.recent);
291
+ globalThis.expect(LocalLogFeed$ReventlessLocal.ringBytes(LocalLogFeed$ReventlessLocal.recent) <= 600).toBe(true);
292
+ globalThis.expect(kept.length !== 0 && kept.length < 50).toBe(true);
293
+ globalThis.expect(Stdlib_Option.getOrThrow(kept.at(-1), undefined).record.message).toEqual("line 50");
294
+ });
295
+ globalThis.test("recentRecords filters like the feed and keeps the most recent", () => {
296
+ LocalLogFeed$ReventlessLocal.install({});
297
+ Logger$ReventlessCore.emit("Warn", "A", "Ordering", undefined, undefined, "w1");
298
+ Logger$ReventlessCore.emit("Info", "A", "Ordering", undefined, undefined, "i1");
299
+ Logger$ReventlessCore.emit("Error", "B", "Ordering", undefined, undefined, "e1");
300
+ Logger$ReventlessCore.emit("Error", "A", "Catalog", undefined, undefined, "e2");
301
+ let messages = (filter, limit) => LocalLogFeed$ReventlessLocal.recentRecords(filter, limit).map(r => r.message);
302
+ globalThis.expect(messages({
303
+ plugins: ["Ordering"],
304
+ level: "Warn"
305
+ }, undefined)).toEqual([
306
+ "w1",
307
+ "e1"
308
+ ]);
309
+ globalThis.expect(messages({
310
+ components: ["A"]
311
+ }, 2)).toEqual([
312
+ "i1",
313
+ "e2"
314
+ ]);
315
+ });
316
+ });
317
+
318
+ let Logger;
319
+
320
+ export {
321
+ Logger,
322
+ sleep,
323
+ until,
324
+ bind,
325
+ connect,
326
+ lines,
327
+ skippedCounts,
328
+ }
329
+ /* Not a pure module */