@bitsocial/pubsub-voting 0.7.5 → 0.7.7

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/README.md CHANGED
@@ -34,6 +34,18 @@ See [DESIGN.md](./DESIGN.md) for the full rationale, including how this resists
34
34
 
35
35
  ## Usage
36
36
 
37
+ Hosts with a dedicated votes blockstore can use `voter.retainsBlock(cid)` during garbage
38
+ collection. This synchronous, network-free check protects every joined contest's admitted
39
+ bundles (including provisional votes and verified fallback winners) and its last encoded
40
+ checkpoint root/chunks. It compares multihashes, so filesystem stores that enumerate CIDs
41
+ with another codec work too. Recheck immediately before deletion under your blockstore's
42
+ operation lock; also protect concurrent reads/writes and retain superseded blocks for a
43
+ grace period so peers can finish pulling older checkpoints. For a shared store, retain
44
+ other applications' blocks too. The method does not run GC, refresh checkpoints, or impose
45
+ a storage quota. Persisted checkpoint snapshots are self-contained and must be retained
46
+ independently of the blockstore.
47
+
48
+
37
49
  The library never starts a node and never takes a host SDK (there is no `pkc` argument). A host passes its own running Helia node in directly and injects its seams into a single `PubsubVoter`. **Identity is not one of the seams**: the voting wallet (`VoteSigner`) belongs to each ballot, passed to `createContestVote`, so one voter on the host's shared node publishes for as many wallets as the host holds keys for — and a client that only renders tallies never touches key material.
38
50
 
39
51
  | Seam | Type | Required | Purpose |
@@ -348,6 +348,15 @@ export interface VoteClient {
348
348
  * `createContest` / `update()` after.
349
349
  */
350
350
  stop(): Promise<void>;
351
+ /**
352
+ * Synchronous GC retention check for joined contests: admitted bundles (including
353
+ * pending verification and fallback winners) and the last encoded checkpoint.
354
+ * Compares multihashes, since filesystem stores may enumerate another CID codec.
355
+ * Recheck immediately before deletion under the host's blockstore lock; protect
356
+ * in-flight reads/writes, other users of a shared store, and a grace period for
357
+ * superseded checkpoints. No I/O and no permission to delete a shared store's data.
358
+ */
359
+ retainsBlock(cid: CID): boolean;
351
360
  /**
352
361
  * Terminal teardown (mirrors pkc-js `destroy`): leave every topic, unregister the fetch
353
362
  * responder, and mark the voter and all its contests destroyed. Unlike `stop`, this is NOT
@@ -468,6 +477,7 @@ export declare class PubsubVoter implements VoteClient {
468
477
  votes: Vote[];
469
478
  signer: VoteSigner;
470
479
  }): Promise<ContestVote>;
480
+ retainsBlock(cid: CID): boolean;
471
481
  stop(): Promise<void>;
472
482
  destroy(): Promise<void>;
473
483
  }
@@ -20,7 +20,7 @@ import { makeVerdictCache } from "../verify/cache.js";
20
20
  import { makeNameResolutionCache } from "../verify/name-resolution-cache.js";
21
21
  import { makeStorage } from "../storage/node.js";
22
22
  import { makeAnnouncer } from "../transport/announce/node.js";
23
- import { encode as encodeDagCbor } from "@ipld/dag-cbor";
23
+ import { encode as encodeDagCbor, code as dagCborCode } from "@ipld/dag-cbor";
24
24
  import { sha256 } from "viem";
25
25
  import { makeBackgroundVerifier } from "../verify/background.js";
26
26
  import { preflightCommunityNames } from "../verify/name-preflight.js";
@@ -1110,6 +1110,10 @@ class ContestEngine {
1110
1110
  throw new VoterDestroyedError();
1111
1111
  if (this.#joined)
1112
1112
  return;
1113
+ // A host may have collected this stopped contest's old checkpoint blocks.
1114
+ // Re-encode/re-store them before serving the cached root on a re-join.
1115
+ this.#checkpointDirty = true;
1116
+ this.#rootRecordCache = undefined;
1113
1117
  const limit = pLimit(GATE_CONCURRENCY);
1114
1118
  const gate = makeGossipGate({
1115
1119
  decodeMessage: decodeVoteMessage,
@@ -1768,6 +1772,13 @@ class ContestEngine {
1768
1772
  latestCheckpointRoot() {
1769
1773
  return this.#rootRecordCache?.record.root;
1770
1774
  }
1775
+ /** Keep all admitted bundles, including pending checks and verified fallback winners. */
1776
+ retainsBlock(cid) {
1777
+ if (!this.#joined)
1778
+ return false;
1779
+ const key = CID.createV1(dagCborCode, cid.multihash).toString();
1780
+ return this.#checks.has(key) || (this.#rootRecordCache?.blocks.some((block) => block.cid.toString() === key) ?? false);
1781
+ }
1771
1782
  /**
1772
1783
  * The current root record plus its checkpoint chunk blocks, for the bulk responder's inline
1773
1784
  * path (see `BulkFetchRootRecord.chunkBlocks`): the blocks are already in hand from the same
@@ -2528,6 +2539,13 @@ export class PubsubVoter {
2528
2539
  this.#unregisterResponder();
2529
2540
  }
2530
2541
  };
2542
+ retainsBlock(cid) {
2543
+ for (const engine of this.#engines.values()) {
2544
+ if (engine.retainsBlock(cid))
2545
+ return true;
2546
+ }
2547
+ return false;
2548
+ }
2531
2549
  async stop() {
2532
2550
  // Reset each read view (detach its engine listeners, clear `#subscribed`) so it can
2533
2551
  // `update()` again — this is what keeps `stop()` reusable. Then leave any engine with no
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/pubsub-voting",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Trustless pubsub voting over a shared libp2p/Helia node.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -74,11 +74,11 @@
74
74
  "@libp2p/identify": "4.1.13",
75
75
  "@libp2p/tcp": "11.0.27",
76
76
  "@multiformats/multiaddr": "13.0.3",
77
- "@pkcprotocol/pkc-js": "0.0.95",
77
+ "@pkcprotocol/pkc-js": "0.0.96",
78
78
  "@release-it/conventional-changelog": "10.0.6",
79
79
  "@types/better-sqlite3": "7.6.13",
80
80
  "@vitest/coverage-v8": "4.1.11",
81
- "commitizen": "4.3.1",
81
+ "commitizen": "4.3.2",
82
82
  "cz-conventional-changelog": "3.3.0",
83
83
  "husky": "9.1.7",
84
84
  "libp2p": "3.3.9",
@@ -86,5 +86,16 @@
86
86
  "strip-json-comments": "5.0.3",
87
87
  "typescript": "5.9.3",
88
88
  "vitest": "4.1.11"
89
+ },
90
+ "overrides": {
91
+ "@pkcprotocol/pkc-js": {
92
+ "undici": "7.29.0",
93
+ "uuid": "13.0.1",
94
+ "ws": "8.21.3"
95
+ },
96
+ "protobufjs": "7.6.6",
97
+ "release-it": {
98
+ "undici": "6.28.0"
99
+ }
89
100
  }
90
101
  }