@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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BatchWriteCoordinator` — the write path for one account+batch.
|
|
3
|
+
*
|
|
4
|
+
* Owns everything needed to **write to a shared postage batch as a partition
|
|
5
|
+
* holder**, and nothing about client communication:
|
|
6
|
+
* - the cross-tab Web Lock (`withBatchWriteLock`),
|
|
7
|
+
* - the partition-lease lifecycle (acquire / refresh / yield / demote),
|
|
8
|
+
* - stamper-state flush under the lock.
|
|
9
|
+
*
|
|
10
|
+
* Two consumers share it:
|
|
11
|
+
* - the proxy iframe — long-lived (`mode: "persistent"`): eager
|
|
12
|
+
* `startLease()` + a refresh timer; an upload that can't get a partition
|
|
13
|
+
* blocks for a slot and then throws.
|
|
14
|
+
* - `sync-account` — one-off (`mode: "oneshot"`): no refresh timer; a write
|
|
15
|
+
* claims a free/expired partition once and throws `PartitionContendedError`
|
|
16
|
+
* when every partition is held by a live foreign device, so the caller
|
|
17
|
+
* skips. The lease then lapses by TTL.
|
|
18
|
+
*
|
|
19
|
+
* The lock SOC on Swarm (`partition-lock.ts`) stays the cross-device authority;
|
|
20
|
+
* this class is the lifecycle layer above `PartitionLease`. See
|
|
21
|
+
* `docs/BatchWriteCoordinator.md`.
|
|
22
|
+
*
|
|
23
|
+
* ## Displacement-during-upload race (issue #336)
|
|
24
|
+
*
|
|
25
|
+
* `refreshTick` runs on a bare `setInterval`, NOT under the write lock, so it
|
|
26
|
+
* can detect that a peer took our partition while an upload is mid-`stamp()`.
|
|
27
|
+
* The fix is two-phase: on confirmed displacement we call
|
|
28
|
+
* `stamper.invalidateLease()` **immediately** (partition stays bound →
|
|
29
|
+
* `leaseStale=true` → the in-flight `stamp()` throws `PartitionLeaseLostError`
|
|
30
|
+
* and the corrupting upload aborts), and only *then* run the
|
|
31
|
+
* unbind/clear cleanup **under the write lock** so no `stamp()` runs in the
|
|
32
|
+
* window between invalidate and unbind. Lock-synchronising the demote alone is
|
|
33
|
+
* insufficient — it would merely order the demote after an upload that already
|
|
34
|
+
* corrupted the peer's slot space.
|
|
35
|
+
*/
|
|
36
|
+
import { Bee, PrivateKey } from "@ethersphere/bee-js";
|
|
37
|
+
import { UtilizationAwareStamper } from "../utils/batch-utilization";
|
|
38
|
+
import type { PartitionLeaseStateSnapshot } from "./partition-lease";
|
|
39
|
+
import type { UploadTarget } from "../proxy/upload";
|
|
40
|
+
import type { StampWorkerPool } from "../proxy/stamp-worker-pool";
|
|
41
|
+
/** Max time the acquire spends polling for a slot when all are held. */
|
|
42
|
+
export declare const SLOT_WAIT_TIMEOUT_MS = 30000;
|
|
43
|
+
/**
|
|
44
|
+
* Thrown by `withWrite` in `wait: "skip"` mode when every partition is held by
|
|
45
|
+
* a live foreign device, i.e. there is genuinely no slot to claim. The caller
|
|
46
|
+
* (`sync-account`) catches this and skips the publish — distinct from an
|
|
47
|
+
* operational error (missing stamper, a lock-SOC write that threw), which
|
|
48
|
+
* propagates so it can be logged as an error rather than as contention.
|
|
49
|
+
*/
|
|
50
|
+
export declare class PartitionContendedError extends Error {
|
|
51
|
+
readonly accountId?: string | undefined;
|
|
52
|
+
constructor(message?: string, accountId?: string | undefined);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A lock-SOC payload means this device has been displaced only if it names a
|
|
56
|
+
* *different*, *live* device. A missing/unreadable payload (e.g. a Bee 500),
|
|
57
|
+
* our own id, the release sentinel, or an *expired* foreign holder are all NOT
|
|
58
|
+
* displacement — the holder keeps its lease and retries. Pure (no instance
|
|
59
|
+
* state) so it can be unit-tested directly; mirrors the proxy's former
|
|
60
|
+
* `isDisplaced`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isDisplaced(payload: {
|
|
63
|
+
holderDeviceId: string;
|
|
64
|
+
leasedUntil: number;
|
|
65
|
+
} | undefined, now: number, selfDeviceId: string): boolean;
|
|
66
|
+
/** Long-lived (proxy) vs one-off (`sync-account`) lease behaviour. */
|
|
67
|
+
export type CoordinatorMode = "persistent" | "oneshot";
|
|
68
|
+
/** Per-transition lease snapshot handed to `onLeaseChange`. */
|
|
69
|
+
export interface LeaseChangeInfo {
|
|
70
|
+
currentPartition: number | undefined;
|
|
71
|
+
isReadOnly: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface WithWriteOptions {
|
|
74
|
+
useWorkers?: boolean;
|
|
75
|
+
workerCount?: number;
|
|
76
|
+
/** Override the mode default: persistent → "block", oneshot → "skip". */
|
|
77
|
+
wait?: "block" | "skip";
|
|
78
|
+
}
|
|
79
|
+
export interface BatchWriteCoordinatorDeps {
|
|
80
|
+
/** Read on every use, so a getter here follows a change of node. */
|
|
81
|
+
bee: Bee;
|
|
82
|
+
/** Postage batch id (hex) — the `withBatchWriteLock` key. */
|
|
83
|
+
batchId: string;
|
|
84
|
+
/** Already created + account-bound by the caller; the coordinator only
|
|
85
|
+
* binds/unbinds the held partition on it. */
|
|
86
|
+
stamper: UtilizationAwareStamper;
|
|
87
|
+
/** This device's identity, captured once by the caller. */
|
|
88
|
+
deviceId: string;
|
|
89
|
+
/**
|
|
90
|
+
* Current known device IDs for this account (from the synced device
|
|
91
|
+
* registry). Read fresh per acquire so it reflects late-announced devices.
|
|
92
|
+
* Drives the Phase-2 intent round that lets disjoint-gateway contenders for
|
|
93
|
+
* a free partition agree on one winner (see partition-intent.ts). Omit for
|
|
94
|
+
* single-device / legacy callers — acquire falls back to guard+TTL only.
|
|
95
|
+
*/
|
|
96
|
+
knownDeviceIds?: () => string[];
|
|
97
|
+
/**
|
|
98
|
+
* Pull the latest device registry from Swarm into local state before a fresh
|
|
99
|
+
* acquire, so `knownDeviceIds` reflects a peer that signed in after this device
|
|
100
|
+
* created the account (otherwise the claimer is "blind" — never reads the
|
|
101
|
+
* peer's beacon — and dual-acquires). Best-effort; the provider throttles.
|
|
102
|
+
* Omit for node/oneshot callers.
|
|
103
|
+
*/
|
|
104
|
+
refreshKnownDeviceIds?: () => Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Gateway-propagation tuning for the intent round (optional overrides; default
|
|
107
|
+
* to the `INTENT_*` constants). Lets the proxy pass runtime-tunable values.
|
|
108
|
+
*/
|
|
109
|
+
intentReadTimeoutMs?: number;
|
|
110
|
+
intentGuardWindowMs?: number;
|
|
111
|
+
intentGuardPollMs?: number;
|
|
112
|
+
accountId: string;
|
|
113
|
+
/** Owns the per-partition lock SOCs (derived from the account key). */
|
|
114
|
+
backupSigner: PrivateKey;
|
|
115
|
+
swarmEncryptionKey: Uint8Array;
|
|
116
|
+
partitionCount: number;
|
|
117
|
+
mode: CoordinatorMode;
|
|
118
|
+
/** Persistent mode only: read/write the local lease-cache hint. */
|
|
119
|
+
readLeaseCache?: () => PartitionLeaseStateSnapshot | undefined;
|
|
120
|
+
writeLeaseCache?: (snap: PartitionLeaseStateSnapshot | undefined) => void;
|
|
121
|
+
/** Flush stamper bucket state after a write (proxy: `saveStamperState`). */
|
|
122
|
+
flushStamperState?: () => Promise<void>;
|
|
123
|
+
/** Build/reuse a parallel-signing worker pool (proxy: `getOrCreateWorkerPool`). */
|
|
124
|
+
getWorkerPool?: (count?: number) => Promise<StampWorkerPool | undefined>;
|
|
125
|
+
/** Fired on every partition / read-only transition. */
|
|
126
|
+
onLeaseChange?: (info: LeaseChangeInfo) => void;
|
|
127
|
+
/** Fired when a partition is (re)acquired (phase-5 publish trigger). */
|
|
128
|
+
onLeaseAcquired?: (partition: number) => void;
|
|
129
|
+
/**
|
|
130
|
+
* Fired each slot-wait poll round while a blocking acquire finds every
|
|
131
|
+
* partition held by live peers. The proxy broadcasts a `lease-request` on
|
|
132
|
+
* the account bus so an idle live holder yields immediately instead of
|
|
133
|
+
* after `IDLE_YIELD_MS` (docs/Account-Bus.md, bus-accelerated leases).
|
|
134
|
+
*/
|
|
135
|
+
onSlotWait?: () => void;
|
|
136
|
+
/**
|
|
137
|
+
* Fired after every release of a held partition — the idle-timer yield, a
|
|
138
|
+
* yield for a peer's `lease-request` (`requestId` echoes it), and teardown —
|
|
139
|
+
* so announcing it on the bus is the coordinator's doing, not something each
|
|
140
|
+
* release path remembers. The proxy publishes `lease-released`.
|
|
141
|
+
*/
|
|
142
|
+
onLeaseReleased?: (partition: number, requestId?: string) => void;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Minimum idle time before a peer's `lease-request` makes this holder yield.
|
|
146
|
+
* Much shorter than `IDLE_YIELD_MS` (the unprompted timer yield) — the request
|
|
147
|
+
* proves a peer is actively waiting — but non-zero so a holder mid-burst
|
|
148
|
+
* (between two uploads) doesn't ping-pong its partition away.
|
|
149
|
+
*/
|
|
150
|
+
export declare const PEER_YIELD_MIN_IDLE_MS = 3000;
|
|
151
|
+
/**
|
|
152
|
+
* A `lease-released` reaches a waiter over the bus well before the released
|
|
153
|
+
* slot's sentinel write is readable from its node, so the scan it wakes often
|
|
154
|
+
* still reads the old holder (#724). Rather than sleep out a poll interval on
|
|
155
|
+
* that — or re-ask, which draws a fresh request id nobody can stand down
|
|
156
|
+
* against, so a second holder yields for the same waiter — the wait re-scans
|
|
157
|
+
* this many times at this interval, asking nobody.
|
|
158
|
+
*/
|
|
159
|
+
export declare const ANNOUNCED_RESCAN_INTERVAL_MS = 1500;
|
|
160
|
+
export declare const ANNOUNCED_RESCAN_ROUNDS = 3;
|
|
161
|
+
export declare class BatchWriteCoordinator {
|
|
162
|
+
private readonly deps;
|
|
163
|
+
/** Active partition-lease; undefined when no lease is held (read-only mode,
|
|
164
|
+
* or single-partition legacy accounts). */
|
|
165
|
+
private partitionLease;
|
|
166
|
+
private partitionRefreshTimer;
|
|
167
|
+
private readOnly;
|
|
168
|
+
/** Set when the next write should (re)acquire a partition: cold start,
|
|
169
|
+
* after a demote, or while read-only. */
|
|
170
|
+
private pendingAcquire;
|
|
171
|
+
/** Wall-clock ms of the last lock-SOC validation (acquire / refresh). */
|
|
172
|
+
private lastLeaseValidatedAt;
|
|
173
|
+
/** Wall-clock ms of the last upload activity; drives the idle-yield. */
|
|
174
|
+
private lastLeaseActivityAt;
|
|
175
|
+
/** Uploads currently executing under the write lock; the refresh tick only
|
|
176
|
+
* yields an idle lease when this is 0. */
|
|
177
|
+
private activeUploadCount;
|
|
178
|
+
/** Wall-clock ms the state-pointer heartbeat has been failing since
|
|
179
|
+
* (undefined = healthy). The heartbeat is best-effort per tick, but a
|
|
180
|
+
* PERSISTENT failure streak walks the last durable pointer out of the
|
|
181
|
+
* takeover lookup span while lock renewals keep extending `leasedUntil` —
|
|
182
|
+
* a takeover would then zero-seed over acked slots. Once the streak
|
|
183
|
+
* exceeds one pointer epoch, `refreshTick` demotes. Reset on a successful
|
|
184
|
+
* heartbeat and whenever the lease binding changes. */
|
|
185
|
+
private pointerHeartbeatFailingSince;
|
|
186
|
+
/** The live slot-wait's state; undefined outside slot-wait. Held as ONE
|
|
187
|
+
* object per wait, and every touch goes through this field, so a DETACHED
|
|
188
|
+
* wait — `ensureLease` races the poll against a `withTimeout`, and the
|
|
189
|
+
* loser keeps running — owns a different object and cannot reach into the
|
|
190
|
+
* live wait's wake or flag. */
|
|
191
|
+
private slotWait;
|
|
192
|
+
/** Set by `teardown()`. Once disposed the coordinator must never re-acquire a
|
|
193
|
+
* partition or arm a refresh timer: an in-flight `withWrite` (e.g. a deferred
|
|
194
|
+
* publish, or a normal upload) can outlive a disconnect/sign-out, and without
|
|
195
|
+
* this guard its `ensureLease` would re-lease the slot and start a detached
|
|
196
|
+
* interval — a ghost lease the proxy can no longer tear down. */
|
|
197
|
+
private disposed;
|
|
198
|
+
/** Bumped whenever held-lease state is reset (`pauseLeaseBackgroundWork`,
|
|
199
|
+
* `finalizeDemote`, `yieldIdleLease`, `teardown`). An `acquire()` that began
|
|
200
|
+
* under an older epoch must not commit (bind / start timers) when it
|
|
201
|
+
* resolves — `Promise.race` cannot cancel a losing acquire, whose
|
|
202
|
+
* continuation would otherwise resurrect a lease the coordinator already
|
|
203
|
+
* cleared. */
|
|
204
|
+
private leaseEpoch;
|
|
205
|
+
constructor(deps: BatchWriteCoordinatorDeps);
|
|
206
|
+
/** Current held partition, or undefined (read-only / legacy / not yet acquired). */
|
|
207
|
+
get currentPartition(): number | undefined;
|
|
208
|
+
/** True when every partition is held by a live peer and this device cannot write. */
|
|
209
|
+
get isReadOnly(): boolean;
|
|
210
|
+
/** The account's partition split — the modulus the proxy ranks holders by
|
|
211
|
+
* when electing which one answers a peer's `lease-request`. */
|
|
212
|
+
get partitionCount(): number;
|
|
213
|
+
/** The bound stamper — exposed for the proxy's `buildConnectionInfo`
|
|
214
|
+
* (appKey / uploadMode). */
|
|
215
|
+
get stamperRef(): UtilizationAwareStamper;
|
|
216
|
+
/** The account this coordinator (and its batch/lease) was built for —
|
|
217
|
+
* exposed so the proxy can refuse to publish a snapshot assembled for a
|
|
218
|
+
* different account through this coordinator's batch. */
|
|
219
|
+
get accountId(): string;
|
|
220
|
+
/** The batch this coordinator's lease lives on — lanes are per (batch,
|
|
221
|
+
* partition), so the proxy routes bus lease messages by both (#589). */
|
|
222
|
+
get batchId(): string;
|
|
223
|
+
/**
|
|
224
|
+
* The single write entry point. Takes the cross-tab Web Lock, ensures a held
|
|
225
|
+
* partition (acquire / slot-wait, or skip-with-throw), runs `op` against a
|
|
226
|
+
* stamper upload target, and flushes stamper state. Subsidised mode is NOT a
|
|
227
|
+
* coordinator concern — the proxy only calls this in stamper mode.
|
|
228
|
+
*/
|
|
229
|
+
withWrite<T>(op: (target: UploadTarget) => Promise<T>, opts?: WithWriteOptions): Promise<T>;
|
|
230
|
+
/** Persistent mode: eagerly acquire a partition + start the refresh timer in
|
|
231
|
+
* the background so the first upload doesn't pay the acquire latency. */
|
|
232
|
+
startLease(): void;
|
|
233
|
+
/**
|
|
234
|
+
* Tear down all lease background work and release the held partition
|
|
235
|
+
* (best-effort) so peers see this device vacate promptly. Used on sign-out /
|
|
236
|
+
* disconnect. Never throws.
|
|
237
|
+
*/
|
|
238
|
+
teardown(): void;
|
|
239
|
+
/**
|
|
240
|
+
* Ensure a partition is held for the current write. Runs under the write
|
|
241
|
+
* lock (called from `withWrite`/`startLease`). Re-arms a pending acquire
|
|
242
|
+
* when no lease is held or we are read-only, then either acquires (with
|
|
243
|
+
* slot-wait in "block" mode, or once-then-throw in "skip" mode) or runs the
|
|
244
|
+
* throttled freshness check on the held lease.
|
|
245
|
+
*/
|
|
246
|
+
private ensureLease;
|
|
247
|
+
/**
|
|
248
|
+
* Acquire a partition via the lock SOC — the single source of truth. The
|
|
249
|
+
* local cache is only a hint (`hydrate`); `acquire` always re-reads and
|
|
250
|
+
* reconciles against the lock SOCs before binding.
|
|
251
|
+
*/
|
|
252
|
+
private acquire;
|
|
253
|
+
/**
|
|
254
|
+
* Wait-for-slot wrapper around `acquire`. When the initial acquire returns
|
|
255
|
+
* read-only (every partition held by a live foreign holder), poll every
|
|
256
|
+
* `LEASE_REFRESH_MS` up to `SLOT_WAIT_TIMEOUT_MS`.
|
|
257
|
+
*/
|
|
258
|
+
private acquireWithSlotWait;
|
|
259
|
+
private pollForSlot;
|
|
260
|
+
private sleepUntilWoken;
|
|
261
|
+
/**
|
|
262
|
+
* Push the held lease's local expiry to the stamper so an in-flight `stamp()`
|
|
263
|
+
* fences itself the moment the lease lapses locally — the write-side
|
|
264
|
+
* complement of the reverse-clobber ack guard (Postage-Batch-Partitioning.md
|
|
265
|
+
* §12, "ack-safe, not write-safe"). Call wherever `leasedUntil` moves while we
|
|
266
|
+
* hold: bind and every renewal. `undefined` (no lease) disables the fence.
|
|
267
|
+
*/
|
|
268
|
+
private syncStamperLeaseDeadline;
|
|
269
|
+
/**
|
|
270
|
+
* Whether the held lease is within {@link LEASE_SKEW_MARGIN_MS} of its local
|
|
271
|
+
* expiry (or already lapsed). When true the freshness check must re-read the
|
|
272
|
+
* lock SOC even inside the throttle window — the reverse-clobber guard.
|
|
273
|
+
*/
|
|
274
|
+
private leaseNearExpiry;
|
|
275
|
+
/**
|
|
276
|
+
* Throttled lock-SOC freshness check on the held lease. Runs under the write
|
|
277
|
+
* lock (via `ensureLease`). Demotes (and throws) when a different live device
|
|
278
|
+
* now holds our partition.
|
|
279
|
+
*/
|
|
280
|
+
private ensureLeaseStillValid;
|
|
281
|
+
/**
|
|
282
|
+
* Phase 1 of demotion (the race fix): mark the bound lease stale so an
|
|
283
|
+
* in-flight `stamp()` throws `PartitionLeaseLostError` and aborts before
|
|
284
|
+
* writing to a slot a peer now owns. The partition stays bound (so the
|
|
285
|
+
* breaker can fire); the unbind happens in `finalizeDemote` under the lock.
|
|
286
|
+
*/
|
|
287
|
+
private signalLeaseLost;
|
|
288
|
+
/**
|
|
289
|
+
* Phase 2 of demotion: drop the claim on the current partition. MUST run
|
|
290
|
+
* under the write lock when called outside a held lock (see `refreshTick`),
|
|
291
|
+
* so no `stamp()` runs between `invalidateLease` and `unbindPartition`.
|
|
292
|
+
*/
|
|
293
|
+
private finalizeDemote;
|
|
294
|
+
/**
|
|
295
|
+
* Periodic refresh: re-run the lock protocol on the held partition every
|
|
296
|
+
* `LEASE_REFRESH_MS`. Runs on a bare interval (NOT under the write lock) so
|
|
297
|
+
* it can detect displacement mid-upload — see the race-fix note in the class
|
|
298
|
+
* doc. Persistent mode only.
|
|
299
|
+
*/
|
|
300
|
+
private startRefreshTimer;
|
|
301
|
+
private refreshTick;
|
|
302
|
+
/**
|
|
303
|
+
* Voluntarily give up the held partition after it has gone idle. Keeps the
|
|
304
|
+
* coordinator able to re-acquire on the next write (re-arm via `ensureLease`).
|
|
305
|
+
* MUST run under the write lock (see the idle-yield note in `refreshTick`).
|
|
306
|
+
*/
|
|
307
|
+
private yieldIdleLease;
|
|
308
|
+
/**
|
|
309
|
+
* Wake a slot-waiting acquire immediately (a peer announced a release over
|
|
310
|
+
* the account bus). No-op outside slot-wait; the woken loop still claims
|
|
311
|
+
* through the lock SOCs, so a spurious wake costs one extra read round.
|
|
312
|
+
*/
|
|
313
|
+
notifySlotMaybeFree(): void;
|
|
314
|
+
/**
|
|
315
|
+
* Whether a peer's `lease-request` would presently be answered — a partition
|
|
316
|
+
* held, nothing in flight, and idle for `PEER_YIELD_MIN_IDLE_MS`. The proxy
|
|
317
|
+
* checks it before taking a rank in the yield election, so a holder mid-burst
|
|
318
|
+
* doesn't occupy the front of the order only to decline in silence.
|
|
319
|
+
*
|
|
320
|
+
* Advisory: `yieldForPeer` re-checks under the write lock, where an upload
|
|
321
|
+
* that slipped in between can still turn the answer into a no-op.
|
|
322
|
+
*/
|
|
323
|
+
get canYieldForPeer(): boolean;
|
|
324
|
+
/**
|
|
325
|
+
* A waiting peer asked for a slot (bus `lease-request`): release the held
|
|
326
|
+
* partition NOW if it is safe — nothing in flight and no write within
|
|
327
|
+
* `PEER_YIELD_MIN_IDLE_MS`. Runs the exact idle-yield path (under the write
|
|
328
|
+
* lock, with the same re-checks), so the Swarm release protocol and counter
|
|
329
|
+
* flushes are identical to a timer yield. Returns the released partition,
|
|
330
|
+
* or undefined when nothing was (or could safely be) yielded; the release
|
|
331
|
+
* is announced through `onLeaseReleased` with the `requestId` it answers.
|
|
332
|
+
*/
|
|
333
|
+
yieldForPeer(requestId?: string): Promise<number | undefined>;
|
|
334
|
+
/**
|
|
335
|
+
* Stop lease background work and drop the in-memory lease + cache. Used when
|
|
336
|
+
* the acquire path times out. A still-valid cached lease is re-adopted before
|
|
337
|
+
* that path runs, so reaching here means there is no live lease to preserve.
|
|
338
|
+
*/
|
|
339
|
+
private pauseLeaseBackgroundWork;
|
|
340
|
+
private buildStamperTarget;
|
|
341
|
+
/** Take the cross-tab Web Lock, run `op`, then flush stamper state. */
|
|
342
|
+
private lockAndFlush;
|
|
343
|
+
/** Take the cross-tab Web Lock without the stamper flush (lease mutations). */
|
|
344
|
+
private lock;
|
|
345
|
+
/** Post-acquisition guard: a multi-device account must hold a partition. */
|
|
346
|
+
private ensureHeldForUpload;
|
|
347
|
+
private emitLeaseChange;
|
|
348
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device roster (Phase 3a hardening) — append-only discovery log.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the single shared *mutable* registry doc, which a device could
|
|
5
|
+
* **clobber** on a high-latency/caching gateway: it read-merge-overwrote the
|
|
6
|
+
* whole doc, so a device that couldn't yet see a peer's just-written entry
|
|
7
|
+
* dropped it. (See the gateway finding in the Phase 3a implementation log.)
|
|
8
|
+
*
|
|
9
|
+
* The roster is a **sequential append-only feed** (`swarm-id-roster-v1:<acct>`):
|
|
10
|
+
* each device appends ONLY its own membership record (`Device`) at the next free
|
|
11
|
+
* index. A device never overwrites a peer's slot, so concurrent announces can't
|
|
12
|
+
* clobber an existing entry — the worst case is two devices racing the same
|
|
13
|
+
* fresh index, where one append is lost and re-added on its next sync (existing
|
|
14
|
+
* entries are untouched). Readers scan all entries and fold by `deviceId`
|
|
15
|
+
* (reusing `mergeDevicesList`, so removals/resurrections converge by Phase 1
|
|
16
|
+
* LWW). Account-level immutables (publicKey, createdAt, partitionCount) live on
|
|
17
|
+
* the robust per-device state feed (`device-state.ts`), not here.
|
|
18
|
+
*
|
|
19
|
+
* Each entry is an encrypted blob (privacy parity with the old registry)
|
|
20
|
+
* referenced by the sequential SOC, reusing `uploadData`/`downloadDataWithChunkAPI`.
|
|
21
|
+
*/
|
|
22
|
+
import { Bee, EthAddress, Identifier, PrivateKey, Topic } from "@ethersphere/bee-js";
|
|
23
|
+
import { type Device } from "../schemas";
|
|
24
|
+
import { type UploadTarget } from "../proxy/upload";
|
|
25
|
+
export declare const ROSTER_TOPIC_PREFIX = "swarm-id-roster-v1";
|
|
26
|
+
/** Test-only: forget every memoised roster length (in-memory module state). */
|
|
27
|
+
export declare function resetRosterScanCache(): void;
|
|
28
|
+
/** Thrown by {@link readRoster} when it cannot distinguish an empty roster from
|
|
29
|
+
* an outage (index 0 stays inconclusive after the retry, nothing folded).
|
|
30
|
+
* Callers must NOT treat this as "no devices" — see {@link ensureInRoster}. */
|
|
31
|
+
export declare class RosterScanInconclusiveError extends Error {
|
|
32
|
+
constructor();
|
|
33
|
+
}
|
|
34
|
+
export declare function rosterTopic(accountId: string): Topic;
|
|
35
|
+
export declare function rosterIdentifier(topic: Topic, index: bigint): Identifier;
|
|
36
|
+
/**
|
|
37
|
+
* Scan the roster and fold to the latest record per `deviceId` (Phase 1
|
|
38
|
+
* `mergeDevicesList`: removals/resurrections converge by LWW). Returns `[]` when
|
|
39
|
+
* the feed is empty (nothing published yet).
|
|
40
|
+
*/
|
|
41
|
+
export declare function readRoster(opts: {
|
|
42
|
+
bee: Bee;
|
|
43
|
+
accountId: string;
|
|
44
|
+
owner: EthAddress;
|
|
45
|
+
}): Promise<Device[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Ensure this device is in the roster with current membership. Appends only when
|
|
48
|
+
* the device is absent or its `removedAt` state differs (a new sign-in, a
|
|
49
|
+
* removal, or a resurrection) — keeping the roster a rare, append-only write.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ensureInRoster(opts: {
|
|
52
|
+
bee: Bee;
|
|
53
|
+
accountKey: PrivateKey;
|
|
54
|
+
owner: EthAddress;
|
|
55
|
+
encryptionKey: string;
|
|
56
|
+
accountId: string;
|
|
57
|
+
device: Device;
|
|
58
|
+
target: UploadTarget;
|
|
59
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-device account-state feed (Phase 3a).
|
|
3
|
+
*
|
|
4
|
+
* Each device publishes its own **full current account view** to its OWN feed
|
|
5
|
+
* (`swarm-id-devstate-v1:<accountId>:<deviceId>`). Because a device writes only
|
|
6
|
+
* its own feed there is no shared SOC address and no cross-device write
|
|
7
|
+
* contention — the `publishAccountState` merge-read-rewrite + `verifyWon` dance
|
|
8
|
+
* is gone. Convergence happens at READ time: `foldAccount` merges every device's
|
|
9
|
+
* latest view with the Phase 1 merge primitives.
|
|
10
|
+
*
|
|
11
|
+
* Device membership (the `deviceId` set) lives in the append-only device roster
|
|
12
|
+
* (`device-roster.ts`), not here; account-level immutables (publicKey, createdAt,
|
|
13
|
+
* partitionCount) ride this robust per-device feed so the fold reconstructs them
|
|
14
|
+
* without a shared doc. Scalar account fields carry per-field LWW clocks so a
|
|
15
|
+
* concurrent change to a different scalar on another device is not dropped
|
|
16
|
+
* (docs/Account-State.md, "Convergence rules").
|
|
17
|
+
*/
|
|
18
|
+
import { Bee, BatchId, EthAddress, PrivateKey, Topic } from "@ethersphere/bee-js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { type AccountStateSnapshot, type ConnectedApp, type Device, type PostageStamp } from "../schemas";
|
|
21
|
+
import { type UploadTarget } from "../proxy/upload";
|
|
22
|
+
import type { SyncResult } from "./types";
|
|
23
|
+
export declare const DEVICE_STATE_TOPIC_PREFIX = "swarm-id-devstate-v1";
|
|
24
|
+
/** Account settings carried in a device view (mirrors `account.settings`). */
|
|
25
|
+
declare const SettingsSchema: z.ZodObject<{
|
|
26
|
+
appSessionDuration: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type AccountSettings = z.infer<typeof SettingsSchema>;
|
|
29
|
+
/**
|
|
30
|
+
* One device's view of the account. `connectedApps`/`postageStamps` carry their
|
|
31
|
+
* Phase 1 LWW/tombstone clocks; scalars carry their own per-field `at`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DeviceStateSnapshotSchemaV1: z.ZodObject<{
|
|
34
|
+
version: z.ZodLiteral<1>;
|
|
35
|
+
accountId: z.ZodString;
|
|
36
|
+
deviceId: z.ZodString;
|
|
37
|
+
timestamp: z.ZodNumber;
|
|
38
|
+
connectedApps: z.ZodArray<z.ZodObject<{
|
|
39
|
+
appUrl: z.ZodString;
|
|
40
|
+
appName: z.ZodString;
|
|
41
|
+
lastConnectedAt: z.ZodNumber;
|
|
42
|
+
appIcon: z.ZodOptional<z.ZodString>;
|
|
43
|
+
appDescription: z.ZodOptional<z.ZodString>;
|
|
44
|
+
connectedUntil: z.ZodOptional<z.ZodNumber>;
|
|
45
|
+
appSecret: z.ZodOptional<z.ZodString>;
|
|
46
|
+
postageStampBatchID: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<BatchId, string>>>;
|
|
47
|
+
postageStampBatchIDAt: z.ZodOptional<z.ZodNumber>;
|
|
48
|
+
revokedAt: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
disconnectedAt: z.ZodOptional<z.ZodNumber>;
|
|
50
|
+
chatBinding: z.ZodOptional<z.ZodObject<{
|
|
51
|
+
message: z.ZodString;
|
|
52
|
+
signature: z.ZodString;
|
|
53
|
+
}, z.core.$strip>>;
|
|
54
|
+
}, z.core.$strip>>;
|
|
55
|
+
postageStamps: z.ZodArray<z.ZodObject<{
|
|
56
|
+
batchID: z.ZodPipe<z.ZodString, z.ZodTransform<BatchId, string>>;
|
|
57
|
+
signerKey: z.ZodPipe<z.ZodString, z.ZodTransform<PrivateKey, string>>;
|
|
58
|
+
name: z.ZodOptional<z.ZodString>;
|
|
59
|
+
nameUpdatedAt: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
utilization: z.ZodNumber;
|
|
61
|
+
usable: z.ZodBoolean;
|
|
62
|
+
depth: z.ZodNumber;
|
|
63
|
+
amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
|
|
64
|
+
bucketDepth: z.ZodNumber;
|
|
65
|
+
blockNumber: z.ZodNumber;
|
|
66
|
+
immutableFlag: z.ZodBoolean;
|
|
67
|
+
exists: z.ZodBoolean;
|
|
68
|
+
batchTTL: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
createdAt: z.ZodNumber;
|
|
70
|
+
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
71
|
+
deletedAt: z.ZodOptional<z.ZodNumber>;
|
|
72
|
+
}, z.core.$strip>>;
|
|
73
|
+
accountName: z.ZodObject<{
|
|
74
|
+
value: z.ZodString;
|
|
75
|
+
at: z.ZodNumber;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
defaultPostageStampBatchID: z.ZodObject<{
|
|
78
|
+
value: z.ZodOptional<z.ZodString>;
|
|
79
|
+
at: z.ZodNumber;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
settings: z.ZodObject<{
|
|
82
|
+
value: z.ZodOptional<z.ZodObject<{
|
|
83
|
+
appSessionDuration: z.ZodOptional<z.ZodNumber>;
|
|
84
|
+
}, z.core.$strip>>;
|
|
85
|
+
at: z.ZodNumber;
|
|
86
|
+
}, z.core.$strip>;
|
|
87
|
+
accountPublicKey: z.ZodString;
|
|
88
|
+
accountCreatedAt: z.ZodNumber;
|
|
89
|
+
partitionCount: z.ZodDefault<z.ZodNumber>;
|
|
90
|
+
}, z.core.$strip>;
|
|
91
|
+
export type DeviceStateSnapshot = z.infer<typeof DeviceStateSnapshotSchemaV1>;
|
|
92
|
+
/** The per-device view a device contributes (mutable collections/scalars + account immutables). */
|
|
93
|
+
export interface DeviceStateView {
|
|
94
|
+
connectedApps: ConnectedApp[];
|
|
95
|
+
postageStamps: PostageStamp[];
|
|
96
|
+
accountName: {
|
|
97
|
+
value: string;
|
|
98
|
+
at: number;
|
|
99
|
+
};
|
|
100
|
+
defaultPostageStampBatchID: {
|
|
101
|
+
value: string | undefined;
|
|
102
|
+
at: number;
|
|
103
|
+
};
|
|
104
|
+
settings: {
|
|
105
|
+
value: AccountSettings | undefined;
|
|
106
|
+
at: number;
|
|
107
|
+
};
|
|
108
|
+
accountPublicKey: string;
|
|
109
|
+
accountCreatedAt: number;
|
|
110
|
+
partitionCount: number;
|
|
111
|
+
}
|
|
112
|
+
/** Folded account state — the shape `applyRefreshed` / the proxy consume. */
|
|
113
|
+
export interface FoldedAccount {
|
|
114
|
+
devices: Device[];
|
|
115
|
+
connectedApps: ConnectedApp[];
|
|
116
|
+
postageStamps: PostageStamp[];
|
|
117
|
+
accountName: string;
|
|
118
|
+
defaultPostageStampBatchID: BatchId | undefined;
|
|
119
|
+
settings: AccountSettings | undefined;
|
|
120
|
+
accountNameAt: number;
|
|
121
|
+
defaultStampAt: number;
|
|
122
|
+
settingsAt: number;
|
|
123
|
+
createdAt: number;
|
|
124
|
+
publicKey: string;
|
|
125
|
+
partitionCount: number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Build this device's `DeviceStateView` from the local account snapshot — the
|
|
129
|
+
* single derivation shared by both publish paths (`sync-account` and the proxy).
|
|
130
|
+
*
|
|
131
|
+
* Each scalar carries its own per-field LWW clock. A field that was never
|
|
132
|
+
* explicitly edited (no `*At` clock on the snapshot) falls back to the account's
|
|
133
|
+
* STABLE `createdAt` — NEVER a fresh `Date.now()`. Re-stamping an unchanged
|
|
134
|
+
* name / default-stamp / settings with a new timestamp on every publish would
|
|
135
|
+
* let a device that never touched the field clobber a peer's genuine concurrent
|
|
136
|
+
* edit under per-field LWW (docs/Account-State.md, "Convergence rules").
|
|
137
|
+
* `createdAt` is identical across devices and
|
|
138
|
+
* predates every edit, so any real change always wins.
|
|
139
|
+
*/
|
|
140
|
+
export declare function accountStateToDeviceView(snapshot: AccountStateSnapshot): DeviceStateView;
|
|
141
|
+
export declare function deviceStateTopic(accountId: string, deviceId: string): Topic;
|
|
142
|
+
export declare function serializeDeviceState(accountId: string, deviceId: string, view: DeviceStateView): Record<string, unknown>;
|
|
143
|
+
export declare function deserializeDeviceState(data: Uint8Array): DeviceStateSnapshot;
|
|
144
|
+
/**
|
|
145
|
+
* Write this device's view to its own feed. No merge / no verifyWon — the feed
|
|
146
|
+
* is owned exclusively by this device. Runs inside the caller's
|
|
147
|
+
* `BatchWriteCoordinator.withWrite` (partition lease + Web Lock already held),
|
|
148
|
+
* which supplies `target`.
|
|
149
|
+
*/
|
|
150
|
+
export declare function writeDeviceState(opts: {
|
|
151
|
+
accountId: string;
|
|
152
|
+
deviceId: string;
|
|
153
|
+
accountKey: PrivateKey;
|
|
154
|
+
encryptionKey: string;
|
|
155
|
+
view: DeviceStateView;
|
|
156
|
+
target: UploadTarget;
|
|
157
|
+
}): Promise<{
|
|
158
|
+
reference: string;
|
|
159
|
+
chunkAddresses: Uint8Array[];
|
|
160
|
+
}>;
|
|
161
|
+
/**
|
|
162
|
+
* Write this device's view AND ensure it's in the append-only device roster. The
|
|
163
|
+
* single write entry point for both `sync-account` (UI-triggered) and the proxy
|
|
164
|
+
* (publish-on-acquire). `ensureInRoster` appends only when this device is absent
|
|
165
|
+
* or its removed-state changed — keeping the roster a rare, clobber-free write.
|
|
166
|
+
*/
|
|
167
|
+
export declare function publishDeviceState(opts: {
|
|
168
|
+
bee: Bee;
|
|
169
|
+
accountId: string;
|
|
170
|
+
device: Device;
|
|
171
|
+
accountKey: PrivateKey;
|
|
172
|
+
owner: EthAddress;
|
|
173
|
+
encryptionKey: string;
|
|
174
|
+
view: DeviceStateView;
|
|
175
|
+
target: UploadTarget;
|
|
176
|
+
/** Per-chunk utilisation hook (mirrors the old publish path). */
|
|
177
|
+
onChunksUploaded?: (chunkAddresses: Uint8Array[]) => Promise<void>;
|
|
178
|
+
}): Promise<SyncResult>;
|
|
179
|
+
/**
|
|
180
|
+
* Read a device's latest view, or `undefined` when its feed is empty/unreachable.
|
|
181
|
+
*/
|
|
182
|
+
export declare function readLatestDeviceState(opts: {
|
|
183
|
+
bee: Bee;
|
|
184
|
+
accountId: string;
|
|
185
|
+
deviceId: string;
|
|
186
|
+
owner: EthAddress;
|
|
187
|
+
}): Promise<DeviceStateSnapshot | undefined>;
|
|
188
|
+
/**
|
|
189
|
+
* Merge every device's latest view into one account state, reusing the Phase 1
|
|
190
|
+
* primitives for the collections and per-field LWW for the scalars. The device
|
|
191
|
+
* set (`rosterDevices`) comes from the append-only roster (discovery); account
|
|
192
|
+
* immutables come from any device view (identical across devices).
|
|
193
|
+
*
|
|
194
|
+
* Equivalent by construction to today's `mergeSnapshotWithRemote` over the same
|
|
195
|
+
* data (see the differential test) — convergence and deletion semantics are
|
|
196
|
+
* unchanged from Phase 1.
|
|
197
|
+
*/
|
|
198
|
+
export declare function foldAccount(views: DeviceStateSnapshot[], rosterDevices: Device[], accountId: string): FoldedAccount;
|
|
199
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read entry point for Phase 3a: reconstruct account state from Swarm by
|
|
3
|
+
* scanning the append-only device roster (discovery) and folding every device's
|
|
4
|
+
* latest state feed. Replaces the single shared-snapshot fetch used by restore
|
|
5
|
+
* / refresh / the proxy.
|
|
6
|
+
*/
|
|
7
|
+
import { Bee, EthAddress } from "@ethersphere/bee-js";
|
|
8
|
+
import { type FoldedAccount } from "./device-state";
|
|
9
|
+
import type { SyncedAccount, Device } from "../schemas";
|
|
10
|
+
export interface FoldAccountResult {
|
|
11
|
+
account: FoldedAccount;
|
|
12
|
+
devices: Device[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Project a folded account onto the `SyncedAccount` record — the single place
|
|
16
|
+
* the field list lives so a new `SyncedAccount`/`FoldedAccount` field can't
|
|
17
|
+
* be silently dropped by a hand-written copy on the sign-in path. `id` and
|
|
18
|
+
* `derivationKey` aren't in the fold (they come from the recovering wallet), so
|
|
19
|
+
* the caller supplies them.
|
|
20
|
+
*
|
|
21
|
+
* `foldAccountFromSwarm` shallow-FREEZES its result arrays so coalesced callers
|
|
22
|
+
* can't corrupt each other's shared view; a persisted `SyncedAccount` outlives
|
|
23
|
+
* that window and is mutated in place downstream, so the arrays are COPIED here.
|
|
24
|
+
*/
|
|
25
|
+
export declare function foldedToSyncedAccount(opts: {
|
|
26
|
+
id: EthAddress;
|
|
27
|
+
derivationKey: string;
|
|
28
|
+
account: FoldedAccount;
|
|
29
|
+
lastModified?: number;
|
|
30
|
+
}): SyncedAccount;
|
|
31
|
+
/**
|
|
32
|
+
* Derive the feed owner + encryption key from the account derivation key, read
|
|
33
|
+
* the roster, fold all device-state feeds. Returns `undefined` when the roster
|
|
34
|
+
* is empty (no device has published — equivalent to today's "no-backup").
|
|
35
|
+
*
|
|
36
|
+
* Concurrent calls for the same account share one in-flight read (see
|
|
37
|
+
* `inFlightFolds`); `bee` is intentionally keyed OUT — every client reads the
|
|
38
|
+
* same feeds. Coalescing across two DIFFERENT `bee` endpoints (e.g. a local
|
|
39
|
+
* cluster and a public gateway mid-transition) is safe, not a bug: a fold is a
|
|
40
|
+
* point-in-time, eventually-consistent LWW/CRDT snapshot (roster + device-state
|
|
41
|
+
* both fold last-writer-wins by their per-field `at` clocks — see
|
|
42
|
+
* `merge-snapshot.ts` / `device-state.ts`). No caller assumes a fold reflects a
|
|
43
|
+
* specific endpoint's live view, so a coalesced caller receiving the other
|
|
44
|
+
* endpoint's equally-valid snapshot is indistinguishable from ordinary gateway
|
|
45
|
+
* staleness — which the LWW fold already tolerates everywhere, and the next
|
|
46
|
+
* (uncoalesced, entry-cleared) fold re-reads and re-converges. Do NOT add `bee`
|
|
47
|
+
* to the key to "fix" this; it would only defeat the coalescing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function foldAccountFromSwarm(opts: {
|
|
50
|
+
bee: Bee;
|
|
51
|
+
derivationKey: string;
|
|
52
|
+
accountId: string;
|
|
53
|
+
}): Promise<FoldAccountResult | undefined>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { deriveAccountDerivationKey, deriveSwarmEncryptionKey, derivePostageSignerKey, } from "../utils/key-derivation";
|
|
2
|
+
export { mergeSnapshotWithRemote, mergeConnectedApps, mergePostageStamps, mergeDevicesList, snapshotContainsContribution, } from "./merge-snapshot";
|
|
3
|
+
export { createSyncAccount } from "./sync-account";
|
|
4
|
+
export type { SyncAccountOptions, SyncAccountFunction } from "./sync-account";
|
|
5
|
+
export { deviceStateTopic, accountStateToDeviceView, writeDeviceState, publishDeviceState, readLatestDeviceState, foldAccount, deserializeDeviceState, DeviceStateSnapshotSchemaV1, DEVICE_STATE_TOPIC_PREFIX, } from "./device-state";
|
|
6
|
+
export type { DeviceStateSnapshot, DeviceStateView, FoldedAccount, AccountSettings, } from "./device-state";
|
|
7
|
+
export { rosterTopic, readRoster, ensureInRoster, ROSTER_TOPIC_PREFIX, } from "./device-roster";
|
|
8
|
+
export { foldAccountFromSwarm, foldedToSyncedAccount, } from "./fold-account-from-swarm";
|
|
9
|
+
export type { FoldAccountResult } from "./fold-account-from-swarm";
|
|
10
|
+
export type { AccountsStoreInterface, PostageStampsStoreInterface, StamperOptions, FlushableStamper, } from "./store-interfaces";
|
|
11
|
+
export type { SyncResult } from "./types";
|