@bitsocial/pubsub-voting 0.0.6

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 (119) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +223 -0
  3. package/dist/chain/bucket.d.ts +13 -0
  4. package/dist/chain/bucket.js +24 -0
  5. package/dist/chain/coalescer.d.ts +56 -0
  6. package/dist/chain/coalescer.js +217 -0
  7. package/dist/chain/ticker.d.ts +15 -0
  8. package/dist/chain/ticker.js +25 -0
  9. package/dist/chain/types.d.ts +90 -0
  10. package/dist/chain/types.js +1 -0
  11. package/dist/checkpoint/codec.d.ts +54 -0
  12. package/dist/checkpoint/codec.js +99 -0
  13. package/dist/client/root-puller.d.ts +49 -0
  14. package/dist/client/root-puller.js +140 -0
  15. package/dist/client/voter.d.ts +239 -0
  16. package/dist/client/voter.js +1343 -0
  17. package/dist/crdt/codec.d.ts +41 -0
  18. package/dist/crdt/codec.js +137 -0
  19. package/dist/crdt/crdt.d.ts +22 -0
  20. package/dist/crdt/crdt.js +127 -0
  21. package/dist/crdt/store.d.ts +8 -0
  22. package/dist/crdt/store.js +23 -0
  23. package/dist/crdt/types.d.ts +87 -0
  24. package/dist/crdt/types.js +1 -0
  25. package/dist/encoding/canonical.d.ts +22 -0
  26. package/dist/encoding/canonical.js +26 -0
  27. package/dist/errors.d.ts +81 -0
  28. package/dist/errors.js +126 -0
  29. package/dist/index.d.ts +32 -0
  30. package/dist/index.js +43 -0
  31. package/dist/rules/constant.d.ts +14 -0
  32. package/dist/rules/constant.js +18 -0
  33. package/dist/rules/erc20-balance.d.ts +30 -0
  34. package/dist/rules/erc20-balance.js +44 -0
  35. package/dist/rules/erc721-min-balance.d.ts +18 -0
  36. package/dist/rules/erc721-min-balance.js +100 -0
  37. package/dist/rules/registry.d.ts +42 -0
  38. package/dist/rules/registry.js +61 -0
  39. package/dist/rules/types.d.ts +74 -0
  40. package/dist/rules/types.js +1 -0
  41. package/dist/schema/common.d.ts +25 -0
  42. package/dist/schema/common.js +24 -0
  43. package/dist/schema/criteria.d.ts +78 -0
  44. package/dist/schema/criteria.js +76 -0
  45. package/dist/schema/directory.d.ts +42 -0
  46. package/dist/schema/directory.js +52 -0
  47. package/dist/schema/votes.d.ts +55 -0
  48. package/dist/schema/votes.js +116 -0
  49. package/dist/signer/eip712.d.ts +103 -0
  50. package/dist/signer/eip712.js +85 -0
  51. package/dist/signer/types.d.ts +31 -0
  52. package/dist/signer/types.js +1 -0
  53. package/dist/storage/browser.d.ts +3 -0
  54. package/dist/storage/browser.js +110 -0
  55. package/dist/storage/memory.d.ts +10 -0
  56. package/dist/storage/memory.js +56 -0
  57. package/dist/storage/node.d.ts +5 -0
  58. package/dist/storage/node.js +106 -0
  59. package/dist/storage/types.d.ts +46 -0
  60. package/dist/storage/types.js +1 -0
  61. package/dist/store/indexeddb.d.ts +9 -0
  62. package/dist/store/indexeddb.js +72 -0
  63. package/dist/store/memory.d.ts +15 -0
  64. package/dist/store/memory.js +22 -0
  65. package/dist/store/select.d.ts +15 -0
  66. package/dist/store/select.js +64 -0
  67. package/dist/store/sqlite.d.ts +11 -0
  68. package/dist/store/sqlite.js +68 -0
  69. package/dist/store/types.d.ts +57 -0
  70. package/dist/store/types.js +1 -0
  71. package/dist/tally/tally.d.ts +44 -0
  72. package/dist/tally/tally.js +89 -0
  73. package/dist/tally/types.d.ts +51 -0
  74. package/dist/tally/types.js +13 -0
  75. package/dist/topic.d.ts +20 -0
  76. package/dist/topic.js +28 -0
  77. package/dist/transport/accepted-dedup.d.ts +30 -0
  78. package/dist/transport/accepted-dedup.js +34 -0
  79. package/dist/transport/announce/browser.d.ts +9 -0
  80. package/dist/transport/announce/browser.js +14 -0
  81. package/dist/transport/announce/node.d.ts +38 -0
  82. package/dist/transport/announce/node.js +162 -0
  83. package/dist/transport/announce/types.d.ts +74 -0
  84. package/dist/transport/announce/types.js +16 -0
  85. package/dist/transport/bundle-store.d.ts +11 -0
  86. package/dist/transport/bundle-store.js +34 -0
  87. package/dist/transport/chase.d.ts +125 -0
  88. package/dist/transport/chase.js +182 -0
  89. package/dist/transport/gossip-validator.d.ts +107 -0
  90. package/dist/transport/gossip-validator.js +99 -0
  91. package/dist/transport/helia.d.ts +55 -0
  92. package/dist/transport/helia.js +114 -0
  93. package/dist/transport/integration/harness.d.ts +65 -0
  94. package/dist/transport/integration/harness.js +278 -0
  95. package/dist/transport/messages.d.ts +97 -0
  96. package/dist/transport/messages.js +117 -0
  97. package/dist/transport/rate-limit.d.ts +11 -0
  98. package/dist/transport/rate-limit.js +20 -0
  99. package/dist/transport/transport.d.ts +20 -0
  100. package/dist/transport/transport.js +35 -0
  101. package/dist/transport/types.d.ts +189 -0
  102. package/dist/transport/types.js +1 -0
  103. package/dist/verify/background.d.ts +81 -0
  104. package/dist/verify/background.js +236 -0
  105. package/dist/verify/bundle.d.ts +58 -0
  106. package/dist/verify/bundle.js +84 -0
  107. package/dist/verify/cache.d.ts +48 -0
  108. package/dist/verify/cache.js +62 -0
  109. package/dist/verify/constraints.d.ts +16 -0
  110. package/dist/verify/constraints.js +35 -0
  111. package/dist/verify/gate-result-cache.d.ts +65 -0
  112. package/dist/verify/gate-result-cache.js +91 -0
  113. package/dist/verify/name-resolution-cache.d.ts +59 -0
  114. package/dist/verify/name-resolution-cache.js +64 -0
  115. package/dist/verify/signature.d.ts +9 -0
  116. package/dist/verify/signature.js +55 -0
  117. package/dist/verify/types.d.ts +101 -0
  118. package/dist/verify/types.js +1 -0
  119. package/package.json +78 -0
@@ -0,0 +1,239 @@
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
+ * Delegated Routing V1 router base URLs to ANNOUNCE provider records to (one unsigned
195
+ * `PUT /routing/v1/providers` per router, `Keys` batched across all joined contests:
196
+ * each contest's criteria CID + current checkpoint root + chunk CIDs — hourly, debounced
197
+ * on checkpoint/root changes, and on `self:peer:update` address changes). Absent or empty
198
+ * means never announce — the default, correct for plain clients: only a publicly dialable
199
+ * node (a seeder) should set this, and the browser build never announces regardless (the
200
+ * announcer is a Node-only module, stubbed inert by the package.json `browser` remap).
201
+ * QUERYING needs no URLs here — `#discoverProviders` rides the injected node's
202
+ * `libp2p.contentRouting`, which the host wires its routers into; this option exists only
203
+ * because the pinned js stack has no working announce path (`provide()` is a noop). See
204
+ * DESIGN.md "Deferred pkc-js work", provider-record announces.
205
+ */
206
+ httpRouterUrls?: string[];
207
+ /**
208
+ * Directory for the voter's persistent caches (gate results, name resolutions), the
209
+ * pkc-js `dataPath` equivalent. On Node the caches are better-sqlite3 databases under
210
+ * `{dataPath}/lru-storage/`; in the browser the path is ignored and the caches live in
211
+ * IndexedDB (via localforage) either way. Defaults to `{cwd}/.bitsocial-pubsub-voting`
212
+ * on Node. Pass `false` for in-memory-only caches (no disk, no IndexedDB — the pkc-js
213
+ * `noData` equivalent): nothing survives the process, but nothing is written either.
214
+ */
215
+ dataPath?: string | false;
216
+ }
217
+ /**
218
+ * The default `VoteClient`. Construct with the host-injected seams: a `helia` node (must carry a
219
+ * gossipsub service, a blockstore, and a libp2p fetch service), a `chains` factory, an optional
220
+ * `signer`, and optional `nameResolvers` (needed once votes carry community names). Contests are
221
+ * addressed by their full criteria document at `createContest` / `createContestVote`; the library
222
+ * has no knowledge of pkc-js or any other host: a host passes its own running Helia node in
223
+ * directly. Republishing a live vote is the client's concern — see
224
+ * {@link republishIntervalBuckets} and DESIGN.md "Republishing is the client's job".
225
+ */
226
+ export declare class PubsubVoter implements VoteClient {
227
+ #private;
228
+ constructor(options: PubsubVoterOptions);
229
+ get readOnly(): boolean;
230
+ createContest(args: {
231
+ criteria: Criteria;
232
+ }): Promise<Contest>;
233
+ createContestVote(args: {
234
+ criteria: Criteria;
235
+ votes: Vote[];
236
+ }): Promise<ContestVote>;
237
+ stop(): Promise<void>;
238
+ destroy(): Promise<void>;
239
+ }