@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.
- package/LICENSE +674 -0
- package/README.md +212 -0
- package/dist/chain/bucket.d.ts +13 -0
- package/dist/chain/bucket.js +24 -0
- package/dist/chain/ticker.d.ts +15 -0
- package/dist/chain/ticker.js +25 -0
- package/dist/chain/types.d.ts +90 -0
- package/dist/chain/types.js +1 -0
- package/dist/checkpoint/codec.d.ts +54 -0
- package/dist/checkpoint/codec.js +99 -0
- package/dist/client/root-puller.d.ts +49 -0
- package/dist/client/root-puller.js +140 -0
- package/dist/client/voter.d.ts +225 -0
- package/dist/client/voter.js +1195 -0
- package/dist/crdt/codec.d.ts +41 -0
- package/dist/crdt/codec.js +137 -0
- package/dist/crdt/crdt.d.ts +22 -0
- package/dist/crdt/crdt.js +127 -0
- package/dist/crdt/store.d.ts +8 -0
- package/dist/crdt/store.js +23 -0
- package/dist/crdt/types.d.ts +87 -0
- package/dist/crdt/types.js +1 -0
- package/dist/encoding/canonical.d.ts +22 -0
- package/dist/encoding/canonical.js +26 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.js +111 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +39 -0
- package/dist/rules/constant.d.ts +14 -0
- package/dist/rules/constant.js +18 -0
- package/dist/rules/erc20-balance.d.ts +30 -0
- package/dist/rules/erc20-balance.js +44 -0
- package/dist/rules/erc721-min-balance.d.ts +18 -0
- package/dist/rules/erc721-min-balance.js +56 -0
- package/dist/rules/registry.d.ts +42 -0
- package/dist/rules/registry.js +61 -0
- package/dist/rules/types.d.ts +74 -0
- package/dist/rules/types.js +1 -0
- package/dist/schema/common.d.ts +25 -0
- package/dist/schema/common.js +24 -0
- package/dist/schema/criteria.d.ts +78 -0
- package/dist/schema/criteria.js +76 -0
- package/dist/schema/directory.d.ts +42 -0
- package/dist/schema/directory.js +52 -0
- package/dist/schema/votes.d.ts +55 -0
- package/dist/schema/votes.js +116 -0
- package/dist/signer/eip712.d.ts +103 -0
- package/dist/signer/eip712.js +85 -0
- package/dist/signer/types.d.ts +31 -0
- package/dist/signer/types.js +1 -0
- package/dist/storage/browser.d.ts +3 -0
- package/dist/storage/browser.js +110 -0
- package/dist/storage/memory.d.ts +10 -0
- package/dist/storage/memory.js +56 -0
- package/dist/storage/node.d.ts +5 -0
- package/dist/storage/node.js +106 -0
- package/dist/storage/types.d.ts +46 -0
- package/dist/storage/types.js +1 -0
- package/dist/store/indexeddb.d.ts +9 -0
- package/dist/store/indexeddb.js +72 -0
- package/dist/store/memory.d.ts +15 -0
- package/dist/store/memory.js +22 -0
- package/dist/store/select.d.ts +15 -0
- package/dist/store/select.js +64 -0
- package/dist/store/sqlite.d.ts +11 -0
- package/dist/store/sqlite.js +68 -0
- package/dist/store/types.d.ts +57 -0
- package/dist/store/types.js +1 -0
- package/dist/tally/tally.d.ts +44 -0
- package/dist/tally/tally.js +89 -0
- package/dist/tally/types.d.ts +51 -0
- package/dist/tally/types.js +13 -0
- package/dist/topic.d.ts +20 -0
- package/dist/topic.js +28 -0
- package/dist/transport/accepted-dedup.d.ts +30 -0
- package/dist/transport/accepted-dedup.js +34 -0
- package/dist/transport/announce/browser.d.ts +9 -0
- package/dist/transport/announce/browser.js +14 -0
- package/dist/transport/announce/node.d.ts +38 -0
- package/dist/transport/announce/node.js +162 -0
- package/dist/transport/announce/types.d.ts +74 -0
- package/dist/transport/announce/types.js +16 -0
- package/dist/transport/bundle-store.d.ts +11 -0
- package/dist/transport/bundle-store.js +34 -0
- package/dist/transport/chase.d.ts +83 -0
- package/dist/transport/chase.js +88 -0
- package/dist/transport/gossip-validator.d.ts +107 -0
- package/dist/transport/gossip-validator.js +99 -0
- package/dist/transport/helia.d.ts +41 -0
- package/dist/transport/helia.js +98 -0
- package/dist/transport/integration/harness.d.ts +60 -0
- package/dist/transport/integration/harness.js +262 -0
- package/dist/transport/messages.d.ts +97 -0
- package/dist/transport/messages.js +117 -0
- package/dist/transport/rate-limit.d.ts +11 -0
- package/dist/transport/rate-limit.js +20 -0
- package/dist/transport/transport.d.ts +20 -0
- package/dist/transport/transport.js +35 -0
- package/dist/transport/types.d.ts +165 -0
- package/dist/transport/types.js +1 -0
- package/dist/verify/background.d.ts +81 -0
- package/dist/verify/background.js +236 -0
- package/dist/verify/bundle.d.ts +58 -0
- package/dist/verify/bundle.js +84 -0
- package/dist/verify/cache.d.ts +48 -0
- package/dist/verify/cache.js +62 -0
- package/dist/verify/constraints.d.ts +16 -0
- package/dist/verify/constraints.js +35 -0
- package/dist/verify/gate-result-cache.d.ts +65 -0
- package/dist/verify/gate-result-cache.js +91 -0
- package/dist/verify/name-resolution-cache.d.ts +59 -0
- package/dist/verify/name-resolution-cache.js +64 -0
- package/dist/verify/signature.d.ts +9 -0
- package/dist/verify/signature.js +55 -0
- package/dist/verify/types.d.ts +101 -0
- package/dist/verify/types.js +1 -0
- 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
|
+
}
|