@reventlessdev/reventless-core 3.0.0-alpha.295 → 3.0.0-alpha.297
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 +33 -0
- package/package.json +6 -6
- package/src/admin/Platform_ComponentDefinitionsApi.res +3 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res +15 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +13 -0
- package/src/components/AutomationSlice/AutomationSlice.res +2 -0
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +126 -9
- package/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs +90 -10
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +122 -1
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +101 -2
- package/src/components/AutomationSlice/AutomationSlice_Route.res +3 -1
- package/src/components/AutomationSlice/AutomationSlice_Route.res.mjs +2 -1
- package/src/components/AutomationSlice/AutomationSlice_Schedules.res +149 -0
- package/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs +193 -0
- package/src/components/Dcb/Dcb_Builder.res +35 -4
- package/src/components/Dcb/Dcb_Builder.res.mjs +33 -5
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +1 -1
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs +3 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +1 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res.mjs +3 -1
- package/src/plugin/component/Plugin_Builder.res +10 -3
- package/src/plugin/component/Plugin_Builder.res.mjs +8 -2
- package/src/plugin/component/Plugin_Structure.res +7 -0
- package/src/plugin/component/Plugin_Structure.res.mjs +5 -2
- package/tests/admin/AdminApiSchemaDriftTest.res +1 -0
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +4 -1
- package/tests/automationslice/AutomationSliceSchedulesTest.res +143 -0
- package/tests/automationslice/AutomationSliceSchedulesTest.res.mjs +247 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// A slice's schedule rows, and how the platform is brought in line with them: the
|
|
2
|
+
// declared time is written unless an event set or cleared the slice-wide key, the
|
|
3
|
+
// platform gets what it is missing and loses what the rows do not hold, and a
|
|
4
|
+
// once-only time already past is not put again.
|
|
5
|
+
|
|
6
|
+
open JestGlobals
|
|
7
|
+
|
|
8
|
+
module Schedules = AutomationSlice_Schedules
|
|
9
|
+
|
|
10
|
+
let at = iso => Date.fromString(iso)->Date.getTime
|
|
11
|
+
let daily6 = Reventless.Schedule.Calendar({calendar: Daily({hour: 6, minute: 0})})
|
|
12
|
+
let daily18 = Reventless.Schedule.Calendar({calendar: Daily({hour: 18, minute: 0})})
|
|
13
|
+
let now = at("2026-10-16T12:00:00Z")
|
|
14
|
+
|
|
15
|
+
// A platform that holds its schedules in a dict and records what it was told.
|
|
16
|
+
let fakePlatform = (~held: array<string>=[]) => {
|
|
17
|
+
let schedules = Dict.fromArray(held->Array.map(name => (name, daily6)))
|
|
18
|
+
let calls = []
|
|
19
|
+
let platform: ReventlessInfra.AutomationSlice.platformSchedules = {
|
|
20
|
+
put: async (~key, time) => {
|
|
21
|
+
calls->Array.push(`put ${key}`)
|
|
22
|
+
schedules->Dict.set(key, time)
|
|
23
|
+
},
|
|
24
|
+
removeName: async name => {
|
|
25
|
+
calls->Array.push(`remove ${name}`)
|
|
26
|
+
schedules->Dict.delete(name)
|
|
27
|
+
},
|
|
28
|
+
names: async () => schedules->Dict.keysToArray,
|
|
29
|
+
nameOf: key => key,
|
|
30
|
+
}
|
|
31
|
+
(platform, schedules, calls)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let liveKeys = rows =>
|
|
35
|
+
rows
|
|
36
|
+
->Dict.valuesToArray
|
|
37
|
+
->Array.filter(row => row.Schedules.time->Option.isSome)
|
|
38
|
+
->Array.map(row => row.key)
|
|
39
|
+
->Array.toSorted(String.compare)
|
|
40
|
+
|
|
41
|
+
let set = (rows, key, time, ~recordedAt=now) =>
|
|
42
|
+
rows->Schedules.apply(Set({key, time}), ~setBy="event", ~recordedAt)
|
|
43
|
+
let clear = (rows, key) => rows->Schedules.apply(Clear({key: key}), ~setBy="event", ~recordedAt=now)
|
|
44
|
+
|
|
45
|
+
describe("the declared time", () => {
|
|
46
|
+
testSync("is written when no event has touched the slice-wide key", () => {
|
|
47
|
+
let rows = Dict.make()
|
|
48
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
49
|
+
expect(rows->Dict.get("default")->Option.flatMap(Schedules.timeOf))->toEqual(Some(daily6))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
testSync("does not replace a time an event set", () => {
|
|
53
|
+
let rows = Dict.make()
|
|
54
|
+
let _ = rows->set("default", daily18)
|
|
55
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
56
|
+
expect(rows->Dict.get("default")->Option.flatMap(Schedules.timeOf))->toEqual(Some(daily18))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
testSync("does not come back after an event cleared it", () => {
|
|
60
|
+
let rows = Dict.make()
|
|
61
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
62
|
+
let _ = rows->clear("default")
|
|
63
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
64
|
+
expect(rows->liveKeys)->toEqual([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
testSync("follows a changed declaration, and goes with it", () => {
|
|
68
|
+
let rows = Dict.make()
|
|
69
|
+
rows->Schedules.declare(Some(daily6), ~now)
|
|
70
|
+
rows->Schedules.declare(Some(daily18), ~now)
|
|
71
|
+
let changed = rows->Dict.get("default")->Option.flatMap(Schedules.timeOf)
|
|
72
|
+
rows->Schedules.declare(None, ~now)
|
|
73
|
+
expect((changed, rows->liveKeys))->toEqual((Some(daily18), []))
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe("a change an event makes", () => {
|
|
78
|
+
testSync("counts an After from when the event was recorded", () => {
|
|
79
|
+
let rows = Dict.make()
|
|
80
|
+
let _ =
|
|
81
|
+
rows->set("o1", Once({moment: After(Minutes(10))}), ~recordedAt=at("2026-10-16T09:00:00Z"))
|
|
82
|
+
expect(rows->Dict.get("o1")->Option.flatMap(Schedules.timeOf))->toEqual(
|
|
83
|
+
Some(
|
|
84
|
+
Reventless.Schedule.Once({
|
|
85
|
+
moment: At({year: 2026, month: 10, day: 16, hour: 9, minute: 10}),
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
testSync("refuses a time that cannot fire, and changes nothing", () => {
|
|
92
|
+
let rows = Dict.make()
|
|
93
|
+
let refused = rows->set("r1", Calendar({calendar: Daily({hour: 25, minute: 0})}))
|
|
94
|
+
expect((refused->Result.isError, rows->liveKeys))->toEqual((true, []))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
testSync("removes a once-only schedule when it fires, not a recurring one", () => {
|
|
98
|
+
let rows = Dict.make()
|
|
99
|
+
let _ = rows->set("o1", Once({moment: After(Minutes(10))}))
|
|
100
|
+
let _ = rows->set("r1", daily18)
|
|
101
|
+
rows->Schedules.fired(~key="o1")
|
|
102
|
+
rows->Schedules.fired(~key="r1")
|
|
103
|
+
expect(rows->liveKeys)->toEqual(["r1"])
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("the reconcile", () => {
|
|
108
|
+
testPromise(
|
|
109
|
+
"puts what the platform is missing and removes what the rows do not hold",
|
|
110
|
+
async () => {
|
|
111
|
+
let rows = Dict.make()
|
|
112
|
+
let _ = rows->set("r1", daily18)
|
|
113
|
+
let _ = rows->set("r2", daily18)
|
|
114
|
+
let (platform, held, _) = fakePlatform(~held=["r2", "stray"])
|
|
115
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=None, ~now)
|
|
116
|
+
expect(held->Dict.keysToArray->Array.toSorted(String.compare))->toEqual(["r1", "r2"])
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
testPromise("does not put again a schedule an event set and the platform holds", async () => {
|
|
121
|
+
let rows = Dict.make()
|
|
122
|
+
let _ = rows->set("r1", daily18)
|
|
123
|
+
let (platform, _, calls) = fakePlatform(~held=["r1"])
|
|
124
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=None, ~now)
|
|
125
|
+
expect(calls)->toEqual([])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
testPromise("puts the declared time again, in case it changed", async () => {
|
|
129
|
+
let rows = Dict.make()
|
|
130
|
+
let (platform, _, calls) = fakePlatform(~held=["default"])
|
|
131
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=Some(daily6), ~now)
|
|
132
|
+
expect(calls)->toEqual(["put default"])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
testPromise("drops a once-only time already past, from the rows and the platform", async () => {
|
|
136
|
+
let rows = Dict.make()
|
|
137
|
+
let _ =
|
|
138
|
+
rows->set("o1", Once({moment: After(Minutes(10))}), ~recordedAt=at("2026-10-16T09:00:00Z"))
|
|
139
|
+
let (platform, held, _) = fakePlatform(~held=["o1"])
|
|
140
|
+
await rows->Schedules.reconcile(platform, ~declaredTime=None, ~now)
|
|
141
|
+
expect((rows->liveKeys, held->Dict.keysToArray))->toEqual(([], []))
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
|
+
import * as AutomationSlice_Schedules$ReventlessCore from "../../src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs";
|
|
8
|
+
|
|
9
|
+
function at(iso) {
|
|
10
|
+
return new Date(iso).getTime();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let daily6 = {
|
|
14
|
+
TAG: "Calendar",
|
|
15
|
+
calendar: {
|
|
16
|
+
TAG: "Daily",
|
|
17
|
+
hour: 6,
|
|
18
|
+
minute: 0
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let daily18 = {
|
|
23
|
+
TAG: "Calendar",
|
|
24
|
+
calendar: {
|
|
25
|
+
TAG: "Daily",
|
|
26
|
+
hour: 18,
|
|
27
|
+
minute: 0
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let now = new Date("2026-10-16T12:00:00Z").getTime();
|
|
32
|
+
|
|
33
|
+
function fakePlatform(heldOpt) {
|
|
34
|
+
let held = heldOpt !== undefined ? heldOpt : [];
|
|
35
|
+
let schedules = Object.fromEntries(held.map(name => [
|
|
36
|
+
name,
|
|
37
|
+
daily6
|
|
38
|
+
]));
|
|
39
|
+
let calls = [];
|
|
40
|
+
let platform_put = async (key, time) => {
|
|
41
|
+
calls.push(`put ` + key);
|
|
42
|
+
schedules[key] = time;
|
|
43
|
+
};
|
|
44
|
+
let platform_removeName = async name => {
|
|
45
|
+
calls.push(`remove ` + name);
|
|
46
|
+
return Stdlib_Dict.$$delete(schedules, name);
|
|
47
|
+
};
|
|
48
|
+
let platform_names = async () => Object.keys(schedules);
|
|
49
|
+
let platform_nameOf = key => key;
|
|
50
|
+
let platform = {
|
|
51
|
+
put: platform_put,
|
|
52
|
+
removeName: platform_removeName,
|
|
53
|
+
names: platform_names,
|
|
54
|
+
nameOf: platform_nameOf
|
|
55
|
+
};
|
|
56
|
+
return [
|
|
57
|
+
platform,
|
|
58
|
+
schedules,
|
|
59
|
+
calls
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function liveKeys(rows) {
|
|
64
|
+
return Object.values(rows).filter(row => Stdlib_Option.isSome(row.time)).map(row => row.key).toSorted(Primitive_string.compare);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function set(rows, key, time, recordedAtOpt) {
|
|
68
|
+
let recordedAt = recordedAtOpt !== undefined ? recordedAtOpt : now;
|
|
69
|
+
return AutomationSlice_Schedules$ReventlessCore.apply(rows, {
|
|
70
|
+
TAG: "Set",
|
|
71
|
+
key: key,
|
|
72
|
+
time: time
|
|
73
|
+
}, "event", recordedAt);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function clear(rows, key) {
|
|
77
|
+
return AutomationSlice_Schedules$ReventlessCore.apply(rows, {
|
|
78
|
+
TAG: "Clear",
|
|
79
|
+
key: key
|
|
80
|
+
}, "event", now);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
globalThis.describe("the declared time", () => {
|
|
84
|
+
globalThis.test("is written when no event has touched the slice-wide key", () => {
|
|
85
|
+
let rows = {};
|
|
86
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
87
|
+
globalThis.expect(Stdlib_Option.flatMap(rows["default"], AutomationSlice_Schedules$ReventlessCore.timeOf)).toEqual(daily6);
|
|
88
|
+
});
|
|
89
|
+
globalThis.test("does not replace a time an event set", () => {
|
|
90
|
+
let rows = {};
|
|
91
|
+
set(rows, "default", daily18, undefined);
|
|
92
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
93
|
+
globalThis.expect(Stdlib_Option.flatMap(rows["default"], AutomationSlice_Schedules$ReventlessCore.timeOf)).toEqual(daily18);
|
|
94
|
+
});
|
|
95
|
+
globalThis.test("does not come back after an event cleared it", () => {
|
|
96
|
+
let rows = {};
|
|
97
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
98
|
+
clear(rows, "default");
|
|
99
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
100
|
+
globalThis.expect(liveKeys(rows)).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
globalThis.test("follows a changed declaration, and goes with it", () => {
|
|
103
|
+
let rows = {};
|
|
104
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily6, now);
|
|
105
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, daily18, now);
|
|
106
|
+
let changed = Stdlib_Option.flatMap(rows["default"], AutomationSlice_Schedules$ReventlessCore.timeOf);
|
|
107
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, undefined, now);
|
|
108
|
+
globalThis.expect([
|
|
109
|
+
changed,
|
|
110
|
+
liveKeys(rows)
|
|
111
|
+
]).toEqual([
|
|
112
|
+
daily18,
|
|
113
|
+
[]
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
globalThis.describe("a change an event makes", () => {
|
|
119
|
+
globalThis.test("counts an After from when the event was recorded", () => {
|
|
120
|
+
let rows = {};
|
|
121
|
+
set(rows, "o1", {
|
|
122
|
+
TAG: "Once",
|
|
123
|
+
moment: {
|
|
124
|
+
TAG: "After",
|
|
125
|
+
_0: {
|
|
126
|
+
TAG: "Minutes",
|
|
127
|
+
_0: 10
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, new Date("2026-10-16T09:00:00Z").getTime());
|
|
131
|
+
globalThis.expect(Stdlib_Option.flatMap(rows["o1"], AutomationSlice_Schedules$ReventlessCore.timeOf)).toEqual({
|
|
132
|
+
TAG: "Once",
|
|
133
|
+
moment: {
|
|
134
|
+
TAG: "At",
|
|
135
|
+
_0: {
|
|
136
|
+
year: 2026,
|
|
137
|
+
month: 10,
|
|
138
|
+
day: 16,
|
|
139
|
+
hour: 9,
|
|
140
|
+
minute: 10
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
globalThis.test("refuses a time that cannot fire, and changes nothing", () => {
|
|
146
|
+
let rows = {};
|
|
147
|
+
let refused = set(rows, "r1", {
|
|
148
|
+
TAG: "Calendar",
|
|
149
|
+
calendar: {
|
|
150
|
+
TAG: "Daily",
|
|
151
|
+
hour: 25,
|
|
152
|
+
minute: 0
|
|
153
|
+
}
|
|
154
|
+
}, undefined);
|
|
155
|
+
globalThis.expect([
|
|
156
|
+
Stdlib_Result.isError(refused),
|
|
157
|
+
liveKeys(rows)
|
|
158
|
+
]).toEqual([
|
|
159
|
+
true,
|
|
160
|
+
[]
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
globalThis.test("removes a once-only schedule when it fires, not a recurring one", () => {
|
|
164
|
+
let rows = {};
|
|
165
|
+
set(rows, "o1", {
|
|
166
|
+
TAG: "Once",
|
|
167
|
+
moment: {
|
|
168
|
+
TAG: "After",
|
|
169
|
+
_0: {
|
|
170
|
+
TAG: "Minutes",
|
|
171
|
+
_0: 10
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, undefined);
|
|
175
|
+
set(rows, "r1", daily18, undefined);
|
|
176
|
+
AutomationSlice_Schedules$ReventlessCore.fired(rows, "o1");
|
|
177
|
+
AutomationSlice_Schedules$ReventlessCore.fired(rows, "r1");
|
|
178
|
+
globalThis.expect(liveKeys(rows)).toEqual(["r1"]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
globalThis.describe("the reconcile", () => {
|
|
183
|
+
globalThis.test("puts what the platform is missing and removes what the rows do not hold", async () => {
|
|
184
|
+
let rows = {};
|
|
185
|
+
set(rows, "r1", daily18, undefined);
|
|
186
|
+
set(rows, "r2", daily18, undefined);
|
|
187
|
+
let match = fakePlatform([
|
|
188
|
+
"r2",
|
|
189
|
+
"stray"
|
|
190
|
+
]);
|
|
191
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], undefined, now);
|
|
192
|
+
globalThis.expect(Object.keys(match[1]).toSorted(Primitive_string.compare)).toEqual([
|
|
193
|
+
"r1",
|
|
194
|
+
"r2"
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
globalThis.test("does not put again a schedule an event set and the platform holds", async () => {
|
|
198
|
+
let rows = {};
|
|
199
|
+
set(rows, "r1", daily18, undefined);
|
|
200
|
+
let match = fakePlatform(["r1"]);
|
|
201
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], undefined, now);
|
|
202
|
+
globalThis.expect(match[2]).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
globalThis.test("puts the declared time again, in case it changed", async () => {
|
|
205
|
+
let rows = {};
|
|
206
|
+
let match = fakePlatform(["default"]);
|
|
207
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], daily6, now);
|
|
208
|
+
globalThis.expect(match[2]).toEqual(["put default"]);
|
|
209
|
+
});
|
|
210
|
+
globalThis.test("drops a once-only time already past, from the rows and the platform", async () => {
|
|
211
|
+
let rows = {};
|
|
212
|
+
set(rows, "o1", {
|
|
213
|
+
TAG: "Once",
|
|
214
|
+
moment: {
|
|
215
|
+
TAG: "After",
|
|
216
|
+
_0: {
|
|
217
|
+
TAG: "Minutes",
|
|
218
|
+
_0: 10
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}, new Date("2026-10-16T09:00:00Z").getTime());
|
|
222
|
+
let match = fakePlatform(["o1"]);
|
|
223
|
+
await AutomationSlice_Schedules$ReventlessCore.reconcile(rows, match[0], undefined, now);
|
|
224
|
+
globalThis.expect([
|
|
225
|
+
liveKeys(rows),
|
|
226
|
+
Object.keys(match[1])
|
|
227
|
+
]).toEqual([
|
|
228
|
+
[],
|
|
229
|
+
[]
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let Schedules;
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
Schedules,
|
|
238
|
+
at,
|
|
239
|
+
daily6,
|
|
240
|
+
daily18,
|
|
241
|
+
now,
|
|
242
|
+
fakePlatform,
|
|
243
|
+
liveKeys,
|
|
244
|
+
set,
|
|
245
|
+
clear,
|
|
246
|
+
}
|
|
247
|
+
/* now Not a pure module */
|