@reventlessdev/reventless-local 3.0.0-alpha.319 → 3.0.0-alpha.321
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.
- package/CHANGELOG.md +19 -0
- package/package.json +11 -11
- package/src/Platform.res +16 -0
- package/src/Platform.res.mjs +17 -0
- package/src/adapter/Api/LocalEvents_Server.res +34 -3
- package/src/adapter/Api/LocalEvents_Server.res.mjs +31 -3
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res +33 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +38 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res +26 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res.mjs +35 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res +64 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +78 -0
- package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res +88 -0
- package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs +112 -0
- package/src/adapter/DomainGraphQL_Server.res +2 -1
- package/src/adapter/DomainGraphQL_Server.res.mjs +3 -1
- package/src/adapter/LocalFeed.res +16 -2
- package/src/adapter/LocalFeed.res.mjs +19 -2
- package/src/adapter/LocalIdleExit.res +71 -0
- package/src/adapter/LocalIdleExit.res.mjs +96 -0
- package/src/adapter/LocalMetrics.res +29 -0
- package/src/adapter/LocalMetrics.res.mjs +61 -0
- package/src/adapter/LocalPlatformObserve.res +130 -5
- package/src/adapter/LocalPlatformObserve.res.mjs +145 -2
- package/src/adapter/LocalRequestLog.res +62 -0
- package/src/adapter/LocalRequestLog.res.mjs +80 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +11 -3
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +10 -3
- package/src/adapter/ViewChangeLog/LocalViewChangeLog.res +95 -0
- package/src/adapter/ViewChangeLog/LocalViewChangeLog.res.mjs +116 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res +33 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res.mjs +30 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res +28 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res.mjs +40 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res +76 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res.mjs +88 -0
- package/src/reexport/CommandTopic.res.mjs +6 -0
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +3 -2
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +1 -1
- package/tests/adapter/LocalEvents_ServerTest.res +11 -0
- package/tests/adapter/LocalEvents_ServerTest.res.mjs +7 -1
- package/tests/adapter/LocalIdleExitTest.res +57 -0
- package/tests/adapter/LocalIdleExitTest.res.mjs +62 -0
- package/tests/adapter/LocalPlatformObserveTest.res +191 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +187 -0
- package/tests/adapter/LocalRequestLogTest.res +46 -0
- package/tests/adapter/LocalRequestLogTest.res.mjs +61 -0
|
@@ -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.
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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 */
|