@reventlessdev/reventless-local 3.0.0-alpha.318 → 3.0.0-alpha.320

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 (59) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +11 -11
  3. package/src/LocalPlatformRegistry.res +7 -1
  4. package/src/LocalPlatformRegistry.res.mjs +7 -5
  5. package/src/Platform.res +16 -0
  6. package/src/Platform.res.mjs +16 -2
  7. package/src/adapter/Api/LocalEvents_Server.res +34 -3
  8. package/src/adapter/Api/LocalEvents_Server.res.mjs +31 -3
  9. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res +24 -0
  10. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +28 -0
  11. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res +26 -0
  12. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res.mjs +35 -0
  13. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res +61 -0
  14. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +75 -0
  15. package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res +88 -0
  16. package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs +112 -0
  17. package/src/adapter/DomainGraphQL_Server.res +2 -1
  18. package/src/adapter/DomainGraphQL_Server.res.mjs +3 -1
  19. package/src/adapter/LocalFeed.res +16 -2
  20. package/src/adapter/LocalFeed.res.mjs +19 -2
  21. package/src/adapter/LocalIdleExit.res +71 -0
  22. package/src/adapter/LocalIdleExit.res.mjs +96 -0
  23. package/src/adapter/LocalMetrics.res +29 -0
  24. package/src/adapter/LocalMetrics.res.mjs +61 -0
  25. package/src/adapter/LocalPlatformObserve.res +129 -5
  26. package/src/adapter/LocalPlatformObserve.res.mjs +144 -2
  27. package/src/adapter/LocalRequestLog.res +62 -0
  28. package/src/adapter/LocalRequestLog.res.mjs +80 -0
  29. package/src/adapter/LocalTapLines.res +2 -0
  30. package/src/adapter/LocalTapLines.res.mjs +4 -1
  31. package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
  32. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +11 -3
  33. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +10 -3
  34. package/src/adapter/ViewChangeLog/LocalViewChangeLog.res +95 -0
  35. package/src/adapter/ViewChangeLog/LocalViewChangeLog.res.mjs +116 -0
  36. package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res +33 -0
  37. package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res.mjs +30 -0
  38. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res +28 -0
  39. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res.mjs +40 -0
  40. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res +76 -0
  41. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res.mjs +88 -0
  42. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  43. package/src/components/LocalAutomationSchedules.res.mjs +1 -1
  44. package/src/components/LocalAutomationSweep.res.mjs +1 -1
  45. package/tests/LocalPlatformRegistryTest.res +17 -0
  46. package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
  47. package/tests/LocalPlatformStartTest.res.mjs +1 -1
  48. package/tests/adapter/GraphQL_SchemaInspectorTest.res +3 -2
  49. package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +1 -1
  50. package/tests/adapter/LocalEvents_ServerTest.res +11 -0
  51. package/tests/adapter/LocalEvents_ServerTest.res.mjs +7 -1
  52. package/tests/adapter/LocalIdleExitTest.res +57 -0
  53. package/tests/adapter/LocalIdleExitTest.res.mjs +62 -0
  54. package/tests/adapter/LocalPlatformObserveTest.res +183 -0
  55. package/tests/adapter/LocalPlatformObserveTest.res.mjs +181 -0
  56. package/tests/adapter/LocalRequestLogTest.res +46 -0
  57. package/tests/adapter/LocalRequestLogTest.res.mjs +61 -0
  58. package/tests/adapter/LocalTapLinesTest.res +25 -0
  59. package/tests/adapter/LocalTapLinesTest.res.mjs +35 -3
@@ -0,0 +1,75 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
7
+ import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
8
+
9
+ let table = "reventless_command_outcomes";
10
+
11
+ function ensureSchema(db) {
12
+ SqliteDriver$ReventlessLocal.exec(db, `CREATE TABLE IF NOT EXISTS ` + table + ` (seq INTEGER PRIMARY KEY AUTOINCREMENT, flow TEXT, msg_id TEXT NOT NULL, item TEXT NOT NULL)`);
13
+ SqliteDriver$ReventlessLocal.exec(db, `CREATE INDEX IF NOT EXISTS ` + table + `_flow ON ` + table + ` (flow, seq)`);
14
+ SqliteDriver$ReventlessLocal.exec(db, `CREATE INDEX IF NOT EXISTS ` + table + `_msg ON ` + table + ` (msg_id)`);
15
+ }
16
+
17
+ function str(prim) {
18
+ return prim;
19
+ }
20
+
21
+ function append(db, e) {
22
+ SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, `INSERT INTO ` + table + ` (flow, msg_id, item) VALUES (?, ?, ?)`), [
23
+ Stdlib_Option.mapOr(e.flow, null, str),
24
+ e.msgId,
25
+ JSON.stringify(e.item)
26
+ ]);
27
+ }
28
+
29
+ function entryOfRow(row) {
30
+ let s = k => Stdlib_Option.flatMap(row[k], Stdlib_JSON.Decode.string);
31
+ let match = Stdlib_Option.flatMap(row["seq"], Stdlib_JSON.Decode.float);
32
+ let match$1 = s("msg_id");
33
+ let match$2 = Stdlib_Option.flatMap(s("item"), i => {
34
+ try {
35
+ return JSON.parse(i);
36
+ } catch (exn) {
37
+ return;
38
+ }
39
+ });
40
+ if (match !== undefined && match$1 !== undefined && match$2 !== undefined) {
41
+ return {
42
+ seq: match | 0,
43
+ flow: s("flow"),
44
+ msgId: match$1,
45
+ item: match$2
46
+ };
47
+ }
48
+ }
49
+
50
+ function read(db, filter, before, first) {
51
+ let where = [];
52
+ let params = [];
53
+ let add = (clause, value) => {
54
+ where.push(clause);
55
+ params.push(value);
56
+ };
57
+ Stdlib_Option.forEach(filter.correlationId, c => add("flow = ?", c));
58
+ Stdlib_Option.forEach(filter.msgId, m => add("msg_id = ?", m));
59
+ Stdlib_Option.forEach(before, b => add("seq < ?", b));
60
+ let sql = `SELECT seq, flow, msg_id, item FROM ` + table + (
61
+ Primitive_object.equal(where, []) ? "" : " WHERE " + where.join(" AND ")
62
+ ) + ` ORDER BY seq DESC LIMIT ?`;
63
+ params.push(first);
64
+ return Stdlib_Array.filterMap(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, sql), params), entryOfRow);
65
+ }
66
+
67
+ export {
68
+ table,
69
+ ensureSchema,
70
+ str,
71
+ append,
72
+ entryOfRow,
73
+ read,
74
+ }
75
+ /* SqliteDriver-ReventlessLocal Not a pure module */
@@ -0,0 +1,88 @@
1
+ // What became of every command the local platform handled, kept and read back by flow or by
2
+ // the command's message id: a tool that missed an outcome live reads it here. Stored as the
3
+ // row changes are (`LocalViewChangeLog`), each as the tap's `commandOutcome` line.
4
+
5
+ module Outcome = ReventlessCore.CommandTopic_Helpers
6
+
7
+ type page = LocalViewChangeLog.page
8
+
9
+ let sqliteReady = ref(None)
10
+
11
+ /** The SQLite store when the platform runs on one, its table made once. */
12
+ let sqlite = () =>
13
+ switch BackendState.getSqliteDb() {
14
+ | Some(db) =>
15
+ if sqliteReady.contents !== Some(db) {
16
+ CommandOutcomeLogStorage_Sqlite.ensureSchema(db)
17
+ sqliteReady := Some(db)
18
+ }
19
+ Some(db)
20
+ | None => None
21
+ }
22
+
23
+ let nextSeq = ref(0)
24
+
25
+ let record = (r: Outcome.commandOutcomeReport) => {
26
+ let entry: CommandOutcomeLogEntry.t = {
27
+ seq: 0,
28
+ flow: ?r.correlationId,
29
+ msgId: r.reference,
30
+ item: LocalTapLines.outcomeLine(r)->JSON.parseOrThrow,
31
+ }
32
+ switch sqlite() {
33
+ | Some(db) => CommandOutcomeLogStorage_Sqlite.append(db, entry)
34
+ | None =>
35
+ nextSeq := nextSeq.contents + 1
36
+ CommandOutcomeLogStorage_InMemory.append({...entry, seq: nextSeq.contents})
37
+ }
38
+ }
39
+
40
+ /** A page of the outcomes matching the filter, newest first; `after` is a page's `endCursor`. */
41
+ let read = (~filter: CommandOutcomeLogEntry.filter, ~first=100, ~after: option<string>=?): page => {
42
+ let before = after->Option.flatMap(a => Int.fromString(a))
43
+ // One more than asked, to know whether there is a next page.
44
+ let rows = switch sqlite() {
45
+ | Some(db) => CommandOutcomeLogStorage_Sqlite.read(db, ~filter, ~before, ~first=first + 1)
46
+ | None => CommandOutcomeLogStorage_InMemory.read(~filter, ~before, ~first=first + 1)
47
+ }
48
+ let shown = rows->Array.slice(~start=0, ~end=first)
49
+ let hasNextPage = rows->Array.length > first
50
+ {
51
+ records: shown->Array.map(e => {LocalViewChangeLog.item: e.item, cursor: e.seq->Int.toString}),
52
+ hasNextPage,
53
+ endCursor: ?(hasNextPage ? shown->Array.at(-1)->Option.map(e => e.seq->Int.toString) : None),
54
+ }
55
+ }
56
+
57
+ let removal: ref<option<unit => unit>> = ref(None)
58
+
59
+ /** Starts keeping every outcome, once per process. One that cannot be kept is logged and passed
60
+ over: the command is already decided. */
61
+ let install = () =>
62
+ if removal.contents->Option.isNone {
63
+ let log = ReventlessCore.Logger.fromEnv()
64
+ removal :=
65
+ Some(
66
+ Outcome.registerCommandOutcome(r =>
67
+ try record(r) catch {
68
+ | exn =>
69
+ log.warn(
70
+ ~comp="CommandOutcomeLog",
71
+ `the outcome of ${r.reference} was not kept: ${exn
72
+ ->JsExn.fromException
73
+ ->Option.flatMap(JsExn.message)
74
+ ->Option.getOr("unknown")}`,
75
+ )
76
+ }
77
+ ),
78
+ )
79
+ }
80
+
81
+ /** Test seam: stops keeping them and forgets the in-memory ones. */
82
+ let resetForTests = () => {
83
+ removal.contents->Option.forEach(remove => remove())
84
+ removal := None
85
+ CommandOutcomeLogStorage_InMemory.clear()
86
+ nextSeq := 0
87
+ sqliteReady := None
88
+ }
@@ -0,0 +1,112 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
+ import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
10
+ import * as LocalTapLines$ReventlessLocal from "../LocalTapLines.res.mjs";
11
+ import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
12
+ import * as CommandOutcomeLogStorage_Sqlite$ReventlessLocal from "./CommandOutcomeLogStorage_Sqlite.res.mjs";
13
+ import * as CommandOutcomeLogStorage_InMemory$ReventlessLocal from "./CommandOutcomeLogStorage_InMemory.res.mjs";
14
+
15
+ let sqliteReady = {
16
+ contents: undefined
17
+ };
18
+
19
+ function sqlite() {
20
+ let db = BackendState$ReventlessLocal.getSqliteDb();
21
+ if (db === undefined) {
22
+ return;
23
+ }
24
+ let db$1 = Primitive_option.valFromOption(db);
25
+ if (sqliteReady.contents !== Primitive_option.some(db$1)) {
26
+ CommandOutcomeLogStorage_Sqlite$ReventlessLocal.ensureSchema(db$1);
27
+ sqliteReady.contents = Primitive_option.some(db$1);
28
+ }
29
+ return Primitive_option.some(db$1);
30
+ }
31
+
32
+ let nextSeq = {
33
+ contents: 0
34
+ };
35
+
36
+ function record(r) {
37
+ let entry_flow = r.correlationId;
38
+ let entry_msgId = r.reference;
39
+ let entry_item = JSON.parse(LocalTapLines$ReventlessLocal.outcomeLine(r));
40
+ let entry = {
41
+ seq: 0,
42
+ flow: entry_flow,
43
+ msgId: entry_msgId,
44
+ item: entry_item
45
+ };
46
+ let db = sqlite();
47
+ if (db !== undefined) {
48
+ return CommandOutcomeLogStorage_Sqlite$ReventlessLocal.append(Primitive_option.valFromOption(db), entry);
49
+ }
50
+ nextSeq.contents = nextSeq.contents + 1 | 0;
51
+ let newrecord = {...entry};
52
+ CommandOutcomeLogStorage_InMemory$ReventlessLocal.append((newrecord.seq = nextSeq.contents, newrecord));
53
+ }
54
+
55
+ function read(filter, firstOpt, after) {
56
+ let first = firstOpt !== undefined ? firstOpt : 100;
57
+ let before = Stdlib_Option.flatMap(after, a => Stdlib_Int.fromString(a, undefined));
58
+ let db = sqlite();
59
+ let rows = db !== undefined ? CommandOutcomeLogStorage_Sqlite$ReventlessLocal.read(Primitive_option.valFromOption(db), filter, before, first + 1 | 0) : CommandOutcomeLogStorage_InMemory$ReventlessLocal.read(filter, before, first + 1 | 0);
60
+ let shown = rows.slice(0, first);
61
+ let hasNextPage = rows.length > first;
62
+ return {
63
+ records: shown.map(e => ({
64
+ item: e.item,
65
+ cursor: e.seq.toString()
66
+ })),
67
+ hasNextPage: hasNextPage,
68
+ endCursor: hasNextPage ? Stdlib_Option.map(shown.at(-1), e => e.seq.toString()) : undefined
69
+ };
70
+ }
71
+
72
+ let removal = {
73
+ contents: undefined
74
+ };
75
+
76
+ function install() {
77
+ if (!Stdlib_Option.isNone(removal.contents)) {
78
+ return;
79
+ }
80
+ let log = Logger$ReventlessCore.fromEnv();
81
+ removal.contents = CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(r => {
82
+ try {
83
+ return record(r);
84
+ } catch (raw_exn) {
85
+ let exn = Primitive_exceptions.internalToException(raw_exn);
86
+ return log.warn("CommandOutcomeLog", undefined, `the outcome of ` + r.reference + ` was not kept: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown"));
87
+ }
88
+ });
89
+ }
90
+
91
+ function resetForTests() {
92
+ Stdlib_Option.forEach(removal.contents, remove => remove());
93
+ removal.contents = undefined;
94
+ CommandOutcomeLogStorage_InMemory$ReventlessLocal.clear();
95
+ nextSeq.contents = 0;
96
+ sqliteReady.contents = undefined;
97
+ }
98
+
99
+ let Outcome;
100
+
101
+ export {
102
+ Outcome,
103
+ sqliteReady,
104
+ sqlite,
105
+ nextSeq,
106
+ record,
107
+ read,
108
+ removal,
109
+ install,
110
+ resetForTests,
111
+ }
112
+ /* Stdlib_JsExn Not a pure module */
@@ -739,12 +739,13 @@ let start = (~port: int=4000, ~contextFactory as _: option<YG.contextFactory>=?,
739
739
  warn: a => log.warn(~comp="GraphQL:Domain", YG.logArgToString(a)),
740
740
  error: a => log.error(~comp="GraphQL:Domain", YG.logArgToString(a)),
741
741
  }
742
- let yoga = YG.createYogaWithContext({
742
+ let yoga = YG.createYogaWithPlugins({
743
743
  "schema": schema,
744
744
  "graphiql": true,
745
745
  "logging": yogaLogging,
746
746
  "maskedErrors": !debug,
747
747
  "context": buildAuthContext,
748
+ "plugins": [LocalRequestLog.plugin()],
748
749
  })
749
750
  // Custom HTTP handler: serves /sdl + /__inmemory/{login,logout}, else yoga.
750
751
  let getSdl = () =>
@@ -18,6 +18,7 @@ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/
18
18
  import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
19
19
  import * as LocalAuth$ReventlessLocal from "./Auth/LocalAuth.res.mjs";
20
20
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
21
+ import * as LocalRequestLog$ReventlessLocal from "./LocalRequestLog.res.mjs";
21
22
  import * as LocalObjectStore$ReventlessLocal from "./ObjectStore/LocalObjectStore.res.mjs";
22
23
  import * as LocalEvents_Server$ReventlessLocal from "./Api/LocalEvents_Server.res.mjs";
23
24
  import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
@@ -610,7 +611,8 @@ function start(portOpt, param, param$1) {
610
611
  graphiql: true,
611
612
  logging: yogaLogging,
612
613
  maskedErrors: !debug,
613
- context: Auth_GraphqlContext$ReventlessLocal.buildAuthContext
614
+ context: Auth_GraphqlContext$ReventlessLocal.buildAuthContext,
615
+ plugins: [LocalRequestLog$ReventlessLocal.plugin()]
614
616
  });
615
617
  let getSdl = () => {
616
618
  let s = activeSchema.contents;
@@ -335,8 +335,22 @@ let newListener = (sink: sink, phase: phase): listener => {
335
335
  let accept = (feed: t, socket: NodeNet.socket) => {
336
336
  let l = newListener(socketSink(socket), Waiting)
337
337
  socket->NodeNet.setEncoding("utf8")
338
- socket->NodeNet.onSocketError(_ => drop(feed, l))
339
- socket->NodeNet.onSocketClose(() => drop(feed, l))
338
+ // A connection is someone watching the platform, until its socket closes.
339
+ LocalIdleExit.connected()
340
+ let gone = ref(false)
341
+ let leave = () =>
342
+ if !gone.contents {
343
+ gone := true
344
+ LocalIdleExit.disconnected()
345
+ }
346
+ socket->NodeNet.onSocketError(_ => {
347
+ drop(feed, l)
348
+ leave()
349
+ })
350
+ socket->NodeNet.onSocketClose(() => {
351
+ drop(feed, l)
352
+ leave()
353
+ })
340
354
  socket->NodeNet.onSocketData(chunk =>
341
355
  if !isLive(l) {
342
356
  l.inbound = l.inbound ++ chunk
@@ -9,6 +9,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
9
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
10
10
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
11
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
12
+ import * as LocalIdleExit$ReventlessLocal from "./LocalIdleExit.res.mjs";
12
13
 
13
14
  let log = Logger$ReventlessCore.fromEnv();
14
15
 
@@ -290,8 +291,24 @@ function newListener(sink, phase) {
290
291
  function accept(feed, socket) {
291
292
  let l = newListener(socketSink(socket), "Waiting");
292
293
  socket.setEncoding("utf8");
293
- socket.on("error", param => drop(feed, l));
294
- socket.on("close", () => drop(feed, l));
294
+ LocalIdleExit$ReventlessLocal.connected();
295
+ let gone = {
296
+ contents: false
297
+ };
298
+ let leave = () => {
299
+ if (!gone.contents) {
300
+ gone.contents = true;
301
+ return LocalIdleExit$ReventlessLocal.disconnected();
302
+ }
303
+ };
304
+ socket.on("error", param => {
305
+ drop(feed, l);
306
+ leave();
307
+ });
308
+ socket.on("close", () => {
309
+ drop(feed, l);
310
+ leave();
311
+ });
295
312
  socket.on("data", chunk => {
296
313
  if (isLive(l)) {
297
314
  return;
@@ -0,0 +1,71 @@
1
+ // A platform started for an editor, and left running across the editor's reload, stops on its
2
+ // own once nothing has watched it for a while: `REVENTLESS_IDLE_EXIT=<seconds>` turns it on.
3
+ // Watching is a connection to the log feed or the event tap, which an editor holds while it is
4
+ // open; a reload reconnects within seconds, quitting the editor never does. The platform then
5
+ // stops the way a SIGTERM stops it, its registry entry removed on the way out.
6
+
7
+ let log = ReventlessCore.Logger.fromEnv()
8
+
9
+ let envVar = "REVENTLESS_IDLE_EXIT"
10
+
11
+ /** The idle time allowed, in milliseconds; none when the variable is unset, empty or not a
12
+ positive number. */
13
+ let graceOf = (value: option<string>): option<int> =>
14
+ value
15
+ ->Option.flatMap(v => Int.fromString(v->String.trim))
16
+ ->Option.flatMap(s => s > 0 ? Some(s * 1000) : None)
17
+
18
+ let grace: ref<option<int>> = ref(None)
19
+ let watchers = ref(0)
20
+ let timer: ref<option<timeoutId>> = ref(None)
21
+
22
+ /** What happens when the time runs out; replaced by tests. */
23
+ let onIdle: ref<unit => unit> = ref(() => NodeProcess.killWithSignal(NodeProcess.pid, "SIGTERM"))
24
+
25
+ let disarm = () => {
26
+ timer.contents->Option.forEach(clearTimeout)
27
+ timer := None
28
+ }
29
+
30
+ let arm = () =>
31
+ grace.contents->Option.forEach(ms => {
32
+ disarm()
33
+ timer := Some(setTimeout(() => {
34
+ timer := None
35
+ log.info(
36
+ ~comp="Platform",
37
+ `nothing watched this platform for ${(ms / 1000)->Int.toString} s — stopping`,
38
+ )
39
+ onIdle.contents()
40
+ }, ms))
41
+ })
42
+
43
+ /** Starts counting idle time, when the environment asks for it: from now until the first
44
+ watcher connects. */
45
+ let start = (~env: dict<string>) => {
46
+ grace := graceOf(env->Dict.get(envVar))
47
+ if watchers.contents == 0 {
48
+ arm()
49
+ }
50
+ }
51
+
52
+ /** A watcher connected: the platform is watched again. */
53
+ let connected = () => {
54
+ watchers := watchers.contents + 1
55
+ disarm()
56
+ }
57
+
58
+ /** A watcher went away; the last one leaving starts the idle time. */
59
+ let disconnected = () => {
60
+ watchers := Math.Int.max(0, watchers.contents - 1)
61
+ if watchers.contents == 0 {
62
+ arm()
63
+ }
64
+ }
65
+
66
+ /** Back to nothing counted: for tests. */
67
+ let reset = () => {
68
+ disarm()
69
+ grace := None
70
+ watchers := 0
71
+ }
@@ -0,0 +1,96 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
7
+
8
+ let log = Logger$ReventlessCore.fromEnv();
9
+
10
+ let envVar = "REVENTLESS_IDLE_EXIT";
11
+
12
+ function graceOf(value) {
13
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(value, v => Stdlib_Int.fromString(v.trim(), undefined)), s => {
14
+ if (s > 0) {
15
+ return s * 1000 | 0;
16
+ }
17
+ });
18
+ }
19
+
20
+ let grace = {
21
+ contents: undefined
22
+ };
23
+
24
+ let watchers = {
25
+ contents: 0
26
+ };
27
+
28
+ let timer = {
29
+ contents: undefined
30
+ };
31
+
32
+ let onIdle = {
33
+ contents: () => {
34
+ process.kill(process.pid, "SIGTERM");
35
+ }
36
+ };
37
+
38
+ function disarm() {
39
+ Stdlib_Option.forEach(timer.contents, prim => {
40
+ clearTimeout(prim);
41
+ });
42
+ timer.contents = undefined;
43
+ }
44
+
45
+ function arm() {
46
+ Stdlib_Option.forEach(grace.contents, ms => {
47
+ disarm();
48
+ timer.contents = Primitive_option.some(setTimeout(() => {
49
+ timer.contents = undefined;
50
+ log.info("Platform", undefined, `nothing watched this platform for ` + (ms / 1000 | 0).toString() + ` s — stopping`);
51
+ onIdle.contents();
52
+ }, ms));
53
+ });
54
+ }
55
+
56
+ function start(env) {
57
+ grace.contents = graceOf(env[envVar]);
58
+ if (watchers.contents === 0) {
59
+ return arm();
60
+ }
61
+ }
62
+
63
+ function connected() {
64
+ watchers.contents = watchers.contents + 1 | 0;
65
+ disarm();
66
+ }
67
+
68
+ function disconnected() {
69
+ watchers.contents = Math.max(0, watchers.contents - 1 | 0);
70
+ if (watchers.contents === 0) {
71
+ return arm();
72
+ }
73
+ }
74
+
75
+ function reset() {
76
+ disarm();
77
+ grace.contents = undefined;
78
+ watchers.contents = 0;
79
+ }
80
+
81
+ export {
82
+ log,
83
+ envVar,
84
+ graceOf,
85
+ grace,
86
+ watchers,
87
+ timer,
88
+ onIdle,
89
+ disarm,
90
+ arm,
91
+ start,
92
+ connected,
93
+ disconnected,
94
+ reset,
95
+ }
96
+ /* log Not a pure module */
@@ -0,0 +1,29 @@
1
+ // The local platform's metrics: counted in memory rather than written to the
2
+ // output, where nothing collects them locally, and served on the platform API
3
+ // (`Platform_Metrics`) and the MCP server (`platform/metrics`).
4
+
5
+ module Metrics = ReventlessCore.Metrics
6
+
7
+ let counts = Metrics.Counts.make()
8
+
9
+ /** Counts every metric from now on; installed when the platform module loads. */
10
+ let install = () => Metrics.Sink.install(Metrics.Counts.sink(counts))
11
+
12
+ let snapshotJson = (s: Metrics.Counts.snapshot): JSON.t =>
13
+ Dict.fromArray([
14
+ ("metric", JSON.Encode.string(s.metric)),
15
+ ("plugin", JSON.Encode.string(s.plugin)),
16
+ ("slice", JSON.Encode.string(s.slice)),
17
+ ("unit", JSON.Encode.string(s.unit)),
18
+ ("count", JSON.Encode.int(s.count)),
19
+ ("sum", JSON.Encode.float(s.sum)),
20
+ ("lastAt", JSON.Encode.string(s.lastAt->Date.fromTime->Date.toISOString)),
21
+ ("perMinute", s.perMinute->Array.map(JSON.Encode.int)->JSON.Encode.array),
22
+ ])->JSON.Encode.object
23
+
24
+ /** The counts now, of the given plugins or of every one. */
25
+ let read = (~plugins: option<array<string>>=?): JSON.t =>
26
+ counts
27
+ ->Metrics.Counts.read(~now=Date.now(), ~plugins?)
28
+ ->Array.map(snapshotJson)
29
+ ->JSON.Encode.array
@@ -0,0 +1,61 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Metrics$ReventlessCore from "@reventlessdev/reventless-core/src/util/Metrics.res.mjs";
4
+
5
+ let counts = Metrics$ReventlessCore.Counts.make();
6
+
7
+ function install() {
8
+ Metrics$ReventlessCore.Sink.install(Metrics$ReventlessCore.Counts.sink(counts));
9
+ }
10
+
11
+ function snapshotJson(s) {
12
+ return Object.fromEntries([
13
+ [
14
+ "metric",
15
+ s.metric
16
+ ],
17
+ [
18
+ "plugin",
19
+ s.plugin
20
+ ],
21
+ [
22
+ "slice",
23
+ s.slice
24
+ ],
25
+ [
26
+ "unit",
27
+ s.unit
28
+ ],
29
+ [
30
+ "count",
31
+ s.count
32
+ ],
33
+ [
34
+ "sum",
35
+ s.sum
36
+ ],
37
+ [
38
+ "lastAt",
39
+ new Date(s.lastAt).toISOString()
40
+ ],
41
+ [
42
+ "perMinute",
43
+ s.perMinute.map(prim => prim)
44
+ ]
45
+ ]);
46
+ }
47
+
48
+ function read(plugins) {
49
+ return Metrics$ReventlessCore.Counts.read(counts, Date.now(), plugins).map(snapshotJson);
50
+ }
51
+
52
+ let Metrics;
53
+
54
+ export {
55
+ Metrics,
56
+ counts,
57
+ install,
58
+ snapshotJson,
59
+ read,
60
+ }
61
+ /* counts Not a pure module */