@reventlessdev/reventless-seed-aws 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +146 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/.sourcedirs.json +1 -0
- package/lib/bs/compiler-info.json +2 -2
- package/lib/bs/src/ReventlessSeedAws.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws.res +87 -75
- package/lib/bs/src/ReventlessSeedAws.res.mjs +34 -30
- package/lib/bs/src/ReventlessSeedAws_Quiesce.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.res +287 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.res.mjs +205 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.res +385 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res +1098 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +991 -0
- package/lib/bs/tests/EndpointResolutionTest.ast +0 -0
- package/lib/bs/tests/EndpointResolutionTest.cmi +0 -0
- package/lib/bs/tests/EndpointResolutionTest.cmj +0 -0
- package/lib/bs/tests/EndpointResolutionTest.cmt +0 -0
- package/lib/bs/tests/EndpointResolutionTest.res +83 -0
- package/lib/bs/tests/EndpointResolutionTest.res.mjs +115 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmj +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.res +84 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.res.mjs +102 -0
- package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmj +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.res +170 -0
- package/lib/bs/tests/ObjectStoreResetTest.res.mjs +211 -0
- package/lib/bs/tests/QuiesceResetTest.ast +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmi +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmj +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmt +0 -0
- package/lib/bs/tests/QuiesceResetTest.res +133 -0
- package/lib/bs/tests/QuiesceResetTest.res.mjs +141 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/EndpointResolutionTest.ast +0 -0
- package/lib/ocaml/EndpointResolutionTest.cmi +0 -0
- package/lib/ocaml/EndpointResolutionTest.cmj +0 -0
- package/lib/ocaml/EndpointResolutionTest.cmt +0 -0
- package/lib/ocaml/EndpointResolutionTest.res +83 -0
- package/lib/ocaml/ObjectStoreReconcileTest.ast +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmj +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.res +84 -0
- package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmj +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreResetTest.res +170 -0
- package/lib/ocaml/QuiesceResetTest.ast +0 -0
- package/lib/ocaml/QuiesceResetTest.cmi +0 -0
- package/lib/ocaml/QuiesceResetTest.cmj +0 -0
- package/lib/ocaml/QuiesceResetTest.cmt +0 -0
- package/lib/ocaml/QuiesceResetTest.res +133 -0
- package/lib/ocaml/ReventlessSeedAws.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws.res +87 -75
- package/lib/ocaml/ReventlessSeedAws_Quiesce.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.res +287 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.res +385 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.res +1098 -0
- package/package.json +7 -4
- package/rescript.json +9 -1
- package/src/ReventlessSeedAws.res +87 -75
- package/src/ReventlessSeedAws.res.mjs +34 -30
- package/src/ReventlessSeedAws_Quiesce.res +287 -0
- package/src/ReventlessSeedAws_Quiesce.res.mjs +205 -0
- package/src/ReventlessSeedAws_Reconcile.res +385 -0
- package/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
- package/src/ReventlessSeedAws_Reset.res +1098 -0
- package/src/ReventlessSeedAws_Reset.res.mjs +991 -0
- package/tests/EndpointResolutionTest.res +83 -0
- package/tests/EndpointResolutionTest.res.mjs +115 -0
- package/tests/ObjectStoreReconcileTest.res +84 -0
- package/tests/ObjectStoreReconcileTest.res.mjs +102 -0
- package/tests/ObjectStoreResetTest.res +170 -0
- package/tests/ObjectStoreResetTest.res.mjs +211 -0
- package/tests/QuiesceResetTest.res +133 -0
- package/tests/QuiesceResetTest.res.mjs +141 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as ReventlessSeedAws_Reset from "../src/ReventlessSeedAws_Reset.res.mjs";
|
|
6
|
+
|
|
7
|
+
function obj(entries) {
|
|
8
|
+
return Object.fromEntries(entries);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function str(prim) {
|
|
12
|
+
return prim;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function storeEntry(bucket, prefix) {
|
|
16
|
+
return Object.fromEntries([
|
|
17
|
+
[
|
|
18
|
+
"bucketName",
|
|
19
|
+
bucket
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"keyPrefix",
|
|
23
|
+
prefix
|
|
24
|
+
]
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function output(entries) {
|
|
29
|
+
return Object.fromEntries(entries);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function store(qualified, bucket, prefix) {
|
|
33
|
+
let match = Stdlib_Option.getOr(ReventlessSeedAws_Reset.splitQualified(qualified), [
|
|
34
|
+
"",
|
|
35
|
+
qualified
|
|
36
|
+
]);
|
|
37
|
+
return {
|
|
38
|
+
qualified: qualified,
|
|
39
|
+
plugin: match[0],
|
|
40
|
+
store: match[1],
|
|
41
|
+
bucketName: bucket,
|
|
42
|
+
keyPrefix: prefix
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
globalThis.describe("parseObjectStores", () => {
|
|
47
|
+
globalThis.test("splits the qualified key into plugin and store", () => {
|
|
48
|
+
let entries = [[
|
|
49
|
+
"Catalog.productImages",
|
|
50
|
+
storeEntry("alpha-stores", "productImages")
|
|
51
|
+
]];
|
|
52
|
+
let other = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
|
|
53
|
+
if (other.TAG === "Ok") {
|
|
54
|
+
let match = other._0;
|
|
55
|
+
if (match.length === 1) {
|
|
56
|
+
let s = match[0];
|
|
57
|
+
globalThis.expect([
|
|
58
|
+
s.plugin,
|
|
59
|
+
s.store,
|
|
60
|
+
s.bucketName,
|
|
61
|
+
s.keyPrefix
|
|
62
|
+
]).toEqual([
|
|
63
|
+
"Catalog",
|
|
64
|
+
"productImages",
|
|
65
|
+
"alpha-stores",
|
|
66
|
+
"productImages"
|
|
67
|
+
]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
JestGlobals.fail(`expected one parsed store, got ` + Stdlib_Option.getOr(JSON.stringify(other), "?"));
|
|
72
|
+
});
|
|
73
|
+
globalThis.test("splits at the first dot only", () => {
|
|
74
|
+
let match = ReventlessSeedAws_Reset.splitQualified("Catalog.product.images");
|
|
75
|
+
if (match !== undefined) {
|
|
76
|
+
globalThis.expect([
|
|
77
|
+
match[0],
|
|
78
|
+
match[1]
|
|
79
|
+
]).toEqual([
|
|
80
|
+
"Catalog",
|
|
81
|
+
"product.images"
|
|
82
|
+
]);
|
|
83
|
+
return;
|
|
84
|
+
} else {
|
|
85
|
+
return JestGlobals.fail("expected a split");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
globalThis.test("absent output yields no stores", () => {
|
|
89
|
+
let stores = ReventlessSeedAws_Reset.parseObjectStores(undefined);
|
|
90
|
+
if (stores.TAG !== "Ok") {
|
|
91
|
+
return JestGlobals.fail(stores._0);
|
|
92
|
+
}
|
|
93
|
+
globalThis.expect(stores._0.length).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
globalThis.test("a malformed entry is an error, not a skip", () => {
|
|
96
|
+
let entries = [[
|
|
97
|
+
"Catalog.productImages",
|
|
98
|
+
Object.fromEntries([[
|
|
99
|
+
"keyPrefix",
|
|
100
|
+
"x"
|
|
101
|
+
]])
|
|
102
|
+
]];
|
|
103
|
+
let message = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
|
|
104
|
+
if (message.TAG === "Ok") {
|
|
105
|
+
return JestGlobals.fail("expected a malformed entry to be refused");
|
|
106
|
+
}
|
|
107
|
+
globalThis.expect(message._0.includes("Catalog.productImages")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
globalThis.test("an unqualified key is an error", () => {
|
|
110
|
+
let entries = [[
|
|
111
|
+
"productImages",
|
|
112
|
+
storeEntry("alpha-stores", "productImages")
|
|
113
|
+
]];
|
|
114
|
+
let message = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
|
|
115
|
+
if (message.TAG === "Ok") {
|
|
116
|
+
return JestGlobals.fail("expected an unqualified key to be refused");
|
|
117
|
+
}
|
|
118
|
+
globalThis.expect(message._0.includes("productImages")).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
globalThis.describe("validateStores", () => {
|
|
123
|
+
globalThis.test("distinct prefixes in one bucket are fine", () => {
|
|
124
|
+
globalThis.expect(ReventlessSeedAws_Reset.validateStores([
|
|
125
|
+
store("Catalog.productImages", "alpha-stores", "productImages"),
|
|
126
|
+
store("Ordering.labels", "alpha-stores", "labels")
|
|
127
|
+
])).toEqual({
|
|
128
|
+
TAG: "Ok",
|
|
129
|
+
_0: undefined
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
globalThis.test("two plugins on one prefix are refused", () => {
|
|
133
|
+
let message = ReventlessSeedAws_Reset.validateStores([
|
|
134
|
+
store("Catalog.productImages", "alpha-stores", "productImages"),
|
|
135
|
+
store("Ordering.productImages", "alpha-stores", "productImages")
|
|
136
|
+
]);
|
|
137
|
+
if (message.TAG === "Ok") {
|
|
138
|
+
return JestGlobals.fail("expected a collision to be refused");
|
|
139
|
+
}
|
|
140
|
+
let message$1 = message._0;
|
|
141
|
+
globalThis.expect(message$1.includes("Catalog.productImages") && message$1.includes("Ordering.productImages")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
globalThis.test("an enclosing prefix is refused", () => {
|
|
144
|
+
let message = ReventlessSeedAws_Reset.validateStores([
|
|
145
|
+
store("Legacy.catalog", "alpha-stores", "Catalog"),
|
|
146
|
+
store("Catalog.productImages", "alpha-stores", "Catalog/productImages")
|
|
147
|
+
]);
|
|
148
|
+
if (message.TAG === "Ok") {
|
|
149
|
+
return JestGlobals.fail("expected an enclosing prefix to be refused");
|
|
150
|
+
}
|
|
151
|
+
globalThis.expect(message._0.includes("encloses")).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
globalThis.test("one prefix in two different buckets is fine", () => {
|
|
154
|
+
globalThis.expect(ReventlessSeedAws_Reset.validateStores([
|
|
155
|
+
store("Catalog.productImages", "catalog-productImages", "productImages"),
|
|
156
|
+
store("Ordering.productImages", "ordering-productImages", "productImages")
|
|
157
|
+
])).toEqual({
|
|
158
|
+
TAG: "Ok",
|
|
159
|
+
_0: undefined
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
globalThis.test("a sibling prefix sharing a character run is fine", () => {
|
|
163
|
+
globalThis.expect(ReventlessSeedAws_Reset.validateStores([
|
|
164
|
+
store("Catalog.images", "alpha-stores", "images"),
|
|
165
|
+
store("Catalog.imagesArchive", "alpha-stores", "images-archive")
|
|
166
|
+
])).toEqual({
|
|
167
|
+
TAG: "Ok",
|
|
168
|
+
_0: undefined
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
globalThis.test("an empty or slash-bearing store name is refused", () => {
|
|
172
|
+
let match = ReventlessSeedAws_Reset.validateStores([store("Catalog.productImages", "alpha-stores", "")]);
|
|
173
|
+
if (match.TAG === "Ok") {
|
|
174
|
+
JestGlobals.fail("expected an empty prefix to be refused");
|
|
175
|
+
}
|
|
176
|
+
let match$1 = ReventlessSeedAws_Reset.validateStores([store("Catalog.a/b", "alpha-stores", "a/b")]);
|
|
177
|
+
if (match$1.TAG === "Ok") {
|
|
178
|
+
return JestGlobals.fail("expected a store name containing a slash to be refused");
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
globalThis.describe("pluginOf", () => {
|
|
184
|
+
globalThis.test("prefers the declared plugin name over the label", () => {
|
|
185
|
+
globalThis.expect(ReventlessSeedAws_Reset.pluginOf({
|
|
186
|
+
projectDir: "../catalog-aws",
|
|
187
|
+
label: "catalog",
|
|
188
|
+
group: "Domain",
|
|
189
|
+
plugin: "Catalog"
|
|
190
|
+
})).toBe("Catalog");
|
|
191
|
+
});
|
|
192
|
+
globalThis.test("falls back to the label when no plugin name is declared", () => {
|
|
193
|
+
globalThis.expect(ReventlessSeedAws_Reset.pluginOf({
|
|
194
|
+
projectDir: ".",
|
|
195
|
+
label: "platform",
|
|
196
|
+
group: "Platform"
|
|
197
|
+
})).toBe("platform");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let Reset;
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
Reset,
|
|
205
|
+
obj,
|
|
206
|
+
str,
|
|
207
|
+
storeEntry,
|
|
208
|
+
output,
|
|
209
|
+
store,
|
|
210
|
+
}
|
|
211
|
+
/* Not a pure module */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Holding a stack's runtimes still for the length of a wipe: what gets held, and
|
|
2
|
+
// what the reset says when a store comes back anyway.
|
|
3
|
+
//
|
|
4
|
+
// A truncate is not durable while the runtimes that own the data are running — a
|
|
5
|
+
// slice runtime keeps its TODO list in memory for the life of its execution
|
|
6
|
+
// environment and re-saves every row it holds at the end of each invocation,
|
|
7
|
+
// including the scheduled sweep that carries no events. So the reset now
|
|
8
|
+
// discovers the stack's Lambda functions alongside its stores and holds them at
|
|
9
|
+
// zero concurrency across the wipe.
|
|
10
|
+
//
|
|
11
|
+
// The pure half of that is what these pin: which ARNs resolve to a function to
|
|
12
|
+
// hold, the bounded fan-out the control-plane calls go through, and the failure
|
|
13
|
+
// message — which used to say "re-run to finish" for a condition re-running
|
|
14
|
+
// could not clear. The impure half is the Lambda control-plane calls themselves.
|
|
15
|
+
|
|
16
|
+
open JestGlobals
|
|
17
|
+
|
|
18
|
+
module Reset = ReventlessSeedAws_Reset
|
|
19
|
+
module Quiesce = ReventlessSeedAws_Quiesce
|
|
20
|
+
|
|
21
|
+
describe("classify", () => {
|
|
22
|
+
testSync("reads the function name out of a Lambda ARN", () =>
|
|
23
|
+
expect(Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllAutomationSlices-28b116c"))
|
|
24
|
+
->toEqual(Reset.Function("AllAutomationSlices-28b116c"))
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// The tagging API returns unqualified ARNs, but a qualified one must still
|
|
28
|
+
// yield the function name — every Lambda control-plane call the hold makes
|
|
29
|
+
// takes a name, and "Name:1" is not one.
|
|
30
|
+
testSync("drops a version or alias qualifier", () =>
|
|
31
|
+
expect(Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllReadModels-0287438:42"))
|
|
32
|
+
->toEqual(Reset.Function("AllReadModels-0287438"))
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
testSync("still classifies tables and buckets", () => {
|
|
36
|
+
expect(Reset.classify("arn:aws:dynamodb:eu-west-1:123456789012:table/Products-0ba6849"))
|
|
37
|
+
->toEqual(Reset.Table("Products-0ba6849"))
|
|
38
|
+
expect(Reset.classify("arn:aws:s3:::alpha-product-images"))->toEqual(
|
|
39
|
+
Reset.Bucket("alpha-product-images"),
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
testSync("ignores anything else the tag scope returns", () =>
|
|
44
|
+
expect(Reset.classify("arn:aws:sqs:eu-west-1:123456789012:OrderingDcbCmdTopic-4b3658c"))
|
|
45
|
+
->toEqual(Reset.Other)
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("mapBounded", () => {
|
|
50
|
+
// Results must line up with their inputs regardless of which worker finished
|
|
51
|
+
// first: the hold pairs each result with the function it came from, and a
|
|
52
|
+
// shuffled array would restore the wrong concurrency to the wrong function.
|
|
53
|
+
test("keeps results in input order under out-of-order completion", async () => {
|
|
54
|
+
let delays = [40, 0, 30, 10, 20, 5]
|
|
55
|
+
let out = await delays->Quiesce.mapBounded(~limit=3, async ms => {
|
|
56
|
+
await ReventlessSeed.Seed.Client.sleep(ms)
|
|
57
|
+
ms
|
|
58
|
+
})
|
|
59
|
+
expect(out)->toEqual(delays)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("never runs more than the limit at once", async () => {
|
|
63
|
+
let inFlight = ref(0)
|
|
64
|
+
let peak = ref(0)
|
|
65
|
+
let _ = await Array.make(~length=12, 0)->Quiesce.mapBounded(~limit=4, async _ => {
|
|
66
|
+
inFlight := inFlight.contents + 1
|
|
67
|
+
peak := Math.Int.max(peak.contents, inFlight.contents)
|
|
68
|
+
await ReventlessSeed.Seed.Client.sleep(5)
|
|
69
|
+
inFlight := inFlight.contents - 1
|
|
70
|
+
})
|
|
71
|
+
expect(peak.contents)->toBe(4)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("an empty list spawns no workers and returns nothing", async () => {
|
|
75
|
+
let out = await []->Quiesce.mapBounded(~limit=4, async x => x)
|
|
76
|
+
expect(out->Array.length)->toBe(0)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("refillMessage", () => {
|
|
81
|
+
// The whole point of the change: the operator has to be able to see WHICH
|
|
82
|
+
// store came back, not just a total.
|
|
83
|
+
testSync("names every store that came back, with its count", () => {
|
|
84
|
+
let message = Reset.refillMessage(
|
|
85
|
+
~refilled=[("AutoShipOrderTodo-61f39a2", 12), ("GeocodeCustomerAddressTodo-50d9488", 1)],
|
|
86
|
+
~heldCount=41,
|
|
87
|
+
)
|
|
88
|
+
expect(message->String.includes("AutoShipOrderTodo-61f39a2"))->toBe(true)
|
|
89
|
+
expect(message->String.includes("GeocodeCustomerAddressTodo-50d9488"))->toBe(true)
|
|
90
|
+
expect(message->String.startsWith("13 item(s)/object(s) came back"))->toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Under a hold nothing could START during the wipe, so the only writer left is
|
|
94
|
+
// an invocation already running when the hold landed — and that one really
|
|
95
|
+
// does go away. This is the only case where "re-run" is honest advice.
|
|
96
|
+
testSync("advises re-running only when the runtimes were held", () => {
|
|
97
|
+
let message = Reset.refillMessage(~refilled=[("AutoShipOrderTodo-61f39a2", 12)], ~heldCount=41)
|
|
98
|
+
expect(message->String.includes("already in flight"))->toBe(true)
|
|
99
|
+
expect(message->String.includes("Re-running clears it"))->toBe(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Without a hold, the writer holds the rows in memory and restores them every
|
|
103
|
+
// invocation — the exact condition the old "re-run to finish" sent operators
|
|
104
|
+
// round in circles on. It must not appear here.
|
|
105
|
+
testSync("refuses to advise re-running when the stack was not held", () => {
|
|
106
|
+
let message = Reset.refillMessage(~refilled=[("AutoShipOrderTodo-61f39a2", 12)], ~heldCount=0)
|
|
107
|
+
expect(message->String.includes("re-running will not help"->String.toLowerCase))->toBe(true)
|
|
108
|
+
expect(message->String.includes("SEED_RESET_NO_QUIESCE"))->toBe(true)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe("noQuiesce", () => {
|
|
113
|
+
let set = v => NodeProcess.env->Dict.set("SEED_RESET_NO_QUIESCE", v)
|
|
114
|
+
|
|
115
|
+
// Fail-closed: the hold is what makes the wipe durable, so anything other than
|
|
116
|
+
// a deliberate opt-out keeps it on.
|
|
117
|
+
testSync("defaults to holding the platform", () => {
|
|
118
|
+
set("")
|
|
119
|
+
expect(Reset.noQuiesce())->toBe(false)
|
|
120
|
+
set("0")
|
|
121
|
+
expect(Reset.noQuiesce())->toBe(false)
|
|
122
|
+
set("no")
|
|
123
|
+
expect(Reset.noQuiesce())->toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
testSync("accepts the spellings an operator would actually type", () => {
|
|
127
|
+
["1", "true", "TRUE", " yes "]->Array.forEach(v => {
|
|
128
|
+
set(v)
|
|
129
|
+
expect(Reset.noQuiesce())->toBe(true)
|
|
130
|
+
})
|
|
131
|
+
set("")
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as ReventlessSeedAws_Reset from "../src/ReventlessSeedAws_Reset.res.mjs";
|
|
5
|
+
import * as ReventlessSeedAws_Quiesce from "../src/ReventlessSeedAws_Quiesce.res.mjs";
|
|
6
|
+
import * as Seed_Client$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Client.res.mjs";
|
|
7
|
+
|
|
8
|
+
globalThis.describe("classify", () => {
|
|
9
|
+
globalThis.test("reads the function name out of a Lambda ARN", () => {
|
|
10
|
+
globalThis.expect(ReventlessSeedAws_Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllAutomationSlices-28b116c")).toEqual({
|
|
11
|
+
TAG: "Function",
|
|
12
|
+
_0: "AllAutomationSlices-28b116c"
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
globalThis.test("drops a version or alias qualifier", () => {
|
|
16
|
+
globalThis.expect(ReventlessSeedAws_Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllReadModels-0287438:42")).toEqual({
|
|
17
|
+
TAG: "Function",
|
|
18
|
+
_0: "AllReadModels-0287438"
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
globalThis.test("still classifies tables and buckets", () => {
|
|
22
|
+
globalThis.expect(ReventlessSeedAws_Reset.classify("arn:aws:dynamodb:eu-west-1:123456789012:table/Products-0ba6849")).toEqual({
|
|
23
|
+
TAG: "Table",
|
|
24
|
+
_0: "Products-0ba6849"
|
|
25
|
+
});
|
|
26
|
+
globalThis.expect(ReventlessSeedAws_Reset.classify("arn:aws:s3:::alpha-product-images")).toEqual({
|
|
27
|
+
TAG: "Bucket",
|
|
28
|
+
_0: "alpha-product-images"
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
globalThis.test("ignores anything else the tag scope returns", () => {
|
|
32
|
+
globalThis.expect(ReventlessSeedAws_Reset.classify("arn:aws:sqs:eu-west-1:123456789012:OrderingDcbCmdTopic-4b3658c")).toEqual("Other");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
globalThis.describe("mapBounded", () => {
|
|
37
|
+
globalThis.test("keeps results in input order under out-of-order completion", async () => {
|
|
38
|
+
let delays = [
|
|
39
|
+
40,
|
|
40
|
+
0,
|
|
41
|
+
30,
|
|
42
|
+
10,
|
|
43
|
+
20,
|
|
44
|
+
5
|
|
45
|
+
];
|
|
46
|
+
let out = await ReventlessSeedAws_Quiesce.mapBounded(delays, 3, async ms => {
|
|
47
|
+
await Seed_Client$ReventlessSeed.sleep(ms);
|
|
48
|
+
return ms;
|
|
49
|
+
});
|
|
50
|
+
globalThis.expect(out).toEqual(delays);
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("never runs more than the limit at once", async () => {
|
|
53
|
+
let inFlight = {
|
|
54
|
+
contents: 0
|
|
55
|
+
};
|
|
56
|
+
let peak = {
|
|
57
|
+
contents: 0
|
|
58
|
+
};
|
|
59
|
+
await ReventlessSeedAws_Quiesce.mapBounded(Stdlib_Array.make(12, 0), 4, async param => {
|
|
60
|
+
inFlight.contents = inFlight.contents + 1 | 0;
|
|
61
|
+
peak.contents = Math.max(peak.contents, inFlight.contents);
|
|
62
|
+
await Seed_Client$ReventlessSeed.sleep(5);
|
|
63
|
+
inFlight.contents = inFlight.contents - 1 | 0;
|
|
64
|
+
});
|
|
65
|
+
globalThis.expect(peak.contents).toBe(4);
|
|
66
|
+
});
|
|
67
|
+
globalThis.test("an empty list spawns no workers and returns nothing", async () => {
|
|
68
|
+
let out = await ReventlessSeedAws_Quiesce.mapBounded([], 4, async x => x);
|
|
69
|
+
globalThis.expect(out.length).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
globalThis.describe("refillMessage", () => {
|
|
74
|
+
globalThis.test("names every store that came back, with its count", () => {
|
|
75
|
+
let message = ReventlessSeedAws_Reset.refillMessage([
|
|
76
|
+
[
|
|
77
|
+
"AutoShipOrderTodo-61f39a2",
|
|
78
|
+
12
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
"GeocodeCustomerAddressTodo-50d9488",
|
|
82
|
+
1
|
|
83
|
+
]
|
|
84
|
+
], 41);
|
|
85
|
+
globalThis.expect(message.includes("AutoShipOrderTodo-61f39a2")).toBe(true);
|
|
86
|
+
globalThis.expect(message.includes("GeocodeCustomerAddressTodo-50d9488")).toBe(true);
|
|
87
|
+
globalThis.expect(message.startsWith("13 item(s)/object(s) came back")).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
globalThis.test("advises re-running only when the runtimes were held", () => {
|
|
90
|
+
let message = ReventlessSeedAws_Reset.refillMessage([[
|
|
91
|
+
"AutoShipOrderTodo-61f39a2",
|
|
92
|
+
12
|
|
93
|
+
]], 41);
|
|
94
|
+
globalThis.expect(message.includes("already in flight")).toBe(true);
|
|
95
|
+
globalThis.expect(message.includes("Re-running clears it")).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
globalThis.test("refuses to advise re-running when the stack was not held", () => {
|
|
98
|
+
let message = ReventlessSeedAws_Reset.refillMessage([[
|
|
99
|
+
"AutoShipOrderTodo-61f39a2",
|
|
100
|
+
12
|
|
101
|
+
]], 0);
|
|
102
|
+
globalThis.expect(message.includes("re-running will not help".toLowerCase())).toBe(true);
|
|
103
|
+
globalThis.expect(message.includes("SEED_RESET_NO_QUIESCE")).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
globalThis.describe("noQuiesce", () => {
|
|
108
|
+
let set = v => {
|
|
109
|
+
process.env["SEED_RESET_NO_QUIESCE"] = v;
|
|
110
|
+
};
|
|
111
|
+
globalThis.test("defaults to holding the platform", () => {
|
|
112
|
+
set("");
|
|
113
|
+
globalThis.expect(ReventlessSeedAws_Reset.noQuiesce()).toBe(false);
|
|
114
|
+
set("0");
|
|
115
|
+
globalThis.expect(ReventlessSeedAws_Reset.noQuiesce()).toBe(false);
|
|
116
|
+
set("no");
|
|
117
|
+
globalThis.expect(ReventlessSeedAws_Reset.noQuiesce()).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
globalThis.test("accepts the spellings an operator would actually type", () => {
|
|
120
|
+
[
|
|
121
|
+
"1",
|
|
122
|
+
"true",
|
|
123
|
+
"TRUE",
|
|
124
|
+
" yes "
|
|
125
|
+
].forEach(v => {
|
|
126
|
+
set(v);
|
|
127
|
+
globalThis.expect(ReventlessSeedAws_Reset.noQuiesce()).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
set("");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let Reset;
|
|
134
|
+
|
|
135
|
+
let Quiesce;
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
Reset,
|
|
139
|
+
Quiesce,
|
|
140
|
+
}
|
|
141
|
+
/* Not a pure module */
|
package/lib/ocaml/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1786902417155)
|
|
2
|
+
#Done(1786902417196)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Which document `resolveEndpoints` reads, and which GraphQL endpoint each arm takes
|
|
2
|
+
// from it.
|
|
3
|
+
//
|
|
4
|
+
// `endpointsFrom` is the pure half of `resolveEndpoints`; the impure half is a
|
|
5
|
+
// `pulumi stack output` subprocess and a `config.json` fetch, and the decision these
|
|
6
|
+
// tests pin is in neither. Upload endpoints are no longer resolved here — under route B
|
|
7
|
+
// the seed mints through the domain API's `Upload_Presign` mutation on the GraphQL
|
|
8
|
+
// endpoint below — so the only decision left is which GraphQL endpoint each source
|
|
9
|
+
// publishes, and the merged-over-per-plugin precedence within the stack-outputs arm.
|
|
10
|
+
|
|
11
|
+
open JestGlobals
|
|
12
|
+
open ReventlessSeed
|
|
13
|
+
|
|
14
|
+
// Every value read here has an env override, and an ambient one in the runner's
|
|
15
|
+
// environment (AWS_REGION is routinely set) would silently stand in for the document
|
|
16
|
+
// under test. An empty string reads as unset.
|
|
17
|
+
let clearOverrides = () =>
|
|
18
|
+
[
|
|
19
|
+
"REVENTLESS_GRAPHQL_ENDPOINT",
|
|
20
|
+
"AWS_REGION",
|
|
21
|
+
"COGNITO_CLIENT_ID",
|
|
22
|
+
"SEED_SKIP_UPLOADS",
|
|
23
|
+
]->Array.forEach(k => NodeProcess.env->Dict.set(k, ""))
|
|
24
|
+
|
|
25
|
+
let obj = entries => JSON.Encode.object(entries->Dict.fromArray)
|
|
26
|
+
let str = JSON.Encode.string
|
|
27
|
+
|
|
28
|
+
let config = (~extra: array<(string, JSON.t)>) =>
|
|
29
|
+
obj(
|
|
30
|
+
Array.concat(
|
|
31
|
+
[
|
|
32
|
+
("apiEndpoint", str("https://api.example/graphql")),
|
|
33
|
+
("region", str("eu-west-1")),
|
|
34
|
+
("cognitoClientId", str("client-abc")),
|
|
35
|
+
],
|
|
36
|
+
extra,
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
let outputs = (~extra: array<(string, JSON.t)>) =>
|
|
41
|
+
obj(
|
|
42
|
+
Array.concat(
|
|
43
|
+
[
|
|
44
|
+
("domainApiEndpoint", str("https://domain.example/graphql")),
|
|
45
|
+
("cognitoRegion", str("eu-west-1")),
|
|
46
|
+
("cognitoUserPoolClientId", str("client-abc")),
|
|
47
|
+
],
|
|
48
|
+
extra,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
describe("endpointsFrom — host shell arm", () => {
|
|
53
|
+
beforeEach(clearOverrides)
|
|
54
|
+
|
|
55
|
+
testSync("reads the GraphQL endpoint from the shell config", () => {
|
|
56
|
+
let eps = ReventlessSeedAws.endpointsFrom(~stack="alpha", HostShellConfig(config(~extra=[])))
|
|
57
|
+
expect(eps.graphql)->toBe("https://api.example/graphql")
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("endpointsFrom — stack outputs arm", () => {
|
|
62
|
+
beforeEach(clearOverrides)
|
|
63
|
+
|
|
64
|
+
testSync("prefers the merged API endpoint over the per-plugin one", () => {
|
|
65
|
+
let eps = ReventlessSeedAws.endpointsFrom(
|
|
66
|
+
~stack="pr-verify",
|
|
67
|
+
StackOutputs(outputs(~extra=[("domainMergedApiEndpoint", str("https://merged.example/"))])),
|
|
68
|
+
)
|
|
69
|
+
expect(eps.graphql)->toBe("https://merged.example/")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
testSync("fails naming both keys when the stack exports neither API endpoint", () =>
|
|
73
|
+
switch ReventlessSeedAws.endpointsFrom(
|
|
74
|
+
~stack="pr-verify",
|
|
75
|
+
StackOutputs(obj([("cognitoRegion", str("eu-west-1"))])),
|
|
76
|
+
) {
|
|
77
|
+
| _ => fail("expected endpointsFrom to fail")
|
|
78
|
+
| exception Seed.Failed(msg) =>
|
|
79
|
+
expect(msg)->toContain("domainMergedApiEndpoint")
|
|
80
|
+
expect(msg)->toContain("domainApiEndpoint")
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|