@solarpunkltd/swarm-id 0.0.0-stage → 0.8.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.
Files changed (111) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +86 -2
  3. package/dist/bus/account-bus.d.ts +104 -0
  4. package/dist/bus/account-delta.d.ts +52 -0
  5. package/dist/bus/bus-context.d.ts +7 -0
  6. package/dist/bus/envelope.d.ts +9 -0
  7. package/dist/bus/messages.d.ts +241 -0
  8. package/dist/bus/presence.d.ts +62 -0
  9. package/dist/bus/signaling-transport.d.ts +79 -0
  10. package/dist/chunk/bmt.d.ts +16 -0
  11. package/dist/chunk/cac.d.ts +17 -0
  12. package/dist/chunk/constants.d.ts +10 -0
  13. package/dist/chunk/encrypted-cac.d.ts +47 -0
  14. package/dist/chunk/encryption.d.ts +85 -0
  15. package/dist/chunk/index.d.ts +5 -0
  16. package/dist/chunks/chat-binding-DlH_ZkTn.js +23 -0
  17. package/dist/chunks/chat-binding-DlH_ZkTn.js.map +1 -0
  18. package/dist/errors.d.ts +35 -0
  19. package/dist/index.d.ts +27 -0
  20. package/dist/internal.d.ts +49 -0
  21. package/dist/internal.esm.js +2 -0
  22. package/dist/internal.esm.js.map +1 -0
  23. package/dist/proxy/act/act.d.ts +58 -0
  24. package/dist/proxy/act/crypto.d.ts +46 -0
  25. package/dist/proxy/act/grantee-list.d.ts +81 -0
  26. package/dist/proxy/act/history.d.ts +182 -0
  27. package/dist/proxy/act/index.d.ts +114 -0
  28. package/dist/proxy/app-stamp-limits.d.ts +62 -0
  29. package/dist/proxy/chunk-upload-stream.d.ts +37 -0
  30. package/dist/proxy/chunking.d.ts +15 -0
  31. package/dist/proxy/collection.d.ts +38 -0
  32. package/dist/proxy/download-data.d.ts +27 -0
  33. package/dist/proxy/errors.d.ts +8 -0
  34. package/dist/proxy/feed-manifest.d.ts +60 -0
  35. package/dist/proxy/feeds/epochs/async-finder.d.ts +70 -0
  36. package/dist/proxy/feeds/epochs/epoch.d.ts +79 -0
  37. package/dist/proxy/feeds/epochs/finder.d.ts +60 -0
  38. package/dist/proxy/feeds/epochs/index.d.ts +30 -0
  39. package/dist/proxy/feeds/epochs/types.d.ts +110 -0
  40. package/dist/proxy/feeds/epochs/updater.d.ts +76 -0
  41. package/dist/proxy/feeds/epochs/utils.d.ts +21 -0
  42. package/dist/proxy/feeds/index.d.ts +4 -0
  43. package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
  44. package/dist/proxy/feeds/sequence/finder.d.ts +11 -0
  45. package/dist/proxy/feeds/sequence/index.d.ts +22 -0
  46. package/dist/proxy/feeds/sequence/types.d.ts +82 -0
  47. package/dist/proxy/feeds/sequence/updater.d.ts +20 -0
  48. package/dist/proxy/index.d.ts +5 -0
  49. package/dist/proxy/manifest-builder.d.ts +164 -0
  50. package/dist/proxy/mantaray.d.ts +76 -0
  51. package/dist/proxy/soc-restamp-slots.d.ts +48 -0
  52. package/dist/proxy/stamp-chunk.d.ts +45 -0
  53. package/dist/proxy/stamp-worker-handler.d.ts +32 -0
  54. package/dist/proxy/stamp-worker-pool.d.ts +52 -0
  55. package/dist/proxy/stamp-worker.d.ts +1 -0
  56. package/dist/proxy/types.d.ts +38 -0
  57. package/dist/proxy/upload.d.ts +207 -0
  58. package/dist/schemas.d.ts +665 -0
  59. package/dist/stamp-worker.iife.js +1 -0
  60. package/dist/storage/debounced-uploader.d.ts +61 -0
  61. package/dist/storage/utilization-store.d.ts +113 -0
  62. package/dist/swarm-id-client.d.ts +1114 -0
  63. package/dist/swarm-id-proxy.d.ts +1031 -0
  64. package/dist/swarm-id.esm.js +2 -0
  65. package/dist/swarm-id.esm.js.map +1 -0
  66. package/dist/sync/batch-write-coordinator.d.ts +347 -0
  67. package/dist/sync/device-roster.d.ts +59 -0
  68. package/dist/sync/device-state.d.ts +199 -0
  69. package/dist/sync/fold-account-from-swarm.d.ts +53 -0
  70. package/dist/sync/index.d.ts +11 -0
  71. package/dist/sync/merge-snapshot.d.ts +57 -0
  72. package/dist/sync/partition-intent.d.ts +218 -0
  73. package/dist/sync/partition-lease.d.ts +531 -0
  74. package/dist/sync/partition-lock.d.ts +218 -0
  75. package/dist/sync/partition-state.d.ts +287 -0
  76. package/dist/sync/session-check.d.ts +29 -0
  77. package/dist/sync/store-interfaces.d.ts +48 -0
  78. package/dist/sync/sync-account.d.ts +45 -0
  79. package/dist/sync/timing-constants.d.ts +32 -0
  80. package/dist/sync/types.d.ts +18 -0
  81. package/dist/types.d.ts +3007 -0
  82. package/dist/utils/account-state-snapshot.d.ts +43 -0
  83. package/dist/utils/active-devices.d.ts +45 -0
  84. package/dist/utils/avatar.d.ts +30 -0
  85. package/dist/utils/batch-utilization.d.ts +840 -0
  86. package/dist/utils/batch-write-lock.d.ts +12 -0
  87. package/dist/utils/bee-error.d.ts +11 -0
  88. package/dist/utils/chat-binding.d.ts +113 -0
  89. package/dist/utils/coalesced-task.d.ts +28 -0
  90. package/dist/utils/collection.d.ts +6 -0
  91. package/dist/utils/connection-info.d.ts +11 -0
  92. package/dist/utils/constants.d.ts +28 -0
  93. package/dist/utils/device-id.d.ts +109 -0
  94. package/dist/utils/hex.d.ts +31 -0
  95. package/dist/utils/json-rpc.d.ts +34 -0
  96. package/dist/utils/key-derivation.d.ts +124 -0
  97. package/dist/utils/lock-soc.d.ts +35 -0
  98. package/dist/utils/postage-contract.d.ts +143 -0
  99. package/dist/utils/postage-stamp-association.d.ts +39 -0
  100. package/dist/utils/promise.d.ts +37 -0
  101. package/dist/utils/sealed-values.d.ts +23 -0
  102. package/dist/utils/seed-encryption.d.ts +17 -0
  103. package/dist/utils/site-allow-list.d.ts +54 -0
  104. package/dist/utils/stamp-lifespan.d.ts +37 -0
  105. package/dist/utils/storage-managers.d.ts +62 -0
  106. package/dist/utils/storage-probe.d.ts +40 -0
  107. package/dist/utils/tag.d.ts +11 -0
  108. package/dist/utils/ttl.d.ts +120 -0
  109. package/dist/utils/url.d.ts +47 -0
  110. package/dist/utils/versioned-storage.d.ts +183 -0
  111. package/package.json +91 -3
@@ -0,0 +1,840 @@
1
+ /**
2
+ * Batch Utilization Tracking for Swarm Storage
3
+ *
4
+ * Tracks slot usage for mutable postage batches. Each of the 65,536 buckets
5
+ * has a uint32 in-memory counter (`dataCounters[bucket]`) representing the
6
+ * next free slot. The counter is initialised at `DATA_COUNTER_START` so the
7
+ * first slots of every bucket are reserved as headroom for utilisation
8
+ * chunks that may incidentally land there — utilisation and data chunks
9
+ * share the same slot space.
10
+ *
11
+ * Per-chunk encryption keys are derived deterministically from the account's
12
+ * `swarmEncryptionKey` plus the chunk index and a small nonce. The nonce is
13
+ * bumped only when an upload would land in the same bucket as a lower-index
14
+ * chunk in the same save, so the on-chain placement of utilisation chunks is
15
+ * tidy and dedup stays stable across re-saves.
16
+ */
17
+ import { type Stamper, BatchId, type Bee, EthAddress, PrivateKey, type EnvelopeWithBatchId } from "@ethersphere/bee-js";
18
+ import { type ContentAddressedChunk } from "../chunk";
19
+ import type { UtilizationStoreDB } from "../storage/utilization-store";
20
+ /**
21
+ * Thrown by `UtilizationAwareStamper.stamp()` when the partition lease was
22
+ * invalidated (peer took over the partition) between binding and stamping.
23
+ * Callers should surface this as "your partition was reclaimed" — the
24
+ * upload cannot complete and any in-flight chunks would silently overwrite
25
+ * the peer's data if stamped anyway.
26
+ */
27
+ export declare class PartitionLeaseLostError extends Error {
28
+ constructor(message?: string);
29
+ }
30
+ /**
31
+ * Thrown by `UtilizationAwareStamper.stampDataChunk()` when the address is one
32
+ * the stamper routes to a reserved slot: a lock SOC, a counter or state chunk,
33
+ * or an intent SOC. Those slots hold the account's own bookkeeping, so their
34
+ * stamp is never handed to an app.
35
+ */
36
+ export declare class ReservedSlotError extends Error {
37
+ constructor(message?: string);
38
+ }
39
+ /**
40
+ * Thrown by `UtilizationAwareStamper.stampDataChunk()` when the chunk's bucket
41
+ * has no slot left in the app slice: the rest of that bucket's lane is kept
42
+ * for the account's own uploads.
43
+ */
44
+ export declare class AppSliceFullError extends Error {
45
+ constructor(message?: string);
46
+ }
47
+ /** Number of buckets in a postage batch (2^16) */
48
+ export declare const NUM_BUCKETS = 65536;
49
+ /** Bucket depth parameter (determines bucket count) */
50
+ export declare const BUCKET_DEPTH = 16;
51
+ /**
52
+ * Number of partitions the per-bucket slot space is divided into for
53
+ * multi-device sharing of a single postage batch. With K=2, partition 0
54
+ * uses data slots `{DATA_COUNTER_START + 0, DATA_COUNTER_START + 2, …}` and
55
+ * partition 1 uses `{DATA_COUNTER_START + 1, DATA_COUNTER_START + 3, …}`.
56
+ * See docs/Postage-Batch-Partitioning.md.
57
+ */
58
+ export declare const PARTITION_COUNT = 2;
59
+ /**
60
+ * Share of each bucket's data lane, counted from its top, that only the
61
+ * account's own uploads may use. An app stamp (`stampDataChunk`) is refused
62
+ * once the lane's counter reaches the rest, the app slice. Apps and the
63
+ * account draw from the one per-partition counter, so no slot is ever issued
64
+ * twice, and the slice needs no state of its own: it is read from the counter
65
+ * that is saved, published and handed over already.
66
+ */
67
+ export declare const ACCOUNT_RESERVED_LANE_SHARE: number;
68
+ /** First data slot index; reserved slots `[0, PARTITION_COUNT)` precede it. */
69
+ export declare const DATA_COUNTER_START = 2;
70
+ export { LEASE_TTL_MS } from "../sync/timing-constants";
71
+ /**
72
+ * How often the holder re-writes its partition lock SOC to bump
73
+ * `leasedUntil`. One third of the TTL gives three "safety net" refresh
74
+ * attempts before a peer would consider the lease expired.
75
+ */
76
+ export declare const LEASE_REFRESH_MS: number;
77
+ /**
78
+ * Local-clock margin for treating a lease as "could have lapsed". A holder must
79
+ * stop writing (and re-validating before ack) once `now >= leasedUntil - margin`
80
+ * so it ceases before a peer — reading the same `leasedUntil` from the lock SOC
81
+ * — could validly take over. Bounds the write/ack exposure to NTP-level skew,
82
+ * the same assumption the lock protocol already makes. Consumed by the stamper's
83
+ * local-lapse fence and the coordinator's reverse-clobber guard.
84
+ */
85
+ export declare const LEASE_SKEW_MARGIN_MS = 2000;
86
+ /**
87
+ * How long a holder may go without an upload before it voluntarily yields
88
+ * its partition (writes the release sentinel + publishes its final counter)
89
+ * so a waiting device can take the slot without waiting out the full TTL.
90
+ * The yielded device re-acquires transparently on its next upload. Enables
91
+ * turn-taking among 3+ devices sharing `PARTITION_COUNT` slots.
92
+ */
93
+ export declare const IDLE_YIELD_MS: number;
94
+ /** Size of each chunk in bytes */
95
+ export declare const CHUNK_SIZE = 4096;
96
+ /**
97
+ * Maximum batch depth at which a uint16 per-bucket counter is sufficient.
98
+ *
99
+ * At depth D the post-stamp counter caps at 2^(D-16). uint16 can hold
100
+ * 0..65535; depth 31 → 32768 (fits), depth 32 → 65536 (overflows).
101
+ * For depth > UINT16_COUNTER_MAX_DEPTH the serializer falls back to uint32.
102
+ *
103
+ * Issue: https://github.com/snaha/swarm-id/issues/243
104
+ */
105
+ export declare const UINT16_COUNTER_MAX_DEPTH = 31;
106
+ /**
107
+ * Per-batch-depth chunk layout for the utilization state.
108
+ *
109
+ * The on-chunk counter representation switches between uint16 and uint32
110
+ * based on whether the maximum slot index for the given batch depth fits
111
+ * in 16 bits. Each utilization chunk is always CHUNK_SIZE bytes; the
112
+ * number of buckets it covers — and thus the number of chunks needed —
113
+ * doubles when counters shrink from 4 bytes to 2.
114
+ */
115
+ export interface ChunkLayout {
116
+ /** Bytes per per-bucket counter on disk / on the wire */
117
+ counterByteSize: 2 | 4;
118
+ /** Number of buckets packed into a single CHUNK_SIZE-byte chunk */
119
+ bucketsPerChunk: number;
120
+ /** Total number of utilization chunks for one batch */
121
+ numUtilizationChunks: number;
122
+ }
123
+ /**
124
+ * Pick the on-disk chunk layout for a given batch depth.
125
+ */
126
+ export declare function getChunkLayout(batchDepth: number): ChunkLayout;
127
+ /**
128
+ * Metadata for a single utilization chunk
129
+ */
130
+ export interface ChunkMetadata {
131
+ /** Chunk index within the depth-dependent layout */
132
+ index: number;
133
+ /**
134
+ * Content hash / CAC reference (same thing for content-addressed chunks)
135
+ * Empty string means never uploaded
136
+ */
137
+ contentHash: string;
138
+ /** Last upload timestamp */
139
+ lastUpload: number;
140
+ /** Whether this chunk needs uploading */
141
+ dirty: boolean;
142
+ /**
143
+ * Encryption-key nonce that produced this chunk's `contentHash`. Used as
144
+ * the starting point for the next save's bucket-collision search so
145
+ * unchanged plaintexts reuse the same key (and skip re-upload).
146
+ */
147
+ nonce: number;
148
+ }
149
+ /**
150
+ * Utilization state for a postage batch
151
+ *
152
+ * Utilization data is stored as 32 chunks (uint16 codec, depth ≤ 31) or
153
+ * 64 chunks (uint32 codec, depth ≥ 32) on Swarm, with IndexedDB caching
154
+ * for performance.
155
+ */
156
+ export interface BatchUtilizationState {
157
+ /** Batch ID this state belongs to */
158
+ batchId: BatchId;
159
+ /** Batch depth — determines on-disk chunk layout (uint16 vs uint32) */
160
+ batchDepth: number;
161
+ /**
162
+ * Per-partition local counter (65,536 entries) — the 0-based `j` in
163
+ * `slot = partitionCount + partition + partitionCount·j` for the partition
164
+ * this state represents (partition 0 in single-device mode). This is the
165
+ * single counter of record: slot selection, persistence, reporting, and
166
+ * cross-device handoff all derive from it. Always uint32 in memory
167
+ * regardless of the on-disk codec; narrowing happens at the serialize
168
+ * boundary.
169
+ */
170
+ dataCounters: Uint32Array;
171
+ /** Metadata for each utilization chunk (32 for uint16, 64 for uint32) */
172
+ chunks: ChunkMetadata[];
173
+ /** Last sync timestamp */
174
+ lastSync: number;
175
+ }
176
+ /**
177
+ * Calculate which bucket a chunk belongs to based on its address.
178
+ * Uses the first 2 bytes of the chunk address as a big-endian uint16.
179
+ *
180
+ * This matches bee-js Stamper implementation.
181
+ *
182
+ * @param chunkAddress - The chunk's content address (32 bytes)
183
+ * @returns Bucket index (0-65535)
184
+ */
185
+ export declare function toBucket(chunkAddress: Uint8Array): number;
186
+ /**
187
+ * Calculate which utilization chunk a bucket belongs to.
188
+ *
189
+ * @param bucketIndex - Bucket index (0-65535)
190
+ * @param batchDepth - Batch depth, drives the chunk layout
191
+ * @returns Chunk index in the layout for this depth
192
+ */
193
+ export declare function getChunkIndexForBucket(bucketIndex: number, batchDepth: number): number;
194
+ /**
195
+ * Extract a CHUNK_SIZE-byte chunk from the dataCounters array using the
196
+ * codec implied by the batch depth.
197
+ *
198
+ * @param dataCounters - Full array of 65,536 counters (kept as Uint32Array
199
+ * in memory regardless of serialization size)
200
+ * @param chunkIndex - Index of chunk to extract
201
+ * @param batchDepth - Batch depth, drives the chunk layout
202
+ * @returns CHUNK_SIZE byte Uint8Array containing serialized counters
203
+ */
204
+ export declare function extractChunk(dataCounters: Uint32Array, chunkIndex: number, batchDepth: number): Uint8Array;
205
+ /**
206
+ * Merge a CHUNK_SIZE-byte chunk back into the dataCounters array using the
207
+ * codec implied by the batch depth.
208
+ *
209
+ * @param dataCounters - Full array of 65,536 counters (modified in place)
210
+ * @param chunkIndex - Index of chunk to merge
211
+ * @param chunkData - CHUNK_SIZE byte Uint8Array containing serialized counters
212
+ * @param batchDepth - Batch depth, drives the chunk layout
213
+ */
214
+ export declare function mergeChunk(dataCounters: Uint32Array, chunkIndex: number, chunkData: Uint8Array, batchDepth: number): void;
215
+ /**
216
+ * Tracks which utilization chunks have been modified and need uploading
217
+ */
218
+ export declare class DirtyChunkTracker {
219
+ private dirtyChunks;
220
+ private batchDepth;
221
+ constructor(batchDepth: number);
222
+ /**
223
+ * Mark a bucket as dirty (marks its containing chunk)
224
+ * @param bucketIndex - Bucket index (0-65535)
225
+ */
226
+ markDirty(bucketIndex: number): void;
227
+ /**
228
+ * Mark a chunk as clean (uploaded successfully)
229
+ * @param chunkIndex - Chunk index
230
+ */
231
+ markClean(chunkIndex: number): void;
232
+ /**
233
+ * Get array of dirty chunk indices
234
+ * @returns Sorted array of chunk indices that need uploading
235
+ */
236
+ getDirtyChunks(): number[];
237
+ /**
238
+ * Check if there are any dirty chunks
239
+ * @returns true if there are chunks waiting to be uploaded
240
+ */
241
+ hasDirtyChunks(): boolean;
242
+ /**
243
+ * Clear all dirty markers
244
+ */
245
+ clear(): void;
246
+ /**
247
+ * Get number of dirty chunks
248
+ * @returns Count of chunks waiting to be uploaded
249
+ */
250
+ get count(): number;
251
+ }
252
+ /**
253
+ * Resolved per-chunk-index encryption material.
254
+ *
255
+ * `nonce` is the smallest non-negative integer that placed the chunk in a
256
+ * bucket distinct from every chunk with a lower `chunkIndex` during the
257
+ * current save. `key` is the HMAC-derived 32-byte key used to produce the
258
+ * encrypted-CAC address (and therefore the chunk's bucket).
259
+ */
260
+ export interface UtilizationChunkKey {
261
+ key: Uint8Array;
262
+ nonce: number;
263
+ }
264
+ /**
265
+ * Derive the per-chunk encryption key used for utilisation chunks.
266
+ *
267
+ * `chunkKey = HMAC-SHA256(swarmEncryptionKey, "swarm-id-util-chunk-v1" || batchId || chunkIndex || nonce)`
268
+ *
269
+ * Same inputs → same key, so dedup at the upload path stays stable across
270
+ * re-saves whenever neither the plaintext nor the chosen nonce changes.
271
+ */
272
+ export declare function deriveUtilizationChunkKey(swarmEncryptionKey: Uint8Array, batchId: BatchId, chunkIndex: number, nonce: number): Promise<Uint8Array>;
273
+ /**
274
+ * Upload an encrypted utilization chunk to Swarm as CAC
275
+ *
276
+ * Architecture: Just upload encrypted chunk data as CAC (immutable)
277
+ *
278
+ * @param bee - Bee client instance
279
+ * @param stamper - Stamper for signing
280
+ * @param data - Chunk data to upload (4KB)
281
+ * @param encryptionKey - Encryption key (32 bytes)
282
+ * @returns CAC reference
283
+ */
284
+ export declare function uploadUtilizationChunk(bee: Bee, stamper: Stamper, data: Uint8Array, encryptionKey: Uint8Array): Promise<Uint8Array>;
285
+ /**
286
+ * Serialize Uint32Array to bytes (little-endian)
287
+ */
288
+ export declare function serializeUint32Array(arr: Uint32Array): Uint8Array;
289
+ /**
290
+ * Deserialize bytes to Uint32Array (little-endian)
291
+ */
292
+ export declare function deserializeUint32Array(bytes: Uint8Array): Uint32Array;
293
+ /**
294
+ * Serialize a Uint32Array to bytes as little-endian uint16 values.
295
+ *
296
+ * Counters exceeding uint16 throw — callers must use the uint32 codec
297
+ * (i.e. batchDepth >= UINT16_COUNTER_MAX_DEPTH + 1) for those depths.
298
+ */
299
+ export declare function serializeUint16Array(arr: Uint32Array): Uint8Array;
300
+ /**
301
+ * Deserialize little-endian uint16 bytes back into a Uint32Array (the
302
+ * in-memory representation stays uint32 even when the on-disk codec is
303
+ * narrower).
304
+ */
305
+ export declare function deserializeUint16Array(bytes: Uint8Array): Uint32Array;
306
+ /**
307
+ * Initialize a new batch utilization state
308
+ *
309
+ * Seeds `dataCounters[bucket]` at `DATA_COUNTER_START` for every bucket so
310
+ * the first stamping operations naturally skip the headroom slots.
311
+ *
312
+ * With 65,536 buckets and ~32–64 utilization chunks per save, the
313
+ * probability of any bucket getting 3+ utilisation chunks in one save is
314
+ * < 10⁻⁷, so a headroom of 2 leaves ample margin.
315
+ */
316
+ export declare function initializeBatchUtilization(batchId: BatchId, batchDepth: number): BatchUtilizationState;
317
+ /**
318
+ * Calculate max slots per bucket based on batch depth
319
+ */
320
+ export declare function calculateMaxSlotsPerBucket(batchDepth: number): number;
321
+ /**
322
+ * Check if a bucket has capacity for more chunks.
323
+ *
324
+ * `dataCounter` is the per-partition `j`, so it must be compared against the
325
+ * partition's lane capacity — not the raw slots-per-bucket. Comparing against
326
+ * the latter let `j` run past the lane and only surfaced as a mid-stamp bee-js
327
+ * throw with counters already incremented (#416).
328
+ */
329
+ export declare function hasBucketCapacity(dataCounter: number, batchDepth: number, partitionCount?: number): boolean;
330
+ /**
331
+ * Physical slot index for partition `partition`'s `j`-th data chunk in a
332
+ * bucket: `partitionCount + partition + partitionCount·j`. The first
333
+ * `partitionCount` slots `[0, partitionCount)` are reserved (one per
334
+ * partition, index = partition) for that partition's lock SOC / counter
335
+ * chunk, so data starts at `partitionCount` and the partitions' data lanes
336
+ * interleave without ever colliding. Legacy single-device is `partitionCount
337
+ * = 1, partition = 0` (reserved slot 0, data from slot 1).
338
+ * See docs/Postage-Batch-Partitioning.md.
339
+ */
340
+ export declare function dataSlot(partition: number, j: number, partitionCount: number): number;
341
+ /**
342
+ * Number of data chunks a single partition may write into one bucket, i.e.
343
+ * the count of distinct `j` values: `floor(slotsPerBucket / partitionCount)
344
+ * - 1` (the `-1` is the partition's reserved slot). Each partition's data
345
+ * lane is independent, so this is the per-partition per-bucket capacity.
346
+ */
347
+ export declare function partitionCapacity(batchDepth: number, partitionCount: number): number;
348
+ /**
349
+ * Fewest data chunks a partition's bucket lane must hold for a batch to be
350
+ * worth buying. At 1 the first chunk written to a bucket already fills that
351
+ * lane, so the batch reports 100% used while effectively empty (#538); at 3
352
+ * some bucket fills after ~1 MB of uniformly placed chunks, a drive too small
353
+ * to be worth its purchase (#566). 7 (depth 20) holds ~66 MB.
354
+ */
355
+ export declare const MIN_USABLE_PARTITION_CAPACITY = 4;
356
+ /**
357
+ * Smallest batch depth with a usable data lane: the first depth whose
358
+ * {@link partitionCapacity} reaches {@link MIN_USABLE_PARTITION_CAPACITY}.
359
+ * Searched rather than written out, so it follows BUCKET_DEPTH,
360
+ * PARTITION_COUNT and the reserved-slot term; capacity doubles with every
361
+ * depth, so the search terminates. With BUCKET_DEPTH 16 and PARTITION_COUNT 2
362
+ * it is 20 — depth 17 has no data slot at all, 18 exactly one, 19 three.
363
+ */
364
+ export declare const MIN_USABLE_BATCH_DEPTH: number;
365
+ /**
366
+ * Load utilization state with cache hierarchy
367
+ *
368
+ * Load order:
369
+ * 1. Try IndexedDB cache (all chunks for the depth's layout)
370
+ * 2. If a bucket has no cached chunk, seed it with a zeroed default
371
+ * 3. If nothing is cached, initialize new state
372
+ *
373
+ * The on-Swarm counter is resumed via the partition-state feed (see
374
+ * `sync/partition-state.ts`), not here — this only reads the local cache.
375
+ *
376
+ * @param batchId - Batch ID
377
+ * @param options - Load options (local IndexedDB cache)
378
+ * @returns Utilization state
379
+ */
380
+ export declare function loadUtilizationState(batchId: BatchId, batchDepth: number, options: {
381
+ cache: UtilizationStoreDB;
382
+ }): Promise<BatchUtilizationState>;
383
+ /**
384
+ * Save utilization state with incremental upload
385
+ *
386
+ * Only uploads dirty chunks to minimize network traffic.
387
+ * Updates IndexedDB cache with new chunk data.
388
+ *
389
+ * @param state - Current utilization state (modified in place)
390
+ * @param options - Save options
391
+ */
392
+ export declare function saveUtilizationState(state: BatchUtilizationState, options: {
393
+ bee: Bee;
394
+ stamper: Stamper;
395
+ encryptionKey: Uint8Array;
396
+ cache: UtilizationStoreDB;
397
+ tracker: DirtyChunkTracker;
398
+ reservedBuckets?: ReadonlySet<number>;
399
+ }): Promise<void>;
400
+ /**
401
+ * Update utilization state after writing data chunks
402
+ *
403
+ * This function:
404
+ * 1. Loads current state from the local cache
405
+ * 2. Updates bucket counters for new data chunks
406
+ * 3. Marks affected utilization chunks as dirty
407
+ * 4. Returns state and tracker for later upload
408
+ *
409
+ * @param batchId - Batch ID
410
+ * @param dataChunks - Data chunks that were written
411
+ * @param batchDepth - Batch depth parameter
412
+ * @param options - Load options (local IndexedDB cache)
413
+ * @returns Updated state and dirty chunk tracker
414
+ */
415
+ export declare function updateAfterWrite(batchId: BatchId, dataChunks: ContentAddressedChunk[], batchDepth: number, options: {
416
+ cache: UtilizationStoreDB;
417
+ }): Promise<{
418
+ state: BatchUtilizationState;
419
+ tracker: DirtyChunkTracker;
420
+ }>;
421
+ /**
422
+ * Calculate current utilization fraction for a batch
423
+ *
424
+ * @param state - Current utilization state
425
+ * @param batchDepth - Batch depth parameter
426
+ * @returns Utilization as decimal fraction (0-1)
427
+ */
428
+ export declare function calculateUtilization(state: BatchUtilizationState, batchDepth: number, partitionCount?: number): number;
429
+ /**
430
+ * Decides which slot of its bucket a chunk is stamped at. Assigning a slot
431
+ * spends it, so a stamp has exactly one assigner however many things can sign.
432
+ */
433
+ export interface SlotAssigner {
434
+ readonly batchId: BatchId;
435
+ assignSlot(chunkAddress: Uint8Array): number;
436
+ }
437
+ /**
438
+ * Stamper that owns slot assignment for a postage batch
439
+ *
440
+ * - Loads the per-bucket counters from cached utilization data on creation
441
+ * - `assignSlot()` decides where a chunk's stamp goes: a reserved slot for the
442
+ * lock / intent SOCs and utilisation chunks, otherwise the next slot of this
443
+ * device's partition lane, behind the lease fence
444
+ * - `stamp()` is `assignSlot()` plus core-sdk's stateless signing. Anything
445
+ * that signs elsewhere (`StampWorkerPool`) takes its slot from `assignSlot()`
446
+ * too, so there is one slot owner whoever signs
447
+ * - Provides a flush() method to persist updates back to cache
448
+ */
449
+ export declare class UtilizationAwareStamper implements Stamper, SlotAssigner {
450
+ private utilizationState;
451
+ private cache;
452
+ private readonly encryptionKey;
453
+ private dirty;
454
+ private dirtyBuckets;
455
+ /**
456
+ * Partition this device holds within the shared postage batch. `undefined`
457
+ * means the legacy single-device path, stamped as partition 0 of 1: the
458
+ * behaviour for every account created before the partition-lease shipped.
459
+ */
460
+ private partition;
461
+ /**
462
+ * Number of partitions the slot space is divided into. `1` is the
463
+ * legacy/no-partitioning value and goes hand-in-hand with `partition`
464
+ * being `undefined`.
465
+ */
466
+ private partitionCountValue;
467
+ /**
468
+ * Per-account lock-SOC addresses (one per partition). When `stamp()` sees
469
+ * a chunk whose address matches one of these, it routes the write to the
470
+ * lock SOC's reserved slot (= the partition index) within the same bucket,
471
+ * bypassing the partition data-slot formula and the local counter bump.
472
+ * Overstamping the same SOC at the same slot does not consume new slot
473
+ * budget, so the heartbeat cadence is sustainable.
474
+ *
475
+ * Populated by `bindLockSocs()` once per stamper lifetime (deterministic
476
+ * from accountId + partitionCount + owner). Independent of `partition`
477
+ * binding so lock-SOC writes work both before and after `bindPartition`.
478
+ */
479
+ private lockSocs;
480
+ /**
481
+ * Hex addresses of the utilisation (counter) chunks for the in-flight save,
482
+ * mapped to the reserved SLOT each must overstamp. `stamp()` routes these to
483
+ * that slot instead of a data slot. The slot is recorded at marking time
484
+ * (not read from `this.partition` at stamp time) because the teardown
485
+ * release publishes through an UNBOUND stamper — `partition` is already
486
+ * undefined there, and falling back to slot 0 would overstamp the OTHER
487
+ * partition's reserved chunks. Unlike the lock SOC, a counter chunk's
488
+ * address changes whenever the counter changes, so writers register the
489
+ * current set via `markReservedUtilizationChunk` and clear it with
490
+ * `clearReservedUtilizationChunks`.
491
+ */
492
+ private reservedUtilizationChunks;
493
+ /**
494
+ * The per-partition intent SOC for an in-flight intent round (Phase 2 —
495
+ * `partition-intent.ts`), mapped to the reserved SLOT it must overstamp (the
496
+ * contended partition index). Routed like the lock SOC / utilisation chunks:
497
+ * `stamp()` places it in the partition's reserved slot, so it NEVER lands in
498
+ * a data lane (`>= DATA_COUNTER_START`) and cannot overstamp user data.
499
+ * Single-entry and self-clearing (set immediately before the upload, cleared
500
+ * in a `finally`), so it stays isolated from the `reservedUtilizationChunks`
501
+ * save lifecycle and never grows. See `reserveIntentSocSlot`.
502
+ */
503
+ private intentSoc;
504
+ /**
505
+ * Promise-chain mutex serializing {@link withIntentSocSlot} critical sections.
506
+ * The intent / occupancy / state-pointer SOC writes all share the single
507
+ * {@link intentSoc} reservation, but their callers run concurrently (the
508
+ * off-lock refresh-tick presence/occupancy beacon vs. an under-lock upload's
509
+ * state-pointer publish). Without this, the two reserve→stamp windows overlap:
510
+ * the second `reserveIntentSocSlot` clobbers the first's address, so the
511
+ * first `stamp()` no longer matches `intentSoc` and falls through to a DATA
512
+ * slot — consuming data budget and bumping the counter under the very publish
513
+ * that is diffing it. The mutex makes each writer wait for the previous to
514
+ * clear its reservation.
515
+ */
516
+ private intentSocLock;
517
+ /**
518
+ * Circuit breaker for in-flight uploads. Flipped to `true` when the proxy
519
+ * detects displacement on a refresh tick (or upload-start lease check), or
520
+ * tears the lease down; subsequent data `stamp()` calls throw
521
+ * `PartitionLeaseLostError` to abort the upload cleanly instead of silently
522
+ * corrupting the peer's slot space. Outlives `unbindPartition` — an unbound
523
+ * stamper would otherwise stamp the rest of that upload at legacy slots —
524
+ * and is cleared only by the next `bindPartition`.
525
+ */
526
+ private leaseStale;
527
+ /**
528
+ * Wall-clock ms this device's lease is valid until (from the lock SOC's
529
+ * `leasedUntil`, pushed by the coordinator on bind and every renewal). While a
530
+ * multi-chunk upload runs, each `stamp()` re-checks it locally (no network) and
531
+ * throws once `Date.now()` passes it (minus a skew margin), so an un-renewable
532
+ * holder stops writing mid-op the instant its own clock says the lease lapsed —
533
+ * closing the "ack-safe, not write-safe" residual to the irreducible skew bound
534
+ * (Postage-Batch-Partitioning.md §12). `undefined` = not fenced (legacy mode,
535
+ * or between bind and the coordinator's first push).
536
+ */
537
+ private leaseValidUntil;
538
+ /**
539
+ * This device's clock, injectable for tests (#385). The local-lapse fence
540
+ * compares it against `leaseValidUntil`, so a test MUST drive the stamper with
541
+ * the SAME clock it gives the `PartitionLease` (whose `leasedUntil` feeds
542
+ * `leaseValidUntil`) — otherwise the two disagree and the fence mis-fires.
543
+ * Defaults to `Date.now()` (production: one wall clock everywhere).
544
+ */
545
+ private readonly now;
546
+ /**
547
+ * Per-partition buckets of the latest published partition-state chunks.
548
+ * Utilisation saves must not place chunks in these buckets (same reserved
549
+ * slot → overstamp → the published resume point is evicted from the
550
+ * reserve). Mirrors `BatchMetadata.stateChunkBuckets`; restored on create.
551
+ */
552
+ private protectedStateBuckets;
553
+ readonly signer: PrivateKey;
554
+ readonly batchId: BatchId;
555
+ readonly depth: number;
556
+ readonly maxSlot: number;
557
+ /**
558
+ * The next physical data slot per bucket, as core-sdk's `Stamper` means it.
559
+ * A snapshot: the counters of record are lane-relative (`dataCounters`), so
560
+ * writing here moves nothing — take a slot with {@link assignSlot}.
561
+ * Allocates a fresh NUM_BUCKETS-entry array per call.
562
+ */
563
+ get buckets(): Uint32Array;
564
+ /** Current partition (undefined in single-device legacy mode). */
565
+ get currentPartition(): number | undefined;
566
+ /** Total partition count (1 in legacy mode). */
567
+ get partitionCount(): number;
568
+ /**
569
+ * Per-bucket local stamping counter, exposed so the lease orchestrator
570
+ * can publish it to the partition-state feed on release. Returns
571
+ * undefined in legacy mode.
572
+ */
573
+ getLocalCounter(): Uint32Array | undefined;
574
+ /**
575
+ * Buckets that contain a per-partition lock SOC. External callers that
576
+ * persist utilisation chunks (e.g. `saveUtilizationState` from the sync
577
+ * path) should treat these as already claimed so utilisation-chunk key
578
+ * search avoids dropping a chunk on the lock SOC's bucket.
579
+ */
580
+ getLockSocBuckets(): ReadonlySet<number>;
581
+ /**
582
+ * All buckets a utilisation save must avoid: the lock-SOC buckets plus the
583
+ * buckets of the latest published partition-state chunks for the partition
584
+ * this stamper writes reserved slots in (`partition ?? 0`). A utilisation
585
+ * chunk landing in a published state chunk's bucket would overstamp the
586
+ * same reserved slot and evict the published chunk from the reserve —
587
+ * destroying the resume point a later takeover depends on.
588
+ */
589
+ getProtectedBuckets(): ReadonlySet<number>;
590
+ /**
591
+ * Buckets occupied by this stamper's current CLEAN utilisation chunks.
592
+ * The partition-state publish avoids them so a randomly-keyed state chunk
593
+ * does not evict a live utilisation chunk from the reserve.
594
+ */
595
+ getCleanChunkBuckets(): ReadonlySet<number>;
596
+ /**
597
+ * Record the buckets occupied by the latest published partition-state
598
+ * chunks for `partition` (in memory + persisted in the batch metadata, so
599
+ * a reload keeps avoiding them). Called after every successful publish
600
+ * (`PartitionLease.release`) and after every successful state read
601
+ * (`claimPartition`) — the set is replaced wholesale; only the latest
602
+ * publish needs protection, superseded chunks are dead.
603
+ */
604
+ setProtectedStateBuckets(partition: number, buckets: number[]): Promise<void>;
605
+ /**
606
+ * Bind this stamper to a leased partition. Called once by the lease
607
+ * orchestrator after `acquire()` succeeds. `localCounter` is the
608
+ * starting per-bucket count for THIS device — fresh zeros for Case A,
609
+ * the resumed high-water from the state feed for Cases B/D.
610
+ */
611
+ bindPartition(opts: {
612
+ partition: number;
613
+ partitionCount: number;
614
+ localCounter: Uint32Array;
615
+ }): void;
616
+ /**
617
+ * Bind the per-partition lock-SOC addresses. Called once per stamper
618
+ * lifetime (the addresses are deterministic from the account). Routes
619
+ * lock-SOC overstamps to a fixed reserved slot per partition so the tight
620
+ * heartbeat cadence doesn't consume new slot budget every refresh.
621
+ *
622
+ * Independent of `bindPartition` — can be called before the partition
623
+ * lease is acquired (the first lock-SOC write inside `acquirePartitionLock`
624
+ * needs this routing to already be in place).
625
+ */
626
+ bindLockSocs(socs: ReadonlyArray<{
627
+ partition: number;
628
+ address: Uint8Array;
629
+ }>): void;
630
+ /**
631
+ * Register a utilisation/state chunk address so the next `stamp()` of it
632
+ * routes to a reserved slot instead of a data slot. `slot` defaults to the
633
+ * currently bound partition — pass it explicitly when the stamper may be
634
+ * unbound at stamp time (the teardown release publishes state chunks after
635
+ * `unbindPartition()`, and a default of 0 would overstamp partition 0's
636
+ * reserved chunks). Idempotent per address.
637
+ */
638
+ markReservedUtilizationChunk(address: Uint8Array, slot?: number): void;
639
+ /** Clear the registered utilisation-chunk addresses after a save. */
640
+ clearReservedUtilizationChunks(): void;
641
+ /**
642
+ * Register the per-partition intent SOC so `stamp()` routes it to `slot`
643
+ * (the contended partition's reserved index) instead of a data slot — never
644
+ * consuming data budget or bumping the counter. `slot` is passed explicitly
645
+ * because the intent round runs BEFORE `bindPartition`, so `this.partition`
646
+ * is not yet set. Call immediately before the upload; pair with
647
+ * `clearIntentSocSlot` in a `finally`.
648
+ */
649
+ reserveIntentSocSlot(address: Uint8Array, slot: number): void;
650
+ /** Clear the registered intent SOC after its upload. */
651
+ clearIntentSocSlot(): void;
652
+ /**
653
+ * Reserve the intent-SOC slot for `address`, run `fn` (the upload), then
654
+ * clear it — serialized against every other `withIntentSocSlot` call on this
655
+ * stamper so concurrent intent/occupancy/state-pointer writes can't clobber
656
+ * one another's reservation (see {@link intentSocLock}). Production callers
657
+ * (`writeReservedPartitionSoc`, `writeStatePointer`) MUST use this instead of
658
+ * the bare `reserveIntentSocSlot`/`clearIntentSocSlot` pair. `partition` is the
659
+ * reserved slot index the SOC routes to (the contended partition's index).
660
+ */
661
+ withIntentSocSlot<T>(address: Uint8Array, partition: number, fn: () => Promise<T>): Promise<T>;
662
+ /**
663
+ * Inverse of `bindPartition` — clears partition slot state on demote.
664
+ * Leaves `lockSocs` intact (still valid for the account; refresh/yield
665
+ * writes may need them), and leaves an invalidated lease invalidated: see
666
+ * `leaseStale`.
667
+ */
668
+ unbindPartition(): void;
669
+ /**
670
+ * Circuit-break: mark the bound lease as stale. Every data `stamp()` from
671
+ * here to the next `bindPartition` throws `PartitionLeaseLostError`, so an
672
+ * in-flight upload aborts cleanly mid-stream when a peer takes our partition
673
+ * or the lease is torn down under it. No-op when no partition is bound:
674
+ * there is no lease to lose, and legacy stamping must stay unfenced.
675
+ */
676
+ invalidateLease(): void;
677
+ /**
678
+ * Record the wall-clock ms this device's lease is valid until (the lock SOC's
679
+ * `leasedUntil`). The coordinator pushes it on bind and every successful
680
+ * renewal; `undefined` disables the local-lapse fence. See `leaseValidUntil`.
681
+ */
682
+ setLeaseValidUntil(ms: number | undefined): void;
683
+ /**
684
+ * True when this device's own clock says the lease has lapsed (or is within
685
+ * the skew margin of it). Pure/local — no network — so it can gate every
686
+ * `stamp()` on the hot path for free. `undefined` deadline → not fenced.
687
+ */
688
+ private leaseLocallyLapsed;
689
+ private constructor();
690
+ /**
691
+ * Create a UtilizationAwareStamper with bucket state from cache
692
+ *
693
+ * @param privateKey - Signer private key
694
+ * @param batchId - Postage batch ID
695
+ * @param depth - Batch depth
696
+ * @param cache - Utilization cache database
697
+ * @param _owner - Owner address (required for validation, reserved for future Swarm upload)
698
+ * @param encryptionKey - Encryption key used to compute the canonical Swarm chunk address on local flush. Both call sites already pass the account's encryption key; "anything stable" works for now, will be tied to multi-device collision avoidance later.
699
+ * @returns New UtilizationAwareStamper instance
700
+ */
701
+ static create(privateKey: Uint8Array | string, batchId: BatchId, depth: number, cache: UtilizationStoreDB, owner: EthAddress, encryptionKey: Uint8Array, now?: () => number): Promise<UtilizationAwareStamper>;
702
+ /**
703
+ * The reserved slot `chunkAddress` overstamps, if it is one of the account's
704
+ * own bookkeeping chunks, else undefined.
705
+ */
706
+ private reservedSlotFor;
707
+ /**
708
+ * Partition lease was reclaimed: abort cleanly before a data stamp lands in
709
+ * slot space the peer now controls. Single-device legacy mode has no lease
710
+ * to invalidate, so neither condition can hold there. Two conditions:
711
+ * - `leaseStale`: the refresh tick CONFIRMED a peer took over (write-verify
712
+ * failed / displacement beacon seen), or the coordinator tore the lease
713
+ * down. Holds across the unbind that follows, where the fall-through
714
+ * in `assignSlot` would stamp at legacy slots.
715
+ * - `leaseLocallyLapsed()`: this device's own clock says the lease expired
716
+ * (skew margin applied). Catches the un-renewable case a disjoint-gateway
717
+ * refresh can't confirm by reading, stopping a long op mid-stream the
718
+ * moment the lease lapses locally, so the data write is fenced to the same
719
+ * skew bound as the ack (Postage-Batch-Partitioning.md §12, "ack-safe").
720
+ *
721
+ * @throws PartitionLeaseLostError
722
+ */
723
+ private requireLiveLease;
724
+ /**
725
+ * Pick the slot `chunkAddress` is stamped at within its bucket, and spend it.
726
+ *
727
+ * Synchronous on purpose: callers fire stamps concurrently, and with no await
728
+ * in here each assignment runs to completion before the next starts. Whoever
729
+ * signs — {@link stamp} here, a `StampWorkerPool` worker — must take the slot
730
+ * from this method and nowhere else.
731
+ *
732
+ * @throws PartitionLeaseLostError when the bound partition's lease is gone
733
+ */
734
+ assignSlot(chunkAddress: Uint8Array): number;
735
+ /**
736
+ * Stamp a chunk address (implements Stamper interface)
737
+ *
738
+ * @param chunkAddress - Address of the chunk to stamp
739
+ * @param timestampMs - Stamp timestamp, defaults to now
740
+ * @returns Envelope with batch ID and signature
741
+ */
742
+ stamp(chunkAddress: Uint8Array, timestampMs?: number): EnvelopeWithBatchId;
743
+ /**
744
+ * Stamp a chunk an app will send through its own Swarm access. Always a
745
+ * data slot: an address the stamper would route to a reserved slot is
746
+ * refused, so an app can never be handed the stamp that overwrites the
747
+ * account's lock, counter, state or intent chunks. And always inside the
748
+ * bucket's app slice, so apps can never take the slots the account keeps
749
+ * for its own uploads.
750
+ *
751
+ * @throws ReservedSlotError for one of the account's own bookkeeping chunks
752
+ * @throws AppSliceFullError when the bucket's app slice is used up
753
+ * @throws PartitionLeaseLostError when the bound partition's lease is gone
754
+ */
755
+ stampDataChunk(chunkAddress: Uint8Array, timestampMs?: number): EnvelopeWithBatchId;
756
+ /**
757
+ * Whether `slot` is a data slot this device's current lane has already
758
+ * taken in `chunkAddress`'s bucket, so stamping `chunkAddress` there again
759
+ * replaces only a chunk this lane put there. A reserved chunk's address is
760
+ * never one.
761
+ */
762
+ isOwnDataSlot(chunkAddress: Uint8Array, slot: number): boolean;
763
+ /**
764
+ * Stamp a single-owner chunk an app had stamped before at the same slot,
765
+ * with a newer timestamp, so a mutable batch replaces the old chunk at that
766
+ * slot rather than spending a new one. No slot is taken and the counter
767
+ * does not move.
768
+ *
769
+ * @param slot - A slot {@link isOwnDataSlot} accepts for `chunkAddress`
770
+ * @param timestampMs - Must be newer than the last stamp at this slot
771
+ * @throws ReservedSlotError for one of the account's own bookkeeping chunks
772
+ * @throws PartitionLeaseLostError when the bound partition's lease is gone
773
+ */
774
+ restampDataChunk(chunkAddress: Uint8Array, slot: number, timestampMs?: number): EnvelopeWithBatchId;
775
+ /** How many data chunks this device's partition lane holds in one bucket */
776
+ laneCapacity(): number;
777
+ /**
778
+ * How far up a bucket's lane an app stamp may go: the lane less the share
779
+ * {@link ACCOUNT_RESERVED_LANE_SHARE} keeps for the account's own uploads.
780
+ */
781
+ appSliceCapacity(): number;
782
+ /**
783
+ * Get bucket state (implements Stamper interface) — see {@link buckets}.
784
+ */
785
+ getState(): Uint32Array;
786
+ /**
787
+ * Flush dirty utilization chunks to cache
788
+ *
789
+ * This persists any bucket state changes made during stamping.
790
+ * Should be called after all stamping operations are complete.
791
+ */
792
+ flush(): Promise<void>;
793
+ /**
794
+ * Get current utilization state
795
+ *
796
+ * @returns Current utilization state
797
+ */
798
+ getUtilizationState(): BatchUtilizationState;
799
+ /**
800
+ * Apply utilization update from another tab.
801
+ * Updates local bucket counters to match leader's state.
802
+ *
803
+ * @param buckets Array of bucket indices and their new counter values
804
+ */
805
+ applyUtilizationUpdate(buckets: Array<{
806
+ index: number;
807
+ value: number;
808
+ }>): void;
809
+ /**
810
+ * Build the partition-local counter from the stamper's current
811
+ * `dataCounters` — i.e. resume exactly where this device left off. The
812
+ * lease orchestrator passes the result into `bindPartition` as the seed
813
+ * counter for a held partition.
814
+ */
815
+ buildLeaseLocalCounter(): Uint32Array;
816
+ /**
817
+ * Read the cached "synced reference" for `partition` — the partition-state
818
+ * feed reference this device's local counter was last in sync with. Used by
819
+ * the acquire path to skip re-downloading unchanged counter chunks.
820
+ */
821
+ getSyncedReference(partition: number): Promise<string | undefined>;
822
+ /**
823
+ * Record the partition-state feed reference this device's local counter is
824
+ * now in sync with (after a download or a publish). Merges into the existing
825
+ * batch metadata so other fields / partitions are preserved.
826
+ */
827
+ setSyncedReference(partition: number, referenceHex: string): Promise<void>;
828
+ /**
829
+ * Get bucket counter values for broadcasting to other tabs.
830
+ * Returns only the dirty buckets with their current values.
831
+ *
832
+ * IMPORTANT: Call this BEFORE flush() as flush() clears dirtyBuckets.
833
+ *
834
+ * @returns Array of bucket index/value pairs for broadcasting
835
+ */
836
+ getBucketUpdatesForBroadcast(): Array<{
837
+ index: number;
838
+ value: number;
839
+ }>;
840
+ }