@solarpunkltd/swarm-id 0.0.0-stage → 0.8.1

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 +89 -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-DO5aGhPl.js +23 -0
  17. package/dist/chunks/chat-binding-DO5aGhPl.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 +671 -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 +1143 -0
  63. package/dist/swarm-id-proxy.d.ts +1079 -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 +348 -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 +3042 -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 +55 -0
  110. package/dist/utils/versioned-storage.d.ts +183 -0
  111. package/package.json +91 -3
@@ -0,0 +1,531 @@
1
+ /**
2
+ * Consolidated partition-lease state.
3
+ *
4
+ * This is the single in-code model of "who holds which partition" for one
5
+ * account+batch. The per-partition lock SOC on Swarm
6
+ * (`partition-lock.ts`) remains the cross-device authority; this class is
7
+ * the one structure every code path consults, replacing the previous trio
8
+ * of scattered representations (the `LeaseState` localStorage blob, the
9
+ * `activeDevices` snapshot mirror, and ad-hoc `holderDeviceId` reads).
10
+ *
11
+ * It holds:
12
+ * - `self` — this device's own lease (undefined when not holding)
13
+ * - `holders` — observed holder per partition, derived from the lock
14
+ * SOCs; this is what models the up-to-`partitionCount`
15
+ * simultaneously-active devices
16
+ * - admin — deviceId / batchId / partitionCount
17
+ *
18
+ * The proxy owns one instance, persists it as a *cache*
19
+ * (`serialize`/`deserialize`), and re-validates against the lock SOC before
20
+ * trusting `self`. Only the proxy mutates lock SOCs; readers (the SwarmID
21
+ * UI) construct a read-only instance and call `refreshFromSwarm` +
22
+ * `getHolders` to inspect the live holders.
23
+ */
24
+ import { Bee, BatchId, PrivateKey, type Stamper } from "@ethersphere/bee-js";
25
+ import { type PartitionLockGeneration, type PartitionLockPayload } from "./partition-lock";
26
+ /** Default guard time δ for the lock protocol (Postage-Batch-Partitioning.md §7). */
27
+ export declare const PARTITION_LOCK_GUARD_MS = 2000;
28
+ /** This device's own lease over a partition. */
29
+ export interface SelfLease {
30
+ partition: number;
31
+ generation: PartitionLockGeneration;
32
+ acquiredAt: number;
33
+ leasedUntil: number;
34
+ }
35
+ /** An observed holder of a partition (from the lock SOC). */
36
+ export interface PartitionHolderEntry {
37
+ deviceId: string;
38
+ generation: PartitionLockGeneration;
39
+ acquiredAt: number;
40
+ leasedUntil: number;
41
+ }
42
+ /** Inputs the orchestrator needs to dispatch a case. */
43
+ export interface PartitionLeaseSnapshotInputs {
44
+ partitionCount: number;
45
+ }
46
+ /** Result of `acquire()` (or the legacy fall-through). */
47
+ export interface AcquireResult {
48
+ /**
49
+ * The partition this device holds, or `undefined` for legacy mode and
50
+ * the read-only path (every partition is held by a live foreign holder).
51
+ */
52
+ partition: number | undefined;
53
+ partitionCount: number;
54
+ localCounter: Uint32Array;
55
+ isReadOnly: boolean;
56
+ /** Latest lock-SOC payload observed after acquire. Undefined when read-only. */
57
+ lockPayload?: PartitionLockPayload;
58
+ }
59
+ /**
60
+ * Outcome of a `refresh()` tick. Distinguishes the two ways a refresh can fail
61
+ * to keep the lease, because the coordinator must react to them differently:
62
+ *
63
+ * - `"held"` — the lease is still ours and was extended.
64
+ * - `"lost"` — could not confirm via write+verify (no lease, or the lock
65
+ * protocol returned `blocked`/`lost-race`/`aborted`). The
66
+ * caller should re-check the static lock SOC before demoting
67
+ * (a transient read-your-writes hiccup must not demote — the
68
+ * Phase-1 optimism).
69
+ * - `"displaced"` — a foreign PRESENCE BEACON with an earlier generation proved
70
+ * a peer already holds this partition. The beacon is read at a
71
+ * fresh per-epoch address — the channel that stays readable on
72
+ * a disjoint gateway where the static lock SOC is frozen — so
73
+ * this is a CONFIRMED displacement. The caller MUST demote
74
+ * directly and must NOT re-gate on the static lock SOC: by the
75
+ * time `refresh()` returns, it has already rewritten our own
76
+ * claim to that address, so a re-read there would see SELF and
77
+ * never confirm (the no-op the deconfliction backstop existed
78
+ * to avoid).
79
+ */
80
+ export type LeaseRefreshOutcome = "held" | "lost" | "displaced";
81
+ /**
82
+ * Serialised form of the lease state, used as the local persistence cache.
83
+ * Never trusted on its own — callers re-validate `self` against the lock
84
+ * SOC before acting on it.
85
+ */
86
+ export interface PartitionLeaseStateSnapshot {
87
+ deviceId: string;
88
+ batchId: string;
89
+ self?: SelfLease;
90
+ }
91
+ export declare class PartitionLease {
92
+ private readonly opts;
93
+ private self;
94
+ /** Per-partition holders observed by the last `refreshFromSwarm()`. */
95
+ private holders;
96
+ /**
97
+ * Partitions whose lock SOC carried an explicit NO_HOLDER release sentinel in
98
+ * the last `refreshFromSwarm()`, mapped to the RELEASED claim's generation —
99
+ * the release WATERMARK. An authoritative "free" — distinct from a
100
+ * stale/expired lock — so the holder-detection paths
101
+ * (`refreshHoldersFromPresence` / `refreshHoldersFromOccupancy`) must NOT let
102
+ * a lingering beacon (the just-released holder's, still within TTL) re-mark it
103
+ * held and block an immediate takeover. The generation watermark additionally
104
+ * lets the intent round and the `refresh()` displacement checks
105
+ * (`beaconOutranksRelease`) tell that holder's stale GHOST beacon (gen ≤
106
+ * watermark) from a genuine post-release claimant (gen > watermark).
107
+ */
108
+ private releasedPartitions;
109
+ /**
110
+ * Last `leasedUntil` observed on a partition's lock SOC that is NOT a live
111
+ * holder — an EXPIRED holder, or a release sentinel (whose `leasedUntil` is the
112
+ * release time). The gone holder heartbeats its rotating state-pointer SOC up to
113
+ * its `leasedUntil`, so this bounds the pointer's last bucket and lets a DELAYED
114
+ * takeover compute the right address (the pointer ages out of the `now` buckets
115
+ * otherwise — `holders` only carries LIVE holders' `leasedUntil`). Keyed by
116
+ * partition; rebuilt each `refreshFromSwarm`.
117
+ */
118
+ private lastSeenLeasedUntil;
119
+ /**
120
+ * Partitions whose lock SOC could not be read conclusively this refresh — the
121
+ * read timed out / hung (a transient failure), NOT a clean "absent" 404. For
122
+ * such a partition, `holderLeasedUntilMs` is unknown, so a takeover that finds
123
+ * no pointer must fail safe (read-only) instead of resuming from zero and
124
+ * re-issuing a gone holder's acked slots. Rebuilt each `refreshFromSwarm`.
125
+ * A clean 404 (genuinely free partition) is NOT recorded here, so first-claim
126
+ * from zero still works.
127
+ */
128
+ private unreadableLocks;
129
+ /**
130
+ * Cached state from this session's last partition-state publish, so the next
131
+ * publish re-uploads only the chunks that changed (incremental). Seeded on the
132
+ * first publish (full), refreshed on every publish, and cleared by `acquire()`
133
+ * (a new session resumes from a peer's counter, not our stale one).
134
+ */
135
+ private publishedReferences;
136
+ private publishedCounter;
137
+ /** Reference-chunk ref (hex) of this session's last publish — re-written to the
138
+ * current rotating bucket on each refresh tick (heartbeat) so an idle holder
139
+ * keeps a fresh state pointer the next reader can find. */
140
+ private lastReferenceHex;
141
+ /**
142
+ * Epoch bucket of the last FULL (non-incremental) publish. An incremental
143
+ * publish leaves unchanged counter chunks in place across epochs, but the
144
+ * rotating pointer/occupancy/intent SOCs share this partition's reserved slot
145
+ * and a LATER epoch's (deterministic) bucket can collide with a long-retained
146
+ * chunk and evict it — only the current+previous epoch's beacon buckets are
147
+ * excluded at publish time (see `writePartitionState`). So we force one FULL
148
+ * re-pin per epoch: every non-zero chunk is re-uploaded at a fresh bucket clear
149
+ * of the current epoch's beacons, so any such eviction self-heals within ~1
150
+ * epoch while the holder lives (only a collision in the final pre-departure
151
+ * epoch is left for the takeover `readFailed` path). undefined after `acquire`.
152
+ */
153
+ private lastFullPublishEpoch;
154
+ /**
155
+ * Set at the start of `release()` (the lease session is closing) and
156
+ * cleared by the next `acquire()`. While set, an overlapping `refresh()`
157
+ * aborts before its claim write — otherwise it could mint a ghost claim
158
+ * NEWER than the generation the in-flight release is fenced to, which the
159
+ * release would then refuse to clear (peers would wait out the TTL).
160
+ */
161
+ private closed;
162
+ /**
163
+ * The claim-time presence/occupancy beacon publish, fired OFF the acquire
164
+ * critical path (best-effort, never rejects — `publishPresenceBeacon`
165
+ * swallows errors). `refresh()`/`release()` await it first so a later beacon
166
+ * (fresher `leasedUntil`) can never be overwritten by the still-in-flight
167
+ * claim beacon, and tests get a deterministic settle point.
168
+ */
169
+ private pendingBeaconPublish;
170
+ constructor(opts: {
171
+ bee: Bee;
172
+ deviceId: string;
173
+ swarmEncryptionKey: Uint8Array;
174
+ /** Backup signer for the partition-lock SOC and partition-state feed. */
175
+ backupSigner: PrivateKey;
176
+ /**
177
+ * Which batch's lanes this lease is about. Required even for a reader:
178
+ * every lock, intent and occupancy address is derived per (batch,
179
+ * partition) since #589, so "who holds partition p" has no answer
180
+ * without naming the batch.
181
+ */
182
+ batchId: BatchId;
183
+ /**
184
+ * Stamper + depth are only needed for the WRITE paths (acquire,
185
+ * refresh, release). They're optional so a reader — e.g. the SwarmID
186
+ * UI's Devices tab — can build a lease purely to `refreshFromSwarm`
187
+ * and inspect holders without a stamper.
188
+ */
189
+ batchDepth?: number;
190
+ /**
191
+ * Stamper used to upload the small payload chunks (lock SOC, state
192
+ * feed). In practice this is the `UtilizationAwareStamper` the proxy
193
+ * uses for data uploads. Omitted for read-only use.
194
+ */
195
+ stamper?: Stamper;
196
+ /** Override for tests; defaults to `() => Date.now()`. */
197
+ now?: () => number;
198
+ /** Override for tests; defaults to PARTITION_LOCK_GUARD_MS. */
199
+ guardMs?: number;
200
+ /**
201
+ * Override for the lock protocol's guard-window wait; defaults to
202
+ * `sleep`. A test asserts the guard through this seam — on the `ms` it
203
+ * is handed — rather than timing `acquire` against a real clock, where
204
+ * the claim's own crypto/IO cost is indistinguishable from the guard.
205
+ */
206
+ wait?: (ms: number) => Promise<void>;
207
+ /**
208
+ * Current known device IDs for this account (from the synced device
209
+ * registry). Read fresh each round so it reflects late-announced
210
+ * devices. When provided with ≥1 rival, a fresh claim of a free
211
+ * partition first runs an intent round (Phase 2 — see
212
+ * `partition-intent.ts`) so disjoint-gateway contenders agree on one
213
+ * winner before binding. Omitted (or empty) → today's guard+TTL path.
214
+ */
215
+ knownDeviceIds?: () => string[];
216
+ /** Override for tests; per-rival intent-read timeout. */
217
+ intentReadTimeoutMs?: number;
218
+ /**
219
+ * Fresh-claim intent-round poll window / interval (gateway-propagation
220
+ * tunable). Default to the `INTENT_GUARD_*` constants; tests pass 0 for a
221
+ * single immediate read.
222
+ */
223
+ intentGuardWindowMs?: number;
224
+ intentGuardPollMs?: number;
225
+ });
226
+ /**
227
+ * Build a `PartitionLease` whose `backupSigner` is derived from the
228
+ * account's `swarmEncryptionKey` (same derivation as the partition-state
229
+ * and partition-lock feeds). Pass `stamper`/`batchId`/`batchDepth` for the
230
+ * write path; omit them for a read-only lease (UI display).
231
+ */
232
+ static fromSwarmEncryptionKey(opts: {
233
+ bee: Bee;
234
+ deviceId: string;
235
+ swarmEncryptionKey: Uint8Array;
236
+ batchId: BatchId;
237
+ batchDepth?: number;
238
+ stamper?: Stamper;
239
+ now?: () => number;
240
+ guardMs?: number;
241
+ wait?: (ms: number) => Promise<void>;
242
+ knownDeviceIds?: () => string[];
243
+ intentReadTimeoutMs?: number;
244
+ intentGuardWindowMs?: number;
245
+ intentGuardPollMs?: number;
246
+ }): Promise<PartitionLease>;
247
+ /**
248
+ * Read every partition's lock SOC and populate `holders` with the live
249
+ * (unexpired, non-released) ones. The single source of truth for "who is
250
+ * active" — used by `isActive`, `heldPartition`, and `pickFreeOrExpired`.
251
+ */
252
+ refreshFromSwarm(partitionCount: number): Promise<void>;
253
+ /**
254
+ * Merge live holders detected from per-device PRESENCE BEACONS (the intent
255
+ * SOCs holders re-publish each refresh) into `holders`. Unlike the static
256
+ * lock SOC, these live at fresh per-epoch addresses, so reading them forces a
257
+ * network retrieval that works on a gateway that 404s the static address —
258
+ * this is what lets a joiner detect an existing holder. Call AFTER
259
+ * `refreshFromSwarm` (union, not replace): either channel showing a live
260
+ * foreign holder marks the partition held. No-op without known rivals.
261
+ */
262
+ refreshHoldersFromPresence(partitionCount: number): Promise<void>;
263
+ /** Network half of `refreshHoldersFromPresence` — no state mutation, so it
264
+ * can be overlapped with `refreshFromSwarm` (whose `releasedPartitions`
265
+ * classification `applyPresence` consumes). */
266
+ private readPresence;
267
+ /** Classification half of `refreshHoldersFromPresence`. Call AFTER
268
+ * `refreshFromSwarm` has populated `releasedPartitions`/`holders`. */
269
+ private applyPresence;
270
+ /**
271
+ * Union in holders from the deviceId-INDEPENDENT occupancy beacons. For each
272
+ * partition, read the current + previous epoch bucket (covering the rotation
273
+ * boundary); a foreign beacon still in lease marks the partition held — even
274
+ * when we don't know the holder's deviceId (the registry-lag case the per-device
275
+ * channels miss). No-op once a self-claim is recorded for the partition.
276
+ *
277
+ * Liveness uses a STRICT `leasedUntil > now` (not the propagation GRACE the
278
+ * per-device beacon uses): an actively-refreshing holder always has a current
279
+ * beacon with `leasedUntil > now`, while a DEPARTED holder's last beacon has
280
+ * genuinely lapsed — so a device legitimately taking over an expired slot
281
+ * isn't falsely blocked by a lingering within-grace beacon (which the lock
282
+ * SOC, when accurate, would already show as expired).
283
+ */
284
+ refreshHoldersFromOccupancy(partitionCount: number): Promise<void>;
285
+ /** Network half of `refreshHoldersFromOccupancy` — see `readPresence`. */
286
+ private readOccupancy;
287
+ /** Classification half of `refreshHoldersFromOccupancy` — see `applyPresence`. */
288
+ private applyOccupancy;
289
+ /** True if `deviceId` is a live holder of any partition (post-refresh). */
290
+ isActive(deviceId: string): boolean;
291
+ /** The partition this device currently holds per the lock SOCs, else undefined. */
292
+ heldPartition(): number | undefined;
293
+ /**
294
+ * Live holders observed by the last `refreshFromSwarm`, one entry per
295
+ * partition currently held. The single read view of "who's active" — the
296
+ * SwarmID UI renders the Devices tab from this.
297
+ */
298
+ getHolders(): {
299
+ partition: number;
300
+ deviceId: string;
301
+ leasedUntil: number;
302
+ }[];
303
+ /**
304
+ * First partition with no live holder that isn't us, else `undefined`,
305
+ * scanning from this device's deterministic home offset
306
+ * (`deviceHomePartition`) and wrapping. Starting from a per-device offset
307
+ * spreads devices across slots even when none can read peers' locks (the
308
+ * disjoint-gateway frozen-cache case); the wrap keeps full coverage so a
309
+ * genuinely free slot is never missed. Drives candidate selection in
310
+ * `acquire`.
311
+ */
312
+ private pickFreeOrExpired;
313
+ /**
314
+ * Acquire a partition. Always consults the lock SOCs (via
315
+ * `refreshFromSwarm`) — there is no local-state shortcut. Refreshes our
316
+ * existing partition when we already hold one; otherwise claims the first
317
+ * free / expired one. Returns read-only when every partition is held by a
318
+ * live foreign holder.
319
+ */
320
+ acquire(snapshot: PartitionLeaseSnapshotInputs): Promise<AcquireResult>;
321
+ /**
322
+ * Speculative half of `claimPartition`'s pointer lookup, overlapped with the
323
+ * `acquire` scan: read the state pointer for the partition we will most
324
+ * likely claim (the home partition) at the `now`/`now−1` buckets — exactly
325
+ * the lookup `readPartitionState` performs when the partition has no known
326
+ * prior holder (`holderLeasedUntilMs` undefined; `readPartitionState`
327
+ * enforces that before trusting the prefetch). Best-effort: `undefined` on a
328
+ * thrown read → the claim falls back to its own serial lookup.
329
+ */
330
+ private speculativeStatePointer;
331
+ /**
332
+ * Read partition-state for counter seeding, then drive
333
+ * `acquirePartitionLock` to claim + verify the partition.
334
+ *
335
+ * On a `lost-race` or `blocked` outcome we fall back to read-only.
336
+ */
337
+ private claimPartition;
338
+ /**
339
+ * Resolves when the claim-time detached beacon publish has settled. Never
340
+ * rejects. Exposed so callers/tests that need the beacon to be readable
341
+ * (e.g. before simulating a joiner) have a deterministic sync point.
342
+ */
343
+ get beaconPublishSettled(): Promise<void>;
344
+ /**
345
+ * Re-run the lock protocol on the currently-held partition to bump
346
+ * `leasedUntil`. Returns `"lost"` when we don't hold a lease (legacy mode)
347
+ * or the lock protocol returned `blocked` / `lost-race` / `aborted`,
348
+ * `"displaced"` when a foreign presence beacon proves a peer holds the
349
+ * partition (the deconfliction backstop), else `"held"`. See
350
+ * `LeaseRefreshOutcome` for how the coordinator must react to each.
351
+ */
352
+ refresh(): Promise<LeaseRefreshOutcome>;
353
+ /**
354
+ * True if some OTHER device has a live presence beacon on `partition` whose
355
+ * generation orders strictly before `ourGeneration` (i.e. it claimed first).
356
+ * Reads the current+previous epoch bucket for each known rival (fresh
357
+ * addresses → gateway-retrievable). No rivals known → false.
358
+ */
359
+ private foreignBeaconBeatsUs;
360
+ /**
361
+ * Whether a foreign live beacon represents a real claim rather than the
362
+ * just-released holder's stale GHOST. A beacon outranks the partition's
363
+ * observed release only if its generation is STRICTLY NEWER than the released
364
+ * claim's (the watermark recorded by `refreshFromSwarm`). At or below the
365
+ * watermark the beacon is the lingering presence/occupancy SOC the releaser
366
+ * left behind — still within its lease TTL at a live per-epoch address, but no
367
+ * longer a claim — so it must not block/displace a peer taking over the freed
368
+ * partition. A genuine post-release claimant always has a newer generation
369
+ * (current timestamp ≫ the old watermark under the clock-sync assumption), so
370
+ * it is still honored — no dual-acquire regression. No watermark → outranks.
371
+ */
372
+ private beaconOutranksRelease;
373
+ /**
374
+ * Like `foreignBeaconBeatsUs`, but reads the deviceId-INDEPENDENT occupancy
375
+ * beacon (current + previous epoch bucket). This resolves a dual-acquire with a
376
+ * peer that is NOT in `knownDeviceIds` — e.g. a live device pruned from the
377
+ * rival set by `activeDeviceIds` (its `lastSignedInAt` aged past
378
+ * `KNOWN_DEVICE_MAX_AGE_MS` even though it never signed out). Without this,
379
+ * `refresh()`'s deviceId-keyed `foreignBeaconBeatsUs` is blind to that peer and
380
+ * a symmetric fresh-claim dual-acquire would stay sticky.
381
+ *
382
+ * The occupancy SOC is a single shared address per (partition, epoch), so two
383
+ * holders clobber each other's writes; only the later-generation loser yields,
384
+ * and it converges within a few refresh ticks (whenever it reads a tick where
385
+ * the earlier-generation winner wrote last). True when a foreign live occupancy
386
+ * beacon orders strictly before our generation.
387
+ */
388
+ private occupancyBeaconBeatsUs;
389
+ /**
390
+ * Re-publish this device's holder presence beacon for `partition` at the
391
+ * CURRENT epoch (the intent SOC, with `leasedUntil` set). A joiner's
392
+ * `refreshHoldersFromPresence` reads this at a fresh per-epoch address (forced
393
+ * network retrieval), which works on a gateway that 404s the static lock SOC.
394
+ * Best-effort — never fails the refresh; the ~10s refresh cadence <
395
+ * INTENT_EPOCH_MS (30s) keeps the current bucket continuously populated for a
396
+ * live holder.
397
+ *
398
+ * Published UNCONDITIONALLY while holding (this method is only reached for a
399
+ * K>1 partition). It must NOT be gated on knowing a rival: a device that
400
+ * created the account hasn't synced its peer into the registry yet, so a
401
+ * rival-gated beacon would leave that holder invisible and a peer who DOES
402
+ * know it would find no beacon and dual-acquire the same partition.
403
+ */
404
+ private publishPresenceBeacon;
405
+ /**
406
+ * Publish the final local counter on the partition-state feed, then
407
+ * write the generation-fenced release sentinel to the lock SOC so peers
408
+ * see an immediate, authoritative release. The sentinel carries the
409
+ * generation of the claim being released (captured BEFORE the slow
410
+ * publish) and is skipped entirely when the lock no longer carries that
411
+ * claim — a detached release outliving a same-device re-acquire must not
412
+ * clobber the successor's claim (#349). No-op when no lease is held.
413
+ */
414
+ release(localCounter: Uint32Array): Promise<void>;
415
+ /**
416
+ * Publish the held partition's counter on the partition-state feed WITHOUT
417
+ * releasing the lock — the commit point that makes an upload's slots durable
418
+ * before its promise resolves (ack-after-publish). The new holder on takeover
419
+ * resumes at exactly this counter, so an un-published (un-acked) in-flight
420
+ * write is safe to overwrite while every acked write is preserved. No-op when
421
+ * no lease is held. Same reserved-slot publish as `release()`, so it is safe
422
+ * through a still-bound stamper.
423
+ */
424
+ publishState(localCounter: Uint32Array): Promise<void>;
425
+ /**
426
+ * Publish `localCounter` on the partition-state feed and record it as our
427
+ * synced reference + protected buckets. Shared by `release()` (final flush)
428
+ * and `publishState()` (per-upload commit). Protecting the publish's buckets
429
+ * stops a later utilisation save from overstamping (evicting) the resume
430
+ * point from the reserve.
431
+ */
432
+ private flushState;
433
+ /**
434
+ * Seed the heartbeat's pointer target (`lastReferenceHex`) without reading
435
+ * partition-state. Used by the coordinator's cached-lease re-adopt fast path
436
+ * (`adoptIfLive`), which binds from local state and never runs
437
+ * `claimPartition` — where the cold acquire path seeds this. Without it a
438
+ * reloaded holder that re-adopts its lease and then stays idle never
439
+ * heartbeats the inherited resume pointer forward, so it ages out of the
440
+ * takeover lookup span (resume-from-zero → re-issue acked slots). The caller
441
+ * passes the partition's persisted synced reference
442
+ * (`UtilizationAwareStamper.getSyncedReference`); `undefined` (fresh partition)
443
+ * keeps the heartbeat a no-op.
444
+ */
445
+ seedReferenceHex(referenceHex: string | undefined): void;
446
+ /**
447
+ * Re-write the state pointer to the CURRENT rotating bucket without
448
+ * re-uploading the counter — the refresh-tick heartbeat. Keeps a fresh pointer
449
+ * in the current bucket so a taking-over device finds the resume point even
450
+ * after this holder has been idle (no uploads) for a while. No-op until the
451
+ * first publish of the session has produced a reference to point at.
452
+ *
453
+ * COLLISION SELF-HEAL: when this epoch's own rotating pointer/beacon buckets
454
+ * (or the constant lock-SOC bucket) now coincide with a RETAINED counter/
455
+ * reference chunk — the ~1/65536-per-epoch case an incremental publish left
456
+ * unrefreshed — a bare pointer write here would OVERSTAMP (evict) that chunk at
457
+ * their shared reserved slot. `retainedChunksCollideThisEpoch` detects it (CPU
458
+ * only, no network on the ~99.998% clear path) and, given the live counter,
459
+ * relocates via a forced full re-pin (`flushState`) so the eviction is repaired
460
+ * the same tick instead of waiting for the next upload. Without a counter to
461
+ * re-pin (or on a SOC-vs-SOC collision, where neither party is movable) it
462
+ * falls through to the plain pointer write — still fail-safe: the collision is
463
+ * bounded to one epoch (the rotating addresses separate next epoch) and a
464
+ * takeover reading a missing chunk gets `readFailed` (→ read-only + retry),
465
+ * never a zero-counter resume that would re-issue acked slots.
466
+ */
467
+ heartbeatStatePointer(localCounter?: Uint32Array): Promise<void>;
468
+ /**
469
+ * True when a RETAINED published state chunk now shares a bucket with one of
470
+ * THIS device's own reserved-slot writers for the current epoch — the rotating
471
+ * state-pointer, the deviceId-independent occupancy beacon, this device's
472
+ * intent beacon, or the constant lock SOC. All land at `slot = partition`, so a
473
+ * shared bucket means one overstamps (evicts) the other. Computed from
474
+ * `this.now()`-derived addresses — the one rendezvous clock shared by
475
+ * `writePartitionState` (via `nowMs`) and the reader (#385), = `Date.now()` in
476
+ * production — so a heartbeat detects exactly the collision its own write would
477
+ * cause.
478
+ *
479
+ * The retained set is EVERY non-zero counter chunk (`publishedReferences`) AND
480
+ * the reference chunk itself (`lastReferenceHex`) — the latter isn't in
481
+ * `publishedReferences` but sits at the same reserved slot and is the very
482
+ * chunk the pointer points at, so evicting it dangles the pointer just as
483
+ * badly. (This is the full `stateBuckets` set the publish protects.) A peer's
484
+ * intent beacon can't be computed here (its deviceId / address is unknown) —
485
+ * that residual stays fail-safe (§12).
486
+ */
487
+ private retainedChunksCollideThisEpoch;
488
+ /** Current partition (undefined when not holding a lease). */
489
+ get currentPartition(): number | undefined;
490
+ /** Wall-clock ms this device's lease is valid until (undefined when none). */
491
+ get leasedUntil(): number | undefined;
492
+ /**
493
+ * Advance the locally-held lease's `leasedUntil` to `now + ttlMs` without
494
+ * any Swarm write. The caller bumps this every refresh tick as a
495
+ * "this device is alive" heartbeat, so the persisted cache stays fresh
496
+ * even when the Swarm lock-SOC write is transiently failing. No-op when no
497
+ * lease is held. (The cross-device lock SOC is refreshed separately by
498
+ * `refresh()`; this only touches local state.)
499
+ */
500
+ bumpLocalLease(ttlMs: number): void;
501
+ /**
502
+ * Re-adopt a still-valid `self` from the persisted cache (after `hydrate`)
503
+ * without any Swarm round-trip. Returns the held partition when the cached
504
+ * lease is still within its TTL, else `undefined` (caller falls back to the
505
+ * cold `acquire()` lock-SOC scan). On adoption it bumps `leasedUntil` and
506
+ * mirrors `self` into `holders` so `getHolders()`/`heldPartition()` agree.
507
+ *
508
+ * This is what lets a reload keep the lease: the fresh page reconstructs the
509
+ * binding from local state alone, surviving transient lock-SOC read/write
510
+ * failures. The next `refresh()` reconciles with Swarm.
511
+ */
512
+ adoptIfLive(): number | undefined;
513
+ /**
514
+ * Serialise the lease state for the local persistence cache. The cache is
515
+ * a fast hint only — callers re-validate `self` against the lock SOC
516
+ * (`refresh()` / `acquire()`) before trusting it.
517
+ */
518
+ serialize(): PartitionLeaseStateSnapshot;
519
+ /**
520
+ * Seed `self` from a persisted cache snapshot. No Swarm activity — the
521
+ * next `refresh()`/`acquire()` re-validates against the lock SOC. Ignores
522
+ * snapshots for a different device or batch.
523
+ */
524
+ hydrate(snapshot: PartitionLeaseStateSnapshot): void;
525
+ /**
526
+ * Assert the write context (stamper + batch) is present. Throws on a
527
+ * read-only instance — acquire / refresh / release require it.
528
+ */
529
+ private requireWriteContext;
530
+ private now;
531
+ }