@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.
- package/LICENSE +202 -0
- package/README.md +89 -2
- package/dist/bus/account-bus.d.ts +104 -0
- package/dist/bus/account-delta.d.ts +52 -0
- package/dist/bus/bus-context.d.ts +7 -0
- package/dist/bus/envelope.d.ts +9 -0
- package/dist/bus/messages.d.ts +241 -0
- package/dist/bus/presence.d.ts +62 -0
- package/dist/bus/signaling-transport.d.ts +79 -0
- package/dist/chunk/bmt.d.ts +16 -0
- package/dist/chunk/cac.d.ts +17 -0
- package/dist/chunk/constants.d.ts +10 -0
- package/dist/chunk/encrypted-cac.d.ts +47 -0
- package/dist/chunk/encryption.d.ts +85 -0
- package/dist/chunk/index.d.ts +5 -0
- package/dist/chunks/chat-binding-DO5aGhPl.js +23 -0
- package/dist/chunks/chat-binding-DO5aGhPl.js.map +1 -0
- package/dist/errors.d.ts +35 -0
- package/dist/index.d.ts +27 -0
- package/dist/internal.d.ts +49 -0
- package/dist/internal.esm.js +2 -0
- package/dist/internal.esm.js.map +1 -0
- package/dist/proxy/act/act.d.ts +58 -0
- package/dist/proxy/act/crypto.d.ts +46 -0
- package/dist/proxy/act/grantee-list.d.ts +81 -0
- package/dist/proxy/act/history.d.ts +182 -0
- package/dist/proxy/act/index.d.ts +114 -0
- package/dist/proxy/app-stamp-limits.d.ts +62 -0
- package/dist/proxy/chunk-upload-stream.d.ts +37 -0
- package/dist/proxy/chunking.d.ts +15 -0
- package/dist/proxy/collection.d.ts +38 -0
- package/dist/proxy/download-data.d.ts +27 -0
- package/dist/proxy/errors.d.ts +8 -0
- package/dist/proxy/feed-manifest.d.ts +60 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts +70 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts +79 -0
- package/dist/proxy/feeds/epochs/finder.d.ts +60 -0
- package/dist/proxy/feeds/epochs/index.d.ts +30 -0
- package/dist/proxy/feeds/epochs/types.d.ts +110 -0
- package/dist/proxy/feeds/epochs/updater.d.ts +76 -0
- package/dist/proxy/feeds/epochs/utils.d.ts +21 -0
- package/dist/proxy/feeds/index.d.ts +4 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
- package/dist/proxy/feeds/sequence/finder.d.ts +11 -0
- package/dist/proxy/feeds/sequence/index.d.ts +22 -0
- package/dist/proxy/feeds/sequence/types.d.ts +82 -0
- package/dist/proxy/feeds/sequence/updater.d.ts +20 -0
- package/dist/proxy/index.d.ts +5 -0
- package/dist/proxy/manifest-builder.d.ts +164 -0
- package/dist/proxy/mantaray.d.ts +76 -0
- package/dist/proxy/soc-restamp-slots.d.ts +48 -0
- package/dist/proxy/stamp-chunk.d.ts +45 -0
- package/dist/proxy/stamp-worker-handler.d.ts +32 -0
- package/dist/proxy/stamp-worker-pool.d.ts +52 -0
- package/dist/proxy/stamp-worker.d.ts +1 -0
- package/dist/proxy/types.d.ts +38 -0
- package/dist/proxy/upload.d.ts +207 -0
- package/dist/schemas.d.ts +671 -0
- package/dist/stamp-worker.iife.js +1 -0
- package/dist/storage/debounced-uploader.d.ts +61 -0
- package/dist/storage/utilization-store.d.ts +113 -0
- package/dist/swarm-id-client.d.ts +1143 -0
- package/dist/swarm-id-proxy.d.ts +1079 -0
- package/dist/swarm-id.esm.js +2 -0
- package/dist/swarm-id.esm.js.map +1 -0
- package/dist/sync/batch-write-coordinator.d.ts +348 -0
- package/dist/sync/device-roster.d.ts +59 -0
- package/dist/sync/device-state.d.ts +199 -0
- package/dist/sync/fold-account-from-swarm.d.ts +53 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/merge-snapshot.d.ts +57 -0
- package/dist/sync/partition-intent.d.ts +218 -0
- package/dist/sync/partition-lease.d.ts +531 -0
- package/dist/sync/partition-lock.d.ts +218 -0
- package/dist/sync/partition-state.d.ts +287 -0
- package/dist/sync/session-check.d.ts +29 -0
- package/dist/sync/store-interfaces.d.ts +48 -0
- package/dist/sync/sync-account.d.ts +45 -0
- package/dist/sync/timing-constants.d.ts +32 -0
- package/dist/sync/types.d.ts +18 -0
- package/dist/types.d.ts +3042 -0
- package/dist/utils/account-state-snapshot.d.ts +43 -0
- package/dist/utils/active-devices.d.ts +45 -0
- package/dist/utils/avatar.d.ts +30 -0
- package/dist/utils/batch-utilization.d.ts +840 -0
- package/dist/utils/batch-write-lock.d.ts +12 -0
- package/dist/utils/bee-error.d.ts +11 -0
- package/dist/utils/chat-binding.d.ts +113 -0
- package/dist/utils/coalesced-task.d.ts +28 -0
- package/dist/utils/collection.d.ts +6 -0
- package/dist/utils/connection-info.d.ts +11 -0
- package/dist/utils/constants.d.ts +28 -0
- package/dist/utils/device-id.d.ts +109 -0
- package/dist/utils/hex.d.ts +31 -0
- package/dist/utils/json-rpc.d.ts +34 -0
- package/dist/utils/key-derivation.d.ts +124 -0
- package/dist/utils/lock-soc.d.ts +35 -0
- package/dist/utils/postage-contract.d.ts +143 -0
- package/dist/utils/postage-stamp-association.d.ts +39 -0
- package/dist/utils/promise.d.ts +37 -0
- package/dist/utils/sealed-values.d.ts +23 -0
- package/dist/utils/seed-encryption.d.ts +17 -0
- package/dist/utils/site-allow-list.d.ts +54 -0
- package/dist/utils/stamp-lifespan.d.ts +37 -0
- package/dist/utils/storage-managers.d.ts +62 -0
- package/dist/utils/storage-probe.d.ts +40 -0
- package/dist/utils/tag.d.ts +11 -0
- package/dist/utils/ttl.d.ts +120 -0
- package/dist/utils/url.d.ts +55 -0
- package/dist/utils/versioned-storage.d.ts +183 -0
- 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
|
+
}
|