@bitsocial/pubsub-votes 0.0.2

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 (117) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +212 -0
  3. package/dist/chain/bucket.d.ts +13 -0
  4. package/dist/chain/bucket.js +24 -0
  5. package/dist/chain/ticker.d.ts +15 -0
  6. package/dist/chain/ticker.js +25 -0
  7. package/dist/chain/types.d.ts +90 -0
  8. package/dist/chain/types.js +1 -0
  9. package/dist/checkpoint/codec.d.ts +54 -0
  10. package/dist/checkpoint/codec.js +99 -0
  11. package/dist/client/root-puller.d.ts +49 -0
  12. package/dist/client/root-puller.js +140 -0
  13. package/dist/client/voter.d.ts +225 -0
  14. package/dist/client/voter.js +1195 -0
  15. package/dist/crdt/codec.d.ts +41 -0
  16. package/dist/crdt/codec.js +137 -0
  17. package/dist/crdt/crdt.d.ts +22 -0
  18. package/dist/crdt/crdt.js +127 -0
  19. package/dist/crdt/store.d.ts +8 -0
  20. package/dist/crdt/store.js +23 -0
  21. package/dist/crdt/types.d.ts +87 -0
  22. package/dist/crdt/types.js +1 -0
  23. package/dist/encoding/canonical.d.ts +22 -0
  24. package/dist/encoding/canonical.js +26 -0
  25. package/dist/errors.d.ts +72 -0
  26. package/dist/errors.js +111 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.js +39 -0
  29. package/dist/rules/constant.d.ts +14 -0
  30. package/dist/rules/constant.js +18 -0
  31. package/dist/rules/erc20-balance.d.ts +30 -0
  32. package/dist/rules/erc20-balance.js +44 -0
  33. package/dist/rules/erc721-min-balance.d.ts +18 -0
  34. package/dist/rules/erc721-min-balance.js +56 -0
  35. package/dist/rules/registry.d.ts +42 -0
  36. package/dist/rules/registry.js +61 -0
  37. package/dist/rules/types.d.ts +74 -0
  38. package/dist/rules/types.js +1 -0
  39. package/dist/schema/common.d.ts +25 -0
  40. package/dist/schema/common.js +24 -0
  41. package/dist/schema/criteria.d.ts +78 -0
  42. package/dist/schema/criteria.js +76 -0
  43. package/dist/schema/directory.d.ts +42 -0
  44. package/dist/schema/directory.js +52 -0
  45. package/dist/schema/votes.d.ts +55 -0
  46. package/dist/schema/votes.js +116 -0
  47. package/dist/signer/eip712.d.ts +103 -0
  48. package/dist/signer/eip712.js +85 -0
  49. package/dist/signer/types.d.ts +31 -0
  50. package/dist/signer/types.js +1 -0
  51. package/dist/storage/browser.d.ts +3 -0
  52. package/dist/storage/browser.js +110 -0
  53. package/dist/storage/memory.d.ts +10 -0
  54. package/dist/storage/memory.js +56 -0
  55. package/dist/storage/node.d.ts +5 -0
  56. package/dist/storage/node.js +106 -0
  57. package/dist/storage/types.d.ts +46 -0
  58. package/dist/storage/types.js +1 -0
  59. package/dist/store/indexeddb.d.ts +9 -0
  60. package/dist/store/indexeddb.js +72 -0
  61. package/dist/store/memory.d.ts +15 -0
  62. package/dist/store/memory.js +22 -0
  63. package/dist/store/select.d.ts +15 -0
  64. package/dist/store/select.js +64 -0
  65. package/dist/store/sqlite.d.ts +11 -0
  66. package/dist/store/sqlite.js +68 -0
  67. package/dist/store/types.d.ts +57 -0
  68. package/dist/store/types.js +1 -0
  69. package/dist/tally/tally.d.ts +44 -0
  70. package/dist/tally/tally.js +89 -0
  71. package/dist/tally/types.d.ts +51 -0
  72. package/dist/tally/types.js +13 -0
  73. package/dist/topic.d.ts +20 -0
  74. package/dist/topic.js +28 -0
  75. package/dist/transport/accepted-dedup.d.ts +30 -0
  76. package/dist/transport/accepted-dedup.js +34 -0
  77. package/dist/transport/announce/browser.d.ts +9 -0
  78. package/dist/transport/announce/browser.js +14 -0
  79. package/dist/transport/announce/node.d.ts +38 -0
  80. package/dist/transport/announce/node.js +162 -0
  81. package/dist/transport/announce/types.d.ts +74 -0
  82. package/dist/transport/announce/types.js +16 -0
  83. package/dist/transport/bundle-store.d.ts +11 -0
  84. package/dist/transport/bundle-store.js +34 -0
  85. package/dist/transport/chase.d.ts +83 -0
  86. package/dist/transport/chase.js +88 -0
  87. package/dist/transport/gossip-validator.d.ts +107 -0
  88. package/dist/transport/gossip-validator.js +99 -0
  89. package/dist/transport/helia.d.ts +41 -0
  90. package/dist/transport/helia.js +98 -0
  91. package/dist/transport/integration/harness.d.ts +60 -0
  92. package/dist/transport/integration/harness.js +262 -0
  93. package/dist/transport/messages.d.ts +97 -0
  94. package/dist/transport/messages.js +117 -0
  95. package/dist/transport/rate-limit.d.ts +11 -0
  96. package/dist/transport/rate-limit.js +20 -0
  97. package/dist/transport/transport.d.ts +20 -0
  98. package/dist/transport/transport.js +35 -0
  99. package/dist/transport/types.d.ts +165 -0
  100. package/dist/transport/types.js +1 -0
  101. package/dist/verify/background.d.ts +81 -0
  102. package/dist/verify/background.js +236 -0
  103. package/dist/verify/bundle.d.ts +58 -0
  104. package/dist/verify/bundle.js +84 -0
  105. package/dist/verify/cache.d.ts +48 -0
  106. package/dist/verify/cache.js +62 -0
  107. package/dist/verify/constraints.d.ts +16 -0
  108. package/dist/verify/constraints.js +35 -0
  109. package/dist/verify/gate-result-cache.d.ts +65 -0
  110. package/dist/verify/gate-result-cache.js +91 -0
  111. package/dist/verify/name-resolution-cache.d.ts +59 -0
  112. package/dist/verify/name-resolution-cache.js +64 -0
  113. package/dist/verify/signature.d.ts +9 -0
  114. package/dist/verify/signature.js +55 -0
  115. package/dist/verify/types.d.ts +101 -0
  116. package/dist/verify/types.js +1 -0
  117. package/package.json +77 -0
@@ -0,0 +1,140 @@
1
+ import pLimit from "p-limit";
2
+ import { batchRootsFetchKey, decodeBatchRootsResponse, decodeRootRecord, rootFetchKey, MAX_BATCH_ROOT_KEYS } from "../transport/messages.js";
3
+ /** See the budget note on {@link RootPuller}. */
4
+ export const COLD_START_PEER_FETCH_LIMIT = 24;
5
+ /** See the retry note on {@link RootPuller}. */
6
+ export const COLD_START_FETCH_DEADLINE_MS = 30_000;
7
+ const COLD_START_FETCH_BACKOFF_MS = 400;
8
+ const COLD_START_FETCH_BACKOFF_CAP_MS = 4_000;
9
+ /**
10
+ * How long a peer's first pending pull waits for same-peer company before its batch flushes.
11
+ * A directory join fires all its cold starts in one synchronous burst, so one tick would
12
+ * usually do; a few ms of slack covers joins interleaved with per-contest async work (topic
13
+ * hashing, chain-client setup) without adding perceptible latency to a lone join.
14
+ */
15
+ const BATCH_WINDOW_MS = 20;
16
+ /**
17
+ * One `pLimit(limitPerPeer)` per peer id, created on first use and dropped once its queue
18
+ * drains, so a long-lived voter does not accumulate limiters for every peer it ever
19
+ * cold-started against.
20
+ */
21
+ function makePerPeerBudget(limitPerPeer) {
22
+ const limiters = new Map();
23
+ return async (peerId, task) => {
24
+ let limiter = limiters.get(peerId);
25
+ if (limiter === undefined) {
26
+ limiter = pLimit(limitPerPeer);
27
+ limiters.set(peerId, limiter);
28
+ }
29
+ try {
30
+ return await limiter(task);
31
+ }
32
+ finally {
33
+ if (limiter.activeCount === 0 && limiter.pendingCount === 0)
34
+ limiters.delete(peerId);
35
+ }
36
+ };
37
+ }
38
+ /** Build the voter-wide puller over the host's fetch service. */
39
+ export function makeRootPuller(fetch) {
40
+ const budget = makePerPeerBudget(COLD_START_PEER_FETCH_LIMIT);
41
+ const pending = new Map();
42
+ /** One budgeted+retried fetch of one key (see the retry note on {@link RootPuller}). */
43
+ const fetchWithRetry = async (peer, key, isLive) => {
44
+ const deadline = Date.now() + COLD_START_FETCH_DEADLINE_MS;
45
+ let lastError;
46
+ for (let attempt = 0;; attempt++) {
47
+ if (attempt > 0) {
48
+ if (!isLive() || Date.now() >= deadline)
49
+ break; // left or out of time
50
+ const ceiling = Math.min(COLD_START_FETCH_BACKOFF_CAP_MS, COLD_START_FETCH_BACKOFF_MS * 2 ** (attempt - 1));
51
+ await new Promise((resolve) => setTimeout(resolve, Math.random() * ceiling));
52
+ if (!isLive())
53
+ return undefined; // left mid-backoff — abandon quietly
54
+ }
55
+ try {
56
+ return await budget(peer.toString(), () => fetch.fetch(peer, key));
57
+ }
58
+ catch (error) {
59
+ lastError = error; // transient (e.g. responder over its inbound-stream cap) — back off and retry
60
+ }
61
+ }
62
+ throw lastError;
63
+ };
64
+ /** The per-topic path: today's `<topic>/root` key; a garbage answer rejects the waiters. */
65
+ const pullSingle = async (peer, topic, waiters) => {
66
+ try {
67
+ const value = await fetchWithRetry(peer, rootFetchKey(topic), () => waiters.some((w) => w.isLive()));
68
+ const record = value === undefined || value === null ? value : decodeRootRecord(value);
69
+ waiters.forEach((w) => w.resolve(record));
70
+ }
71
+ catch (error) {
72
+ waiters.forEach((w) => w.reject(error));
73
+ }
74
+ };
75
+ /**
76
+ * The batch path: one stream, one key carrying every pending topic, answers distributed by
77
+ * request order. NOT_FOUND (a responder without the batch key), a malformed response, or a
78
+ * length mismatch all degrade to the per-topic path — never to silence.
79
+ */
80
+ const pullBatch = async (peer, topics) => {
81
+ const order = [...topics.keys()];
82
+ const isLive = () => [...topics.values()].some((waiters) => waiters.some((w) => w.isLive()));
83
+ try {
84
+ const value = await fetchWithRetry(peer, batchRootsFetchKey(order), isLive);
85
+ if (value !== undefined && value !== null) {
86
+ let records;
87
+ try {
88
+ records = decodeBatchRootsResponse(value);
89
+ }
90
+ catch {
91
+ records = undefined; // hostile/buggy answer — fall through to per-topic
92
+ }
93
+ if (records !== undefined && records.length === order.length) {
94
+ order.forEach((topic, i) => topics.get(topic).forEach((w) => w.resolve(records[i] ?? null)));
95
+ return;
96
+ }
97
+ }
98
+ // Old responder (NOT_FOUND) or malformed batch answer: degrade to per-topic keys.
99
+ await Promise.all([...topics.entries()].map(([topic, waiters]) => pullSingle(peer, topic, waiters)));
100
+ }
101
+ catch (error) {
102
+ for (const waiters of topics.values())
103
+ waiters.forEach((w) => w.reject(error));
104
+ }
105
+ };
106
+ const flush = (peerId) => {
107
+ const batch = pending.get(peerId);
108
+ if (batch === undefined)
109
+ return;
110
+ pending.delete(peerId);
111
+ clearTimeout(batch.timer);
112
+ // A lone topic keeps today's per-topic key — no batch envelope for the common
113
+ // single-board join; two or more ride one batch stream.
114
+ if (batch.topics.size === 1) {
115
+ for (const [topic, waiters] of batch.topics)
116
+ void pullSingle(batch.peer, topic, waiters);
117
+ }
118
+ else {
119
+ void pullBatch(batch.peer, batch.topics);
120
+ }
121
+ };
122
+ return {
123
+ pull: (peer, topic, isLive) => new Promise((resolve, reject) => {
124
+ const peerId = peer.toString();
125
+ let batch = pending.get(peerId);
126
+ if (batch === undefined) {
127
+ const timer = setTimeout(() => flush(peerId), BATCH_WINDOW_MS);
128
+ timer.unref?.();
129
+ batch = { peer, topics: new Map(), timer };
130
+ pending.set(peerId, batch);
131
+ }
132
+ const waiters = batch.topics.get(topic) ?? [];
133
+ waiters.push({ isLive, resolve, reject });
134
+ batch.topics.set(topic, waiters);
135
+ // A full batch flushes immediately; the next pull opens a fresh window.
136
+ if (batch.topics.size >= MAX_BATCH_ROOT_KEYS)
137
+ flush(peerId);
138
+ })
139
+ };
140
+ }
@@ -0,0 +1,225 @@
1
+ import { type Criteria } from "../schema/criteria.js";
2
+ import { type Vote, type VotesBundle } from "../schema/votes.js";
3
+ import type { ChainClientFactory, NameResolver } from "../chain/types.js";
4
+ import type { HeliaInstance } from "../transport/types.js";
5
+ import type { RuleRegistry } from "../rules/types.js";
6
+ import type { ContestTally } from "../tally/types.js";
7
+ import type { VoteSigner } from "../signer/types.js";
8
+ /**
9
+ * The recommended cadence, in buckets, at which a client should re-publish a live vote to keep
10
+ * it alive: half its expiry window, rounded up. A bundle is valid for `voteExpiryBuckets` after
11
+ * its `blockNumber` (see DESIGN.md "Passive expiry"), so re-signing at the halfway point leaves
12
+ * one full missed cycle of slack before the vote decays. Derived per-contest from the criteria —
13
+ * there is no global interval.
14
+ *
15
+ * This library does NOT re-publish on its own: deciding when (or whether) to refresh a vote is
16
+ * the consuming client's job (see DESIGN.md "Republishing is the client's job"). This helper, the
17
+ * `blockNumber` on a published `VotesBundle`, and `criteria.voteExpiryBuckets` /
18
+ * `criteria.blocksPerBucket` are what a client uses to schedule its own refreshes: a vote sampled
19
+ * at bucket `b` expires once the current bucket exceeds `b + voteExpiryBuckets`; refresh by
20
+ * calling `createContestVote({ criteria, votes }).publish()` again before then.
21
+ */
22
+ export declare function republishIntervalBuckets(criteria: Criteria): number;
23
+ /**
24
+ * Public facade — three objects, mirroring pkc-js / plebbit-js:
25
+ * - {@link PubsubVoter} (`VoteClient`): the factory. Holds the host-injected dependencies once
26
+ * and owns one engine per contest, keyed by topic. A contest is addressed by its full criteria
27
+ * document — `createContest({ criteria })` validates it and derives the topic — so a directory
28
+ * host like 5chan authors its 63 documents however it likes (e.g. merged from a local
29
+ * manifest) and creates each contest, without wiring dependencies 63 times.
30
+ * - {@link Contest} (`createContest`): one contest's reactive **read** view. `update()` starts
31
+ * syncing and emits `update` (carrying a fresh `tally`) / `error`, like `subplebbit.update()`.
32
+ * - {@link ContestVote} (`createContestVote`): one publishable **ballot**. `publish()` signs and
33
+ * broadcasts it once, emitting `publishingstatechange` / `error`, like a plebbit publication.
34
+ *
35
+ * The injected seams (helia, chains, signer, nameResolvers) are the ONLY host contact surface, so
36
+ * the same core runs under pkc-js, plebbit, or a raw node. The host passes its running Helia node
37
+ * directly — no adapter — and the library drives that node's gossipsub service and blockstore
38
+ * itself. A voter built without a `signer` is read-only (tallies readable, publishing disabled).
39
+ *
40
+ * Republishing a live vote so it does not decay is deliberately NOT this library's job — it
41
+ * publishes each vote once and the client decides when to refresh (see
42
+ * {@link republishIntervalBuckets} and DESIGN.md "Republishing is the client's job").
43
+ */
44
+ /** A vote publication's lifecycle, walked by {@link ContestVote.publish}. */
45
+ export type PublishingState = "stopped" | "signing" | "publishing" | "succeeded" | "failed";
46
+ /** What {@link ContestVote.publish} resolves: the signed bundle plus a peer-reach hint. */
47
+ export interface PublishOutcome {
48
+ /** The signed bundle; its `blockNumber` drives the client's own refresh schedule. */
49
+ readonly bundle: VotesBundle;
50
+ /**
51
+ * How many peers gossipsub sent this vote *directly* to — first-hop fan-out, not total network
52
+ * reach and not an acceptance confirmation (each recipient still runs the forward-gate before
53
+ * re-forwarding). Handy as a coarse "did this reach anyone?" signal: `0` means the message went
54
+ * nowhere. Note gossipsub instead *rejects* the publish with `NoPeersSubscribedToTopic` when it
55
+ * would reach zero peers, unless the host enables `allowPublishToZeroTopicPeers` — so a
56
+ * resolved `recipientCount === 0` only happens under that host setting.
57
+ */
58
+ readonly recipientCount: number;
59
+ }
60
+ /** One contest's reactive read view: subscribe, keep the tally in sync, read it. */
61
+ export interface Contest {
62
+ /** The criteria document this contest runs (already validated). */
63
+ readonly criteria: Criteria;
64
+ /** The gossipsub topic = "bitsocial-votes/" + CID(dag-cbor(criteria)). */
65
+ readonly topic: string;
66
+ /**
67
+ * The last computed tally, refreshed before each `update` event; `undefined` until the first
68
+ * compute (i.e. before `update()` resolves). For a forced fresh read use {@link getTally}.
69
+ */
70
+ readonly tally: ContestTally | undefined;
71
+ /**
72
+ * Join the topic behind the validate-before-forward gate, pull the cold-start checkpoint, and
73
+ * begin emitting `update` events as incoming votes change the state. Resolves once the initial
74
+ * tally is computed (so {@link tally} is populated). Idempotent.
75
+ */
76
+ update(): Promise<void>;
77
+ /** Stop syncing and leave the topic. Does not stop the host node. Idempotent. */
78
+ stop(): Promise<void>;
79
+ /** Compute the current contest ranking fresh, bypassing the cache. */
80
+ getTally(): Promise<ContestTally>;
81
+ /**
82
+ * Fired when incoming votes change the state; `tally` carries the freshly recomputed
83
+ * ranking. Background check settlements fire it too: a cold join emits a first tally with
84
+ * `chainVerified: false` rows immediately, then re-emits as the batched gate reads and name
85
+ * resolutions land (see DESIGN.md "Background chain verification").
86
+ */
87
+ on(event: "update", cb: () => void): void;
88
+ /**
89
+ * Fired on a contest-level failure: the tally's chain read throws, or the background chain
90
+ * verifier hits an infra-class failure (RPC/resolver down — its bundles stay pending and
91
+ * retry, but the degradation is surfaced here instead of silently stalling).
92
+ */
93
+ on(event: "error", cb: (error: unknown) => void): void;
94
+ }
95
+ /** One publishable ballot for a contest. Create it, then `publish()`. */
96
+ export interface ContestVote {
97
+ /** The `contestId` this ballot targets. */
98
+ readonly contestId: string;
99
+ /** The gossipsub topic the bundle publishes to. */
100
+ readonly topic: string;
101
+ /** The votes this ballot will sign and broadcast (empty array = a withdrawal). */
102
+ readonly votes: readonly Vote[];
103
+ /** Where in the publish lifecycle this ballot is. */
104
+ readonly publishingState: PublishingState;
105
+ /** The signed bundle, once `publish()` has produced it (`undefined` before then). */
106
+ readonly bundle: VotesBundle | undefined;
107
+ /**
108
+ * Sign the votes into a bundle for the current bucket, add it to the local CRDT, and broadcast
109
+ * it once as a live delta. Joins the topic first if needed. Resolves a {@link PublishOutcome}:
110
+ * the `VotesBundle` (whose `blockNumber` the client uses to schedule its own refresh — see
111
+ * {@link republishIntervalBuckets}) plus `recipientCount`, the number of peers gossipsub sent
112
+ * the vote directly to. Emits `publishingstatechange` as it goes; throws (and emits `error`) on
113
+ * failure, and `ReadOnlyError` with no signer. This library does not re-publish: to keep the
114
+ * vote alive, call `publish()` again before it expires.
115
+ */
116
+ publish(): Promise<PublishOutcome>;
117
+ /** Fired on each `publishingState` transition. */
118
+ on(event: "publishingstatechange", cb: (state: PublishingState) => void): void;
119
+ /** Fired if publishing fails. */
120
+ on(event: "error", cb: (error: unknown) => void): void;
121
+ }
122
+ /** The factory: one set of injected dependencies, many contests. */
123
+ export interface VoteClient {
124
+ /** True when constructed without a signer: every contest is read-only. */
125
+ readonly readOnly: boolean;
126
+ /**
127
+ * Create the reactive read view for one contest from its full criteria document. The document
128
+ * is strictly validated (`CriteriaSchema` + the rule registry — an unimplemented rule throws
129
+ * `UnknownRuleError`) and its topic derived (`topic = CID(dag-cbor(criteria))`). Returns the
130
+ * stable per-topic object (one CRDT/engine per topic per node), so repeated calls with
131
+ * byte-identical criteria return the same `Contest`.
132
+ */
133
+ createContest(args: {
134
+ criteria: Criteria;
135
+ }): Promise<Contest>;
136
+ /**
137
+ * Create a publishable ballot for one contest, validated and addressed exactly like
138
+ * `createContest`. Each call returns a fresh `ContestVote` over the shared per-topic engine;
139
+ * pass `votes: []` to build a withdrawal.
140
+ */
141
+ createContestVote(args: {
142
+ criteria: Criteria;
143
+ votes: Vote[];
144
+ }): Promise<ContestVote>;
145
+ /**
146
+ * Leave every topic this client joined, resetting each read view so it can `update()` again.
147
+ * The checkpoint fetch responder unregisters itself once the last topic is left (see
148
+ * `PubsubVoter`'s lazy responder lifecycle). The client stays fully reusable — call
149
+ * `createContest` / `update()` after.
150
+ */
151
+ stop(): Promise<void>;
152
+ /**
153
+ * Terminal teardown (mirrors pkc-js `destroy`): leave every topic, unregister the fetch
154
+ * responder, and mark the voter and all its contests destroyed. Unlike `stop`, this is NOT
155
+ * reusable — every subsequent `createContest` / `createContestVote`, and any pre-existing
156
+ * `Contest.update()` / `ContestVote.publish()`, throws `VoterDestroyedError`. Construct a new
157
+ * `PubsubVoter` to participate again. There is no store to dispose (republishing / persistence
158
+ * is the client's concern).
159
+ */
160
+ destroy(): Promise<void>;
161
+ }
162
+ /** Construction options for {@link PubsubVoter}. */
163
+ export interface PubsubVoterOptions {
164
+ /**
165
+ * The host's running Helia node (the value `createHelia` returns; for pkc-js it is
166
+ * `pkc.clients.libp2pJsClients[key]._helia`). The only host-node seam, passed
167
+ * directly with no adapter. It MUST carry a gossipsub service at
168
+ * `libp2p.services.pubsub`, a usable `blockstore`, and a libp2p fetch service at
169
+ * `libp2p.services.fetch`; the constructor throws `MissingPubsubError` /
170
+ * `MissingBlockstoreError` / `MissingFetchError` otherwise (a plain Helia node has
171
+ * no pubsub). The library never starts or stops this node.
172
+ */
173
+ helia: HeliaInstance;
174
+ /**
175
+ * Builds a chain client from a `{ chain, config }` pair. Each contest builds its
176
+ * own clients from `criteria.requires.chains` via this factory, so chains are
177
+ * per-contest data, not a global injection.
178
+ */
179
+ chains: ChainClientFactory;
180
+ /** Identity. Omit for a read-only voter (renders tallies, cannot publish). */
181
+ signer?: VoteSigner;
182
+ /** Rule overrides that shadow built-ins by `type` (a flat `type -> rule` map). */
183
+ rules?: RuleRegistry;
184
+ /**
185
+ * Community-name resolvers, same instances a host gives pkc-js (e.g.
186
+ * `@bitsocial/bso-resolver` for `name.bso`). The tally resolves each vote's
187
+ * `community.name` claim through the first resolver whose `canResolve` matches and
188
+ * drops bundles whose name does not resolve to the claimed `publicKey`. Omit only
189
+ * if no contest's votes carry names — with no resolver for a carried name's TLD,
190
+ * the claim cannot be verified and the bundle is dropped (never counted unchecked).
191
+ */
192
+ nameResolvers?: NameResolver[];
193
+ /**
194
+ * Directory for the voter's persistent caches (gate results, name resolutions), the
195
+ * pkc-js `dataPath` equivalent. On Node the caches are better-sqlite3 databases under
196
+ * `{dataPath}/lru-storage/`; in the browser the path is ignored and the caches live in
197
+ * IndexedDB (via localforage) either way. Defaults to `{cwd}/.bitsocial-pubsub-votes`
198
+ * on Node. Pass `false` for in-memory-only caches (no disk, no IndexedDB — the pkc-js
199
+ * `noData` equivalent): nothing survives the process, but nothing is written either.
200
+ */
201
+ dataPath?: string | false;
202
+ }
203
+ /**
204
+ * The default `VoteClient`. Construct with the host-injected seams: a `helia` node (must carry a
205
+ * gossipsub service, a blockstore, and a libp2p fetch service), a `chains` factory, an optional
206
+ * `signer`, and optional `nameResolvers` (needed once votes carry community names). Contests are
207
+ * addressed by their full criteria document at `createContest` / `createContestVote`; the library
208
+ * has no knowledge of pkc-js or any other host: a host passes its own running Helia node in
209
+ * directly. Republishing a live vote is the client's concern — see
210
+ * {@link republishIntervalBuckets} and DESIGN.md "Republishing is the client's job".
211
+ */
212
+ export declare class PubsubVoter implements VoteClient {
213
+ #private;
214
+ constructor(options: PubsubVoterOptions);
215
+ get readOnly(): boolean;
216
+ createContest(args: {
217
+ criteria: Criteria;
218
+ }): Promise<Contest>;
219
+ createContestVote(args: {
220
+ criteria: Criteria;
221
+ votes: Vote[];
222
+ }): Promise<ContestVote>;
223
+ stop(): Promise<void>;
224
+ destroy(): Promise<void>;
225
+ }