amalgm 0.1.207 → 0.1.209
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.
|
@@ -17,6 +17,7 @@ const CLAIM_LIMIT = 16;
|
|
|
17
17
|
const CLAIM_LEASE_MS = 30_000;
|
|
18
18
|
const RETRY_MS = 1_000;
|
|
19
19
|
const REAUTHORIZE_MS = 30_000;
|
|
20
|
+
const REPO_OBJECT_REQUEST_TIMEOUT_MS = 15_000;
|
|
20
21
|
|
|
21
22
|
function createSharedRealtimeTunnel({
|
|
22
23
|
send,
|
|
@@ -24,6 +25,7 @@ function createSharedRealtimeTunnel({
|
|
|
24
25
|
localPort,
|
|
25
26
|
localHeaders,
|
|
26
27
|
warn,
|
|
28
|
+
repoObjectRequestTimeoutMs = REPO_OBJECT_REQUEST_TIMEOUT_MS,
|
|
27
29
|
}) {
|
|
28
30
|
const mutationInflight = new Map();
|
|
29
31
|
const promotionInflight = new Map();
|
|
@@ -277,8 +279,17 @@ function createSharedRealtimeTunnel({
|
|
|
277
279
|
repoObjectTimer.unref?.();
|
|
278
280
|
}
|
|
279
281
|
|
|
282
|
+
function expireRepoObjectRequests(now = Date.now()) {
|
|
283
|
+
for (const inflight of [repoUploadInflight, repoDownloadInflight]) {
|
|
284
|
+
for (const [requestId, object] of inflight) {
|
|
285
|
+
if (now - object.sentAt >= repoObjectRequestTimeoutMs) inflight.delete(requestId);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
280
290
|
async function drainRepoObjects() {
|
|
281
291
|
if (!enabled) return;
|
|
292
|
+
expireRepoObjectRequests();
|
|
282
293
|
if (repoUploadInflight.size === 0) {
|
|
283
294
|
const result = await localStateJson('POST', '/state/repo-objects/uploads/claim', {});
|
|
284
295
|
const upload = result?.upload;
|
|
@@ -295,6 +306,7 @@ function createSharedRealtimeTunnel({
|
|
|
295
306
|
repoUploadInflight.set(requestId, {
|
|
296
307
|
resourceId: upload.resourceId,
|
|
297
308
|
sha256: upload.sha256,
|
|
309
|
+
sentAt: Date.now(),
|
|
298
310
|
});
|
|
299
311
|
}
|
|
300
312
|
}
|
|
@@ -313,6 +325,7 @@ function createSharedRealtimeTunnel({
|
|
|
313
325
|
repoDownloadInflight.set(requestId, {
|
|
314
326
|
resourceId: download.resourceId,
|
|
315
327
|
sha256: download.sha256,
|
|
328
|
+
sentAt: Date.now(),
|
|
316
329
|
});
|
|
317
330
|
}
|
|
318
331
|
}
|
|
@@ -462,35 +475,45 @@ function createSharedRealtimeTunnel({
|
|
|
462
475
|
}
|
|
463
476
|
|
|
464
477
|
function handleRepoObjectStored(frame) {
|
|
465
|
-
const object = repoUploadInflight.get(frame.request_id)
|
|
478
|
+
const object = repoUploadInflight.get(frame.request_id)
|
|
479
|
+
|| [...repoUploadInflight.values()].find((entry) => entry.sha256 === frame.sha256);
|
|
466
480
|
if (!object || object.sha256 !== frame.sha256) return;
|
|
467
481
|
void localStateJson('POST', '/state/repo-objects/uploads/ack', {
|
|
468
482
|
resourceId: object.resourceId,
|
|
469
483
|
sha256: object.sha256,
|
|
470
484
|
})
|
|
471
485
|
.then(() => {
|
|
472
|
-
repoUploadInflight
|
|
486
|
+
for (const [requestId, entry] of repoUploadInflight) {
|
|
487
|
+
if (entry.sha256 === object.sha256) repoUploadInflight.delete(requestId);
|
|
488
|
+
}
|
|
473
489
|
scheduleRepoObjects();
|
|
474
490
|
})
|
|
475
491
|
.catch(() => {
|
|
476
|
-
repoUploadInflight
|
|
492
|
+
for (const [requestId, entry] of repoUploadInflight) {
|
|
493
|
+
if (entry.sha256 === object.sha256) repoUploadInflight.delete(requestId);
|
|
494
|
+
}
|
|
477
495
|
scheduleRepoObjects(RETRY_MS);
|
|
478
496
|
});
|
|
479
497
|
}
|
|
480
498
|
|
|
481
499
|
function handleRepoObjectData(frame) {
|
|
482
|
-
const object = repoDownloadInflight.get(frame.request_id)
|
|
500
|
+
const object = repoDownloadInflight.get(frame.request_id)
|
|
501
|
+
|| [...repoDownloadInflight.values()].find((entry) => entry.sha256 === frame.sha256);
|
|
483
502
|
if (!object || object.sha256 !== frame.sha256) return;
|
|
484
503
|
void localStateJson('POST', '/state/repo-objects/downloads/receive', {
|
|
485
504
|
resourceId: object.resourceId,
|
|
486
505
|
sha256: object.sha256,
|
|
487
506
|
dataBase64: frame.data_b64,
|
|
488
507
|
}).then(() => {
|
|
489
|
-
repoDownloadInflight
|
|
508
|
+
for (const [requestId, entry] of repoDownloadInflight) {
|
|
509
|
+
if (entry.sha256 === object.sha256) repoDownloadInflight.delete(requestId);
|
|
510
|
+
}
|
|
490
511
|
scheduleRepoObjects();
|
|
491
512
|
}).catch((error) => {
|
|
492
513
|
warn(`repo object ${object.sha256} was not stored locally: ${error?.message || error}`);
|
|
493
|
-
repoDownloadInflight
|
|
514
|
+
for (const [requestId, entry] of repoDownloadInflight) {
|
|
515
|
+
if (entry.sha256 === object.sha256) repoDownloadInflight.delete(requestId);
|
|
516
|
+
}
|
|
494
517
|
scheduleRepoObjects(RETRY_MS);
|
|
495
518
|
});
|
|
496
519
|
}
|
package/package.json
CHANGED
|
@@ -122,3 +122,70 @@ test('repo object chunks travel through the shared tunnel one at a time', async
|
|
|
122
122
|
server.close();
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
test('repo object chunks retry when an acknowledgement is lost', async () => {
|
|
127
|
+
const uploadBytes = Buffer.from('retry this content-addressed chunk');
|
|
128
|
+
const uploadSha = crypto.createHash('sha256').update(uploadBytes).digest('hex');
|
|
129
|
+
const resourceId = 'shr_cp1abcdefghijklmnopqrstuvwx';
|
|
130
|
+
let acknowledgements = 0;
|
|
131
|
+
const server = http.createServer((req, res) => {
|
|
132
|
+
req.resume();
|
|
133
|
+
req.on('end', () => {
|
|
134
|
+
res.setHeader('content-type', 'application/json');
|
|
135
|
+
if (req.url === '/state/shared/replicas') {
|
|
136
|
+
res.end('{"replicas":[]}');
|
|
137
|
+
} else if (req.url.startsWith('/state/stream')) {
|
|
138
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
139
|
+
res.write('\n');
|
|
140
|
+
} else if (req.url === '/state/repo-objects/uploads/claim') {
|
|
141
|
+
res.end(JSON.stringify({
|
|
142
|
+
upload: {
|
|
143
|
+
resourceId,
|
|
144
|
+
sha256: uploadSha,
|
|
145
|
+
bytes: uploadBytes.length,
|
|
146
|
+
dataBase64: uploadBytes.toString('base64'),
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
} else if (req.url === '/state/repo-objects/downloads/claim') {
|
|
150
|
+
res.end('{"download":null}');
|
|
151
|
+
} else if (req.url === '/state/repo-objects/uploads/ack') {
|
|
152
|
+
acknowledgements += 1;
|
|
153
|
+
res.end('{"changed":true,"completed":true}');
|
|
154
|
+
} else {
|
|
155
|
+
res.end('{}');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); });
|
|
160
|
+
|
|
161
|
+
const frames = [];
|
|
162
|
+
const tunnel = createSharedRealtimeTunnel({
|
|
163
|
+
send: (frame) => { frames.push(frame); return true; },
|
|
164
|
+
sendLarge: (frame) => { frames.push(frame); return true; },
|
|
165
|
+
localPort: () => server.address().port,
|
|
166
|
+
localHeaders: (headers) => headers,
|
|
167
|
+
warn: () => {},
|
|
168
|
+
repoObjectRequestTimeoutMs: 25,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
tunnel.enable(['shared.mutation.v1']);
|
|
173
|
+
await until(
|
|
174
|
+
() => frames.filter((frame) => frame.type === 'repo.object.put').length >= 2,
|
|
175
|
+
'the unacknowledged upload retry',
|
|
176
|
+
);
|
|
177
|
+
const puts = frames.filter((frame) => frame.type === 'repo.object.put');
|
|
178
|
+
assert.notEqual(puts[0].request_id, puts[1].request_id);
|
|
179
|
+
assert.equal(puts[0].sha256, puts[1].sha256);
|
|
180
|
+
tunnel.handleFrame({
|
|
181
|
+
type: 'repo.object.stored',
|
|
182
|
+
request_id: puts[0].request_id,
|
|
183
|
+
sha256: uploadSha,
|
|
184
|
+
bytes: uploadBytes.length,
|
|
185
|
+
});
|
|
186
|
+
await until(() => acknowledgements === 1, 'the late upload acknowledgement');
|
|
187
|
+
} finally {
|
|
188
|
+
tunnel.stop();
|
|
189
|
+
server.close();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
@@ -120,6 +120,52 @@ test('only a value different from both the materialized baseline and the rail is
|
|
|
120
120
|
branches: [{ name: 'main', oid: oidC }],
|
|
121
121
|
};
|
|
122
122
|
assert.equal(hasLocalContribution(local, remoteAdvance, baseline), false);
|
|
123
|
+
assert.equal(hasLocalContribution(
|
|
124
|
+
{
|
|
125
|
+
head: { branch: 'main', oid: oidB, detached: false },
|
|
126
|
+
refs: [{ name: 'main', oid: oidB }],
|
|
127
|
+
},
|
|
128
|
+
remoteAdvance,
|
|
129
|
+
{
|
|
130
|
+
branch: 'main',
|
|
131
|
+
baseOid: oidB,
|
|
132
|
+
branches: [{ name: 'main', oid: oidB }],
|
|
133
|
+
},
|
|
134
|
+
null,
|
|
135
|
+
{
|
|
136
|
+
head: { branch: 'main', oid: oidA, detached: false },
|
|
137
|
+
branches: [{ name: 'main', oid: oidA }],
|
|
138
|
+
},
|
|
139
|
+
), false);
|
|
140
|
+
assert.equal(hasLocalContribution(
|
|
141
|
+
{
|
|
142
|
+
head: { branch: 'main', oid: oidB, detached: false },
|
|
143
|
+
refs: [{ name: 'main', oid: oidB }],
|
|
144
|
+
},
|
|
145
|
+
remoteAdvance,
|
|
146
|
+
baseline,
|
|
147
|
+
null,
|
|
148
|
+
{
|
|
149
|
+
head: { branch: 'main', oid: oidB, detached: false },
|
|
150
|
+
branches: [{ name: 'main', oid: oidB }],
|
|
151
|
+
},
|
|
152
|
+
), false);
|
|
153
|
+
assert.equal(hasLocalContribution(
|
|
154
|
+
local,
|
|
155
|
+
plan,
|
|
156
|
+
{
|
|
157
|
+
...baseline,
|
|
158
|
+
branches: [...baseline.branches, { name: 'feature', oid: null }],
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
{
|
|
162
|
+
head: { branch: 'main', oid: oidA, detached: false },
|
|
163
|
+
branches: [
|
|
164
|
+
{ name: 'main', oid: oidA },
|
|
165
|
+
{ name: 'feature', oid: oidB },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
), true);
|
|
123
169
|
assert.equal(hasLocalContribution(local, plan, baseline, {
|
|
124
170
|
checkpoint: 'repo-checkpoint@7',
|
|
125
171
|
machine: 'machine-a',
|
|
@@ -231,7 +231,11 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousC
|
|
|
231
231
|
const atBaseline = baseline
|
|
232
232
|
&& card.head?.branch === baseline.branch
|
|
233
233
|
&& card.head?.oid === baseline.baseOid;
|
|
234
|
-
|
|
234
|
+
const alreadyPublished = previousCard && samePosition(card, previousCard);
|
|
235
|
+
const reversesPublishedSharedPosition = previousCard
|
|
236
|
+
&& samePosition(previousCard, plan.card)
|
|
237
|
+
&& !alreadyPublished;
|
|
238
|
+
if ((!onRail && !atBaseline && !alreadyPublished) || reversesPublishedSharedPosition) return true;
|
|
235
239
|
|
|
236
240
|
const currentRefs = oidMap(card.refs);
|
|
237
241
|
const railRefs = oidMap(plan.branches);
|
|
@@ -245,11 +249,15 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousC
|
|
|
245
249
|
const current = currentRefs.get(name) ?? null;
|
|
246
250
|
const shared = railRefs.get(name) ?? null;
|
|
247
251
|
const previous = baselineRefs.get(name) ?? null;
|
|
248
|
-
const
|
|
249
|
-
|
|
252
|
+
const published = previousRefs.get(name) ?? null;
|
|
253
|
+
const matchesBaseline = baselineRefs.has(name) && current === previous;
|
|
254
|
+
const matchesPublished = previousRefs.has(name) && current === published;
|
|
255
|
+
const reversesPublishedSharedValue = previousRefs.has(name)
|
|
256
|
+
&& published === shared
|
|
257
|
+
&& current !== published;
|
|
250
258
|
if (
|
|
251
259
|
current !== shared
|
|
252
|
-
&& (!
|
|
260
|
+
&& ((!matchesBaseline && !matchesPublished) || reversesPublishedSharedValue)
|
|
253
261
|
) return true;
|
|
254
262
|
}
|
|
255
263
|
|