@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.
- package/CHANGELOG.md +19 -0
- package/package.json +11 -11
- package/src/LocalPlatformRegistry.res +7 -1
- package/src/LocalPlatformRegistry.res.mjs +7 -5
- package/src/Platform.res +16 -0
- package/src/Platform.res.mjs +16 -2
- 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 +24 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +28 -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 +61 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +75 -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 +129 -5
- package/src/adapter/LocalPlatformObserve.res.mjs +144 -2
- package/src/adapter/LocalRequestLog.res +62 -0
- package/src/adapter/LocalRequestLog.res.mjs +80 -0
- package/src/adapter/LocalTapLines.res +2 -0
- package/src/adapter/LocalTapLines.res.mjs +4 -1
- package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
- 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/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/LocalAutomationSchedules.res.mjs +1 -1
- package/src/components/LocalAutomationSweep.res.mjs +1 -1
- package/tests/LocalPlatformRegistryTest.res +17 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
- package/tests/LocalPlatformStartTest.res.mjs +1 -1
- 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 +183 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +181 -0
- package/tests/adapter/LocalRequestLogTest.res +46 -0
- package/tests/adapter/LocalRequestLogTest.res.mjs +61 -0
- package/tests/adapter/LocalTapLinesTest.res +25 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +35 -3
|
@@ -16,6 +16,7 @@ import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/compon
|
|
|
16
16
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
17
17
|
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
18
18
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
19
|
+
import * as RowChange$ReventlessCore from "@reventlessdev/reventless-core/src/RowChange.res.mjs";
|
|
19
20
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
20
21
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
21
22
|
import * as SortKey_Filter$ReventlessLocal from "./SortKey_Filter.res.mjs";
|
|
@@ -749,9 +750,15 @@ function Make(Bus) {
|
|
|
749
750
|
};
|
|
750
751
|
fieldResolvers[match.resolvedField] = resolver;
|
|
751
752
|
});
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
753
|
+
let changeResolver = async (root, _args, ctx) => {
|
|
754
|
+
if (OwnerScope$Reventless.isExempt(OwnerScope$Reventless.resolve(extractIdentity(ctx), undefined))) {
|
|
755
|
+
return Stdlib_Option.getOr(sourceField(root, RowChange$ReventlessCore.key), null);
|
|
756
|
+
} else {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
fieldResolvers[RowChange$ReventlessCore.key] = changeResolver;
|
|
761
|
+
server.registerFieldResolvers(returnTypeName, fieldResolvers);
|
|
755
762
|
let allSdl = [byIdSdl].concat(byIdsSdl).concat(refsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
|
|
756
763
|
let resolvers = {};
|
|
757
764
|
resolvers[singleQueryName] = byIdResolver;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Every row change the local platform makes, kept and read back by flow, event, view or row:
|
|
2
|
+
// a tool that missed a change live reads it here. In the SQLite store when the platform runs
|
|
3
|
+
// on one, else in memory for as long as it runs (Postgres included, whose store is not
|
|
4
|
+
// this machine's). Each change is the tap's `viewChange` line, so live and stored read alike.
|
|
5
|
+
|
|
6
|
+
module ViewChange = ReventlessCore.ViewChange
|
|
7
|
+
|
|
8
|
+
/** A change as a page lists it: the tap's line, and the cursor to read on after it. */
|
|
9
|
+
type node = {item: JSON.t, cursor: string}
|
|
10
|
+
|
|
11
|
+
type page = {records: array<node>, hasNextPage: bool, endCursor?: string}
|
|
12
|
+
|
|
13
|
+
let sqliteReady = ref(None)
|
|
14
|
+
|
|
15
|
+
/** The SQLite store when the platform runs on one, its table made once. */
|
|
16
|
+
let sqlite = () =>
|
|
17
|
+
switch BackendState.getSqliteDb() {
|
|
18
|
+
| Some(db) =>
|
|
19
|
+
if sqliteReady.contents !== Some(db) {
|
|
20
|
+
ViewChangeLogStorage_Sqlite.ensureSchema(db)
|
|
21
|
+
sqliteReady := Some(db)
|
|
22
|
+
}
|
|
23
|
+
Some(db)
|
|
24
|
+
| None => None
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let nextSeq = ref(0)
|
|
28
|
+
|
|
29
|
+
let record = (c: ViewChange.t) => {
|
|
30
|
+
let item = LocalTapLines.viewChangeLine(c)->JSON.parseOrThrow
|
|
31
|
+
let entry: ViewChangeLogEntry.t = {
|
|
32
|
+
seq: 0,
|
|
33
|
+
flow: ?c.correlationId,
|
|
34
|
+
view: c.view,
|
|
35
|
+
key: c.key,
|
|
36
|
+
msgIds: c.sources->Array.map(s => s.msgId),
|
|
37
|
+
item,
|
|
38
|
+
}
|
|
39
|
+
switch sqlite() {
|
|
40
|
+
| Some(db) => ViewChangeLogStorage_Sqlite.append(db, entry)
|
|
41
|
+
| None =>
|
|
42
|
+
nextSeq := nextSeq.contents + 1
|
|
43
|
+
ViewChangeLogStorage_InMemory.append({...entry, seq: nextSeq.contents})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A page of the changes matching the filter, newest first; `after` is a page's `endCursor`. */
|
|
48
|
+
let read = (~filter: ViewChangeLogEntry.filter, ~first=100, ~after: option<string>=?): page => {
|
|
49
|
+
let before = after->Option.flatMap(a => Int.fromString(a))
|
|
50
|
+
// One more than asked, to know whether there is a next page.
|
|
51
|
+
let rows = switch sqlite() {
|
|
52
|
+
| Some(db) => ViewChangeLogStorage_Sqlite.read(db, ~filter, ~before, ~first=first + 1)
|
|
53
|
+
| None => ViewChangeLogStorage_InMemory.read(~filter, ~before, ~first=first + 1)
|
|
54
|
+
}
|
|
55
|
+
let shown = rows->Array.slice(~start=0, ~end=first)
|
|
56
|
+
let hasNextPage = rows->Array.length > first
|
|
57
|
+
{
|
|
58
|
+
records: shown->Array.map(e => {item: e.item, cursor: e.seq->Int.toString}),
|
|
59
|
+
hasNextPage,
|
|
60
|
+
endCursor: ?(hasNextPage ? shown->Array.at(-1)->Option.map(e => e.seq->Int.toString) : None),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let removal: ref<option<unit => unit>> = ref(None)
|
|
65
|
+
|
|
66
|
+
/** Starts keeping every row change, once per process. A change that cannot be kept is logged
|
|
67
|
+
and passed over: the row is already written. */
|
|
68
|
+
let install = () =>
|
|
69
|
+
if removal.contents->Option.isNone {
|
|
70
|
+
let log = ReventlessCore.Logger.fromEnv()
|
|
71
|
+
removal :=
|
|
72
|
+
Some(
|
|
73
|
+
ViewChange.register(c =>
|
|
74
|
+
try record(c) catch {
|
|
75
|
+
| exn =>
|
|
76
|
+
log.warn(
|
|
77
|
+
~comp="ViewChangeLog",
|
|
78
|
+
`a row change of ${c.view} was not kept: ${exn
|
|
79
|
+
->JsExn.fromException
|
|
80
|
+
->Option.flatMap(JsExn.message)
|
|
81
|
+
->Option.getOr("unknown")}`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Test seam: stops keeping them and forgets the in-memory ones. */
|
|
89
|
+
let resetForTests = () => {
|
|
90
|
+
removal.contents->Option.forEach(remove => remove())
|
|
91
|
+
removal := None
|
|
92
|
+
ViewChangeLogStorage_InMemory.clear()
|
|
93
|
+
nextSeq := 0
|
|
94
|
+
sqliteReady := None
|
|
95
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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 ViewChange$ReventlessCore from "@reventlessdev/reventless-core/src/ViewChange.res.mjs";
|
|
10
|
+
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
11
|
+
import * as LocalTapLines$ReventlessLocal from "../LocalTapLines.res.mjs";
|
|
12
|
+
import * as ViewChangeLogStorage_Sqlite$ReventlessLocal from "./ViewChangeLogStorage_Sqlite.res.mjs";
|
|
13
|
+
import * as ViewChangeLogStorage_InMemory$ReventlessLocal from "./ViewChangeLogStorage_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
|
+
ViewChangeLogStorage_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(c) {
|
|
37
|
+
let item = JSON.parse(LocalTapLines$ReventlessLocal.viewChangeLine(c));
|
|
38
|
+
let entry_flow = c.correlationId;
|
|
39
|
+
let entry_view = c.view;
|
|
40
|
+
let entry_key = c.key;
|
|
41
|
+
let entry_msgIds = c.sources.map(s => s.msgId);
|
|
42
|
+
let entry = {
|
|
43
|
+
seq: 0,
|
|
44
|
+
flow: entry_flow,
|
|
45
|
+
view: entry_view,
|
|
46
|
+
key: entry_key,
|
|
47
|
+
msgIds: entry_msgIds,
|
|
48
|
+
item: item
|
|
49
|
+
};
|
|
50
|
+
let db = sqlite();
|
|
51
|
+
if (db !== undefined) {
|
|
52
|
+
return ViewChangeLogStorage_Sqlite$ReventlessLocal.append(Primitive_option.valFromOption(db), entry);
|
|
53
|
+
}
|
|
54
|
+
nextSeq.contents = nextSeq.contents + 1 | 0;
|
|
55
|
+
let newrecord = {...entry};
|
|
56
|
+
ViewChangeLogStorage_InMemory$ReventlessLocal.append((newrecord.seq = nextSeq.contents, newrecord));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function read(filter, firstOpt, after) {
|
|
60
|
+
let first = firstOpt !== undefined ? firstOpt : 100;
|
|
61
|
+
let before = Stdlib_Option.flatMap(after, a => Stdlib_Int.fromString(a, undefined));
|
|
62
|
+
let db = sqlite();
|
|
63
|
+
let rows = db !== undefined ? ViewChangeLogStorage_Sqlite$ReventlessLocal.read(Primitive_option.valFromOption(db), filter, before, first + 1 | 0) : ViewChangeLogStorage_InMemory$ReventlessLocal.read(filter, before, first + 1 | 0);
|
|
64
|
+
let shown = rows.slice(0, first);
|
|
65
|
+
let hasNextPage = rows.length > first;
|
|
66
|
+
return {
|
|
67
|
+
records: shown.map(e => ({
|
|
68
|
+
item: e.item,
|
|
69
|
+
cursor: e.seq.toString()
|
|
70
|
+
})),
|
|
71
|
+
hasNextPage: hasNextPage,
|
|
72
|
+
endCursor: hasNextPage ? Stdlib_Option.map(shown.at(-1), e => e.seq.toString()) : undefined
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let removal = {
|
|
77
|
+
contents: undefined
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function install() {
|
|
81
|
+
if (!Stdlib_Option.isNone(removal.contents)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let log = Logger$ReventlessCore.fromEnv();
|
|
85
|
+
removal.contents = ViewChange$ReventlessCore.register(c => {
|
|
86
|
+
try {
|
|
87
|
+
return record(c);
|
|
88
|
+
} catch (raw_exn) {
|
|
89
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
90
|
+
return log.warn("ViewChangeLog", undefined, `a row change of ` + c.view + ` was not kept: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown"));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resetForTests() {
|
|
96
|
+
Stdlib_Option.forEach(removal.contents, remove => remove());
|
|
97
|
+
removal.contents = undefined;
|
|
98
|
+
ViewChangeLogStorage_InMemory$ReventlessLocal.clear();
|
|
99
|
+
nextSeq.contents = 0;
|
|
100
|
+
sqliteReady.contents = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let ViewChange;
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
ViewChange,
|
|
107
|
+
sqliteReady,
|
|
108
|
+
sqlite,
|
|
109
|
+
nextSeq,
|
|
110
|
+
record,
|
|
111
|
+
read,
|
|
112
|
+
removal,
|
|
113
|
+
install,
|
|
114
|
+
resetForTests,
|
|
115
|
+
}
|
|
116
|
+
/* Stdlib_JsExn Not a pure module */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// One stored row change: the line the tap sends for it (view, key, kind, fields, sources,
|
|
2
|
+
// flow, the row before and after, redacted), with the columns a read filters by.
|
|
3
|
+
|
|
4
|
+
type t = {
|
|
5
|
+
seq: int,
|
|
6
|
+
flow?: string,
|
|
7
|
+
view: string,
|
|
8
|
+
key: string,
|
|
9
|
+
/** The message ids of the events behind it. */
|
|
10
|
+
msgIds: array<string>,
|
|
11
|
+
/** The tap's `viewChange` line, as JSON. */
|
|
12
|
+
item: JSON.t,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What a read asks for: every field narrows, none reads every change. */
|
|
16
|
+
type filter = {
|
|
17
|
+
correlationId?: string,
|
|
18
|
+
msgId?: string,
|
|
19
|
+
view?: string,
|
|
20
|
+
key?: string,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let matches = (f: filter, e: t) =>
|
|
24
|
+
f.correlationId->Option.mapOr(true, c => e.flow == Some(c)) &&
|
|
25
|
+
f.msgId->Option.mapOr(true, m => e.msgIds->Array.includes(m)) &&
|
|
26
|
+
f.view->Option.mapOr(true, v => e.view == v) &&
|
|
27
|
+
f.key->Option.mapOr(true, k => e.key == k)
|
|
28
|
+
|
|
29
|
+
let filterOfJson = (j: option<JSON.t>): filter => {
|
|
30
|
+
let d = j->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
|
|
31
|
+
let s = k => d->Dict.get(k)->Option.flatMap(JSON.Decode.string)
|
|
32
|
+
{correlationId: ?s("correlationId"), msgId: ?s("msgId"), view: ?s("view"), key: ?s("key")}
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
|
|
7
|
+
function matches(f, e) {
|
|
8
|
+
if (Stdlib_Option.mapOr(f.correlationId, true, c => Primitive_object.equal(e.flow, c)) && Stdlib_Option.mapOr(f.msgId, true, m => e.msgIds.includes(m)) && Stdlib_Option.mapOr(f.view, true, v => e.view === v)) {
|
|
9
|
+
return Stdlib_Option.mapOr(f.key, true, k => e.key === k);
|
|
10
|
+
} else {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function filterOfJson(j) {
|
|
16
|
+
let d = Stdlib_Option.getOr(Stdlib_Option.flatMap(j, Stdlib_JSON.Decode.object), {});
|
|
17
|
+
let s = k => Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
|
|
18
|
+
return {
|
|
19
|
+
correlationId: s("correlationId"),
|
|
20
|
+
msgId: s("msgId"),
|
|
21
|
+
view: s("view"),
|
|
22
|
+
key: s("key")
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
matches,
|
|
28
|
+
filterOfJson,
|
|
29
|
+
}
|
|
30
|
+
/* No side effect */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The row changes of an in-memory platform, oldest first, for as long as it runs.
|
|
2
|
+
|
|
3
|
+
type entry = ViewChangeLogEntry.t
|
|
4
|
+
|
|
5
|
+
let entries: array<entry> = []
|
|
6
|
+
|
|
7
|
+
let append = (e: entry) => entries->Array.push(e)
|
|
8
|
+
|
|
9
|
+
/** Up to `first` entries matching the filter, newest first, below the `before` sequence number. */
|
|
10
|
+
let read = (~filter: ViewChangeLogEntry.filter, ~before: option<int>, ~first: int): array<
|
|
11
|
+
entry,
|
|
12
|
+
> => {
|
|
13
|
+
let out = []
|
|
14
|
+
let i = ref(entries->Array.length - 1)
|
|
15
|
+
while i.contents >= 0 && out->Array.length < first {
|
|
16
|
+
let e = entries->Array.getUnsafe(i.contents)
|
|
17
|
+
if before->Option.mapOr(true, b => e.seq < b) && ViewChangeLogEntry.matches(filter, e) {
|
|
18
|
+
out->Array.push(e)
|
|
19
|
+
}
|
|
20
|
+
i := i.contents - 1
|
|
21
|
+
}
|
|
22
|
+
out
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let lastSeq = () => entries->Array.at(-1)->Option.mapOr(0, e => e.seq)
|
|
26
|
+
|
|
27
|
+
/** Test seam. */
|
|
28
|
+
let clear = () => entries->Array.splice(~start=0, ~remove=entries->Array.length, ~insert=[])
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as ViewChangeLogEntry$ReventlessLocal from "./ViewChangeLogEntry.res.mjs";
|
|
5
|
+
|
|
6
|
+
let entries = [];
|
|
7
|
+
|
|
8
|
+
function append(e) {
|
|
9
|
+
entries.push(e);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function read(filter, before, first) {
|
|
13
|
+
let out = [];
|
|
14
|
+
let i = entries.length - 1 | 0;
|
|
15
|
+
while (i >= 0 && out.length < first) {
|
|
16
|
+
let e = entries[i];
|
|
17
|
+
if (Stdlib_Option.mapOr(before, true, b => e.seq < b) && ViewChangeLogEntry$ReventlessLocal.matches(filter, e)) {
|
|
18
|
+
out.push(e);
|
|
19
|
+
}
|
|
20
|
+
i = i - 1 | 0;
|
|
21
|
+
};
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function lastSeq() {
|
|
26
|
+
return Stdlib_Option.mapOr(entries.at(-1), 0, e => e.seq);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clear() {
|
|
30
|
+
entries.splice(0, entries.length);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
entries,
|
|
35
|
+
append,
|
|
36
|
+
read,
|
|
37
|
+
lastSeq,
|
|
38
|
+
clear,
|
|
39
|
+
}
|
|
40
|
+
/* No side effect */
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The row changes of a SQLite platform, kept in its store across restarts.
|
|
2
|
+
|
|
3
|
+
type entry = ViewChangeLogEntry.t
|
|
4
|
+
|
|
5
|
+
let table = "reventless_view_changes"
|
|
6
|
+
|
|
7
|
+
let ensureSchema = db => {
|
|
8
|
+
db->SqliteDriver.exec(
|
|
9
|
+
`CREATE TABLE IF NOT EXISTS ${table} (seq INTEGER PRIMARY KEY AUTOINCREMENT, flow TEXT, view TEXT NOT NULL, key TEXT NOT NULL, msg_ids TEXT NOT NULL, item TEXT NOT NULL)`,
|
|
10
|
+
)
|
|
11
|
+
db->SqliteDriver.exec(`CREATE INDEX IF NOT EXISTS ${table}_flow ON ${table} (flow, seq)`)
|
|
12
|
+
db->SqliteDriver.exec(`CREATE INDEX IF NOT EXISTS ${table}_row ON ${table} (view, key, seq)`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let str = JSON.Encode.string
|
|
16
|
+
|
|
17
|
+
let append = (db, e: entry) =>
|
|
18
|
+
db
|
|
19
|
+
->SqliteDriver.prepare(
|
|
20
|
+
`INSERT INTO ${table} (flow, view, key, msg_ids, item) VALUES (?, ?, ?, ?, ?)`,
|
|
21
|
+
)
|
|
22
|
+
->SqliteDriver.run([
|
|
23
|
+
e.flow->Option.mapOr(JSON.Encode.null, str),
|
|
24
|
+
str(e.view),
|
|
25
|
+
str(e.key),
|
|
26
|
+
str(e.msgIds->Array.join(",")),
|
|
27
|
+
str(e.item->JSON.stringify),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
let entryOfRow = (row: dict<JSON.t>): option<entry> => {
|
|
31
|
+
let s = k => row->Dict.get(k)->Option.flatMap(JSON.Decode.string)
|
|
32
|
+
switch (
|
|
33
|
+
row->Dict.get("seq")->Option.flatMap(JSON.Decode.float),
|
|
34
|
+
s("view"),
|
|
35
|
+
s("key"),
|
|
36
|
+
s("item")->Option.flatMap(i =>
|
|
37
|
+
try Some(JSON.parseOrThrow(i)) catch {
|
|
38
|
+
| _ => None
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
) {
|
|
42
|
+
| (Some(seq), Some(view), Some(key), Some(item)) =>
|
|
43
|
+
Some({
|
|
44
|
+
seq: seq->Float.toInt,
|
|
45
|
+
flow: ?s("flow"),
|
|
46
|
+
view,
|
|
47
|
+
key,
|
|
48
|
+
msgIds: s("msg_ids")->Option.mapOr([], m => m == "" ? [] : m->String.split(",")),
|
|
49
|
+
item,
|
|
50
|
+
})
|
|
51
|
+
| _ => None
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Up to `first` entries matching the filter, newest first, below the `before` sequence number.
|
|
56
|
+
The flow, view and key narrow in SQL; a message id is tested on the rows read. */
|
|
57
|
+
let read = (db, ~filter: ViewChangeLogEntry.filter, ~before: option<int>, ~first: int): array<
|
|
58
|
+
entry,
|
|
59
|
+
> => {
|
|
60
|
+
let where = []
|
|
61
|
+
let params = []
|
|
62
|
+
let add = (clause, value) => {
|
|
63
|
+
where->Array.push(clause)
|
|
64
|
+
params->Array.push(value)
|
|
65
|
+
}
|
|
66
|
+
filter.correlationId->Option.forEach(c => add("flow = ?", str(c)))
|
|
67
|
+
filter.view->Option.forEach(v => add("view = ?", str(v)))
|
|
68
|
+
filter.key->Option.forEach(k => add("key = ?", str(k)))
|
|
69
|
+
filter.msgId->Option.forEach(m => add("(',' || msg_ids || ',') LIKE ?", str(`%,${m},%`)))
|
|
70
|
+
before->Option.forEach(b => add("seq < ?", JSON.Encode.int(b)))
|
|
71
|
+
let sql = `SELECT seq, flow, view, key, msg_ids, item FROM ${table}${where == []
|
|
72
|
+
? ""
|
|
73
|
+
: " WHERE " ++ where->Array.join(" AND ")} ORDER BY seq DESC LIMIT ?`
|
|
74
|
+
params->Array.push(JSON.Encode.int(first))
|
|
75
|
+
db->SqliteDriver.prepare(sql)->SqliteDriver.all(params)->Array.filterMap(entryOfRow)
|
|
76
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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_view_changes";
|
|
10
|
+
|
|
11
|
+
function ensureSchema(db) {
|
|
12
|
+
SqliteDriver$ReventlessLocal.exec(db, `CREATE TABLE IF NOT EXISTS ` + table + ` (seq INTEGER PRIMARY KEY AUTOINCREMENT, flow TEXT, view TEXT NOT NULL, key TEXT NOT NULL, msg_ids 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 + `_row ON ` + table + ` (view, key, seq)`);
|
|
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, view, key, msg_ids, item) VALUES (?, ?, ?, ?, ?)`), [
|
|
23
|
+
Stdlib_Option.mapOr(e.flow, null, str),
|
|
24
|
+
e.view,
|
|
25
|
+
e.key,
|
|
26
|
+
e.msgIds.join(","),
|
|
27
|
+
JSON.stringify(e.item)
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function entryOfRow(row) {
|
|
32
|
+
let s = k => Stdlib_Option.flatMap(row[k], Stdlib_JSON.Decode.string);
|
|
33
|
+
let match = Stdlib_Option.flatMap(row["seq"], Stdlib_JSON.Decode.float);
|
|
34
|
+
let match$1 = s("view");
|
|
35
|
+
let match$2 = s("key");
|
|
36
|
+
let match$3 = Stdlib_Option.flatMap(s("item"), i => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(i);
|
|
39
|
+
} catch (exn) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (match !== undefined && match$1 !== undefined && match$2 !== undefined && match$3 !== undefined) {
|
|
44
|
+
return {
|
|
45
|
+
seq: match | 0,
|
|
46
|
+
flow: s("flow"),
|
|
47
|
+
view: match$1,
|
|
48
|
+
key: match$2,
|
|
49
|
+
msgIds: Stdlib_Option.mapOr(s("msg_ids"), [], m => {
|
|
50
|
+
if (m === "") {
|
|
51
|
+
return [];
|
|
52
|
+
} else {
|
|
53
|
+
return m.split(",");
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
item: match$3
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function read(db, filter, before, first) {
|
|
62
|
+
let where = [];
|
|
63
|
+
let params = [];
|
|
64
|
+
let add = (clause, value) => {
|
|
65
|
+
where.push(clause);
|
|
66
|
+
params.push(value);
|
|
67
|
+
};
|
|
68
|
+
Stdlib_Option.forEach(filter.correlationId, c => add("flow = ?", c));
|
|
69
|
+
Stdlib_Option.forEach(filter.view, v => add("view = ?", v));
|
|
70
|
+
Stdlib_Option.forEach(filter.key, k => add("key = ?", k));
|
|
71
|
+
Stdlib_Option.forEach(filter.msgId, m => add("(',' || msg_ids || ',') LIKE ?", `%,` + m + `,%`));
|
|
72
|
+
Stdlib_Option.forEach(before, b => add("seq < ?", b));
|
|
73
|
+
let sql = `SELECT seq, flow, view, key, msg_ids, item FROM ` + table + (
|
|
74
|
+
Primitive_object.equal(where, []) ? "" : " WHERE " + where.join(" AND ")
|
|
75
|
+
) + ` ORDER BY seq DESC LIMIT ?`;
|
|
76
|
+
params.push(first);
|
|
77
|
+
return Stdlib_Array.filterMap(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, sql), params), entryOfRow);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
table,
|
|
82
|
+
ensureSchema,
|
|
83
|
+
str,
|
|
84
|
+
append,
|
|
85
|
+
entryOfRow,
|
|
86
|
+
read,
|
|
87
|
+
}
|
|
88
|
+
/* SqliteDriver-ReventlessLocal Not a pure module */
|
|
@@ -59,7 +59,7 @@ function Make(Bus) {
|
|
|
59
59
|
return await ops.tick(key, at);
|
|
60
60
|
});
|
|
61
61
|
Stdlib_Promise.$$catch(TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(component)).then(ops => ops.reconcileSchedules()), exn => {
|
|
62
|
-
Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `the schedules could not be reconciled: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown")));
|
|
62
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, undefined, `the schedules could not be reconciled: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown")));
|
|
63
63
|
return Promise.resolve();
|
|
64
64
|
});
|
|
65
65
|
return component;
|
|
@@ -18,7 +18,7 @@ function timerName(slice, key) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function tickFailed(slice, key, message) {
|
|
21
|
-
Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSchedules", undefined, `tick ` + key + ` of ` + slice + ` failed: ` + message));
|
|
21
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSchedules", undefined, undefined, `tick ` + key + ` of ` + slice + ` failed: ` + message));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function register(slice, tick) {
|
|
@@ -40,7 +40,7 @@ async function sweepOnce() {
|
|
|
40
40
|
} catch (raw_exn) {
|
|
41
41
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
42
42
|
let message = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
43
|
-
return Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSweep", undefined, `sweep of ` + param[0] + ` failed: ` + message));
|
|
43
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSweep", undefined, undefined, `sweep of ` + param[0] + ` failed: ` + message));
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
running.contents = false;
|
|
@@ -199,4 +199,21 @@ describe("LocalPlatformRegistry.tapPort", () => {
|
|
|
199
199
|
|
|
200
200
|
expect(LocalPlatformRegistry.list(~cwd, ())->Array.map(e => e.tapPort))->toEqual([Some(4100)])
|
|
201
201
|
})
|
|
202
|
+
|
|
203
|
+
testSync("names where the platform API is served, and keeps it when a port is published", () => {
|
|
204
|
+
let cwd = tempRoot()
|
|
205
|
+
let _ = LocalPlatformRegistry.write(
|
|
206
|
+
~port=4000,
|
|
207
|
+
~endpoint="http://localhost:4000/graphql",
|
|
208
|
+
~loginEndpoint="http://localhost:4000/__inmemory/login",
|
|
209
|
+
~store=memoryStore,
|
|
210
|
+
~platformEndpoint="http://localhost:4001/graphql",
|
|
211
|
+
~cwd,
|
|
212
|
+
)
|
|
213
|
+
LocalPlatformRegistry.publishTapPort(~port=4000, ~tapPort=4100, ~cwd)
|
|
214
|
+
|
|
215
|
+
expect(
|
|
216
|
+
LocalPlatformRegistry.list(~cwd, ())->Array.map(e => (e.platformEndpoint, e.tapPort)),
|
|
217
|
+
)->toEqual([(Some("http://localhost:4001/graphql"), Some(4100))])
|
|
218
|
+
})
|
|
202
219
|
})
|
|
@@ -24,7 +24,7 @@ let memoryStore = {
|
|
|
24
24
|
|
|
25
25
|
function writeAt(cwd, port, store, pidOpt) {
|
|
26
26
|
let pid = pidOpt !== undefined ? pidOpt : process.pid;
|
|
27
|
-
return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, pid, cwd);
|
|
27
|
+
return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, undefined, pid, cwd);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
globalThis.describe("LocalPlatformRegistry", () => {
|
|
@@ -173,9 +173,21 @@ globalThis.describe("LocalPlatformRegistry.tapPort", () => {
|
|
|
173
173
|
globalThis.test("ignores a platform that never published an entry", () => LocalPlatformRegistry$ReventlessLocal.publishTapPort(4000, 1, tempRoot()));
|
|
174
174
|
globalThis.test("round-trips the port a platform serving a tap publishes", () => {
|
|
175
175
|
let cwd = tempRoot();
|
|
176
|
-
LocalPlatformRegistry$ReventlessLocal.write(4000, "http://localhost:4000/graphql", "http://localhost:4000/__inmemory/login", memoryStore, 4100, undefined, cwd);
|
|
176
|
+
LocalPlatformRegistry$ReventlessLocal.write(4000, "http://localhost:4000/graphql", "http://localhost:4000/__inmemory/login", memoryStore, 4100, undefined, undefined, cwd);
|
|
177
177
|
globalThis.expect(LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).map(e => e.tapPort)).toEqual([4100]);
|
|
178
178
|
});
|
|
179
|
+
globalThis.test("names where the platform API is served, and keeps it when a port is published", () => {
|
|
180
|
+
let cwd = tempRoot();
|
|
181
|
+
LocalPlatformRegistry$ReventlessLocal.write(4000, "http://localhost:4000/graphql", "http://localhost:4000/__inmemory/login", memoryStore, undefined, "http://localhost:4001/graphql", undefined, cwd);
|
|
182
|
+
LocalPlatformRegistry$ReventlessLocal.publishTapPort(4000, 4100, cwd);
|
|
183
|
+
globalThis.expect(LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).map(e => [
|
|
184
|
+
e.platformEndpoint,
|
|
185
|
+
e.tapPort
|
|
186
|
+
])).toEqual([[
|
|
187
|
+
"http://localhost:4001/graphql",
|
|
188
|
+
4100
|
|
189
|
+
]]);
|
|
190
|
+
});
|
|
179
191
|
});
|
|
180
192
|
|
|
181
193
|
let deadPid = 2147483646;
|
|
@@ -28,7 +28,7 @@ let memoryStore = {
|
|
|
28
28
|
|
|
29
29
|
function writeAt(cwd, port, store, pidOpt) {
|
|
30
30
|
let pid = pidOpt !== undefined ? pidOpt : process.pid;
|
|
31
|
-
return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, pid, cwd);
|
|
31
|
+
return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, undefined, pid, cwd);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function withoutBypass(f) {
|
|
@@ -610,8 +610,9 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
610
610
|
// TestState, TestStateEdge, TestStateConnection, TestStateFilter (connection filter)
|
|
611
611
|
// + TestStateRef (the reference door's projection)
|
|
612
612
|
// + CommandResult union + CommandAccepted + CommandRejectionCause + CommandRejected
|
|
613
|
-
// + CommandPending (auto-injected whenever the fragment emits any mutation field)
|
|
614
|
-
|
|
613
|
+
// + CommandPending (auto-injected whenever the fragment emits any mutation field)
|
|
614
|
+
// + RowChange (every view row's last change).
|
|
615
|
+
expect(inspection.types->Array.length)->toBe(11)
|
|
615
616
|
expect(inspection.mutations->Array.length)->toBe(1)
|
|
616
617
|
// single Test_State(id), list Test_States(...), Test_StatesByIds(ids: [String!]!)
|
|
617
618
|
// and Test_StatesRefs(ids: [ID!]!) — the by-ids read projected to a reference.
|
|
@@ -430,7 +430,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
|
|
|
430
430
|
authorization: undefined
|
|
431
431
|
}]);
|
|
432
432
|
let inspection = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment);
|
|
433
|
-
globalThis.expect(inspection.types.length).toBe(
|
|
433
|
+
globalThis.expect(inspection.types.length).toBe(11);
|
|
434
434
|
globalThis.expect(inspection.mutations.length).toBe(1);
|
|
435
435
|
globalThis.expect(inspection.queries.length).toBe(4);
|
|
436
436
|
globalThis.expect(inspection.sdlPreview.includes("type TestState")).toBe(true);
|