@ethersphere/bee-js 13.0.1-upcoming.gd3bc2f5 → 13.1.0
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 +58 -0
- package/dist/cjs/bee.js +3 -1
- package/dist/cjs/index.js +7 -4
- package/dist/cjs/modules/rolling-feed.js +188 -0
- package/dist/index.browser.min.js +1 -1
- package/dist/index.browser.min.js.map +1 -1
- package/dist/mjs/bee.js +3 -1
- package/dist/mjs/index.js +2 -1
- package/dist/mjs/modules/rolling-feed.js +189 -0
- package/dist/types/bee.d.ts +5 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/modules/rolling-feed.d.ts +56 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,8 @@ The `toString` method uses `toHex`.
|
|
|
127
127
|
| SOCReader | SingleOwnerChunk reader | `bee.soc.makeReader` |
|
|
128
128
|
| FeedWriter | Feed writer | `bee.feed.makeWriter` |
|
|
129
129
|
| FeedReader | Feed reader | `bee.feed.makeReader` |
|
|
130
|
+
| RollingFeedWriter | Rolling feed writer | `bee.rollingFeed.makeWriter` |
|
|
131
|
+
| RollingFeedReader | Rolling feed reader | `bee.rollingFeed.makeReader` |
|
|
130
132
|
|
|
131
133
|
### Bee API
|
|
132
134
|
|
|
@@ -332,6 +334,62 @@ const bee = new Bee('http://localhost:1633')
|
|
|
332
334
|
const uploadResult = await bee.collection.uploadFromDirectory(batchId, './path/to/gallery/')
|
|
333
335
|
```
|
|
334
336
|
|
|
337
|
+
### Rolling feed (periodically-restarting sequential feed)
|
|
338
|
+
|
|
339
|
+
A rolling feed avoids the unbounded growth of a plain sequential feed by restarting it every
|
|
340
|
+
`periodLength` seconds, so old postage-batch eviction never breaks the latest update. See
|
|
341
|
+
[ROLLING_FEED.md](./ROLLING_FEED.md) for the full design.
|
|
342
|
+
|
|
343
|
+
A rolling feed only stays readable while the writer keeps publishing. Keeping it alive is the
|
|
344
|
+
application's job, not the SDK's, so a writer belongs on a timer — republish on every tick, even
|
|
345
|
+
when the data has not changed.
|
|
346
|
+
|
|
347
|
+
#### Writer
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
import { Bee, PrivateKey, Topic } from '@ethersphere/bee-js'
|
|
351
|
+
|
|
352
|
+
const bee = new Bee('http://localhost:1633')
|
|
353
|
+
const topic = Topic.fromString('my-feed')
|
|
354
|
+
const signer = new PrivateKey('...')
|
|
355
|
+
const periodLength = 600 // 10 minutes
|
|
356
|
+
|
|
357
|
+
const writer = bee.rollingFeed.makeWriter(topic, signer, periodLength)
|
|
358
|
+
|
|
359
|
+
let latest = 'Hello, World!'
|
|
360
|
+
await writer.uploadPayload(batchId, latest)
|
|
361
|
+
|
|
362
|
+
// tick twice per period so a slow or delayed write still lands inside its own period
|
|
363
|
+
const handle = setInterval(async () => {
|
|
364
|
+
try {
|
|
365
|
+
await writer.uploadPayload(batchId, latest)
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.error('rolling feed heartbeat failed', error)
|
|
368
|
+
}
|
|
369
|
+
}, (periodLength / 2) * 1000)
|
|
370
|
+
|
|
371
|
+
process.on('SIGTERM', () => clearInterval(handle))
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
If the writer was down long enough to leave gaps behind, call `writer.catchUp(batchId)` before
|
|
375
|
+
resuming the timer to backfill the missed periods. It throws when no populated period is found
|
|
376
|
+
within `maxBackfill`, so it is for restarts, not for the very first run.
|
|
377
|
+
|
|
378
|
+
#### Reader
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
import { Bee, EthAddress, Topic } from '@ethersphere/bee-js'
|
|
382
|
+
|
|
383
|
+
const bee = new Bee('http://localhost:1633')
|
|
384
|
+
const topic = Topic.fromString('my-feed')
|
|
385
|
+
const owner = new EthAddress('...')
|
|
386
|
+
const periodLength = 600 // must match the writer
|
|
387
|
+
|
|
388
|
+
const reader = bee.rollingFeed.makeReader(topic, owner, periodLength)
|
|
389
|
+
const result = await reader.downloadPayload()
|
|
390
|
+
console.log(result.payload.toUtf8()) // prints 'Hello, World!'
|
|
391
|
+
```
|
|
392
|
+
|
|
335
393
|
### Customize http/https agent and headers
|
|
336
394
|
|
|
337
395
|
```js
|
package/dist/cjs/bee.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Bee = void 0;
|
|
4
|
-
const cafe_utility_1 = require("cafe-utility");
|
|
5
4
|
const core_sdk_1 = require("@ethersphere/core-sdk");
|
|
5
|
+
const cafe_utility_1 = require("cafe-utility");
|
|
6
6
|
const envelope_1 = require("./api/envelope");
|
|
7
7
|
const rchash_1 = require("./api/rchash");
|
|
8
8
|
const soc_1 = require("./chunk/soc");
|
|
@@ -18,6 +18,7 @@ const file_1 = require("./modules/file");
|
|
|
18
18
|
const grantee_1 = require("./modules/grantee");
|
|
19
19
|
const messaging_1 = require("./modules/messaging");
|
|
20
20
|
const pin_1 = require("./modules/pin");
|
|
21
|
+
const rolling_feed_1 = require("./modules/rolling-feed");
|
|
21
22
|
const settlement_1 = require("./modules/settlement");
|
|
22
23
|
const soc_2 = require("./modules/soc");
|
|
23
24
|
const stake_1 = require("./modules/stake");
|
|
@@ -90,6 +91,7 @@ class Bee {
|
|
|
90
91
|
this.chunk = new chunk_1.Chunk(context);
|
|
91
92
|
this.file = new file_1.File(context);
|
|
92
93
|
this.collection = new collection_1.Collection(context);
|
|
94
|
+
this.rollingFeed = new rolling_feed_1.RollingFeed(context);
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
95
97
|
* Creates a Content Addressed Chunk.
|
package/dist/cjs/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.Bee = exports.Size = exports.Utils = exports.Duration = exports.
|
|
39
|
+
exports.Bee = exports.SUPPORTED_BEE_VERSION_EXACT = exports.SUPPORTED_BEE_VERSION = exports.Size = exports.Utils = exports.Duration = exports.RollingFeedWriter = exports.RollingFeedReader = exports.MantarayNode = exports.TransactionId = exports.Topic = exports.Stamper = exports.Span = exports.Signature = exports.Reference = exports.PublicKey = exports.PrivateKey = exports.PeerAddress = exports.Identifier = exports.FeedIndex = exports.EthAddress = exports.ChunkSplitter = exports.ChunkBuilder = exports.Bytes = exports.BatchId = void 0;
|
|
40
40
|
const bee_1 = require("./bee");
|
|
41
41
|
Object.defineProperty(exports, "Bee", { enumerable: true, get: function () { return bee_1.Bee; } });
|
|
42
42
|
var core_sdk_1 = require("@ethersphere/core-sdk");
|
|
@@ -58,9 +58,9 @@ Object.defineProperty(exports, "Topic", { enumerable: true, get: function () { r
|
|
|
58
58
|
Object.defineProperty(exports, "TransactionId", { enumerable: true, get: function () { return core_sdk_1.TransactionId; } });
|
|
59
59
|
var manifest_1 = require("./manifest/manifest");
|
|
60
60
|
Object.defineProperty(exports, "MantarayNode", { enumerable: true, get: function () { return manifest_1.MantarayNode; } });
|
|
61
|
-
var
|
|
62
|
-
Object.defineProperty(exports, "
|
|
63
|
-
Object.defineProperty(exports, "
|
|
61
|
+
var rolling_feed_1 = require("./modules/rolling-feed");
|
|
62
|
+
Object.defineProperty(exports, "RollingFeedReader", { enumerable: true, get: function () { return rolling_feed_1.RollingFeedReader; } });
|
|
63
|
+
Object.defineProperty(exports, "RollingFeedWriter", { enumerable: true, get: function () { return rolling_feed_1.RollingFeedWriter; } });
|
|
64
64
|
__exportStar(require("./types"), exports);
|
|
65
65
|
__exportStar(require("./utils/constants"), exports);
|
|
66
66
|
var duration_1 = require("./utils/duration");
|
|
@@ -70,3 +70,6 @@ exports.Utils = __importStar(require("./utils/expose"));
|
|
|
70
70
|
var size_1 = require("./utils/size");
|
|
71
71
|
Object.defineProperty(exports, "Size", { enumerable: true, get: function () { return size_1.Size; } });
|
|
72
72
|
__exportStar(require("./utils/tokens"), exports);
|
|
73
|
+
var version_1 = require("./version");
|
|
74
|
+
Object.defineProperty(exports, "SUPPORTED_BEE_VERSION", { enumerable: true, get: function () { return version_1.SUPPORTED_BEE_VERSION; } });
|
|
75
|
+
Object.defineProperty(exports, "SUPPORTED_BEE_VERSION_EXACT", { enumerable: true, get: function () { return version_1.SUPPORTED_BEE_VERSION_EXACT; } });
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RollingFeedReader = exports.RollingFeedWriter = exports.RollingFeed = void 0;
|
|
4
|
+
const core_sdk_1 = require("@ethersphere/core-sdk");
|
|
5
|
+
const __1 = require("..");
|
|
6
|
+
const feed_1 = require("../api/feed");
|
|
7
|
+
const soc_1 = require("../chunk/soc");
|
|
8
|
+
const feed_2 = require("../feed");
|
|
9
|
+
const identifier_1 = require("../feed/identifier");
|
|
10
|
+
const error_1 = require("../utils/error");
|
|
11
|
+
// RollingFeedReader.downloadPayload/downloadReference only ever fall back one period, so
|
|
12
|
+
// backfilling deeper than that serves no reader by default - see catchUp's maxBackfill param.
|
|
13
|
+
const DEFAULT_MAX_BACKFILL = 1;
|
|
14
|
+
function periodIndex(t, periodLength) {
|
|
15
|
+
if (periodLength <= 0) {
|
|
16
|
+
throw new __1.BeeError('Period length must be greater than zero!');
|
|
17
|
+
}
|
|
18
|
+
return Math.floor(t / periodLength);
|
|
19
|
+
}
|
|
20
|
+
function topicFor(baseTopic, periodIdx) {
|
|
21
|
+
const baseTopicBytes = baseTopic.toUint8Array();
|
|
22
|
+
return new core_sdk_1.Topic((0, core_sdk_1.keccak256)(__1.Bytes.concat(baseTopicBytes, (0, core_sdk_1.numberToUint64)(BigInt(periodIdx), 'BE'))));
|
|
23
|
+
}
|
|
24
|
+
async function isPeriodPopulated(requestOptions, owner, topic) {
|
|
25
|
+
try {
|
|
26
|
+
await (0, feed_1.probeFeed)(requestOptions, owner, topic);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (e instanceof error_1.BeeResponseError) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function fetchLatestReference(requestOptions, owner, topic) {
|
|
37
|
+
const { feedIndex } = await (0, feed_1.probeFeed)(requestOptions, owner, topic);
|
|
38
|
+
const update = await (0, feed_2.downloadFeedUpdate)(requestOptions, owner, topic, feedIndex, true);
|
|
39
|
+
return {
|
|
40
|
+
reference: new core_sdk_1.Reference(update.payload.toUint8Array()),
|
|
41
|
+
feedIndex,
|
|
42
|
+
feedIndexNext: feedIndex.next(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
class RollingFeed {
|
|
46
|
+
constructor(context) {
|
|
47
|
+
this.context = context;
|
|
48
|
+
}
|
|
49
|
+
makeWriter(baseTopic, signer, periodLength) {
|
|
50
|
+
return new RollingFeedWriter(this.context, baseTopic, signer, periodLength);
|
|
51
|
+
}
|
|
52
|
+
makeReader(baseTopic, owner, periodLength) {
|
|
53
|
+
return new RollingFeedReader(this.context, baseTopic, owner, periodLength);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.RollingFeed = RollingFeed;
|
|
57
|
+
class RollingFeedWriter {
|
|
58
|
+
constructor(context, baseTopic, signer, periodLength) {
|
|
59
|
+
this.context = context;
|
|
60
|
+
this.baseTopic = baseTopic;
|
|
61
|
+
this.signer = signer;
|
|
62
|
+
this.periodLength = periodLength;
|
|
63
|
+
}
|
|
64
|
+
async uploadPayload(postageBatchId, payload, options) {
|
|
65
|
+
const requestOptions = this.context.getRequestOptionsForCall();
|
|
66
|
+
const stamp = new core_sdk_1.BatchId(postageBatchId);
|
|
67
|
+
const { currentTopic, nextTopic, mirrorOptions } = this.currentAndNextTopics(options);
|
|
68
|
+
const [result] = await Promise.all([
|
|
69
|
+
(0, feed_2.updateFeedWithPayload)(requestOptions, this.signer, currentTopic, payload, stamp, options),
|
|
70
|
+
(0, feed_2.updateFeedWithPayload)(requestOptions, this.signer, nextTopic, payload, stamp, mirrorOptions),
|
|
71
|
+
]);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
async uploadReference(postageBatchId, reference, options) {
|
|
75
|
+
const requestOptions = this.context.getRequestOptionsForCall();
|
|
76
|
+
const stamp = new core_sdk_1.BatchId(postageBatchId);
|
|
77
|
+
const { currentTopic, nextTopic, mirrorOptions } = this.currentAndNextTopics(options);
|
|
78
|
+
const [result] = await Promise.all([
|
|
79
|
+
(0, feed_2.updateFeedWithReference)(requestOptions, this.signer, currentTopic, reference, stamp, options),
|
|
80
|
+
(0, feed_2.updateFeedWithReference)(requestOptions, this.signer, nextTopic, reference, stamp, mirrorOptions),
|
|
81
|
+
]);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* True unless the period right before `periodIdx` (default: current) never got mirrored
|
|
86
|
+
* forward into it, i.e. the writer went silent for at least one whole period.
|
|
87
|
+
*/
|
|
88
|
+
async isCaughtUp(periodIdx) {
|
|
89
|
+
const requestOptions = this.context.getRequestOptionsForCall();
|
|
90
|
+
const targetPeriod = periodIdx ?? periodIndex(Date.now() / 1000, this.periodLength);
|
|
91
|
+
const owner = this.signer.publicKey().address();
|
|
92
|
+
return isPeriodPopulated(requestOptions, owner, topicFor(this.baseTopic, targetPeriod));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Backfills up to `maxBackfill` periods strictly between the last populated one and
|
|
96
|
+
* `periodIdx` (default: current) with that period's last known payload/reference. Never
|
|
97
|
+
* writes `periodIdx` itself - keeping it populated during silence is the caller's job (a
|
|
98
|
+
* periodic `uploadPayload`/`uploadReference` heartbeat), not catchUp's. A conditional
|
|
99
|
+
* "write it if empty" would race that heartbeat: there's no compare-and-swap on a SOC
|
|
100
|
+
* address, so a stale write can still land after a concurrent fresh one and shadow it.
|
|
101
|
+
*
|
|
102
|
+
* `maxBackfill` bounds both the backward scan and the backfill itself, since scanning
|
|
103
|
+
* further back than you're willing to backfill only wastes round trips. Defaults to 1,
|
|
104
|
+
* matching the reader's one-period fallback - nothing deeper is ever read anyway. Throws
|
|
105
|
+
* if no populated period is found within that bound.
|
|
106
|
+
*/
|
|
107
|
+
async catchUp(postageBatchId, periodIdx, maxBackfill = DEFAULT_MAX_BACKFILL) {
|
|
108
|
+
const requestOptions = this.context.getRequestOptionsForCall();
|
|
109
|
+
const stamp = new core_sdk_1.BatchId(postageBatchId);
|
|
110
|
+
const owner = this.signer.publicKey().address();
|
|
111
|
+
const targetPeriod = periodIdx ?? periodIndex(Date.now() / 1000, this.periodLength);
|
|
112
|
+
// periods before 0 can't exist (period index is derived from Unix time), so the scan
|
|
113
|
+
// must not probe them even when the lookback window would otherwise reach that far
|
|
114
|
+
const scanFloor = Math.max(0, targetPeriod - 1 - maxBackfill);
|
|
115
|
+
let lastGoodPeriod = targetPeriod - 1;
|
|
116
|
+
while (lastGoodPeriod >= scanFloor) {
|
|
117
|
+
if (await isPeriodPopulated(requestOptions, owner, topicFor(this.baseTopic, lastGoodPeriod))) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
lastGoodPeriod--;
|
|
121
|
+
}
|
|
122
|
+
if (lastGoodPeriod < scanFloor) {
|
|
123
|
+
throw new __1.BeeError(`No populated period found within ${maxBackfill} periods to catch up from!`);
|
|
124
|
+
}
|
|
125
|
+
if (lastGoodPeriod === targetPeriod - 1) {
|
|
126
|
+
return; // no gap - nothing to backfill
|
|
127
|
+
}
|
|
128
|
+
const sourceTopic = topicFor(this.baseTopic, lastGoodPeriod);
|
|
129
|
+
const { feedIndex: sourceIndex } = await (0, feed_1.probeFeed)(requestOptions, owner, sourceTopic);
|
|
130
|
+
const sourceChunk = await (0, feed_2.downloadFeedUpdateAsCAC)(requestOptions, owner, sourceTopic, sourceIndex);
|
|
131
|
+
const periodsToBackfill = Array.from({ length: targetPeriod - 1 - lastGoodPeriod }, (_, i) => lastGoodPeriod + 1 + i);
|
|
132
|
+
await Promise.all(periodsToBackfill.map(async (period) => {
|
|
133
|
+
const identifier = (0, identifier_1.makeFeedIdentifier)(topicFor(this.baseTopic, period), 0);
|
|
134
|
+
return (0, soc_1.uploadSingleOwnerChunkWithWrappedChunk)(requestOptions, this.signer, stamp, identifier, sourceChunk);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
currentAndNextTopics(options) {
|
|
138
|
+
const currentPeriod = periodIndex(Date.now() / 1000, this.periodLength);
|
|
139
|
+
return {
|
|
140
|
+
currentTopic: topicFor(this.baseTopic, currentPeriod),
|
|
141
|
+
nextTopic: topicFor(this.baseTopic, currentPeriod + 1),
|
|
142
|
+
mirrorOptions: { ...options, index: undefined },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.RollingFeedWriter = RollingFeedWriter;
|
|
147
|
+
class RollingFeedReader {
|
|
148
|
+
constructor(context, baseTopic, owner, periodLength) {
|
|
149
|
+
this.context = context;
|
|
150
|
+
this.baseTopic = baseTopic;
|
|
151
|
+
this.owner = owner;
|
|
152
|
+
this.periodLength = periodLength;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Reads the current period's feed; falls back to the previous period once if empty,
|
|
156
|
+
* to tolerate clock skew between writer and reader.
|
|
157
|
+
*/
|
|
158
|
+
async downloadPayload(options) {
|
|
159
|
+
const requestOptions = this.context.getRequestOptionsForCall();
|
|
160
|
+
const currentPeriod = periodIndex(Date.now() / 1000, this.periodLength);
|
|
161
|
+
try {
|
|
162
|
+
return await (0, feed_1.fetchLatestFeedUpdate)(requestOptions, this.owner, topicFor(this.baseTopic, currentPeriod), options);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
if (!(e instanceof error_1.BeeResponseError)) {
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
return (0, feed_1.fetchLatestFeedUpdate)(requestOptions, this.owner, topicFor(this.baseTopic, currentPeriod - 1), options);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Same as `downloadPayload`, but for a reference to data uploaded elsewhere.
|
|
173
|
+
*/
|
|
174
|
+
async downloadReference() {
|
|
175
|
+
const requestOptions = this.context.getRequestOptionsForCall();
|
|
176
|
+
const currentPeriod = periodIndex(Date.now() / 1000, this.periodLength);
|
|
177
|
+
try {
|
|
178
|
+
return await fetchLatestReference(requestOptions, this.owner, topicFor(this.baseTopic, currentPeriod));
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (!(e instanceof error_1.BeeResponseError)) {
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
return fetchLatestReference(requestOptions, this.owner, topicFor(this.baseTopic, currentPeriod - 1));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.RollingFeedReader = RollingFeedReader;
|