@anterior/brrr 0.1.1
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/.envrc +1 -0
- package/brrr-test-integration +7 -0
- package/package.json +33 -0
- package/package.nix +6 -0
- package/src/app.test.ts +705 -0
- package/src/app.ts +201 -0
- package/src/backends/dynamo.test-integration.ts +20 -0
- package/src/backends/dynamo.ts +184 -0
- package/src/backends/in-memory.test.ts +19 -0
- package/src/backends/in-memory.ts +110 -0
- package/src/backends/redis.test-integration.ts +61 -0
- package/src/backends/redis.ts +48 -0
- package/src/call.ts +19 -0
- package/src/codec.test.ts +91 -0
- package/src/codec.ts +12 -0
- package/src/connection.ts +186 -0
- package/src/emitter.ts +20 -0
- package/src/errors.ts +50 -0
- package/src/internal-codecs.ts +28 -0
- package/src/local-app.ts +93 -0
- package/src/naive-json-codec.test.ts +9 -0
- package/src/naive-json-codec.ts +72 -0
- package/src/store.test.ts +398 -0
- package/src/store.ts +251 -0
- package/src/symbol.ts +5 -0
- package/src/tagged-tuple.test.ts +69 -0
- package/src/tagged-tuple.ts +96 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
before,
|
|
4
|
+
beforeEach,
|
|
5
|
+
mock,
|
|
6
|
+
type MockTimersOptions,
|
|
7
|
+
suite,
|
|
8
|
+
test,
|
|
9
|
+
} from "node:test";
|
|
10
|
+
import {
|
|
11
|
+
deepStrictEqual,
|
|
12
|
+
doesNotReject,
|
|
13
|
+
ok,
|
|
14
|
+
strictEqual,
|
|
15
|
+
} from "node:assert/strict";
|
|
16
|
+
import {
|
|
17
|
+
type Cache,
|
|
18
|
+
type MemKey,
|
|
19
|
+
Memory,
|
|
20
|
+
PendingReturns,
|
|
21
|
+
type Store,
|
|
22
|
+
} from "./store.ts";
|
|
23
|
+
import { InMemoryStore } from "./backends/in-memory.ts";
|
|
24
|
+
import type { Call } from "./call.ts";
|
|
25
|
+
import { PendingReturn, TaggedTuple } from "./tagged-tuple.ts";
|
|
26
|
+
|
|
27
|
+
await suite(import.meta.filename, async () => {
|
|
28
|
+
await suite(PendingReturns.name, async () => {
|
|
29
|
+
await test("Encoded payload can be encoded & decoded", async () => {
|
|
30
|
+
const original = new PendingReturns(0, [
|
|
31
|
+
new PendingReturn("a", "b", "c"),
|
|
32
|
+
]);
|
|
33
|
+
const encoded = original.encode();
|
|
34
|
+
const decoded = PendingReturns.decode(encoded);
|
|
35
|
+
deepStrictEqual(original, decoded);
|
|
36
|
+
deepStrictEqual(encoded, decoded.encode());
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await test("Encoded payload with undefined timestamp can be encoded & decoded", async () => {
|
|
40
|
+
const original = new PendingReturns(undefined, [
|
|
41
|
+
new PendingReturn("a", "b", "c"),
|
|
42
|
+
]);
|
|
43
|
+
const encoded = original.encode();
|
|
44
|
+
const decoded = PendingReturns.decode(encoded);
|
|
45
|
+
deepStrictEqual(original, decoded);
|
|
46
|
+
deepStrictEqual(encoded, decoded.encode());
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await suite(Memory.name, async () => {
|
|
51
|
+
let store: Store;
|
|
52
|
+
let memory: Memory;
|
|
53
|
+
|
|
54
|
+
const fixture = {
|
|
55
|
+
call: {
|
|
56
|
+
taskName: "test-task",
|
|
57
|
+
payload: new Uint8Array([1, 2, 3]),
|
|
58
|
+
callHash: "test-call-hash",
|
|
59
|
+
} satisfies Call,
|
|
60
|
+
pendingReturns: {
|
|
61
|
+
key: {
|
|
62
|
+
type: "pending_returns",
|
|
63
|
+
callHash: "test-pending-return-hash",
|
|
64
|
+
} satisfies MemKey,
|
|
65
|
+
},
|
|
66
|
+
newReturn: new PendingReturn("some-root", "some-parent", "some-topic"),
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
store = new InMemoryStore();
|
|
71
|
+
memory = new Memory(store);
|
|
72
|
+
await memory.setCall(fixture.call);
|
|
73
|
+
await memory.setValue(fixture.call.callHash, fixture.call.payload);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await test("getCall", async () => {
|
|
77
|
+
const retrieved = await memory.getCall(fixture.call.callHash);
|
|
78
|
+
deepStrictEqual(retrieved, fixture.call);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await test("setCall", async () => {
|
|
82
|
+
const newCall: Call = {
|
|
83
|
+
taskName: "new-task",
|
|
84
|
+
payload: new Uint8Array([4, 5, 6]),
|
|
85
|
+
callHash: "new-call-hash",
|
|
86
|
+
};
|
|
87
|
+
await memory.setCall(newCall);
|
|
88
|
+
const retrieved = await memory.getCall(newCall.callHash);
|
|
89
|
+
deepStrictEqual(retrieved, newCall);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test("hasValue", async () => {
|
|
93
|
+
ok(await memory.hasValue(fixture.call.callHash));
|
|
94
|
+
ok(!(await memory.hasValue("non-existing-call-hash")));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await test("getValue", async () => {
|
|
98
|
+
const retrieved = await memory.getValue(fixture.call.callHash);
|
|
99
|
+
deepStrictEqual(retrieved, fixture.call.payload);
|
|
100
|
+
strictEqual(await memory.getValue("non-existing-call-hash"), undefined);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await test("setValue", async () => {
|
|
104
|
+
const newPayload = new Uint8Array([7, 8, 9]);
|
|
105
|
+
await memory.setValue(fixture.call.callHash, newPayload);
|
|
106
|
+
const retrieved = await memory.getValue(fixture.call.callHash);
|
|
107
|
+
deepStrictEqual(retrieved, newPayload);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await suite("addPendingReturn", async () => {
|
|
111
|
+
const mockTimersOptions = {
|
|
112
|
+
apis: ["Date"],
|
|
113
|
+
now: 5000,
|
|
114
|
+
} as const satisfies MockTimersOptions;
|
|
115
|
+
|
|
116
|
+
before(() => {
|
|
117
|
+
mock.timers.enable(mockTimersOptions);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await test("simple cases to document & test shouldSchedule", async () => {
|
|
121
|
+
const hash = "some-hash";
|
|
122
|
+
const base = new PendingReturn("root", "parent", "topic");
|
|
123
|
+
|
|
124
|
+
const cases = [
|
|
125
|
+
// base case
|
|
126
|
+
[hash, base, true],
|
|
127
|
+
// same one, shouldn't schedule again
|
|
128
|
+
[hash, base, false],
|
|
129
|
+
// different root, should schedule - it's a retry
|
|
130
|
+
[hash, new PendingReturn("diff-root", "parent", "topic"), true],
|
|
131
|
+
// new callHash, new PR, should schedule
|
|
132
|
+
["diff-hash", base, true],
|
|
133
|
+
// continuation, shouldn't schedule again
|
|
134
|
+
[hash, new PendingReturn("root", "parent", "diff-topic"), false],
|
|
135
|
+
[hash, new PendingReturn("root", "diff-parent", "topic"), false],
|
|
136
|
+
[hash, new PendingReturn("root", "diff-parent", "diff-topic"), false],
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
for (const [hash, pr, shouldSchedule] of cases) {
|
|
140
|
+
strictEqual(await memory.addPendingReturns(hash, pr), shouldSchedule);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ensure all returns are stored
|
|
144
|
+
const encoded = await store.get({
|
|
145
|
+
type: "pending_returns",
|
|
146
|
+
callHash: hash,
|
|
147
|
+
});
|
|
148
|
+
deepStrictEqual(
|
|
149
|
+
PendingReturns.decode(encoded!).encodedReturns,
|
|
150
|
+
new Set(cases.map((it) => TaggedTuple.encodeToString(it[1]))),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await test("First-time call triggers schedule and stores return", async () => {
|
|
155
|
+
const shouldSchedule = await memory.addPendingReturns(
|
|
156
|
+
fixture.call.callHash,
|
|
157
|
+
fixture.newReturn,
|
|
158
|
+
);
|
|
159
|
+
ok(shouldSchedule);
|
|
160
|
+
const raw = await store.get({
|
|
161
|
+
type: "pending_returns",
|
|
162
|
+
callHash: fixture.call.callHash,
|
|
163
|
+
});
|
|
164
|
+
ok(raw);
|
|
165
|
+
const decoded = PendingReturns.decode(raw);
|
|
166
|
+
ok(
|
|
167
|
+
decoded.encodedReturns.has(
|
|
168
|
+
TaggedTuple.encodeToString(fixture.newReturn),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
strictEqual(decoded.scheduledAt, mockTimersOptions.now / 1000);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await test("Repeated call with same return does not call schedule again", async () => {
|
|
175
|
+
await memory.addPendingReturns(
|
|
176
|
+
fixture.call.callHash,
|
|
177
|
+
fixture.newReturn,
|
|
178
|
+
);
|
|
179
|
+
const shouldSchedule = await memory.addPendingReturns(
|
|
180
|
+
fixture.call.callHash,
|
|
181
|
+
fixture.newReturn,
|
|
182
|
+
);
|
|
183
|
+
ok(!shouldSchedule);
|
|
184
|
+
const raw = await store.get({
|
|
185
|
+
type: "pending_returns",
|
|
186
|
+
callHash: fixture.call.callHash,
|
|
187
|
+
});
|
|
188
|
+
ok(raw);
|
|
189
|
+
const decoded = PendingReturns.decode(raw);
|
|
190
|
+
deepStrictEqual(
|
|
191
|
+
decoded.encodedReturns,
|
|
192
|
+
new Set([TaggedTuple.encodeToString(fixture.newReturn)]),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await test("Handles different returns properly", async () => {
|
|
197
|
+
await memory.addPendingReturns(
|
|
198
|
+
fixture.call.callHash,
|
|
199
|
+
fixture.newReturn,
|
|
200
|
+
);
|
|
201
|
+
const completelyDifferentReturn = new PendingReturn(
|
|
202
|
+
"completely",
|
|
203
|
+
"different",
|
|
204
|
+
"return",
|
|
205
|
+
);
|
|
206
|
+
const shouldSchedule = await memory.addPendingReturns(
|
|
207
|
+
fixture.call.callHash,
|
|
208
|
+
completelyDifferentReturn,
|
|
209
|
+
);
|
|
210
|
+
ok(!shouldSchedule);
|
|
211
|
+
const raw = await store.get({
|
|
212
|
+
type: "pending_returns",
|
|
213
|
+
callHash: fixture.call.callHash,
|
|
214
|
+
});
|
|
215
|
+
ok(raw);
|
|
216
|
+
const decoded = PendingReturns.decode(raw);
|
|
217
|
+
deepStrictEqual(
|
|
218
|
+
decoded.encodedReturns,
|
|
219
|
+
new Set(
|
|
220
|
+
[fixture.newReturn, completelyDifferentReturn].map((it) =>
|
|
221
|
+
TaggedTuple.encodeToString(it),
|
|
222
|
+
),
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await test("Repeated call with different rootId should schedule again", async () => {
|
|
228
|
+
const returnWithDifferentRoot = new PendingReturn(
|
|
229
|
+
"other-root",
|
|
230
|
+
fixture.newReturn.callHash,
|
|
231
|
+
fixture.newReturn.topic,
|
|
232
|
+
);
|
|
233
|
+
const shouldSchedule = await memory.addPendingReturns(
|
|
234
|
+
fixture.call.callHash,
|
|
235
|
+
returnWithDifferentRoot,
|
|
236
|
+
);
|
|
237
|
+
ok(shouldSchedule);
|
|
238
|
+
const raw = await store.get({
|
|
239
|
+
type: "pending_returns",
|
|
240
|
+
callHash: fixture.call.callHash,
|
|
241
|
+
});
|
|
242
|
+
ok(raw);
|
|
243
|
+
const decoded = PendingReturns.decode(raw);
|
|
244
|
+
ok(
|
|
245
|
+
decoded.encodedReturns.has(
|
|
246
|
+
TaggedTuple.encodeToString(returnWithDifferentRoot),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
strictEqual(decoded.scheduledAt, mockTimersOptions.now / 1000);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await suite("withPendingReturnRemove", async () => {
|
|
254
|
+
const mockFn =
|
|
255
|
+
mock.fn<(returns: Iterable<PendingReturn>) => Promise<void>>();
|
|
256
|
+
|
|
257
|
+
afterEach(() => {
|
|
258
|
+
mockFn.mock.resetCalls();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await test("don't call f if no pending return is found", async () => {
|
|
262
|
+
await memory.withPendingReturnsRemove(fixture.call.callHash, mockFn);
|
|
263
|
+
strictEqual(mockFn.mock.callCount(), 0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await test("invokes f with pending returns and deletes the key", async () => {
|
|
267
|
+
const pendingReturns = new PendingReturns(undefined, [
|
|
268
|
+
new PendingReturn("a", "b", "c"),
|
|
269
|
+
new PendingReturn("d", "e", "f"),
|
|
270
|
+
]);
|
|
271
|
+
await store.set(fixture.pendingReturns.key, pendingReturns.encode());
|
|
272
|
+
await memory.withPendingReturnsRemove(
|
|
273
|
+
fixture.pendingReturns.key.callHash,
|
|
274
|
+
(returns) => {
|
|
275
|
+
deepStrictEqual(
|
|
276
|
+
[...returns].map((it) => TaggedTuple.encodeToString(it)),
|
|
277
|
+
[...pendingReturns.encodedReturns],
|
|
278
|
+
);
|
|
279
|
+
return mockFn(returns);
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
strictEqual(mockFn.mock.callCount(), 1);
|
|
283
|
+
strictEqual(await store.get(fixture.pendingReturns.key), undefined);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
export async function storeContractTest(
|
|
290
|
+
acquireResource: () => Promise<{ store: Store } & AsyncDisposable>,
|
|
291
|
+
) {
|
|
292
|
+
await suite("store-contract", async () => {
|
|
293
|
+
const fixture = {
|
|
294
|
+
key: {
|
|
295
|
+
type: "call",
|
|
296
|
+
callHash: "test-call-hash",
|
|
297
|
+
} satisfies MemKey,
|
|
298
|
+
value: new Uint8Array([1, 2, 3, 4, 5]),
|
|
299
|
+
otherKey: {
|
|
300
|
+
type: "call",
|
|
301
|
+
callHash: "other-test-call-hash",
|
|
302
|
+
},
|
|
303
|
+
} as const;
|
|
304
|
+
|
|
305
|
+
await test("Basic get", async () => {
|
|
306
|
+
await using resource = await acquireResource();
|
|
307
|
+
const store = resource.store;
|
|
308
|
+
await store.set(fixture.key, fixture.value);
|
|
309
|
+
const retrieved = await store.get(fixture.key);
|
|
310
|
+
deepStrictEqual(retrieved, fixture.value);
|
|
311
|
+
strictEqual(await store.get(fixture.otherKey), undefined);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await test("Basic has", async () => {
|
|
315
|
+
await using resource = await acquireResource();
|
|
316
|
+
await resource.store.set(fixture.key, fixture.value);
|
|
317
|
+
ok(await resource.store.has(fixture.key));
|
|
318
|
+
ok(!(await resource.store.has(fixture.otherKey)));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await test("Basic set", async () => {
|
|
322
|
+
await using resource = await acquireResource();
|
|
323
|
+
await resource.store.set(fixture.key, fixture.value);
|
|
324
|
+
const newKey: MemKey = {
|
|
325
|
+
type: "call",
|
|
326
|
+
callHash: "new-call-hash",
|
|
327
|
+
};
|
|
328
|
+
const newValue = new Uint8Array([6, 7, 8, 9, 10]);
|
|
329
|
+
await resource.store.set(newKey, newValue);
|
|
330
|
+
const retrieved = await resource.store.get(newKey);
|
|
331
|
+
deepStrictEqual(retrieved, newValue);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await test("Basic delete", async () => {
|
|
335
|
+
await using resource = await acquireResource();
|
|
336
|
+
await resource.store.set(fixture.key, fixture.value);
|
|
337
|
+
await resource.store.delete(fixture.key);
|
|
338
|
+
strictEqual(await resource.store.get(fixture.key), undefined);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await test("Basic setNewValue", async () => {
|
|
342
|
+
await using resource = await acquireResource();
|
|
343
|
+
await resource.store.set(fixture.key, fixture.value);
|
|
344
|
+
const newValue = new Uint8Array([6, 7, 8, 9, 10]);
|
|
345
|
+
ok(!(await resource.store.setNewValue(fixture.key, newValue)));
|
|
346
|
+
await doesNotReject(
|
|
347
|
+
resource.store.setNewValue(fixture.otherKey, newValue),
|
|
348
|
+
);
|
|
349
|
+
const retrieved = await resource.store.get(fixture.otherKey);
|
|
350
|
+
deepStrictEqual(retrieved, newValue);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await test("Basic compareAndSet", async () => {
|
|
354
|
+
await using resource = await acquireResource();
|
|
355
|
+
await resource.store.set(fixture.key, fixture.value);
|
|
356
|
+
const newValue = new Uint8Array([6, 7, 8, 9, 10]);
|
|
357
|
+
await resource.store.compareAndSet(fixture.key, newValue, fixture.value);
|
|
358
|
+
const retrieved = await resource.store.get(fixture.key);
|
|
359
|
+
deepStrictEqual(retrieved, newValue);
|
|
360
|
+
ok(
|
|
361
|
+
!(await resource.store.compareAndSet(
|
|
362
|
+
fixture.otherKey,
|
|
363
|
+
newValue,
|
|
364
|
+
fixture.value,
|
|
365
|
+
)),
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await test("Basic compareAndDelete", async () => {
|
|
370
|
+
await using resource = await acquireResource();
|
|
371
|
+
await resource.store.set(fixture.key, fixture.value);
|
|
372
|
+
await resource.store.compareAndDelete(fixture.key, fixture.value);
|
|
373
|
+
strictEqual(await resource.store.get(fixture.key), undefined);
|
|
374
|
+
ok(
|
|
375
|
+
!(await resource.store.compareAndDelete(
|
|
376
|
+
fixture.otherKey,
|
|
377
|
+
fixture.value,
|
|
378
|
+
)),
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function cacheContractTest(
|
|
385
|
+
acquireResource: () => Promise<{ cache: Cache } & AsyncDisposable>,
|
|
386
|
+
) {
|
|
387
|
+
await suite("cache-contract", async () => {
|
|
388
|
+
const key = "test-incr-key";
|
|
389
|
+
|
|
390
|
+
await test("Basic incr", async () => {
|
|
391
|
+
await using resource = await acquireResource();
|
|
392
|
+
const initialValue = await resource.cache.incr(key);
|
|
393
|
+
strictEqual(initialValue, 1);
|
|
394
|
+
const nextValue = await resource.cache.incr(key);
|
|
395
|
+
strictEqual(nextValue, 2);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Call } from "./call.ts";
|
|
2
|
+
import { bencoder, decoder } from "./internal-codecs.ts";
|
|
3
|
+
import { CasRetryLimitReachedError, NotFoundError } from "./errors.ts";
|
|
4
|
+
import { PendingReturn, TaggedTuple } from "./tagged-tuple.ts";
|
|
5
|
+
|
|
6
|
+
export interface PendingReturnsPayload {
|
|
7
|
+
readonly scheduled_at: number | undefined;
|
|
8
|
+
readonly returns: unknown[][];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class PendingReturns {
|
|
12
|
+
public readonly scheduledAt: number | undefined;
|
|
13
|
+
public readonly encodedReturns: ReadonlySet<string>;
|
|
14
|
+
|
|
15
|
+
public constructor(
|
|
16
|
+
scheduledAt: number | undefined,
|
|
17
|
+
returns: Iterable<PendingReturn>,
|
|
18
|
+
) {
|
|
19
|
+
this.scheduledAt = scheduledAt;
|
|
20
|
+
this.encodedReturns = new Set([...returns].map(TaggedTuple.encodeToString));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static decode(encoded: Uint8Array): PendingReturns {
|
|
24
|
+
const { scheduled_at, returns } = bencoder.decode(
|
|
25
|
+
encoded,
|
|
26
|
+
"utf-8",
|
|
27
|
+
) as PendingReturnsPayload;
|
|
28
|
+
return new this(
|
|
29
|
+
scheduled_at,
|
|
30
|
+
[...new Set(returns)].map((it) => {
|
|
31
|
+
return TaggedTuple.fromTuple(
|
|
32
|
+
PendingReturn,
|
|
33
|
+
it as [number, string, string, string],
|
|
34
|
+
);
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public encode(): Uint8Array {
|
|
40
|
+
return bencoder.encode({
|
|
41
|
+
scheduled_at: this.scheduledAt,
|
|
42
|
+
returns: [...this.encodedReturns]
|
|
43
|
+
.map((it) =>
|
|
44
|
+
TaggedTuple.asTuple(TaggedTuple.decodeFromString(PendingReturn, it)),
|
|
45
|
+
)
|
|
46
|
+
.sort(),
|
|
47
|
+
} satisfies PendingReturnsPayload);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MemKey {
|
|
52
|
+
readonly type: "pending_returns" | "call" | "value";
|
|
53
|
+
readonly callHash: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Store {
|
|
57
|
+
/**
|
|
58
|
+
* Check if the store has a value for the given key.
|
|
59
|
+
*/
|
|
60
|
+
has(key: MemKey): Promise<boolean>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the value for the given key.
|
|
64
|
+
*/
|
|
65
|
+
get(key: MemKey): Promise<Uint8Array | undefined>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set the value for the given key.
|
|
69
|
+
*/
|
|
70
|
+
set(key: MemKey, value: Uint8Array): Promise<void>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Delete the value for the given key.
|
|
74
|
+
*/
|
|
75
|
+
delete(key: MemKey): Promise<void>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Set a new value for the given key.
|
|
79
|
+
* Returns true if the value was set, false if the key already exists.
|
|
80
|
+
*/
|
|
81
|
+
setNewValue(key: MemKey, value: Uint8Array): Promise<boolean>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Compare and set a value for the given key.
|
|
85
|
+
* Returns true if the value was set, false if the expected value did not match.
|
|
86
|
+
*/
|
|
87
|
+
compareAndSet(
|
|
88
|
+
key: MemKey,
|
|
89
|
+
value: Uint8Array,
|
|
90
|
+
expected: Uint8Array,
|
|
91
|
+
): Promise<boolean>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compare and delete a value for the given key.
|
|
95
|
+
* Returns true if the value was deleted, false if the expected value did not match.
|
|
96
|
+
*/
|
|
97
|
+
compareAndDelete(key: MemKey, expected: Uint8Array): Promise<boolean>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface Cache {
|
|
101
|
+
/**
|
|
102
|
+
* Increment the value for the given key.
|
|
103
|
+
*/
|
|
104
|
+
incr(key: string): Promise<number>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class Memory {
|
|
108
|
+
private static readonly casRetryLimit = 100;
|
|
109
|
+
|
|
110
|
+
private readonly store: Store;
|
|
111
|
+
|
|
112
|
+
public constructor(store: Store) {
|
|
113
|
+
this.store = store;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public async getCall(callHash: string): Promise<Call> {
|
|
117
|
+
const memKey: MemKey = {
|
|
118
|
+
type: "call",
|
|
119
|
+
callHash,
|
|
120
|
+
};
|
|
121
|
+
const encoded = await this.store.get(memKey);
|
|
122
|
+
if (!encoded) {
|
|
123
|
+
throw new NotFoundError(memKey);
|
|
124
|
+
}
|
|
125
|
+
const { task_name, payload } = bencoder.decode(encoded) as {
|
|
126
|
+
task_name: Uint8Array;
|
|
127
|
+
payload: Uint8Array;
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
taskName: decoder.decode(task_name),
|
|
131
|
+
payload,
|
|
132
|
+
callHash,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async setCall(call: Call): Promise<void> {
|
|
137
|
+
const encoded = bencoder.encode({
|
|
138
|
+
task_name: call.taskName,
|
|
139
|
+
payload: call.payload,
|
|
140
|
+
});
|
|
141
|
+
await this.store.set(
|
|
142
|
+
{
|
|
143
|
+
type: "call",
|
|
144
|
+
callHash: call.callHash,
|
|
145
|
+
},
|
|
146
|
+
encoded,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public async hasValue(callHash: string): Promise<boolean> {
|
|
151
|
+
return this.store.has({
|
|
152
|
+
type: "value",
|
|
153
|
+
callHash,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public async getValue(callHash: string): Promise<Uint8Array | undefined> {
|
|
158
|
+
return this.store.get({
|
|
159
|
+
type: "value",
|
|
160
|
+
callHash,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async setValue(callHash: string, payload: Uint8Array): Promise<void> {
|
|
165
|
+
await this.store.set(
|
|
166
|
+
{
|
|
167
|
+
type: "value",
|
|
168
|
+
callHash,
|
|
169
|
+
},
|
|
170
|
+
payload,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async addPendingReturns(
|
|
175
|
+
callHash: string,
|
|
176
|
+
newReturn: PendingReturn,
|
|
177
|
+
): Promise<boolean> {
|
|
178
|
+
const memKey: MemKey = {
|
|
179
|
+
type: "pending_returns",
|
|
180
|
+
callHash,
|
|
181
|
+
};
|
|
182
|
+
let shouldSchedule = false;
|
|
183
|
+
await this.withCas(async () => {
|
|
184
|
+
shouldSchedule = false;
|
|
185
|
+
const existingEncoded = await this.store.get(memKey);
|
|
186
|
+
if (!existingEncoded) {
|
|
187
|
+
const newReturns = new PendingReturns(Math.floor(Date.now() / 1000), [
|
|
188
|
+
newReturn,
|
|
189
|
+
]);
|
|
190
|
+
shouldSchedule = true;
|
|
191
|
+
return await this.store.setNewValue(memKey, newReturns.encode());
|
|
192
|
+
}
|
|
193
|
+
const pendingReturns = PendingReturns.decode(existingEncoded);
|
|
194
|
+
shouldSchedule = [...pendingReturns.encodedReturns].some((it) =>
|
|
195
|
+
TaggedTuple.decodeFromString(PendingReturn, it).isRepeatedCall(
|
|
196
|
+
newReturn,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
const newReturns = new PendingReturns(
|
|
200
|
+
pendingReturns.scheduledAt,
|
|
201
|
+
pendingReturns.encodedReturns
|
|
202
|
+
.union(new Set([TaggedTuple.encodeToString(newReturn)]))
|
|
203
|
+
.values()
|
|
204
|
+
.map((it) => TaggedTuple.decodeFromString(PendingReturn, it)),
|
|
205
|
+
);
|
|
206
|
+
return this.store.compareAndSet(
|
|
207
|
+
memKey,
|
|
208
|
+
newReturns.encode(),
|
|
209
|
+
existingEncoded,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
return shouldSchedule;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public async withPendingReturnsRemove(
|
|
216
|
+
callHash: string,
|
|
217
|
+
f: (returns: ReadonlySet<PendingReturn>) => Promise<void>,
|
|
218
|
+
) {
|
|
219
|
+
const memKey: MemKey = {
|
|
220
|
+
type: "pending_returns",
|
|
221
|
+
callHash,
|
|
222
|
+
};
|
|
223
|
+
const handled = new Set<PendingReturn>();
|
|
224
|
+
return this.withCas(async () => {
|
|
225
|
+
const pendingEncoded = await this.store.get(memKey);
|
|
226
|
+
if (!pendingEncoded) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
const toHandle = new Set(
|
|
230
|
+
PendingReturns.decode(pendingEncoded)
|
|
231
|
+
.encodedReturns.difference(handled)
|
|
232
|
+
.values()
|
|
233
|
+
.map((it) => TaggedTuple.decodeFromString(PendingReturn, it)),
|
|
234
|
+
);
|
|
235
|
+
await f(toHandle);
|
|
236
|
+
for (const it of toHandle) {
|
|
237
|
+
handled.add(it);
|
|
238
|
+
}
|
|
239
|
+
return this.store.compareAndDelete(memKey, pendingEncoded);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private async withCas(f: () => Promise<boolean>): Promise<void> {
|
|
244
|
+
for (let i = 0; i < Memory.casRetryLimit; i++) {
|
|
245
|
+
if (await f()) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw new CasRetryLimitReachedError(Memory.casRetryLimit);
|
|
250
|
+
}
|
|
251
|
+
}
|