@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 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.SUPPORTED_BEE_VERSION_EXACT = exports.SUPPORTED_BEE_VERSION = 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;
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 version_1 = require("./version");
62
- Object.defineProperty(exports, "SUPPORTED_BEE_VERSION", { enumerable: true, get: function () { return version_1.SUPPORTED_BEE_VERSION; } });
63
- Object.defineProperty(exports, "SUPPORTED_BEE_VERSION_EXACT", { enumerable: true, get: function () { return version_1.SUPPORTED_BEE_VERSION_EXACT; } });
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;