@serfab/cadre-core 0.10.0 → 0.11.0

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 (105) hide show
  1. package/README.md +255 -255
  2. package/dist/cadre-node.d.ts +203 -28
  3. package/dist/cadre-node.js +445 -84
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-cohort.d.ts +26 -0
  6. package/dist/control-cohort.js +26 -0
  7. package/dist/control-cohort.js.map +1 -1
  8. package/dist/control-database.d.ts +82 -117
  9. package/dist/control-database.js +217 -295
  10. package/dist/control-database.js.map +1 -1
  11. package/dist/control-formation-recorder.d.ts +8 -7
  12. package/dist/control-formation-recorder.js +8 -7
  13. package/dist/control-formation-recorder.js.map +1 -1
  14. package/dist/control-schema.d.ts +1 -1
  15. package/dist/control-schema.js +789 -774
  16. package/dist/control-schema.js.map +1 -1
  17. package/dist/control-write-retry.d.ts +15 -5
  18. package/dist/control-write-retry.js +19 -18
  19. package/dist/control-write-retry.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/peer-addr-book.d.ts +92 -0
  24. package/dist/peer-addr-book.js +220 -0
  25. package/dist/peer-addr-book.js.map +1 -0
  26. package/dist/seed-bootstrap.d.ts +7 -0
  27. package/dist/seed-bootstrap.js +15 -13
  28. package/dist/seed-bootstrap.js.map +1 -1
  29. package/dist/strand-cohort.d.ts +4 -37
  30. package/dist/strand-cohort.js +4 -15
  31. package/dist/strand-cohort.js.map +1 -1
  32. package/dist/strand-database.d.ts +11 -16
  33. package/dist/strand-database.js +19 -7
  34. package/dist/strand-database.js.map +1 -1
  35. package/dist/strand-formation-manager.d.ts +8 -8
  36. package/dist/strand-formation-manager.js +15 -18
  37. package/dist/strand-formation-manager.js.map +1 -1
  38. package/dist/strand-instance-manager.d.ts +52 -21
  39. package/dist/strand-instance-manager.js +100 -34
  40. package/dist/strand-instance-manager.js.map +1 -1
  41. package/dist/strand-membership-writer.d.ts +6 -4
  42. package/dist/strand-membership-writer.js +38 -36
  43. package/dist/strand-membership-writer.js.map +1 -1
  44. package/dist/types.d.ts +39 -31
  45. package/dist/types.js.map +1 -1
  46. package/package.json +8 -7
  47. package/src/arachnode-stub.ts +159 -159
  48. package/src/bootstrap-peer-store-file.ts +44 -44
  49. package/src/bootstrap-peer-store.ts +172 -172
  50. package/src/cadre-node.ts +5158 -4771
  51. package/src/canonical-json.ts +30 -30
  52. package/src/control-authorization.ts +114 -114
  53. package/src/control-cohort.ts +130 -103
  54. package/src/control-database.ts +2406 -2506
  55. package/src/control-formation-recorder.ts +305 -304
  56. package/src/control-schema.ts +801 -786
  57. package/src/control-stream.ts +205 -205
  58. package/src/control-write-retry.ts +483 -473
  59. package/src/delegate-admission.ts +302 -302
  60. package/src/device-token.ts +94 -94
  61. package/src/diagnostics/connection-path.ts +277 -277
  62. package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
  63. package/src/ed25519-key.ts +134 -134
  64. package/src/enrollment.ts +199 -199
  65. package/src/file-durable-slot.ts +61 -61
  66. package/src/formation-approval.ts +604 -604
  67. package/src/fs-atomic.ts +107 -107
  68. package/src/hibernation-manager.ts +437 -437
  69. package/src/identity-key.ts +109 -109
  70. package/src/index.ts +1 -0
  71. package/src/key-store-file.ts +132 -132
  72. package/src/key-store.ts +99 -99
  73. package/src/membership-connection-gater.ts +204 -204
  74. package/src/node-local-snapshot.ts +260 -260
  75. package/src/peer-addr-book.ts +250 -0
  76. package/src/peer-authorization.ts +259 -259
  77. package/src/peer-record.ts +164 -164
  78. package/src/push-credentials.ts +84 -84
  79. package/src/push-node.ts +64 -64
  80. package/src/push-notifier-apns.ts +247 -247
  81. package/src/push-notifier-fcm.ts +220 -220
  82. package/src/push-notifier-shared.ts +28 -28
  83. package/src/push-notifier.ts +52 -52
  84. package/src/relay-addrs.ts +111 -111
  85. package/src/relay-reservation.ts +823 -823
  86. package/src/seed-bootstrap.ts +15 -15
  87. package/src/seed-trust-policy.ts +140 -140
  88. package/src/strand-addr-protocol.ts +441 -441
  89. package/src/strand-backfill.ts +366 -366
  90. package/src/strand-cohort.ts +38 -77
  91. package/src/strand-database.ts +202 -204
  92. package/src/strand-formation-manager.ts +491 -494
  93. package/src/strand-formation-protocol.ts +692 -692
  94. package/src/strand-instance-manager.ts +108 -50
  95. package/src/strand-member-key.ts +39 -39
  96. package/src/strand-member-registry.ts +182 -182
  97. package/src/strand-membership-writer.ts +1432 -1430
  98. package/src/strand-solicitation.ts +472 -472
  99. package/src/strand-transport-key.ts +58 -58
  100. package/src/strand-wake-payload.ts +32 -32
  101. package/src/strand-wake-protocol.ts +314 -314
  102. package/src/strand-watcher.ts +314 -314
  103. package/src/trusted-owner-store-file.ts +41 -41
  104. package/src/trusted-owner-store.ts +171 -171
  105. package/src/types.ts +1309 -1303
package/dist/types.d.ts CHANGED
@@ -82,8 +82,28 @@ export interface HibernationTimeouts {
82
82
  */
83
83
  export declare const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts>;
84
84
  /**
85
- * Storage provider type - either an IRawStorage instance or a factory function.
85
+ * Storage provider - either an `IRawStorage` instance or a factory function.
86
86
  * Factory functions are useful for creating strand-specific storage instances.
87
+ *
88
+ * **Called once per scope per runtime lifetime.** The scopes are the literal string
89
+ * `'control'` (once per `CadreNode.start()`) and each strand id (once per
90
+ * `startStrand`). Hibernation — `quiesceStrand` then `resumeStrand` — reuses the
91
+ * store already resolved for that strand and does NOT re-enter this callback.
92
+ *
93
+ * It IS re-entered for a scope after that scope's runtime has stopped
94
+ * (`stopStrand`, or a `stop()` then `start()` cycle on one `CadreNode`), so it must
95
+ * be able to hand back a store over the same durable backend a second time. A
96
+ * factory may mint a fresh object per call; it must not *need* to.
97
+ *
98
+ * cadre-core disposes only its own cache wrapper — it never closes the store you
99
+ * returned. Closing the underlying handle stays the embedder's job.
100
+ *
101
+ * **Prefer the factory form for any persistent backend.** One instance handed to
102
+ * every scope also shares ONE cache wrapper (`wrapStorageWithCache` memoizes per
103
+ * inner instance), and no scope solely owns it — so the first scope to stop retires
104
+ * the wrapper the still-running scopes are reading through. See
105
+ * `tickets/backlog/debt-storage-cache-wrap-unrefcounted.md`; an in-memory store is
106
+ * unaffected, since it is never wrapped.
87
107
  */
88
108
  export type RawStorageProvider = IRawStorage | ((strandId: string) => IRawStorage);
89
109
  /**
@@ -92,6 +112,7 @@ export type RawStorageProvider = IRawStorage | ((strandId: string) => IRawStorag
92
112
  export interface StorageConfig {
93
113
  /**
94
114
  * Storage provider - either an IRawStorage instance or a factory function.
115
+ * See {@link RawStorageProvider} for how often cadre-core calls it.
95
116
  *
96
117
  * For Node.js environments, use FileRawStorage from @optimystic/db-p2p-storage-fs:
97
118
  * ```typescript
@@ -212,9 +233,10 @@ export interface NetworkConfig {
212
233
  * Tuning for the proactive control-cohort dial routine
213
234
  * ({@link CadreNode.reconcileControlCohort}), which keeps a party's control
214
235
  * nodes connected so the `CadreControl` collections form a replicating cohort.
215
- * Both fields are optional; omit for the defaults
236
+ * Every field is optional; omit for the defaults
216
237
  * ({@link DEFAULT_CONTROL_COHORT_TARGET_DEGREE} /
217
- * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS}).
238
+ * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS} /
239
+ * {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} / `STRAND_PEER_ADDR_REFRESH_MS`).
218
240
  */
219
241
  controlCohort?: {
220
242
  /**
@@ -228,6 +250,20 @@ export interface NetworkConfig {
228
250
  * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS}.
229
251
  */
230
252
  reconcileMs?: number;
253
+ /**
254
+ * Budget for ONE sibling's proactive dial, in ms. Bounds a pass at
255
+ * (dialed siblings) × this, instead of leaving it to each address's own
256
+ * libp2p attempt timeout multiplied by address fan-out and relay hops.
257
+ * Defaults to {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS}.
258
+ */
259
+ dialTimeoutMs?: number;
260
+ /**
261
+ * How often each running strand re-resolves its siblings' strand-network
262
+ * addresses into its own libp2p address book — a step of the reconcile pass,
263
+ * on its own much longer throttle. Defaults to `STRAND_PEER_ADDR_REFRESH_MS`
264
+ * (10 min); must stay well under the peerStore's one-hour address expiry.
265
+ */
266
+ strandAddrRefreshMs?: number;
231
267
  };
232
268
  }
233
269
  /**
@@ -475,13 +511,6 @@ export interface StrandInstance {
475
511
  strandId: string;
476
512
  status: StrandStatus;
477
513
  sAppInfo?: SAppInfo;
478
- /**
479
- * The lifecycle mode this instance's runtime was built with — `bootstrap`
480
- * (purely local transactor) or `networked` (cluster transactor). Refreshed on
481
- * every runtime (re)build, so a resume that shifts `bootstrap → networked`
482
- * updates it. Read-only reporting; drives no behavior.
483
- */
484
- mode: StrandMode;
485
514
  /** The libp2p node for this strand (only when active/idle) */
486
515
  libp2pNode?: Libp2p;
487
516
  /** The Quereus database for this strand (only when active/idle) */
@@ -549,20 +578,6 @@ export interface SAppConfig {
549
578
  /** Latency hint for hibernation behavior (optional, defaults to config) */
550
579
  latencyHint?: LatencyHint;
551
580
  }
552
- /**
553
- * Lifecycle mode for a strand.
554
- *
555
- * - `bootstrap`: newly created locally; no remote peers exist for this strand yet.
556
- * Schema apply and data operations route through a local (non-network) transactor
557
- * so a solo node can fully initialize without any peer round trips.
558
- * - `networked`: strand has (or is expected to have) remote peers. Operations route
559
- * through the network transactor.
560
- *
561
- * Transition `bootstrap -> networked` happens at first remote-peer enrollment for
562
- * the strand; because the transactor is fixed at plugin registration time, the
563
- * transition requires stopping and restarting the strand.
564
- */
565
- export type StrandMode = 'bootstrap' | 'networked';
566
581
  /**
567
582
  * Full strand configuration when adding a strand via the API
568
583
  */
@@ -571,13 +586,6 @@ export interface StrandConfig {
571
586
  strandRow: StrandRow;
572
587
  /** sApp configuration provided by the hosting application */
573
588
  sAppConfig: SAppConfig;
574
- /**
575
- * Lifecycle mode for this strand instance. When omitted, CadreNode infers the
576
- * mode from cohort membership (`bootstrap` for a solo/first node with no other
577
- * `CadrePeer` rows, `networked` once the cohort has other members). An explicit
578
- * value always wins.
579
- */
580
- mode?: StrandMode;
581
589
  /**
582
590
  * Whether THIS node is the strand's founder — the party that provisioned and
583
591
  * published the strand (the responder in formation, the creator in host/solo
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA2EA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA6C;IAC5E,QAAQ,EAAE;QACR,WAAW,EAAE,QAAQ,EAAS,aAAa;QAC3C,gBAAgB,EAAE,QAAQ,EAAI,kBAAkB;QAChD,eAAe,EAAE,QAAQ,EAAK,MAAM;QACpC,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,QAAQ,CAAE,MAAM;KACrC;IACD,WAAW,EAAE;QACX,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,YAAY;QAC1C,gBAAgB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,wBAAwB;QAC1D,eAAe,EAAE,EAAE,GAAG,IAAI,EAAI,mBAAmB;QACjD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe;KACnD;IACD,UAAU,EAAE;QACV,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,WAAW;QACzC,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAG,uBAAuB;QACzD,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,kBAAkB;QACpD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,gBAAgB;KACxD;IACD,OAAO,EAAE;QACP,WAAW,EAAE,EAAE,GAAG,IAAI,EAAQ,aAAa;QAC3C,gBAAgB,EAAE,EAAE,GAAG,IAAI,EAAG,wBAAwB;QACtD,eAAe,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAG,eAAe;QACjD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe;KAC5D;CACF,CAAC;AA2KF;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,CAAC;AAYnD;;;;;;;;;;;GAWG;AACH,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA2EA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA6C;IAC5E,QAAQ,EAAE;QACR,WAAW,EAAE,QAAQ,EAAS,aAAa;QAC3C,gBAAgB,EAAE,QAAQ,EAAI,kBAAkB;QAChD,eAAe,EAAE,QAAQ,EAAK,MAAM;QACpC,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,QAAQ,CAAE,MAAM;KACrC;IACD,WAAW,EAAE;QACX,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,YAAY;QAC1C,gBAAgB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,wBAAwB;QAC1D,eAAe,EAAE,EAAE,GAAG,IAAI,EAAI,mBAAmB;QACjD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe;KACnD;IACD,UAAU,EAAE;QACV,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,WAAW;QACzC,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAG,uBAAuB;QACzD,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,kBAAkB;QACpD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,gBAAgB;KACxD;IACD,OAAO,EAAE;QACP,WAAW,EAAE,EAAE,GAAG,IAAI,EAAQ,aAAa;QAC3C,gBAAgB,EAAE,EAAE,GAAG,IAAI,EAAG,wBAAwB;QACtD,eAAe,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAG,eAAe;QACjD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe;KAC5D;CACF,CAAC;AA+MF;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,CAAC;AAYnD;;;;;;;;;;;GAWG;AACH,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,+BAA+B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serfab/cadre-core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Core library for Sereus cadre nodes - manages control networks and strand participation",
6
6
  "main": "dist/index.js",
@@ -67,6 +67,7 @@
67
67
  "@libp2p/tcp": "^11.0.10",
68
68
  "@libp2p/webrtc": "6.0.14",
69
69
  "@libp2p/websockets": "^10.1.3",
70
+ "@optimystic/db-p2p-storage-fs": "^0.24.0",
70
71
  "@types/debug": "^4.1.12",
71
72
  "@types/node": "^25.0.10",
72
73
  "@vitest/coverage-v8": "^4.0.17",
@@ -79,12 +80,12 @@
79
80
  "@libp2p/interface": "^3.1.0",
80
81
  "@libp2p/peer-id": "^6.0.4",
81
82
  "@multiformats/multiaddr": "^12.5.1",
82
- "@optimystic/db-core": "^0.22.0",
83
- "@optimystic/db-p2p": "^0.22.0",
84
- "@optimystic/quereus-plugin-crypto": "^0.22.0",
85
- "@optimystic/quereus-plugin-optimystic": "^0.22.0",
86
- "@quereus/quereus": "^4.10.0",
87
- "@serfab/quereus-plugin-sereus": "^0.10.0",
83
+ "@optimystic/db-core": "^0.24.0",
84
+ "@optimystic/db-p2p": "^0.24.0",
85
+ "@optimystic/quereus-plugin-crypto": "^0.24.0",
86
+ "@optimystic/quereus-plugin-optimystic": "^0.24.0",
87
+ "@quereus/quereus": "^4.14.0",
88
+ "@serfab/quereus-plugin-sereus": "^0.11.0",
88
89
  "debug": "^4.4.3",
89
90
  "libp2p": "^3.1.3",
90
91
  "uint8arrays": "^5.1.0"
@@ -1,159 +1,159 @@
1
- import debug from 'debug';
2
- import type { NodeProfile, ArachnodeConfig } from './types.js';
3
-
4
- const log = debug('sereus:cadre:arachnode');
5
-
6
- /**
7
- * Ring configuration for a storage node
8
- */
9
- export interface RingConfig {
10
- /** Ring number (0 = full keyspace, higher = more partitions) */
11
- ring: number;
12
- /** Partition within the ring (depends on ring level) */
13
- partition: number;
14
- /** Keyspace range this node is responsible for */
15
- keyspaceStart: Uint8Array;
16
- keyspaceEnd: Uint8Array;
17
- }
18
-
19
- /**
20
- * Stub implementation of Arachnode ring participation.
21
- *
22
- * Arachnode uses a concentric ring system where:
23
- * - Ring Zulu (transaction ring): All nodes participate for transaction verification
24
- * - Storage rings (0, 1, 2, 3...): Nodes join based on their storage capacity
25
- * - Ring 0: Full keyspace (requires most storage)
26
- * - Ring 1: 2 partitions
27
- * - Ring 2: 4 partitions
28
- * - Ring 3: 8 partitions
29
- * - etc.
30
- *
31
- * This is a stub that will be replaced when arachnode is fully implemented.
32
- */
33
- export class ArachnodeStub {
34
- private readonly profile: NodeProfile;
35
- private readonly config: ArachnodeConfig;
36
- private ringConfig?: RingConfig;
37
- private running = false;
38
-
39
- constructor(profile: NodeProfile, config: ArachnodeConfig) {
40
- this.profile = profile;
41
- this.config = config;
42
- log('ArachnodeStub created for profile=%s, config=%o', profile, config);
43
- }
44
-
45
- /**
46
- * Start participating in rings
47
- */
48
- async start(): Promise<void> {
49
- if (this.running) return;
50
- this.running = true;
51
-
52
- if (this.config.enableRingZulu) {
53
- log('Joining Ring Zulu (transaction ring)');
54
- // Stub: In real implementation, would register with transaction verification network
55
- }
56
-
57
- // Storage nodes join storage rings
58
- if (this.profile === 'storage' && this.config.storageRing) {
59
- const { ring, partition = 0 } = this.config.storageRing;
60
-
61
- log('Joining storage ring %d, partition %d', ring, partition);
62
-
63
- // Calculate keyspace range for this partition
64
- // Stub: Using placeholder keyspace calculation
65
- this.ringConfig = {
66
- ring,
67
- partition,
68
- keyspaceStart: this.calculateKeyspaceStart(ring, partition),
69
- keyspaceEnd: this.calculateKeyspaceEnd(ring, partition)
70
- };
71
-
72
- // Stub: In real implementation, would:
73
- // 1. Register with the storage ring
74
- // 2. Begin accepting block storage requests for our keyspace
75
- // 3. Participate in replication with other ring members
76
- }
77
-
78
- log('ArachnodeStub started');
79
- }
80
-
81
- /**
82
- * Stop participating in rings
83
- */
84
- async stop(): Promise<void> {
85
- if (!this.running) return;
86
- this.running = false;
87
-
88
- if (this.ringConfig) {
89
- log('Leaving storage ring %d, partition %d',
90
- this.ringConfig.ring, this.ringConfig.partition);
91
- // Stub: In real implementation, would gracefully leave the ring
92
- this.ringConfig = undefined;
93
- }
94
-
95
- if (this.config.enableRingZulu) {
96
- log('Leaving Ring Zulu');
97
- // Stub: In real implementation, would leave transaction ring
98
- }
99
-
100
- log('ArachnodeStub stopped');
101
- }
102
-
103
- /**
104
- * Get current ring configuration
105
- */
106
- getRingConfig(): RingConfig | undefined {
107
- return this.ringConfig;
108
- }
109
-
110
- /**
111
- * Check if participating in Ring Zulu
112
- */
113
- isInRingZulu(): boolean {
114
- return this.running && this.config.enableRingZulu;
115
- }
116
-
117
- /**
118
- * Check if participating in a storage ring
119
- */
120
- isInStorageRing(): boolean {
121
- return this.running && this.ringConfig !== undefined;
122
- }
123
-
124
- // Stub keyspace calculations
125
- private calculateKeyspaceStart(ring: number, partition: number): Uint8Array {
126
- // Stub: Real implementation would calculate based on ring partition scheme
127
- const numPartitions = Math.pow(2, ring);
128
- const partitionSize = 256 / numPartitions;
129
- const start = new Uint8Array(32);
130
- start[0] = Math.floor(partition * partitionSize);
131
- return start;
132
- }
133
-
134
- private calculateKeyspaceEnd(ring: number, partition: number): Uint8Array {
135
- // Stub: Real implementation would calculate based on ring partition scheme
136
- const numPartitions = Math.pow(2, ring);
137
- const partitionSize = 256 / numPartitions;
138
- const end = new Uint8Array(32);
139
- end[0] = Math.floor((partition + 1) * partitionSize) - 1;
140
- end.fill(0xFF, 1); // Fill rest with max values
141
- return end;
142
- }
143
- }
144
-
145
- /**
146
- * Create an arachnode instance for a strand
147
- */
148
- export function createArachnodeStub(
149
- profile: NodeProfile,
150
- config?: Partial<ArachnodeConfig>
151
- ): ArachnodeStub {
152
- const fullConfig: ArachnodeConfig = {
153
- enableRingZulu: config?.enableRingZulu ?? true,
154
- storageRing: profile === 'storage' ? (config?.storageRing ?? { ring: 0 }) : undefined
155
- };
156
-
157
- return new ArachnodeStub(profile, fullConfig);
158
- }
159
-
1
+ import debug from 'debug';
2
+ import type { NodeProfile, ArachnodeConfig } from './types.js';
3
+
4
+ const log = debug('sereus:cadre:arachnode');
5
+
6
+ /**
7
+ * Ring configuration for a storage node
8
+ */
9
+ export interface RingConfig {
10
+ /** Ring number (0 = full keyspace, higher = more partitions) */
11
+ ring: number;
12
+ /** Partition within the ring (depends on ring level) */
13
+ partition: number;
14
+ /** Keyspace range this node is responsible for */
15
+ keyspaceStart: Uint8Array;
16
+ keyspaceEnd: Uint8Array;
17
+ }
18
+
19
+ /**
20
+ * Stub implementation of Arachnode ring participation.
21
+ *
22
+ * Arachnode uses a concentric ring system where:
23
+ * - Ring Zulu (transaction ring): All nodes participate for transaction verification
24
+ * - Storage rings (0, 1, 2, 3...): Nodes join based on their storage capacity
25
+ * - Ring 0: Full keyspace (requires most storage)
26
+ * - Ring 1: 2 partitions
27
+ * - Ring 2: 4 partitions
28
+ * - Ring 3: 8 partitions
29
+ * - etc.
30
+ *
31
+ * This is a stub that will be replaced when arachnode is fully implemented.
32
+ */
33
+ export class ArachnodeStub {
34
+ private readonly profile: NodeProfile;
35
+ private readonly config: ArachnodeConfig;
36
+ private ringConfig?: RingConfig;
37
+ private running = false;
38
+
39
+ constructor(profile: NodeProfile, config: ArachnodeConfig) {
40
+ this.profile = profile;
41
+ this.config = config;
42
+ log('ArachnodeStub created for profile=%s, config=%o', profile, config);
43
+ }
44
+
45
+ /**
46
+ * Start participating in rings
47
+ */
48
+ async start(): Promise<void> {
49
+ if (this.running) return;
50
+ this.running = true;
51
+
52
+ if (this.config.enableRingZulu) {
53
+ log('Joining Ring Zulu (transaction ring)');
54
+ // Stub: In real implementation, would register with transaction verification network
55
+ }
56
+
57
+ // Storage nodes join storage rings
58
+ if (this.profile === 'storage' && this.config.storageRing) {
59
+ const { ring, partition = 0 } = this.config.storageRing;
60
+
61
+ log('Joining storage ring %d, partition %d', ring, partition);
62
+
63
+ // Calculate keyspace range for this partition
64
+ // Stub: Using placeholder keyspace calculation
65
+ this.ringConfig = {
66
+ ring,
67
+ partition,
68
+ keyspaceStart: this.calculateKeyspaceStart(ring, partition),
69
+ keyspaceEnd: this.calculateKeyspaceEnd(ring, partition)
70
+ };
71
+
72
+ // Stub: In real implementation, would:
73
+ // 1. Register with the storage ring
74
+ // 2. Begin accepting block storage requests for our keyspace
75
+ // 3. Participate in replication with other ring members
76
+ }
77
+
78
+ log('ArachnodeStub started');
79
+ }
80
+
81
+ /**
82
+ * Stop participating in rings
83
+ */
84
+ async stop(): Promise<void> {
85
+ if (!this.running) return;
86
+ this.running = false;
87
+
88
+ if (this.ringConfig) {
89
+ log('Leaving storage ring %d, partition %d',
90
+ this.ringConfig.ring, this.ringConfig.partition);
91
+ // Stub: In real implementation, would gracefully leave the ring
92
+ this.ringConfig = undefined;
93
+ }
94
+
95
+ if (this.config.enableRingZulu) {
96
+ log('Leaving Ring Zulu');
97
+ // Stub: In real implementation, would leave transaction ring
98
+ }
99
+
100
+ log('ArachnodeStub stopped');
101
+ }
102
+
103
+ /**
104
+ * Get current ring configuration
105
+ */
106
+ getRingConfig(): RingConfig | undefined {
107
+ return this.ringConfig;
108
+ }
109
+
110
+ /**
111
+ * Check if participating in Ring Zulu
112
+ */
113
+ isInRingZulu(): boolean {
114
+ return this.running && this.config.enableRingZulu;
115
+ }
116
+
117
+ /**
118
+ * Check if participating in a storage ring
119
+ */
120
+ isInStorageRing(): boolean {
121
+ return this.running && this.ringConfig !== undefined;
122
+ }
123
+
124
+ // Stub keyspace calculations
125
+ private calculateKeyspaceStart(ring: number, partition: number): Uint8Array {
126
+ // Stub: Real implementation would calculate based on ring partition scheme
127
+ const numPartitions = Math.pow(2, ring);
128
+ const partitionSize = 256 / numPartitions;
129
+ const start = new Uint8Array(32);
130
+ start[0] = Math.floor(partition * partitionSize);
131
+ return start;
132
+ }
133
+
134
+ private calculateKeyspaceEnd(ring: number, partition: number): Uint8Array {
135
+ // Stub: Real implementation would calculate based on ring partition scheme
136
+ const numPartitions = Math.pow(2, ring);
137
+ const partitionSize = 256 / numPartitions;
138
+ const end = new Uint8Array(32);
139
+ end[0] = Math.floor((partition + 1) * partitionSize) - 1;
140
+ end.fill(0xFF, 1); // Fill rest with max values
141
+ return end;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Create an arachnode instance for a strand
147
+ */
148
+ export function createArachnodeStub(
149
+ profile: NodeProfile,
150
+ config?: Partial<ArachnodeConfig>
151
+ ): ArachnodeStub {
152
+ const fullConfig: ArachnodeConfig = {
153
+ enableRingZulu: config?.enableRingZulu ?? true,
154
+ storageRing: profile === 'storage' ? (config?.storageRing ?? { ring: 0 }) : undefined
155
+ };
156
+
157
+ return new ArachnodeStub(profile, fullConfig);
158
+ }
159
+
@@ -1,44 +1,44 @@
1
- /**
2
- * Node-only file-backed {@link BootstrapPeerStore}: one JSON file per party under
3
- * a configured directory (the node's state directory — `cadre-cli` passes
4
- * `ResolvedConfig.nodeStateDir`), holding the node-local, non-replicated
5
- * cold-start dial targets a seed nominated.
6
- *
7
- * Nothing but the file itself lives here: the envelope, load policy, per-entry
8
- * validation (shape only — never signatures; see the `bootstrap-peer-store.ts`
9
- * module comment for why nothing persisted here is trust-bearing) and the
10
- * serialised snapshot writes are cross-platform in
11
- * `PersistentBootstrapPeerStore` / `node-local-snapshot.ts`, and the file is a
12
- * `FileDurableSlot`. This module exists only so the `node:fs` edge stays out of
13
- * the package's cross-platform default entry (`./index.js`) — import it from the
14
- * dedicated subpath instead:
15
- *
16
- * ```ts
17
- * import { FileBootstrapPeerStore } from '@serfab/cadre-core/bootstrap-peer-store-file';
18
- * ```
19
- *
20
- * A React Native / browser entry graph never resolves this path, so the
21
- * `node:fs` edge never reaches a bundler that cannot satisfy it (same isolation
22
- * pattern as `trusted-owner-store-file.ts` / `key-store-file.ts`).
23
- */
24
- import { FileDurableSlot } from './file-durable-slot.js';
25
- import { PersistentBootstrapPeerStore } from './bootstrap-peer-store.js';
26
- import type { BootstrapPeerStore } from './bootstrap-peer-store.js';
27
-
28
- /** Base name of the store file: `<dir>/bootstrap-peers.<encoded partyId>.json`. */
29
- const SLOT_NAME = 'bootstrap-peers';
30
-
31
- /**
32
- * File-backed {@link BootstrapPeerStore}. Open via {@link open}, which loads the
33
- * existing file and returns the cross-platform store over it; an absent, corrupt, or wrong-party file is a cold
34
- * start (empty store) and structurally junk entries are dropped, while a
35
- * present-but-unreadable file throws. Those rules and their reasoning live on
36
- * `NodeLocalSnapshot.open` — including the in-process-only write serialisation,
37
- * which two nodes sharing one directory for one party would defeat.
38
- */
39
- export const FileBootstrapPeerStore = {
40
- /** Load (or cold-start) the party's retained dial targets from a file in `dir`. */
41
- async open(dir: string, partyId: string): Promise<BootstrapPeerStore> {
42
- return PersistentBootstrapPeerStore.open(new FileDurableSlot(dir, SLOT_NAME, partyId), partyId);
43
- }
44
- };
1
+ /**
2
+ * Node-only file-backed {@link BootstrapPeerStore}: one JSON file per party under
3
+ * a configured directory (the node's state directory — `cadre-cli` passes
4
+ * `ResolvedConfig.nodeStateDir`), holding the node-local, non-replicated
5
+ * cold-start dial targets a seed nominated.
6
+ *
7
+ * Nothing but the file itself lives here: the envelope, load policy, per-entry
8
+ * validation (shape only — never signatures; see the `bootstrap-peer-store.ts`
9
+ * module comment for why nothing persisted here is trust-bearing) and the
10
+ * serialised snapshot writes are cross-platform in
11
+ * `PersistentBootstrapPeerStore` / `node-local-snapshot.ts`, and the file is a
12
+ * `FileDurableSlot`. This module exists only so the `node:fs` edge stays out of
13
+ * the package's cross-platform default entry (`./index.js`) — import it from the
14
+ * dedicated subpath instead:
15
+ *
16
+ * ```ts
17
+ * import { FileBootstrapPeerStore } from '@serfab/cadre-core/bootstrap-peer-store-file';
18
+ * ```
19
+ *
20
+ * A React Native / browser entry graph never resolves this path, so the
21
+ * `node:fs` edge never reaches a bundler that cannot satisfy it (same isolation
22
+ * pattern as `trusted-owner-store-file.ts` / `key-store-file.ts`).
23
+ */
24
+ import { FileDurableSlot } from './file-durable-slot.js';
25
+ import { PersistentBootstrapPeerStore } from './bootstrap-peer-store.js';
26
+ import type { BootstrapPeerStore } from './bootstrap-peer-store.js';
27
+
28
+ /** Base name of the store file: `<dir>/bootstrap-peers.<encoded partyId>.json`. */
29
+ const SLOT_NAME = 'bootstrap-peers';
30
+
31
+ /**
32
+ * File-backed {@link BootstrapPeerStore}. Open via {@link open}, which loads the
33
+ * existing file and returns the cross-platform store over it; an absent, corrupt, or wrong-party file is a cold
34
+ * start (empty store) and structurally junk entries are dropped, while a
35
+ * present-but-unreadable file throws. Those rules and their reasoning live on
36
+ * `NodeLocalSnapshot.open` — including the in-process-only write serialisation,
37
+ * which two nodes sharing one directory for one party would defeat.
38
+ */
39
+ export const FileBootstrapPeerStore = {
40
+ /** Load (or cold-start) the party's retained dial targets from a file in `dir`. */
41
+ async open(dir: string, partyId: string): Promise<BootstrapPeerStore> {
42
+ return PersistentBootstrapPeerStore.open(new FileDurableSlot(dir, SLOT_NAME, partyId), partyId);
43
+ }
44
+ };