@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,507 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Pervasives from "@rescript/runtime/lib/es6/Pervasives.js";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
+ import * as Stdlib_Float from "@rescript/runtime/lib/es6/Stdlib_Float.js";
8
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
10
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
11
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
12
+
13
+ function metaOfFlat(flat) {
14
+ return Object.fromEntries(Stdlib_Array.filterMap(Message$ReventlessCore.metaKeys, k => Stdlib_Option.map(flat[k], v => [
15
+ k,
16
+ v
17
+ ])));
18
+ }
19
+
20
+ function storedOfFlat(log, position, entityId, payload) {
21
+ let flat = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(payload), {});
22
+ let str = k => Stdlib_Option.flatMap(flat[k], Stdlib_JSON.Decode.string);
23
+ let meta = metaOfFlat(flat);
24
+ let t = str("recordedAt");
25
+ return {
26
+ log: log,
27
+ position: position,
28
+ eventType: Stdlib_Option.getOr(str("event"), "unknown"),
29
+ entityId: entityId,
30
+ tags: [],
31
+ payload: Stdlib_Option.getOr(flat["data"], null),
32
+ meta: meta,
33
+ recordedAt: t !== undefined ? t : Stdlib_Option.getOr(str("time"), "")
34
+ };
35
+ }
36
+
37
+ function numberOf(v) {
38
+ if (v === undefined) {
39
+ return 0;
40
+ }
41
+ switch (typeof v) {
42
+ case "string" :
43
+ return Stdlib_Option.getOr(Stdlib_Float.fromString(v), 0);
44
+ case "number" :
45
+ return v;
46
+ default:
47
+ return 0;
48
+ }
49
+ }
50
+
51
+ let readers = {};
52
+
53
+ let pluginOfLog = {};
54
+
55
+ function register(log, kind, readBack) {
56
+ readers[log] = {
57
+ kind: kind,
58
+ readBack: readBack
59
+ };
60
+ }
61
+
62
+ function notePlugin(log, plugin) {
63
+ pluginOfLog[log] = plugin;
64
+ }
65
+
66
+ function pluginOf(log) {
67
+ return pluginOfLog[log];
68
+ }
69
+
70
+ function logs() {
71
+ return Object.keys(readers).toSorted(Primitive_string.compare);
72
+ }
73
+
74
+ function positionToString(p) {
75
+ return p.toString();
76
+ }
77
+
78
+ let notes = new Map();
79
+
80
+ function note(log, msgId, position) {
81
+ notes.set(msgId, [
82
+ log,
83
+ position
84
+ ]);
85
+ if (notes.size <= 10000) {
86
+ return;
87
+ }
88
+ let match = notes.keys().next();
89
+ if (match.done) {
90
+ return;
91
+ }
92
+ let oldest = match.value;
93
+ if (oldest !== undefined) {
94
+ notes.delete(oldest);
95
+ return;
96
+ }
97
+ }
98
+
99
+ function take(msgId) {
100
+ let found = notes.get(msgId);
101
+ if (found !== undefined) {
102
+ notes.delete(msgId);
103
+ return found;
104
+ }
105
+ }
106
+
107
+ function resetForTests() {
108
+ Object.keys(readers).forEach(k => Stdlib_Dict.$$delete(readers, k));
109
+ Object.keys(pluginOfLog).forEach(k => Stdlib_Dict.$$delete(pluginOfLog, k));
110
+ notes.clear();
111
+ }
112
+
113
+ function recordToJson(r) {
114
+ return Object.fromEntries([
115
+ [
116
+ "log",
117
+ r.log
118
+ ],
119
+ [
120
+ "position",
121
+ r.position
122
+ ],
123
+ [
124
+ "eventType",
125
+ r.eventType
126
+ ],
127
+ [
128
+ "tags",
129
+ r.tags.map(t => Object.fromEntries([
130
+ [
131
+ "key",
132
+ t.key
133
+ ],
134
+ [
135
+ "value",
136
+ t.value
137
+ ]
138
+ ]))
139
+ ],
140
+ [
141
+ "payload",
142
+ r.payload
143
+ ],
144
+ [
145
+ "meta",
146
+ r.meta
147
+ ],
148
+ [
149
+ "recordedAt",
150
+ r.recordedAt
151
+ ]
152
+ ].concat(Stdlib_Option.mapOr(r.plugin, [], p => [[
153
+ "plugin",
154
+ p
155
+ ]])).concat(Stdlib_Option.mapOr(r.entityId, [], e => [[
156
+ "entityId",
157
+ e
158
+ ]])));
159
+ }
160
+
161
+ function filterOfJson(json) {
162
+ let d = Stdlib_Option.getOr(Stdlib_Option.flatMap(json, Stdlib_JSON.Decode.object), {});
163
+ let s = k => Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
164
+ let a = k => Stdlib_Option.map(Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.array), items => Stdlib_Array.filterMap(items, Stdlib_JSON.Decode.string));
165
+ return {
166
+ plugins: a("plugins"),
167
+ logs: a("logs"),
168
+ eventTypes: a("eventTypes"),
169
+ entityId: s("entityId"),
170
+ tagKey: s("tagKey"),
171
+ tagValue: s("tagValue"),
172
+ msgId: s("msgId"),
173
+ correlationId: s("correlationId"),
174
+ causationId: s("causationId"),
175
+ user: s("user"),
176
+ timeFrom: s("timeFrom"),
177
+ timeTo: s("timeTo")
178
+ };
179
+ }
180
+
181
+ function metaString(meta, key) {
182
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(meta), d => d[key]), Stdlib_JSON.Decode.string);
183
+ }
184
+
185
+ function selectsLog(f, log, plugin) {
186
+ if (Stdlib_Option.mapOr(f.logs, true, logs => logs.includes(log))) {
187
+ return Stdlib_Option.mapOr(f.plugins, true, plugins => Stdlib_Option.mapOr(plugin, false, p => plugins.includes(p)));
188
+ } else {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ function matches(f, r) {
194
+ let equalsMeta = (wanted, key) => Stdlib_Option.mapOr(wanted, true, w => Primitive_object.equal(metaString(r.meta, key), w));
195
+ let match = f.tagKey;
196
+ let match$1 = f.tagValue;
197
+ let tagMatch = match !== undefined ? (
198
+ match$1 !== undefined ? r.tags.some(t => {
199
+ if (t.key === match) {
200
+ return t.value === match$1;
201
+ } else {
202
+ return false;
203
+ }
204
+ }) : r.tags.some(t => t.key === match)
205
+ ) : (
206
+ match$1 !== undefined ? r.tags.some(t => t.value === match$1) : true
207
+ );
208
+ let id = f.entityId;
209
+ let entityMatch = id !== undefined ? Primitive_object.equal(r.entityId, id) || r.tags.some(t => t.value === id) : true;
210
+ if (tagMatch && entityMatch && Stdlib_Option.mapOr(f.eventTypes, true, types => types.includes(r.eventType)) && equalsMeta(f.msgId, "msgId") && equalsMeta(f.correlationId, "correlationId") && equalsMeta(f.causationId, "causationId") && equalsMeta(f.user, "user") && Stdlib_Option.mapOr(f.timeFrom, true, from => r.recordedAt >= from)) {
211
+ return Stdlib_Option.mapOr(f.timeTo, true, to_ => r.recordedAt <= to_);
212
+ } else {
213
+ return false;
214
+ }
215
+ }
216
+
217
+ function toBase64Url(s) {
218
+ return btoa(s).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
219
+ }
220
+
221
+ function fromBase64Url(s) {
222
+ let b = s.replaceAll("-", "+").replaceAll("_", "/");
223
+ return atob(b + "=".repeat((4 - b.length % 4 | 0) % 4));
224
+ }
225
+
226
+ function encodeCursor(states) {
227
+ return toBase64Url(JSON.stringify(Object.fromEntries(states.map(param => {
228
+ let state = param[1];
229
+ let tmp;
230
+ if (typeof state !== "object") {
231
+ tmp = "done";
232
+ } else {
233
+ let p = state._0;
234
+ tmp = p.TAG === "Below" ? Object.fromEntries([[
235
+ "below",
236
+ p._0
237
+ ]]) : Object.fromEntries([[
238
+ "atMost",
239
+ p._0
240
+ ]]);
241
+ }
242
+ return [
243
+ param[0],
244
+ tmp
245
+ ];
246
+ }))));
247
+ }
248
+
249
+ function decodeCursor(cursor) {
250
+ let d;
251
+ try {
252
+ d = Stdlib_JSON.Decode.object(JSON.parse(fromBase64Url(cursor)));
253
+ } catch (exn) {
254
+ return;
255
+ }
256
+ if (d === undefined) {
257
+ return;
258
+ }
259
+ let out = {};
260
+ Object.entries(d).forEach(param => {
261
+ let v = param[1];
262
+ let log = param[0];
263
+ if (v === null || Array.isArray(v)) {
264
+ return;
265
+ }
266
+ switch (typeof v) {
267
+ case "string" :
268
+ if (v !== "done") {
269
+ return;
270
+ }
271
+ out[log] = "Done";
272
+ return;
273
+ case "object" :
274
+ let match = Stdlib_Option.flatMap(v["below"], Stdlib_JSON.Decode.float);
275
+ let match$1 = Stdlib_Option.flatMap(v["atMost"], Stdlib_JSON.Decode.float);
276
+ if (match !== undefined) {
277
+ out[log] = {
278
+ TAG: "Until",
279
+ _0: {
280
+ TAG: "Below",
281
+ _0: match
282
+ }
283
+ };
284
+ return;
285
+ } else if (match$1 !== undefined) {
286
+ out[log] = {
287
+ TAG: "Until",
288
+ _0: {
289
+ TAG: "AtMost",
290
+ _0: match$1
291
+ }
292
+ };
293
+ return;
294
+ } else {
295
+ return;
296
+ }
297
+ default:
298
+ return;
299
+ }
300
+ });
301
+ return out;
302
+ }
303
+
304
+ function toRecord(s, r) {
305
+ return {
306
+ plugin: s.plugin,
307
+ log: r.log,
308
+ position: r.position.toString(),
309
+ eventType: r.eventType,
310
+ entityId: r.entityId,
311
+ tags: r.tags,
312
+ payload: r.payload,
313
+ meta: r.meta,
314
+ recordedAt: r.recordedAt
315
+ };
316
+ }
317
+
318
+ function newerFirst(param, param$1) {
319
+ let b = param$1[1];
320
+ let sb = param$1[0];
321
+ let a = param[1];
322
+ let sa = param[0];
323
+ if (a.recordedAt !== b.recordedAt) {
324
+ if (a.recordedAt > b.recordedAt) {
325
+ return -1;
326
+ } else {
327
+ return 1;
328
+ }
329
+ }
330
+ let pa = Stdlib_Option.getOr(sa.plugin, "");
331
+ let pb = Stdlib_Option.getOr(sb.plugin, "");
332
+ if (pa !== pb) {
333
+ return Primitive_string.compare(pa, pb);
334
+ } else if (sa.log !== sb.log) {
335
+ return Primitive_string.compare(sa.log, sb.log);
336
+ } else {
337
+ return b.position - a.position;
338
+ }
339
+ }
340
+
341
+ async function read(filter, first, after) {
342
+ let pageSize = Math.min(Math.max(Stdlib_Option.getOr(first, 50), 0), 1000);
343
+ let incoming = Stdlib_Option.flatMap(after, decodeCursor);
344
+ let sources = Stdlib_Array.filterMap(logs(), log => {
345
+ let plugin = pluginOfLog[log];
346
+ let match = readers[log];
347
+ let match$1 = selectsLog(filter, log, plugin);
348
+ if (match === undefined) {
349
+ return;
350
+ }
351
+ if (!match$1) {
352
+ return;
353
+ }
354
+ let start = incoming !== undefined ? Stdlib_Option.getOr(incoming[log], "Done") : ({
355
+ TAG: "Until",
356
+ _0: {
357
+ TAG: "AtMost",
358
+ _0: Pervasives.infinity
359
+ }
360
+ });
361
+ if (typeof start !== "object") {
362
+ return;
363
+ } else {
364
+ return {
365
+ log: log,
366
+ plugin: plugin,
367
+ reader: match,
368
+ scan: Stdlib_Option.isSome(incoming) ? start._0 : undefined,
369
+ fetched: false,
370
+ exhausted: false,
371
+ buffer: [],
372
+ state: Stdlib_Option.isSome(incoming) ? start : "Done"
373
+ };
374
+ }
375
+ });
376
+ let batch = Math.max(pageSize + 1 | 0, 50);
377
+ let fetch = async s => {
378
+ let rows = await s.reader.readBack(s.scan, batch, filter.correlationId);
379
+ if (!s.fetched) {
380
+ s.fetched = true;
381
+ if (Stdlib_Option.isNone(incoming)) {
382
+ let newest = rows[0];
383
+ s.state = newest !== undefined ? ({
384
+ TAG: "Until",
385
+ _0: {
386
+ TAG: "AtMost",
387
+ _0: newest.position
388
+ }
389
+ }) : "Done";
390
+ }
391
+ }
392
+ let oldest = rows.at(-1);
393
+ if (oldest !== undefined) {
394
+ s.scan = {
395
+ TAG: "Below",
396
+ _0: oldest.position
397
+ };
398
+ }
399
+ if (rows.length < batch) {
400
+ s.exhausted = true;
401
+ }
402
+ s.buffer = s.buffer.concat(rows.filter(r => matches(filter, r)));
403
+ };
404
+ let headOf = async s => {
405
+ let head = s.buffer[0];
406
+ if (head !== undefined) {
407
+ return head;
408
+ } else if (s.exhausted) {
409
+ return;
410
+ } else {
411
+ await fetch(s);
412
+ return await headOf(s);
413
+ }
414
+ };
415
+ let newest = async () => {
416
+ let heads = [];
417
+ for (let i = 0, i_finish = sources.length; i < i_finish; ++i) {
418
+ let s = sources[i];
419
+ let r = await headOf(s);
420
+ if (r !== undefined) {
421
+ heads.push([
422
+ s,
423
+ r
424
+ ]);
425
+ }
426
+ }
427
+ return heads.toSorted(newerFirst)[0];
428
+ };
429
+ let stateNow = () => sources.map(s => [
430
+ s.log,
431
+ s.state
432
+ ]);
433
+ let edges = [];
434
+ let stop = false;
435
+ while (!stop && edges.length < pageSize) {
436
+ let match = await newest();
437
+ if (match !== undefined) {
438
+ let r = match[1];
439
+ let s = match[0];
440
+ s.buffer = s.buffer.slice(1, s.buffer.length);
441
+ s.state = s.exhausted && s.buffer.length === 0 ? "Done" : ({
442
+ TAG: "Until",
443
+ _0: {
444
+ TAG: "Below",
445
+ _0: r.position
446
+ }
447
+ });
448
+ edges.push([
449
+ toRecord(s, r),
450
+ encodeCursor(stateNow())
451
+ ]);
452
+ } else {
453
+ stop = true;
454
+ }
455
+ };
456
+ let hasNextPage = Stdlib_Option.isSome(await newest());
457
+ sources.forEach(s => {
458
+ if (s.exhausted && s.buffer.length === 0) {
459
+ s.state = "Done";
460
+ return;
461
+ }
462
+ });
463
+ return {
464
+ edges: edges,
465
+ hasNextPage: hasNextPage,
466
+ endCursor: edges.length !== 0 || Stdlib_Option.isSome(incoming) ? encodeCursor(stateNow()) : undefined
467
+ };
468
+ }
469
+
470
+ let maxNotes = 10000;
471
+
472
+ let defaultPageSize = 50;
473
+
474
+ let maxPageSize = 1000;
475
+
476
+ export {
477
+ metaOfFlat,
478
+ storedOfFlat,
479
+ numberOf,
480
+ readers,
481
+ pluginOfLog,
482
+ register,
483
+ notePlugin,
484
+ pluginOf,
485
+ logs,
486
+ positionToString,
487
+ maxNotes,
488
+ notes,
489
+ note,
490
+ take,
491
+ resetForTests,
492
+ recordToJson,
493
+ filterOfJson,
494
+ metaString,
495
+ selectsLog,
496
+ matches,
497
+ toBase64Url,
498
+ fromBase64Url,
499
+ encodeCursor,
500
+ decodeCursor,
501
+ defaultPageSize,
502
+ maxPageSize,
503
+ toRecord,
504
+ newerFirst,
505
+ read,
506
+ }
507
+ /* notes Not a pure module */
@@ -7,12 +7,13 @@
7
7
  // what let it stop being configured. The noisy half (a line per event on stdout)
8
8
  // stays opt-in. `REVENTLESS_EVENT_TAP=off` disables the socket too.
9
9
  // See docs/plans/done/one-local-platform-one-store.md.
10
-
11
- let log = ReventlessCore.Logger.fromEnv()
10
+ //
11
+ // The socket is a `LocalFeed`: each reader has a bounded queue and may send a
12
+ // filter line on connect. See docs/plans/the-local-platform-serves-what-is-watched.md.
12
13
 
13
14
  let envVar = "REVENTLESS_EVENT_TAP"
14
15
 
15
- type setting =
16
+ type setting = LocalFeed.setting =
16
17
  | Off
17
18
  | Ephemeral
18
19
  | Fixed(int)
@@ -25,16 +26,7 @@ let envValue = (~env: dict<string>): option<string> =>
25
26
  A port only when the value IS one in the unprivileged range: the runner has
26
27
  passed `ndjson` since the tap existed, and someone writing `=1` means "on", not
27
28
  port 1. Everything else — unset included — takes the ephemeral default. */
28
- let settingFromEnv = (~env=NodeProcess.env): setting =>
29
- switch envValue(~env) {
30
- | Some("off") | Some("false") | Some("none") => Off
31
- | Some(v) =>
32
- switch Int.fromString(v) {
33
- | Some(p) if p >= 1024 && p <= 65535 => Fixed(p)
34
- | _ => Ephemeral
35
- }
36
- | None => Ephemeral
37
- }
29
+ let settingFromEnv = (~env=NodeProcess.env): setting => LocalFeed.settingOf(envValue(~env))
38
30
 
39
31
  /** Whether to ALSO write each line to stdout. Opt-in and unchanged: this is the
40
32
  half with a visible cost, and it exists for the child whose stdout the runner
@@ -45,33 +37,26 @@ let stdoutEnabled = (~env=NodeProcess.env): bool =>
45
37
  | Some(_) => true
46
38
  }
47
39
 
48
- let connections: ref<array<NodeNet.socket>> = ref([])
49
- let server: ref<option<NodeNet.server>> = ref(None)
50
- let listeningPort: ref<option<int>> = ref(None)
40
+ /** The feed the socket serves. A filter line narrows it by plugin, event type and
41
+ correlation id; the other filter fields are about log lines and are ignored
42
+ here. */
43
+ let feed = LocalFeed.make(~name="EventTap", ~honours=[Plugins, EventTypes, CorrelationId], ())
51
44
 
52
45
  /** The port the socket is actually bound to, known only once `listen` calls back —
53
46
  which is why {!start} takes `~onBound` rather than the caller reading this
54
47
  straight after. `None` until then, and if the bind fails. */
55
- let port = () => listeningPort.contents
48
+ let port = () => feed->LocalFeed.port
56
49
 
57
50
  /** Whether anyone is reading, so the hot path can skip building a line nobody
58
51
  wants. */
59
- let hasReaders = () => connections.contents->Array.length > 0
52
+ let hasReaders = () => feed->LocalFeed.hasReaders
60
53
 
61
- let drop = (socket: NodeNet.socket) =>
62
- connections := connections.contents->Array.filter(s => s !== socket)
54
+ /** Sends a line with what a filter can ask about it. */
55
+ let publish = (item: LocalFeed.item): unit => feed->LocalFeed.publish(item)
63
56
 
64
57
  /** Guarded per socket: a write to a closed peer throws, and one dead reader must
65
58
  not cost the others their events or the platform its run. */
66
- let broadcast = (line: string): unit =>
67
- if hasReaders() {
68
- connections.contents->Array.forEach(socket =>
69
- switch socket->NodeNet.write(line ++ "\n") {
70
- | _ => ()
71
- | exception _ => drop(socket)
72
- }
73
- )
74
- }
59
+ let broadcast = (line: string): unit => feed->LocalFeed.broadcast(line)
75
60
 
76
61
  /** Binds the tap socket and reports the port it got.
77
62
 
@@ -80,60 +65,13 @@ let broadcast = (line: string): unit =>
80
65
  failure mode: a platform whose diagnostic socket cannot bind must still serve,
81
66
  and simply carries no tap port. */
82
67
  let start = (~env=NodeProcess.env, ~onBound: int => unit=_ => (), ()): unit =>
83
- switch (settingFromEnv(~env), server.contents) {
84
- | (Off, _) | (_, Some(_)) => ()
85
- | (setting, None) =>
86
- let requested = switch setting {
87
- | Fixed(p) => p
88
- // 0 lets the OS pick, so parallel platforms in one directory never collide and
89
- // no unrelated service can be advertised as ours.
90
- | Off | Ephemeral => 0
91
- }
92
- let s = NodeNet.createServer(socket => {
93
- socket->NodeNet.setEncoding("utf8")
94
- socket->NodeNet.onSocketError(_ => drop(socket))
95
- socket->NodeNet.onSocketClose(() => drop(socket))
96
- connections := connections.contents->Array.concat([socket])
97
- })
98
- s->NodeNet.onServerError(err => {
99
- log.warn(
100
- ~comp="EventTap",
101
- `could not listen: ${err->JsExn.message->Option.getOr("unknown error")}`,
102
- )
103
- server := None
104
- listeningPort := None
105
- })
106
- // A diagnostic listener must never be why the process stays alive.
107
- s->NodeNet.unref
108
- s->NodeNet.listen(requested, "127.0.0.1", () =>
109
- switch s->NodeNet.address->Nullable.toOption {
110
- | Some(addr) =>
111
- let bound = addr["port"]
112
- listeningPort := Some(bound)
113
- log.info(~comp="EventTap", `listening on 127.0.0.1:${bound->Int.toString}`)
114
- onBound(bound)
115
- | None => ()
116
- }
117
- )
118
- server := Some(s)
119
- }
68
+ feed->LocalFeed.start(~setting=settingFromEnv(~env), ~onBound, ())
120
69
 
121
70
  /** Test seam: forgets connections and server without closing anything. */
122
- let resetForTests = () => {
123
- connections := []
124
- server := None
125
- listeningPort := None
126
- }
71
+ let resetForTests = () => feed->LocalFeed.resetForTests
127
72
 
128
73
  let addConnectionForTests = (socket: NodeNet.socket) =>
129
- connections := connections.contents->Array.concat([socket])
74
+ feed->LocalFeed.addConnectionForTests(socket)
130
75
 
131
76
  /** Test seam: actually releases the port, so a suite can bind it again. */
132
- let stopForTests = (): promise<unit> =>
133
- switch server.contents {
134
- | None => Promise.resolve()
135
- | Some(s) =>
136
- connections.contents->Array.forEach(NodeNet.destroySocket)
137
- resetForTests()
138
- Promise.make((resolve, _reject) => s->NodeNet.closeServer(() => resolve()))
139
- }
77
+ let stopForTests = (): promise<unit> => feed->LocalFeed.stopForTests