@boxxy/dispatch 0.1.0 → 0.1.3
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/README.md +4 -1
- package/dist/attempts/attempt-machine.d.ts +804 -0
- package/dist/attempts/attempt-moves.d.ts +2 -0
- package/dist/attempts/bind-attempt-session.d.ts +14 -0
- package/dist/attempts/build-attempt-change.d.ts +73 -0
- package/dist/attempts/build-attempt-fact.d.ts +3 -0
- package/dist/attempts/end-item-attempts.d.ts +18 -0
- package/dist/attempts/is-same-guard.d.ts +5 -0
- package/dist/attempts/pick-call-attempt.d.ts +10 -0
- package/dist/attempts/stop-stray-session.d.ts +11 -0
- package/dist/attempts/types.d.ts +142 -0
- package/dist/attempts/write-attempt-move.d.ts +7 -0
- package/dist/attempts/write-session-bind.d.ts +3 -0
- package/dist/claims/build-snapshot-hash.d.ts +2 -0
- package/dist/claims/claim-next.d.ts +6 -0
- package/dist/claims/pick-kept-session.d.ts +9 -0
- package/dist/claims/request-wake.d.ts +3 -0
- package/dist/claims/resume-claim.d.ts +3 -0
- package/dist/claims/start-claim.d.ts +3 -0
- package/dist/claims/types.d.ts +44 -0
- package/dist/closeouts/build-settle-plans.d.ts +17 -0
- package/dist/closeouts/close-out-attempt.d.ts +3 -0
- package/dist/closeouts/effect-handlers.d.ts +2 -0
- package/dist/closeouts/pick-settlement.d.ts +14 -0
- package/dist/closeouts/settle-attempt.d.ts +3 -0
- package/dist/closeouts/types.d.ts +50 -0
- package/dist/index.d.ts +38 -0
- package/dist/notices/constants.d.ts +4 -0
- package/dist/notices/create-null-notifier.d.ts +2 -0
- package/dist/notices/send-notices.d.ts +2 -0
- package/dist/notices/types.d.ts +11 -0
- package/dist/outbox/build-backoff-ms.d.ts +2 -0
- package/dist/outbox/build-initial-sender-state.d.ts +2 -0
- package/dist/outbox/build-outbox-status.d.ts +15 -0
- package/dist/outbox/constants.d.ts +2 -0
- package/dist/outbox/measure-wire-bytes.d.ts +2 -0
- package/dist/outbox/pick-sender-move.d.ts +10 -0
- package/dist/outbox/run-sender-once.d.ts +2 -0
- package/dist/outbox/take-batch.d.ts +3 -0
- package/dist/outbox/types.d.ts +61 -0
- package/dist/pull-requests/build-link-change.d.ts +11 -0
- package/dist/pull-requests/build-state-fact.d.ts +8 -0
- package/dist/pull-requests/collect-attempt-trailers.d.ts +2 -0
- package/dist/pull-requests/collect-seen-state.d.ts +7 -0
- package/dist/pull-requests/constants.d.ts +3 -0
- package/dist/pull-requests/link-pull-request.d.ts +2 -0
- package/dist/pull-requests/parse-pull-request-url.d.ts +2 -0
- package/dist/pull-requests/types.d.ts +37 -0
- package/dist/pull-requests/watch-pull-requests.d.ts +2 -0
- package/dist/readings/act-on-reading.d.ts +3 -0
- package/dist/readings/find-waiting-since.d.ts +9 -0
- package/dist/readings/pick-reading-action.d.ts +2 -0
- package/dist/readings/types.d.ts +79 -0
- package/dist/sessions/build-keep-writes.d.ts +9 -0
- package/dist/sessions/build-kept-clear.d.ts +2 -0
- package/dist/sessions/forget-sessions.d.ts +2 -0
- package/dist/sessions/is-kept-due.d.ts +2 -0
- package/dist/sessions/resume-kept-session.d.ts +6 -0
- package/dist/sessions/types.d.ts +19 -0
- package/dist/test-utils/apply-memory-change.d.ts +3 -0
- package/dist/test-utils/build-memory-reader.d.ts +3 -0
- package/dist/test-utils/build-mock-bind-change.d.ts +7 -0
- package/dist/test-utils/build-mock-claim-change.d.ts +2 -0
- package/dist/test-utils/build-mock-file-item-change.d.ts +2 -0
- package/dist/test-utils/build-mock-lever-change.d.ts +2 -0
- package/dist/test-utils/build-mock-new-id.d.ts +2 -0
- package/dist/test-utils/build-mock-note-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pause-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pull-request-change.d.ts +8 -0
- package/dist/test-utils/build-mock-relation-change.d.ts +7 -0
- package/dist/test-utils/build-mock-running-reading.d.ts +6 -0
- package/dist/test-utils/build-mock-settle-change.d.ts +7 -0
- package/dist/test-utils/build-mock-start-change.d.ts +2 -0
- package/dist/test-utils/claim-store-attempt-reader-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-claim-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-close-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-contract.d.ts +2 -0
- package/dist/test-utils/claim-store-copy-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-exclusive-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-guard-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-item-reader-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-outbox-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-race-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-rollback-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-seat-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-session-cases.d.ts +2 -0
- package/dist/test-utils/collect-memory-limit-fills.d.ts +3 -0
- package/dist/test-utils/compare-text.d.ts +1 -0
- package/dist/test-utils/create-fake-backoff.d.ts +2 -0
- package/dist/test-utils/create-fake-clock.d.ts +5 -0
- package/dist/test-utils/create-fake-fact-sink.d.ts +8 -0
- package/dist/test-utils/create-fake-forge.d.ts +17 -0
- package/dist/test-utils/create-fake-worker.d.ts +42 -0
- package/dist/test-utils/create-memory-claim-store.d.ts +2 -0
- package/dist/test-utils/create-test-dispatch.d.ts +46 -0
- package/dist/test-utils/forge-contract.d.ts +15 -0
- package/dist/test-utils/is-same-held-seat.d.ts +2 -0
- package/dist/test-utils/types.d.ts +19 -0
- package/dist/test-utils/worker-contract.d.ts +14 -0
- package/dist/timeouts.d.ts +2 -0
- package/dist/types.d.ts +13 -0
- package/package.json +15 -2
- package/src/attempts/attempt-machine.ts +147 -0
- package/src/attempts/attempt-moves.ts +218 -0
- package/src/attempts/bind-attempt-session.ts +61 -0
- package/src/attempts/build-attempt-change.ts +540 -0
- package/src/attempts/build-attempt-fact.ts +135 -0
- package/src/attempts/end-item-attempts.ts +95 -0
- package/src/attempts/is-same-guard.ts +22 -0
- package/src/attempts/pick-call-attempt.ts +32 -0
- package/src/attempts/stop-stray-session.ts +48 -0
- package/src/attempts/types.ts +231 -0
- package/src/attempts/write-attempt-move.ts +41 -0
- package/src/attempts/write-session-bind.ts +47 -0
- package/src/claims/build-snapshot-hash.ts +9 -0
- package/src/claims/claim-next.ts +306 -0
- package/src/claims/pick-kept-session.ts +41 -0
- package/src/claims/request-wake.ts +17 -0
- package/src/claims/resume-claim.ts +56 -0
- package/src/claims/start-claim.ts +99 -0
- package/src/claims/types.ts +67 -0
- package/src/closeouts/build-settle-plans.ts +173 -0
- package/src/closeouts/close-out-attempt.ts +156 -0
- package/src/closeouts/effect-handlers.ts +232 -0
- package/src/closeouts/pick-settlement.ts +80 -0
- package/src/closeouts/settle-attempt.ts +161 -0
- package/src/closeouts/types.ts +87 -0
- package/src/index.ts +61 -0
- package/src/notices/constants.ts +7 -0
- package/src/notices/create-null-notifier.ts +6 -0
- package/src/notices/send-notices.ts +41 -0
- package/src/notices/types.ts +14 -0
- package/src/outbox/build-backoff-ms.ts +14 -0
- package/src/outbox/build-initial-sender-state.ts +14 -0
- package/src/outbox/build-outbox-status.ts +43 -0
- package/src/outbox/constants.ts +15 -0
- package/src/outbox/measure-wire-bytes.ts +31 -0
- package/src/outbox/pick-sender-move.ts +37 -0
- package/src/outbox/run-sender-once.ts +174 -0
- package/src/outbox/take-batch.ts +35 -0
- package/src/outbox/types.ts +76 -0
- package/src/pull-requests/build-link-change.ts +95 -0
- package/src/pull-requests/build-state-fact.ts +39 -0
- package/src/pull-requests/collect-attempt-trailers.ts +20 -0
- package/src/pull-requests/collect-seen-state.ts +19 -0
- package/src/pull-requests/constants.ts +5 -0
- package/src/pull-requests/link-pull-request.ts +119 -0
- package/src/pull-requests/parse-pull-request-url.ts +30 -0
- package/src/pull-requests/types.ts +63 -0
- package/src/pull-requests/watch-pull-requests.ts +308 -0
- package/src/readings/act-on-reading.ts +318 -0
- package/src/readings/find-waiting-since.ts +46 -0
- package/src/readings/pick-reading-action.ts +315 -0
- package/src/readings/types.ts +98 -0
- package/src/sessions/build-keep-writes.ts +55 -0
- package/src/sessions/build-kept-clear.ts +12 -0
- package/src/sessions/forget-sessions.ts +106 -0
- package/src/sessions/is-kept-due.ts +27 -0
- package/src/sessions/resume-kept-session.ts +25 -0
- package/src/sessions/types.ts +39 -0
- package/src/test-utils/apply-memory-change.ts +252 -0
- package/src/test-utils/build-memory-reader.ts +364 -0
- package/src/test-utils/build-mock-bind-change.ts +43 -0
- package/src/test-utils/build-mock-claim-change.ts +52 -0
- package/src/test-utils/build-mock-file-item-change.ts +20 -0
- package/src/test-utils/build-mock-lever-change.ts +18 -0
- package/src/test-utils/build-mock-new-id.ts +12 -0
- package/src/test-utils/build-mock-note-change.ts +18 -0
- package/src/test-utils/build-mock-pause-change.ts +19 -0
- package/src/test-utils/build-mock-pull-request-change.ts +63 -0
- package/src/test-utils/build-mock-relation-change.ts +40 -0
- package/src/test-utils/build-mock-running-reading.ts +19 -0
- package/src/test-utils/build-mock-settle-change.ts +70 -0
- package/src/test-utils/build-mock-start-change.ts +38 -0
- package/src/test-utils/claim-store-attempt-reader-cases.ts +357 -0
- package/src/test-utils/claim-store-claim-cases.ts +314 -0
- package/src/test-utils/claim-store-close-cases.ts +95 -0
- package/src/test-utils/claim-store-contract.ts +28 -0
- package/src/test-utils/claim-store-copy-cases.ts +100 -0
- package/src/test-utils/claim-store-exclusive-cases.ts +243 -0
- package/src/test-utils/claim-store-guard-cases.ts +383 -0
- package/src/test-utils/claim-store-item-reader-cases.ts +471 -0
- package/src/test-utils/claim-store-outbox-cases.ts +281 -0
- package/src/test-utils/claim-store-race-cases.ts +150 -0
- package/src/test-utils/claim-store-rollback-cases.ts +175 -0
- package/src/test-utils/claim-store-seat-cases.ts +270 -0
- package/src/test-utils/claim-store-session-cases.ts +289 -0
- package/src/test-utils/collect-memory-limit-fills.ts +15 -0
- package/src/test-utils/compare-text.ts +8 -0
- package/src/test-utils/create-fake-backoff.ts +16 -0
- package/src/test-utils/create-fake-clock.ts +20 -0
- package/src/test-utils/create-fake-fact-sink.ts +74 -0
- package/src/test-utils/create-fake-forge.ts +117 -0
- package/src/test-utils/create-fake-worker.ts +303 -0
- package/src/test-utils/create-memory-claim-store.ts +163 -0
- package/src/test-utils/create-test-dispatch.ts +597 -0
- package/src/test-utils/forge-contract.ts +171 -0
- package/src/test-utils/is-same-held-seat.ts +11 -0
- package/src/test-utils/types.ts +23 -0
- package/src/test-utils/worker-contract.ts +192 -0
- package/src/timeouts.ts +17 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { brandSchemas } from '@boxxy/model';
|
|
2
|
+
import { buildMockV1ItemRow } from '@boxxy/model/test-utils/build-mock-v1-item-row';
|
|
3
|
+
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
4
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
5
|
+
|
|
6
|
+
export const claimStoreOutboxCases: readonly ClaimStoreCase[] = [
|
|
7
|
+
[
|
|
8
|
+
'#markFactsSent drops the facts from the unsent facts and the counts',
|
|
9
|
+
async (store) => {
|
|
10
|
+
const first = buildMockFileItemChange(
|
|
11
|
+
buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(1000) }),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const second = buildMockFileItemChange(
|
|
15
|
+
buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(2000) }),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
await store.exclusive(async (transaction) => {
|
|
19
|
+
await transaction.applyChange(first);
|
|
20
|
+
await transaction.applyChange(second);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await store.exclusive((transaction) =>
|
|
24
|
+
transaction.markFactsSent(
|
|
25
|
+
first.facts.map((fact) => fact.id),
|
|
26
|
+
brandSchemas.instant.parse(3000),
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const outbox: unknown = await store.read(async (transaction) => {
|
|
31
|
+
const unsent = await transaction.readUnsentFacts(10);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
unsent: unsent.map((fact) => fact.factId),
|
|
35
|
+
counts: await transaction.readOutboxCounts(),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
received: outbox,
|
|
41
|
+
expected: {
|
|
42
|
+
unsent: second.facts.map((fact) => fact.id),
|
|
43
|
+
counts: { unsent: 1, parked: 0, oldestUnsentAt: 2000 },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
'#readUnsentFacts returns at most the limit, oldest first',
|
|
50
|
+
async (store) => {
|
|
51
|
+
await store.exclusive(async (transaction) => {
|
|
52
|
+
await transaction.applyChange(buildMockFileItemChange(buildMockV1ItemRow()));
|
|
53
|
+
await transaction.applyChange(buildMockFileItemChange(buildMockV1ItemRow()));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const unsent = await store.read((transaction) => transaction.readUnsentFacts(1));
|
|
57
|
+
|
|
58
|
+
return { received: unsent.map((fact) => fact.seq), expected: [1] };
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
'#readOutboxCounts reads no row from an empty outbox',
|
|
63
|
+
async (store) => {
|
|
64
|
+
const counts: unknown = await store.read((transaction) => transaction.readOutboxCounts());
|
|
65
|
+
|
|
66
|
+
return { received: counts, expected: { unsent: 0, parked: 0, oldestUnsentAt: null } };
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
'#readOutboxCounts reads the earliest time of the unsent facts, whatever their order',
|
|
71
|
+
async (store) => {
|
|
72
|
+
const later = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(2000) });
|
|
73
|
+
const earlier = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(1000) });
|
|
74
|
+
|
|
75
|
+
await store.exclusive(async (transaction) => {
|
|
76
|
+
await transaction.applyChange(buildMockFileItemChange(later));
|
|
77
|
+
await transaction.applyChange(buildMockFileItemChange(earlier));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const counts: unknown = await store.read((transaction) => transaction.readOutboxCounts());
|
|
81
|
+
|
|
82
|
+
return { received: counts, expected: { unsent: 2, parked: 0, oldestUnsentAt: 1000 } };
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
'#parkFacts leaves a parked fact out of the unsent facts and counts it parked',
|
|
87
|
+
async (store) => {
|
|
88
|
+
const parked = buildMockFileItemChange(
|
|
89
|
+
buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(1000) }),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const waiting = buildMockFileItemChange(
|
|
93
|
+
buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(2000) }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await store.exclusive(async (transaction) => {
|
|
97
|
+
await transaction.applyChange(parked);
|
|
98
|
+
await transaction.applyChange(waiting);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await store.exclusive((transaction) =>
|
|
102
|
+
transaction.parkFacts(
|
|
103
|
+
parked.facts.map((fact) => fact.id),
|
|
104
|
+
'too-large',
|
|
105
|
+
brandSchemas.instant.parse(3000),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const outbox: unknown = await store.read(async (transaction) => {
|
|
110
|
+
const unsent = await transaction.readUnsentFacts(10);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
unsent: unsent.map((fact) => fact.factId),
|
|
114
|
+
counts: await transaction.readOutboxCounts(),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
received: outbox,
|
|
120
|
+
expected: {
|
|
121
|
+
unsent: waiting.facts.map((fact) => fact.id),
|
|
122
|
+
counts: { unsent: 1, parked: 1, oldestUnsentAt: 2000 },
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
'#markFactsSent leaves a parked fact parked',
|
|
129
|
+
async (store) => {
|
|
130
|
+
const change = buildMockFileItemChange(buildMockV1ItemRow());
|
|
131
|
+
const ids = change.facts.map((fact) => fact.id);
|
|
132
|
+
|
|
133
|
+
await store.exclusive(async (transaction) => {
|
|
134
|
+
await transaction.applyChange(change);
|
|
135
|
+
await transaction.parkFacts(ids, 'unknown-type', brandSchemas.instant.parse(3000));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await store.exclusive((transaction) =>
|
|
139
|
+
transaction.markFactsSent(ids, brandSchemas.instant.parse(4000)),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const counts: unknown = await store.read((transaction) => transaction.readOutboxCounts());
|
|
143
|
+
|
|
144
|
+
return { received: counts, expected: { unsent: 0, parked: 1, oldestUnsentAt: null } };
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
'#parkFacts leaves a sent fact as it is',
|
|
149
|
+
async (store) => {
|
|
150
|
+
const change = buildMockFileItemChange(buildMockV1ItemRow());
|
|
151
|
+
const ids = change.facts.map((fact) => fact.id);
|
|
152
|
+
|
|
153
|
+
await store.exclusive(async (transaction) => {
|
|
154
|
+
await transaction.applyChange(change);
|
|
155
|
+
await transaction.markFactsSent(ids, brandSchemas.instant.parse(3000));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await store.exclusive((transaction) =>
|
|
159
|
+
transaction.parkFacts(ids, 'too-large', brandSchemas.instant.parse(4000)),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const counts: unknown = await store.read((transaction) => transaction.readOutboxCounts());
|
|
163
|
+
|
|
164
|
+
return { received: counts, expected: { unsent: 0, parked: 0, oldestUnsentAt: null } };
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
'#unparkFacts clears every park mark, returns the count and the facts read unsent again',
|
|
169
|
+
async (store) => {
|
|
170
|
+
const first = buildMockFileItemChange(buildMockV1ItemRow());
|
|
171
|
+
const second = buildMockFileItemChange(buildMockV1ItemRow());
|
|
172
|
+
|
|
173
|
+
await store.exclusive(async (transaction) => {
|
|
174
|
+
await transaction.applyChange(first);
|
|
175
|
+
await transaction.applyChange(second);
|
|
176
|
+
|
|
177
|
+
await transaction.parkFacts(
|
|
178
|
+
[...first.facts, ...second.facts].map((fact) => fact.id),
|
|
179
|
+
'too-large',
|
|
180
|
+
brandSchemas.instant.parse(3000),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const cleared = await store.exclusive((transaction) => transaction.unparkFacts());
|
|
185
|
+
const unsent = await store.read((transaction) => transaction.readUnsentFacts(10));
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
received: { cleared, unsent: unsent.map((fact) => [fact.parkedAt, fact.parkedReason]) },
|
|
189
|
+
expected: {
|
|
190
|
+
cleared: 2,
|
|
191
|
+
unsent: [
|
|
192
|
+
[null, null],
|
|
193
|
+
[null, null],
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
[
|
|
200
|
+
'#readItemBehind counts the unsent facts of one item, parked ones too',
|
|
201
|
+
async (store) => {
|
|
202
|
+
const item = buildMockV1ItemRow({ filedAt: brandSchemas.instant.parse(1000) });
|
|
203
|
+
const other = buildMockV1ItemRow();
|
|
204
|
+
const filed = buildMockFileItemChange(item);
|
|
205
|
+
|
|
206
|
+
await store.exclusive(async (transaction) => {
|
|
207
|
+
await transaction.applyChange(filed);
|
|
208
|
+
await transaction.applyChange(buildMockFileItemChange(other));
|
|
209
|
+
|
|
210
|
+
await transaction.parkFacts(
|
|
211
|
+
filed.facts.map((fact) => fact.id),
|
|
212
|
+
'too-large',
|
|
213
|
+
brandSchemas.instant.parse(3000),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const behind: unknown = await store.read((transaction) =>
|
|
218
|
+
transaction.readItemBehind(item.id),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return { received: behind, expected: { facts: 1, oldestAt: 1000 } };
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
'#readItemBehind reads nothing behind for an item whose facts are sent',
|
|
226
|
+
async (store) => {
|
|
227
|
+
const item = buildMockV1ItemRow();
|
|
228
|
+
const filed = buildMockFileItemChange(item);
|
|
229
|
+
|
|
230
|
+
await store.exclusive(async (transaction) => {
|
|
231
|
+
await transaction.applyChange(filed);
|
|
232
|
+
|
|
233
|
+
await transaction.markFactsSent(
|
|
234
|
+
filed.facts.map((fact) => fact.id),
|
|
235
|
+
brandSchemas.instant.parse(3000),
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const behind: unknown = await store.read((transaction) =>
|
|
240
|
+
transaction.readItemBehind(item.id),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return { received: behind, expected: { facts: 0, oldestAt: null } };
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
'#markNoticeTried counts a failed try and keeps the notice unsent',
|
|
248
|
+
async (store) => {
|
|
249
|
+
const item = buildMockV1ItemRow();
|
|
250
|
+
|
|
251
|
+
const notice = {
|
|
252
|
+
id: 0,
|
|
253
|
+
item: item.id,
|
|
254
|
+
reason: 'attempts used up',
|
|
255
|
+
at: brandSchemas.instant.parse(3000),
|
|
256
|
+
tries: 0,
|
|
257
|
+
sentAt: null,
|
|
258
|
+
} as const;
|
|
259
|
+
|
|
260
|
+
await store.exclusive(async (transaction) => {
|
|
261
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
262
|
+
|
|
263
|
+
await transaction.applyChange({
|
|
264
|
+
at: notice.at,
|
|
265
|
+
writes: [
|
|
266
|
+
{ op: 'insert', table: 'notices', row: notice },
|
|
267
|
+
{ op: 'insert', table: 'notices', row: notice },
|
|
268
|
+
],
|
|
269
|
+
facts: [],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await transaction.markNoticeTried(1, brandSchemas.instant.parse(3500), false);
|
|
273
|
+
await transaction.markNoticeTried(2, brandSchemas.instant.parse(3500), true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const unsent: unknown = await store.read((transaction) => transaction.listUnsentNotices(10));
|
|
277
|
+
|
|
278
|
+
return { received: unsent, expected: [{ ...notice, id: 1, tries: 1 }] };
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { brandSchemas } from '@boxxy/model';
|
|
2
|
+
import type { AttemptRow, StoreChange } from '@boxxy/model';
|
|
3
|
+
import { buildMockV1AttemptRow } from '@boxxy/model/test-utils/build-mock-v1-attempt-row';
|
|
4
|
+
import { buildMockV1ItemRow } from '@boxxy/model/test-utils/build-mock-v1-item-row';
|
|
5
|
+
import invariant from 'tiny-invariant';
|
|
6
|
+
import { buildAttemptChange } from '../attempts/build-attempt-change.ts';
|
|
7
|
+
import type { MoveEvent } from '../attempts/types.ts';
|
|
8
|
+
import { buildMockClaimChange } from './build-mock-claim-change.ts';
|
|
9
|
+
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
10
|
+
import { buildMockNewId } from './build-mock-new-id.ts';
|
|
11
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
12
|
+
|
|
13
|
+
export const claimStoreRaceCases: readonly ClaimStoreCase[] = [
|
|
14
|
+
[
|
|
15
|
+
'#applyChange applies the attempt builder claim with its lease, seats, decision and fact',
|
|
16
|
+
async (store) => {
|
|
17
|
+
const item = buildMockV1ItemRow();
|
|
18
|
+
const workers = brandSchemas.limitName.parse('workers');
|
|
19
|
+
const pool = brandSchemas.poolName.parse('main');
|
|
20
|
+
const rulesVersion = brandSchemas.rulesVersion.parse('a'.repeat(64));
|
|
21
|
+
|
|
22
|
+
const built = buildAttemptChange({
|
|
23
|
+
actor: 'boxxy:runner:runner-1',
|
|
24
|
+
now: brandSchemas.instant.parse(5000),
|
|
25
|
+
newId: buildMockNewId(),
|
|
26
|
+
attempt: null,
|
|
27
|
+
event: {
|
|
28
|
+
kind: 'claimed',
|
|
29
|
+
attempt: buildMockV1AttemptRow().id,
|
|
30
|
+
pick: {
|
|
31
|
+
kind: 'start',
|
|
32
|
+
item: item.id,
|
|
33
|
+
phase: brandSchemas.phaseName.parse('build'),
|
|
34
|
+
round: 1,
|
|
35
|
+
pool,
|
|
36
|
+
fields: { repo: ['acme/lamps'] },
|
|
37
|
+
limits: [workers],
|
|
38
|
+
},
|
|
39
|
+
number: 1,
|
|
40
|
+
runner: brandSchemas.runnerId.parse('runner-1'),
|
|
41
|
+
leaseGen: 1,
|
|
42
|
+
recipe: { hash: brandSchemas.recipeHash.parse('c'.repeat(64)), rulesVersion },
|
|
43
|
+
skillsRef: null,
|
|
44
|
+
spawnRequest: { prompt: 'Fix the flicker.' },
|
|
45
|
+
spawnRequestHash: buildMockV1AttemptRow().spawnRequestHash,
|
|
46
|
+
input: { findings: [] },
|
|
47
|
+
mode: 'agent',
|
|
48
|
+
caller: null,
|
|
49
|
+
decision: {
|
|
50
|
+
item: item.id,
|
|
51
|
+
kind: 'start',
|
|
52
|
+
pool,
|
|
53
|
+
rulesVersion,
|
|
54
|
+
snapshotHash: brandSchemas.snapshotHash.parse('b'.repeat(64)),
|
|
55
|
+
storeVersion: 1,
|
|
56
|
+
pepysOffsets: {},
|
|
57
|
+
faults: [],
|
|
58
|
+
limits: [{ limit: workers, held: 0, max: 2 }],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
invariant(built.kind === 'change', 'the attempt machine allows a claim');
|
|
64
|
+
|
|
65
|
+
await store.exclusive(async (transaction) => {
|
|
66
|
+
await transaction.syncLimits(
|
|
67
|
+
[{ name: workers, max: 2, counts: {} }],
|
|
68
|
+
brandSchemas.instant.parse(500),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result = await store.exclusive((transaction) => transaction.applyChange(built.change));
|
|
75
|
+
|
|
76
|
+
const state: unknown = await store.read(async (transaction) => {
|
|
77
|
+
const attempts = await transaction.listItemAttempts(item.id);
|
|
78
|
+
const limits = await transaction.listLimits();
|
|
79
|
+
const facts = await transaction.readUnsentFacts(10);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
attempts: attempts.map((attempt) => [attempt.state, attempt.input]),
|
|
83
|
+
held: limits.map((limit) => limit.held),
|
|
84
|
+
facts: facts.map((fact) => fact.type),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
received: { result, state },
|
|
90
|
+
expected: {
|
|
91
|
+
result: { kind: 'applied' },
|
|
92
|
+
state: {
|
|
93
|
+
attempts: [['starting', { findings: [] }]],
|
|
94
|
+
held: [1],
|
|
95
|
+
facts: ['boxxy.item.filed', 'boxxy.attempt.claimed', 'boxxy.decision.recorded'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
'#applyChange refuses the second of two attempt moves built from one read',
|
|
103
|
+
async (store) => {
|
|
104
|
+
const attempt = buildMockV1AttemptRow({ state: 'running' });
|
|
105
|
+
|
|
106
|
+
await store.exclusive(async (transaction) => {
|
|
107
|
+
await transaction.applyChange(
|
|
108
|
+
buildMockFileItemChange(buildMockV1ItemRow({ id: attempt.item })),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await transaction.applyChange(buildMockClaimChange(attempt));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const read = await store.read((transaction) => transaction.readAttempt(attempt.id));
|
|
115
|
+
|
|
116
|
+
invariant(read !== null, 'the claim stored the attempt');
|
|
117
|
+
|
|
118
|
+
const first = buildMoveChange(read, { kind: 'suspend-asked' });
|
|
119
|
+
const second = buildMoveChange(read, { kind: 'stalled' });
|
|
120
|
+
|
|
121
|
+
const results = await store.exclusive(async (transaction) => [
|
|
122
|
+
await transaction.applyChange(first),
|
|
123
|
+
await transaction.applyChange(second),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
received: results,
|
|
128
|
+
expected: [
|
|
129
|
+
{ kind: 'applied' },
|
|
130
|
+
{ kind: 'guard-mismatch', table: 'attempts', key: { id: attempt.id } },
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// the attempt builder's change, which the store takes as it is
|
|
138
|
+
function buildMoveChange(attempt: AttemptRow, event: MoveEvent): StoreChange {
|
|
139
|
+
const built = buildAttemptChange({
|
|
140
|
+
actor: 'boxxy:runner:runner-1',
|
|
141
|
+
now: brandSchemas.instant.parse(5000),
|
|
142
|
+
newId: buildMockNewId(),
|
|
143
|
+
attempt,
|
|
144
|
+
event,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
invariant(built.kind === 'change', 'the attempt machine allows the move');
|
|
148
|
+
|
|
149
|
+
return built.change;
|
|
150
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { brandSchemas } from '@boxxy/model';
|
|
2
|
+
import type { StoreChange } from '@boxxy/model';
|
|
3
|
+
import { buildMockFactId } from '@boxxy/model/test-utils/build-mock-fact-id';
|
|
4
|
+
import { buildMockItemRules } from '@boxxy/model/test-utils/build-mock-item-rules';
|
|
5
|
+
import { buildMockV1AttemptRow } from '@boxxy/model/test-utils/build-mock-v1-attempt-row';
|
|
6
|
+
import { buildMockV1ItemRow } from '@boxxy/model/test-utils/build-mock-v1-item-row';
|
|
7
|
+
import { buildMockClaimChange } from './build-mock-claim-change.ts';
|
|
8
|
+
import { buildMockFileItemChange } from './build-mock-file-item-change.ts';
|
|
9
|
+
import { buildMockSettleChange } from './build-mock-settle-change.ts';
|
|
10
|
+
import type { ClaimStoreCase } from './types.ts';
|
|
11
|
+
|
|
12
|
+
export const claimStoreRollbackCases: readonly ClaimStoreCase[] = [
|
|
13
|
+
[
|
|
14
|
+
'#applyChange takes back the lease and seat releases of a settle whose guard fails',
|
|
15
|
+
async (store) => {
|
|
16
|
+
const workers = brandSchemas.limitName.parse('workers');
|
|
17
|
+
const item = buildMockV1ItemRow();
|
|
18
|
+
const attempt = buildMockV1AttemptRow({ item: item.id, state: 'starting' });
|
|
19
|
+
|
|
20
|
+
await store.exclusive(async (transaction) => {
|
|
21
|
+
await transaction.syncLimits(
|
|
22
|
+
[{ name: workers, max: 1, counts: {} }],
|
|
23
|
+
brandSchemas.instant.parse(500),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
await transaction.applyChange(buildMockFileItemChange(item));
|
|
27
|
+
await transaction.applyChange(buildMockClaimChange(attempt, [workers]));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const result = await store.exclusive((transaction) =>
|
|
31
|
+
transaction.applyChange(
|
|
32
|
+
buildMockSettleChange({ attempt: { ...attempt, state: 'running' }, state: 'failed' }),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const state: unknown = await store.read(async (transaction) => {
|
|
37
|
+
const read = await transaction.readAttempt(attempt.id);
|
|
38
|
+
const facts = await transaction.readUnsentFacts(10);
|
|
39
|
+
const limits = await transaction.listLimits();
|
|
40
|
+
|
|
41
|
+
const view = await transaction.readItem(
|
|
42
|
+
item.id,
|
|
43
|
+
brandSchemas.instant.parse(5000),
|
|
44
|
+
buildMockItemRules(),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
attempt: read?.state,
|
|
49
|
+
facts: facts.length,
|
|
50
|
+
held: limits.map((limit) => limit.held),
|
|
51
|
+
item: view?.derived,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
received: { result, state },
|
|
57
|
+
expected: {
|
|
58
|
+
result: { kind: 'guard-mismatch', table: 'attempts', key: { id: attempt.id } },
|
|
59
|
+
state: { attempt: 'starting', facts: 2, held: [1], item: { state: 'running' } },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'#applyChange takes back every write of a change that throws',
|
|
66
|
+
async (store) => {
|
|
67
|
+
const workers = brandSchemas.limitName.parse('workers');
|
|
68
|
+
const filed = buildMockV1ItemRow();
|
|
69
|
+
const claimed = buildMockV1ItemRow();
|
|
70
|
+
const attempt = buildMockV1AttemptRow({ item: claimed.id, state: 'starting' });
|
|
71
|
+
const fileChange = buildMockFileItemChange(filed);
|
|
72
|
+
const claim = buildMockClaimChange(attempt, [workers]);
|
|
73
|
+
const reusedFactId: StoreChange = { ...claim, facts: [...claim.facts, ...fileChange.facts] };
|
|
74
|
+
|
|
75
|
+
await store.exclusive(async (transaction) => {
|
|
76
|
+
await transaction.syncLimits(
|
|
77
|
+
[{ name: workers, max: 1, counts: {} }],
|
|
78
|
+
brandSchemas.instant.parse(500),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await transaction.applyChange(fileChange);
|
|
82
|
+
await transaction.applyChange(buildMockFileItemChange(claimed));
|
|
83
|
+
await transaction.applyChange(reusedFactId).catch(() => null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const state: unknown = await store.read(async (transaction) => ({
|
|
87
|
+
attempt: await transaction.readAttempt(attempt.id),
|
|
88
|
+
seats: await transaction.listLimitSeats(),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
return { received: state, expected: { attempt: null, seats: [] } };
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
'#applyChange keeps a heartbeat started beside a change it refuses',
|
|
96
|
+
async (store) => {
|
|
97
|
+
const filed = buildMockV1ItemRow();
|
|
98
|
+
const missing = buildMockV1ItemRow();
|
|
99
|
+
const runner = brandSchemas.runnerId.parse('runner-1');
|
|
100
|
+
|
|
101
|
+
await store.exclusive((transaction) =>
|
|
102
|
+
Promise.all([
|
|
103
|
+
transaction.applyChange({
|
|
104
|
+
at: brandSchemas.instant.parse(3000),
|
|
105
|
+
writes: [
|
|
106
|
+
{ op: 'insert', table: 'items', row: filed },
|
|
107
|
+
{
|
|
108
|
+
op: 'update',
|
|
109
|
+
table: 'items',
|
|
110
|
+
key: { id: missing.id },
|
|
111
|
+
expect: {},
|
|
112
|
+
set: { title: 'Mend the gate' },
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
facts: [
|
|
116
|
+
{
|
|
117
|
+
id: buildMockFactId(),
|
|
118
|
+
type: 'boxxy.item.edited',
|
|
119
|
+
entity: `boxxy:item:${missing.id}`,
|
|
120
|
+
actor: 'boxxy:owner:local',
|
|
121
|
+
payload: { title: 'Mend the gate', byAttempt: null },
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}),
|
|
125
|
+
transaction
|
|
126
|
+
.listLimits()
|
|
127
|
+
.then(() => transaction.writeHeartbeat(runner, brandSchemas.instant.parse(1000))),
|
|
128
|
+
]),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const read: unknown = await store.read((transaction) => transaction.readRunner(runner));
|
|
132
|
+
|
|
133
|
+
return { received: read, expected: { id: 'runner-1', heartbeatAt: 1000, paused: false } };
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
'#applyChange runs two changes started together on one call one at a time',
|
|
138
|
+
async (store) => {
|
|
139
|
+
const claimed = buildMockV1ItemRow();
|
|
140
|
+
const later = buildMockV1ItemRow();
|
|
141
|
+
const first = buildMockV1AttemptRow({ item: claimed.id, state: 'starting', leaseGen: 1 });
|
|
142
|
+
|
|
143
|
+
const second = buildMockV1AttemptRow({
|
|
144
|
+
item: claimed.id,
|
|
145
|
+
state: 'starting',
|
|
146
|
+
leaseGen: 2,
|
|
147
|
+
number: 2,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await store.exclusive(async (transaction) => {
|
|
151
|
+
await transaction.applyChange(buildMockFileItemChange(claimed));
|
|
152
|
+
await transaction.applyChange(buildMockClaimChange(first));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const results = await store.exclusive((transaction) =>
|
|
156
|
+
Promise.all([
|
|
157
|
+
transaction.applyChange(buildMockClaimChange(second)),
|
|
158
|
+
transaction.applyChange(buildMockFileItemChange(later)),
|
|
159
|
+
]),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const read = await store.read((transaction) =>
|
|
163
|
+
transaction.readItem(later.id, brandSchemas.instant.parse(5000), buildMockItemRules()),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
received: { results, item: read?.id },
|
|
168
|
+
expected: {
|
|
169
|
+
results: [{ kind: 'lease-held' }, { kind: 'applied' }],
|
|
170
|
+
item: later.id,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
];
|