@amalgm/live 0.2.59 → 0.2.60
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/PURPOSE.md +17 -5
- package/dist/transfer/download.d.ts +65 -25
- package/dist/transfer/download.d.ts.map +1 -1
- package/dist/transfer/download.js +339 -84
- package/dist/transfer/download.js.map +1 -1
- package/dist/transfer/ranges.d.ts +20 -35
- package/dist/transfer/ranges.d.ts.map +1 -1
- package/dist/transfer/ranges.js +35 -121
- package/dist/transfer/ranges.js.map +1 -1
- package/dist/transfer/refs.d.ts +15 -14
- package/dist/transfer/refs.d.ts.map +1 -1
- package/dist/transfer/refs.js +33 -37
- package/dist/transfer/refs.js.map +1 -1
- package/dist-cjs/transfer/download.js +339 -85
- package/dist-cjs/transfer/download.js.map +1 -1
- package/dist-cjs/transfer/ranges.js +37 -124
- package/dist-cjs/transfer/ranges.js.map +1 -1
- package/dist-cjs/transfer/refs.js +34 -38
- package/dist-cjs/transfer/refs.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,150 +1,63 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Pack span planning.
|
|
4
4
|
*
|
|
5
|
-
* Sealed envelopes of many small objects live inside one immutable pack.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* ranges is cheaper to read through than to split a request over, so
|
|
10
|
-
* merge across gaps up to RANGE_MERGE_GAP_BYTES
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Sealed envelopes of many small objects live inside one immutable pack. A
|
|
6
|
+
* download wave knows every member it needs before any byte moves, so the
|
|
7
|
+
* members of one pack are planned into a few broad byte spans — one ranged
|
|
8
|
+
* GET per span — instead of a request per member. A small hole between
|
|
9
|
+
* wanted ranges is cheaper to read through than to split a request over, so
|
|
10
|
+
* spans merge across gaps up to RANGE_MERGE_GAP_BYTES; a span never grows
|
|
11
|
+
* past the caller's byte cap, so one planned read always fits the wave's
|
|
12
|
+
* in-flight memory bound. Wrong or shifted bytes can never leak: every
|
|
13
|
+
* dispensed slice is still an authenticated envelope the reader opens and
|
|
14
|
+
* hash-verifies.
|
|
13
15
|
*/
|
|
14
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.
|
|
16
|
-
exports.
|
|
17
|
-
exports.createSpanFetcher = createSpanFetcher;
|
|
17
|
+
exports.RANGE_MERGE_GAP_BYTES = void 0;
|
|
18
|
+
exports.planPackSpans = planPackSpans;
|
|
18
19
|
/** Read through holes up to this size rather than splitting a range request. */
|
|
19
20
|
exports.RANGE_MERGE_GAP_BYTES = 64 * 1024;
|
|
20
|
-
/** Merged span bytes one fetcher may hold in flight at once. Admission of
|
|
21
|
-
* spans — not just of the objects above them — is byte-bounded, so a wide
|
|
22
|
-
* same-turn fan-out can never hold every pack's sealed bytes at once. */
|
|
23
|
-
exports.SPAN_FETCH_MAX_IN_FLIGHT_BYTES = 64 * 1024 * 1024;
|
|
24
21
|
function invalidSpan(span) {
|
|
25
22
|
return !Number.isSafeInteger(span.offset) || span.offset < 0
|
|
26
23
|
|| !Number.isSafeInteger(span.length) || span.length < 1
|
|
27
24
|
|| !Number.isSafeInteger(span.offset + span.length);
|
|
28
25
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
/** Plan one pack's wanted member ranges into broad fetch spans. Pure
|
|
27
|
+
* planning — no IO. Members merge across holes up to `mergeGap`; a span
|
|
28
|
+
* stops growing at `maxSpanBytes`, except that a single member larger than
|
|
29
|
+
* the cap still gets exactly one span of its own. */
|
|
30
|
+
function planPackSpans(members, mergeGap, maxSpanBytes) {
|
|
31
|
+
if (members.length === 0)
|
|
32
|
+
return { spans: [], memberSpan: [] };
|
|
33
33
|
if (!Number.isSafeInteger(mergeGap) || mergeGap < 0) {
|
|
34
|
-
throw new Error('
|
|
34
|
+
throw new Error('pack span plan received an invalid merge gap');
|
|
35
35
|
}
|
|
36
|
-
if (
|
|
37
|
-
throw new Error('
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
if (!Number.isSafeInteger(maxSpanBytes) || maxSpanBytes < 1) {
|
|
37
|
+
throw new Error('pack span plan received an invalid span byte cap');
|
|
38
|
+
}
|
|
39
|
+
if (members.some(invalidSpan))
|
|
40
|
+
throw new Error('pack span plan received an invalid byte span');
|
|
41
|
+
const order = members.map((member, index) => ({ member, index }))
|
|
42
|
+
.sort((a, b) => a.member.offset - b.member.offset || a.member.length - b.member.length);
|
|
40
43
|
const spans = [];
|
|
41
|
-
const
|
|
42
|
-
for (const {
|
|
43
|
-
const end =
|
|
44
|
+
const memberSpan = new Array(members.length);
|
|
45
|
+
for (const { member, index } of order) {
|
|
46
|
+
const end = member.offset + member.length;
|
|
44
47
|
const current = spans[spans.length - 1];
|
|
45
|
-
if (current
|
|
48
|
+
if (current
|
|
49
|
+
&& member.offset <= current.end + mergeGap
|
|
50
|
+
&& (Math.max(current.end, end) - current.offset <= maxSpanBytes || end <= current.end)) {
|
|
46
51
|
current.end = Math.max(current.end, end);
|
|
47
52
|
}
|
|
48
53
|
else {
|
|
49
|
-
spans.push({ offset:
|
|
54
|
+
spans.push({ offset: member.offset, end });
|
|
50
55
|
}
|
|
51
|
-
|
|
56
|
+
memberSpan[index] = spans.length - 1;
|
|
52
57
|
}
|
|
53
58
|
return {
|
|
54
59
|
spans: spans.map((span) => ({ offset: span.offset, length: span.end - span.offset })),
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function createSpanFetcher(ports) {
|
|
59
|
-
const pending = new Map();
|
|
60
|
-
let scheduled = false;
|
|
61
|
-
// The memory law: merged span bytes in flight are admission-bounded. A
|
|
62
|
-
// span above the cap admits alone rather than deadlocking.
|
|
63
|
-
let inFlightSpanBytes = 0;
|
|
64
|
-
const waitingSpans = [];
|
|
65
|
-
const spanWeight = (length) => Math.min(length, exports.SPAN_FETCH_MAX_IN_FLIGHT_BYTES);
|
|
66
|
-
const admitSpan = (length) => new Promise((admit) => {
|
|
67
|
-
const weight = spanWeight(length);
|
|
68
|
-
if (inFlightSpanBytes === 0 || inFlightSpanBytes + weight <= exports.SPAN_FETCH_MAX_IN_FLIGHT_BYTES) {
|
|
69
|
-
inFlightSpanBytes += weight;
|
|
70
|
-
admit();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
waitingSpans.push({ weight, admit });
|
|
74
|
-
});
|
|
75
|
-
const releaseSpan = (length) => {
|
|
76
|
-
inFlightSpanBytes -= spanWeight(length);
|
|
77
|
-
while (waitingSpans.length > 0) {
|
|
78
|
-
const next = waitingSpans[0];
|
|
79
|
-
if (inFlightSpanBytes !== 0
|
|
80
|
-
&& inFlightSpanBytes + next.weight > exports.SPAN_FETCH_MAX_IN_FLIGHT_BYTES)
|
|
81
|
-
return;
|
|
82
|
-
waitingSpans.shift();
|
|
83
|
-
inFlightSpanBytes += next.weight;
|
|
84
|
-
next.admit();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
const flushUrl = async (url, waiters) => {
|
|
88
|
-
let plan;
|
|
89
|
-
try {
|
|
90
|
-
plan = planSpans(waiters.map((waiter) => waiter.range), exports.RANGE_MERGE_GAP_BYTES);
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
waiters.forEach((waiter) => waiter.reject(error));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
await Promise.all(plan.spans.map(async (span, spanIndex) => {
|
|
97
|
-
const served = [];
|
|
98
|
-
plan.slices.forEach((slice, index) => {
|
|
99
|
-
if (slice.span === spanIndex)
|
|
100
|
-
served.push({ waiter: waiters[index], start: slice.start });
|
|
101
|
-
});
|
|
102
|
-
await admitSpan(span.length);
|
|
103
|
-
try {
|
|
104
|
-
const bytes = await ports.fetchSpan(url, span);
|
|
105
|
-
if (bytes.length !== span.length) {
|
|
106
|
-
throw new Error(`pack range answered ${bytes.length} bytes where ${span.length} were requested`);
|
|
107
|
-
}
|
|
108
|
-
// Copies, not views: a dispensed slice must never pin the whole
|
|
109
|
-
// merged span buffer for as long as its consumer holds it.
|
|
110
|
-
served.forEach(({ waiter, start }) => waiter.resolve(bytes.slice(start, start + waiter.range.length)));
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
served.forEach(({ waiter }) => waiter.reject(error));
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
releaseSpan(span.length);
|
|
117
|
-
}
|
|
118
|
-
}));
|
|
119
|
-
};
|
|
120
|
-
const flush = () => {
|
|
121
|
-
scheduled = false;
|
|
122
|
-
const batches = [...pending.entries()];
|
|
123
|
-
pending.clear();
|
|
124
|
-
for (const [url, waiters] of batches)
|
|
125
|
-
void flushUrl(url, waiters);
|
|
126
|
-
};
|
|
127
|
-
return {
|
|
128
|
-
fetch(ref) {
|
|
129
|
-
if (ref.offset == null && ref.length == null) {
|
|
130
|
-
return ports.fetchSpan(ref.url, null);
|
|
131
|
-
}
|
|
132
|
-
const range = { offset: ref.offset ?? -1, length: ref.length ?? -1 };
|
|
133
|
-
if (invalidSpan(range)) {
|
|
134
|
-
return Promise.reject(new Error('content ref carries an invalid pack range'));
|
|
135
|
-
}
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
const waiters = pending.get(ref.url);
|
|
138
|
-
if (waiters)
|
|
139
|
-
waiters.push({ range, resolve, reject });
|
|
140
|
-
else
|
|
141
|
-
pending.set(ref.url, [{ range, resolve, reject }]);
|
|
142
|
-
if (!scheduled) {
|
|
143
|
-
scheduled = true;
|
|
144
|
-
setTimeout(flush, 0);
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
},
|
|
60
|
+
memberSpan,
|
|
148
61
|
};
|
|
149
62
|
}
|
|
150
63
|
//# sourceMappingURL=ranges.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ranges.js","sourceRoot":"","sources":["../../src/transfer/ranges.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"ranges.js","sourceRoot":"","sources":["../../src/transfer/ranges.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,49 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Content-ref
|
|
3
|
+
* Content-ref resolution for direct content-storage fetches.
|
|
4
4
|
*
|
|
5
|
-
* Downloads fetch every storage object straight from content storage.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Downloads fetch every storage object straight from content storage. A
|
|
6
|
+
* planned wave resolves the locations of its entire want-list up front — a
|
|
7
|
+
* handful of batched refs calls, never one per object — so pack members can
|
|
8
|
+
* be fetched as a few broad spans instead of a request per member. The
|
|
9
|
+
* authority answers each object with a short-lived URL and, when the
|
|
10
|
+
* object's sealed envelope rides inside an immutable pack, the exact byte
|
|
11
|
+
* range holding it. URLs are used immediately and never cached — their
|
|
12
|
+
* expiry is the gateway's revocation lever.
|
|
12
13
|
*/
|
|
13
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
15
|
exports.CONTENT_REFS_BATCH_OBJECTS = void 0;
|
|
15
|
-
exports.
|
|
16
|
+
exports.resolveContentRefs = resolveContentRefs;
|
|
17
|
+
const parallel_js_1 = require("./parallel.js");
|
|
16
18
|
/** One law shared with the gateway: the most objects one refs call may name. */
|
|
17
19
|
exports.CONTENT_REFS_BATCH_OBJECTS = 2048;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
/** Refs batches a wave resolves concurrently. Batches are control frames —
|
|
21
|
+
* small either way — so this only bounds simultaneous authority questions. */
|
|
22
|
+
const REFS_RESOLVE_CONCURRENCY = 4;
|
|
23
|
+
/** Resolve every named object's location, batching by the shared wire law.
|
|
24
|
+
* Answers align with the input. */
|
|
25
|
+
async function resolveContentRefs(ports, objects) {
|
|
26
|
+
if (objects.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
const refs = new Array(objects.length);
|
|
29
|
+
const starts = [];
|
|
30
|
+
for (let start = 0; start < objects.length; start += exports.CONTENT_REFS_BATCH_OBJECTS) {
|
|
31
|
+
starts.push(start);
|
|
32
|
+
}
|
|
33
|
+
await (0, parallel_js_1.parallelForEach)(starts, REFS_RESOLVE_CONCURRENCY, async (start) => {
|
|
34
|
+
const batch = objects.slice(start, start + exports.CONTENT_REFS_BATCH_OBJECTS);
|
|
35
|
+
const answered = await ports.requestRefs(batch);
|
|
36
|
+
if (answered.length !== batch.length) {
|
|
37
|
+
throw new Error('content refs answered the wrong number of objects');
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!scheduled) {
|
|
42
|
-
scheduled = true;
|
|
43
|
-
setTimeout(() => { void flush(); }, 0);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
},
|
|
47
|
-
};
|
|
39
|
+
answered.forEach((ref, index) => {
|
|
40
|
+
refs[start + index] = ref;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return refs;
|
|
48
44
|
}
|
|
49
45
|
//# sourceMappingURL=refs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refs.js","sourceRoot":"","sources":["../../src/transfer/refs.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"refs.js","sourceRoot":"","sources":["../../src/transfer/refs.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAkCH,gDAqBC;AArDD,+CAAgD;AAEhD,gFAAgF;AACnE,QAAA,0BAA0B,GAAG,IAAI,CAAC;AAE/C;8EAC8E;AAC9E,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAuBnC;mCACmC;AAC5B,KAAK,UAAU,kBAAkB,CACtC,KAAuB,EACvB,OAAqC;IAErC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAa,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,IAAI,kCAA0B,EAAE,CAAC;QAChF,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,MAAM,IAAA,6BAAe,EAAC,MAAM,EAAE,wBAAwB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QACtE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,kCAA0B,CAAC,CAAC;QACvE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAChD,IAAI,QAAQ,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACvE,CAAC;QACD,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAC9B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC;QAC5B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amalgm/live",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.60",
|
|
4
4
|
"description": "Portable realtime SDK for Amalgm — wire contracts, mutation discipline, and delivery state machines behind host-injected adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist-cjs/index.js",
|