@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
@@ -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
- if (Object.keys(fieldResolvers).length !== 0) {
753
- server.registerFieldResolvers(returnTypeName, fieldResolvers);
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
- expect(inspection.types->Array.length)->toBe(10)
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(10);
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);