@lazily-hub/lazily-js 0.18.0 → 0.20.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.
- package/BENCHMARKS.md +11 -11
- package/README.md +13 -3
- package/package.json +34 -2
- package/src/coordination.d.ts +81 -0
- package/src/coordination.js +283 -0
- package/src/membership.d.ts +52 -0
- package/src/membership.js +178 -0
- package/src/presence.d.ts +52 -0
- package/src/presence.js +154 -0
- package/src/rateshape.d.ts +79 -0
- package/src/rateshape.js +238 -0
- package/src/reactive.js +133 -92
- package/src/resilience.d.ts +67 -0
- package/src/resilience.js +231 -0
- package/src/service.d.ts +61 -0
- package/src/service.js +234 -0
- package/src/temporal.d.ts +85 -0
- package/src/temporal.js +226 -0
- package/src/windowing.d.ts +52 -0
- package/src/windowing.js +185 -0
package/BENCHMARKS.md
CHANGED
|
@@ -19,7 +19,7 @@ like-for-like single-process counterpart here and are intentionally omitted.
|
|
|
19
19
|
|
|
20
20
|
<!-- benchmark-results:start -->
|
|
21
21
|
|
|
22
|
-
Generated for package `@lazily-hub/lazily-js` version `0.
|
|
22
|
+
Generated for package `@lazily-hub/lazily-js` version `0.19.0`.
|
|
23
23
|
|
|
24
24
|
Environment: Node.js `26.4.0` on `linux x64`.
|
|
25
25
|
|
|
@@ -33,16 +33,16 @@ Mean wall-clock time per iteration; 95% CI half-width from the standard error.
|
|
|
33
33
|
|
|
34
34
|
| Group | Case | Mean | 95% CI | p75 | p99 | Samples |
|
|
35
35
|
|---|---|---:|---:|---:|---:|---:|
|
|
36
|
-
| cached_reads | context |
|
|
37
|
-
| cold_first_get | context |
|
|
38
|
-
| dependency_fan_out | context / 32 |
|
|
39
|
-
| dependency_fan_out | context / 256 |
|
|
40
|
-
| set_cell_invalidation | high_fan_out / 512 |
|
|
41
|
-
| memo_equality_suppression | context | 2.
|
|
42
|
-
| effect_flushing | context |
|
|
43
|
-
| batch_storms | context / 64 |
|
|
44
|
-
| typed_cache_reads | context_cell |
|
|
45
|
-
| typed_cache_reads | context_slot |
|
|
36
|
+
| cached_reads | context | 31.707 ns | ± 1.826 ns | 29.867 ns | 78.222 ns | 100 |
|
|
37
|
+
| cold_first_get | context | 698.810 ns | ± 46.591 ns | 600.000 ns | 1.410 us | 100 |
|
|
38
|
+
| dependency_fan_out | context / 32 | 5.263 us | ± 858.863 ns | 4.753 us | 16.289 us | 100 |
|
|
39
|
+
| dependency_fan_out | context / 256 | 47.575 us | ± 3.908 us | 45.595 us | 132.218 us | 100 |
|
|
40
|
+
| set_cell_invalidation | high_fan_out / 512 | 6.692 us | ± 3.311 us | 5.840 us | 111.143 us | 100 |
|
|
41
|
+
| memo_equality_suppression | context | 2.546 us | ± 256.417 ns | 2.553 us | 5.435 us | 100 |
|
|
42
|
+
| effect_flushing | context | 129.166 ns | ± 11.208 ns | 131.217 ns | 406.377 ns | 100 |
|
|
43
|
+
| batch_storms | context / 64 | 12.092 us | ± 123.522 ns | 12.286 us | 14.168 us | 100 |
|
|
44
|
+
| typed_cache_reads | context_cell | 33.701 ns | ± 2.005 ns | 32.755 ns | 55.480 ns | 100 |
|
|
45
|
+
| typed_cache_reads | context_slot | 42.440 ns | ± 2.855 ns | 40.074 ns | 106.268 ns | 100 |
|
|
46
46
|
|
|
47
47
|
<!-- benchmark-results:end -->
|
|
48
48
|
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ notes and platform carve-outs lives in
|
|
|
38
38
|
<!-- coverage-table:start -->
|
|
39
39
|
| Feature | Rust | Python | Kotlin | JS | Dart | Zig | Go | C++ |
|
|
40
40
|
| --------- | :----: | :------: | :------: | :--: | :----: | :---: | :--: | :---: |
|
|
41
|
-
| Reactive graph — `Cell` / `Slot` / `Signal`
|
|
41
|
+
| Reactive graph — core `Cell` / `Slot` / `Effect` (+ derived `Signal` = `Slot.eager`) / memo / batch | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
42
42
|
| Keyed-map materialization (`SlotMap`) — mint-on-access derived slots: transparency + deferral (`#lzmatmode`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
43
43
|
| Thread-safe keyed map (`ThreadSafeSlotMap`) — `Send + Sync` + materialization confluence (`#lzmatmode`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
44
44
|
| Async keyed map (`AsyncSlotMap`) — eventual transparency (`#lzmatmode`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
@@ -78,6 +78,14 @@ notes and platform carve-outs lives in
|
|
|
78
78
|
| Permission boundary (`PeerPermissions` / `RemoteOp`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
79
79
|
| Capability negotiation (`SessionHandshake`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
80
80
|
| Instrumentation / benchmarks | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
81
|
+
| Temporal sources — `TimerCell` / `IntervalCell` / `CronCell` / `DeadlineCell` over a logical clock (`#lztime`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
82
|
+
| Rate-shaping operators — `DebounceCell` / `ThrottleCell` / `SampleCell` / `ProbabilisticSampleCell` (`#lzrateshape`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
83
|
+
| Membership + failure detection — `MembershipCell` (SWIM + Phi-accrual) / `PeerSet` / `PeerChangeEvent` (`#lzmemb`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
84
|
+
| Distributed coordination — `LeaseCell` / `LeaderCell` / `LockCell` / `SemaphoreCell` / `BarrierCell`+`QuorumCell` (`#lzcoord`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
85
|
+
| Presence + ephemeral plane — `PresenceCell` / `AwarenessCell` / `EphemeralCell` + `Ephemeral`/`Durable` markers (`#lzpresence`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
86
|
+
| Stream windowing — `TumblingWindow` / `SlidingWindow` / `SessionWindow` over the merge algebra (`#lzwindow`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
87
|
+
| Fault tolerance — `CircuitBreakerCell` / `RetryPolicyCell` / `BulkheadCell` / `TimeoutCell` (`#lzresilience`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
88
|
+
| Embedded-service plane — `HealthCell` / `ReadinessCell` / `DiscoveryCell` / `ServiceRegistry` (`#lzservice`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
81
89
|
<!-- coverage-table:end -->
|
|
82
90
|
|
|
83
91
|
Two JS ✅ marks are backed by runtime-specific mechanisms while keeping the core isomorphic:
|
|
@@ -119,10 +127,12 @@ JavaScript. The family is:
|
|
|
119
127
|
|
|
120
128
|
- **Slot** - lazy, memoized derived value;
|
|
121
129
|
- **Cell** - mutable source value;
|
|
122
|
-
- **Signal** - eager derived value that re-materializes as soon as a dependency
|
|
123
|
-
invalidates;
|
|
124
130
|
- **Effect** - side-effecting observer with cleanup.
|
|
125
131
|
|
|
132
|
+
The core primitives are **Cell** / **Slot** / **Effect**. **`Signal` is a
|
|
133
|
+
derived construct, not a core primitive** — `Signal ≡ Slot.eager`, a memo Slot
|
|
134
|
+
plus a puller Effect that re-materializes as soon as a dependency invalidates.
|
|
135
|
+
|
|
126
136
|
Dependencies are discovered dynamically while a slot/effect/signal computes.
|
|
127
137
|
Invalidation is pull-based and glitch-free; `memo`/`Signal` use equality guards
|
|
128
138
|
to suppress downstream work when a recompute produces the same value. `batch`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazily-hub/lazily-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Native JavaScript port of the lazily reactive core: a full reactive graph (Cell/Slot/Signal/Effect), sync/async/thread-safe keyed reactive maps (ReactiveMap/CellMap/SlotMap), the lazily-spec IPC wire types + C-ABI FFI boundary (isomorphic channel + Node native binding), keyed cell collections + LIS reconciliation, the memoized semantic tree, the move-aware sequence CRDT, the Fugue/RGA text CRDT, manufactured text identity, full-Harel state charts, an FFI state-projection consumer, and an in-library instrumentation/benchmark API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,6 +77,38 @@
|
|
|
77
77
|
"types": "./src/relay.d.ts",
|
|
78
78
|
"default": "./src/relay.js"
|
|
79
79
|
},
|
|
80
|
+
"./temporal": {
|
|
81
|
+
"types": "./src/temporal.d.ts",
|
|
82
|
+
"default": "./src/temporal.js"
|
|
83
|
+
},
|
|
84
|
+
"./rateshape": {
|
|
85
|
+
"types": "./src/rateshape.d.ts",
|
|
86
|
+
"default": "./src/rateshape.js"
|
|
87
|
+
},
|
|
88
|
+
"./membership": {
|
|
89
|
+
"types": "./src/membership.d.ts",
|
|
90
|
+
"default": "./src/membership.js"
|
|
91
|
+
},
|
|
92
|
+
"./coordination": {
|
|
93
|
+
"types": "./src/coordination.d.ts",
|
|
94
|
+
"default": "./src/coordination.js"
|
|
95
|
+
},
|
|
96
|
+
"./presence": {
|
|
97
|
+
"types": "./src/presence.d.ts",
|
|
98
|
+
"default": "./src/presence.js"
|
|
99
|
+
},
|
|
100
|
+
"./windowing": {
|
|
101
|
+
"types": "./src/windowing.d.ts",
|
|
102
|
+
"default": "./src/windowing.js"
|
|
103
|
+
},
|
|
104
|
+
"./resilience": {
|
|
105
|
+
"types": "./src/resilience.d.ts",
|
|
106
|
+
"default": "./src/resilience.js"
|
|
107
|
+
},
|
|
108
|
+
"./service": {
|
|
109
|
+
"types": "./src/service.d.ts",
|
|
110
|
+
"default": "./src/service.js"
|
|
111
|
+
},
|
|
80
112
|
"./sem-tree": {
|
|
81
113
|
"types": "./src/sem-tree.d.ts",
|
|
82
114
|
"default": "./src/sem-tree.js"
|
|
@@ -116,7 +148,7 @@
|
|
|
116
148
|
}
|
|
117
149
|
},
|
|
118
150
|
"scripts": {
|
|
119
|
-
"build": "node --check src/index.js && node --check src/transport.js && node --check src/shm-backend.js && node --check src/reactive.js && node --check src/reactive-async.js && node --check src/reactive-family.js && node --check src/async-reactive-family.js && node --check src/thread-safe.js && node --check src/thread-safe-reactive-family.js && node --check src/ffi.js && node --check src/ffi-node.js && node --check src/instrumentation.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.js && node --check src/queue.js && node --check src/merge.js && node --check src/relay.js && node --check src/sem-tree.js && node --check src/stable-id.js && node --check src/seq-crdt.js && node --check src/text-crdt.js && node --check src/indexeddb-outbox.js && node --check src/utf8-offsets.js && node --check src/lossless-tree-crdt.js && node --check src/state-projection.js && node --check src/graph-view.js && node --check src/signaling.js && node --check src/distributed.js",
|
|
151
|
+
"build": "node --check src/index.js && node --check src/transport.js && node --check src/shm-backend.js && node --check src/reactive.js && node --check src/reactive-async.js && node --check src/reactive-family.js && node --check src/async-reactive-family.js && node --check src/thread-safe.js && node --check src/thread-safe-reactive-family.js && node --check src/ffi.js && node --check src/ffi-node.js && node --check src/instrumentation.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.js && node --check src/queue.js && node --check src/merge.js && node --check src/relay.js && node --check src/rateshape.js && node --check src/membership.js && node --check src/coordination.js && node --check src/presence.js && node --check src/windowing.js && node --check src/resilience.js && node --check src/service.js && node --check src/temporal.js && node --check src/sem-tree.js && node --check src/stable-id.js && node --check src/seq-crdt.js && node --check src/text-crdt.js && node --check src/indexeddb-outbox.js && node --check src/utf8-offsets.js && node --check src/lossless-tree-crdt.js && node --check src/state-projection.js && node --check src/graph-view.js && node --check src/signaling.js && node --check src/distributed.js",
|
|
120
152
|
"test:formal": "node scripts/formal-check.mjs",
|
|
121
153
|
"bench": "node bench/context.bench.mjs",
|
|
122
154
|
"bench:scale": "node --max-old-space-size=8192 bench/scale.bench.mjs",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Distributed coordination (#lzcoord) — the JS port.
|
|
2
|
+
// See coordination.js and `lazily-spec/docs/coordination.md`.
|
|
3
|
+
|
|
4
|
+
import type { Context, CellHandle } from "./reactive.js";
|
|
5
|
+
|
|
6
|
+
export class LeaseCore<P = unknown> {
|
|
7
|
+
fence: number;
|
|
8
|
+
isHeld(now: number): boolean;
|
|
9
|
+
holder(now: number): P | null;
|
|
10
|
+
acquire(peer: P, now: number, ttl: number): number | null;
|
|
11
|
+
renew(peer: P, now: number, ttl: number): boolean;
|
|
12
|
+
release(peer: P): void;
|
|
13
|
+
tick(now: number): boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class LeaseCell<P = unknown> {
|
|
17
|
+
constructor(ctx: Context);
|
|
18
|
+
readonly holderCell: CellHandle<P | null>;
|
|
19
|
+
acquire(peer: P, now: number, ttl: number): number | null;
|
|
20
|
+
renew(peer: P, now: number, ttl: number): boolean;
|
|
21
|
+
release(peer: P, now: number): void;
|
|
22
|
+
tick(now: number): boolean;
|
|
23
|
+
holder(now: number): P | null;
|
|
24
|
+
isHeld(now: number): boolean;
|
|
25
|
+
fence(): number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type LeaderRoleLabel = "Leader" | "Follower" | "Candidate";
|
|
29
|
+
export const LeaderRole: Readonly<{ Leader: "Leader"; Follower: "Follower"; Candidate: "Candidate" }>;
|
|
30
|
+
|
|
31
|
+
export class LeaderCell<P = unknown> {
|
|
32
|
+
constructor(ctx: Context, me: P);
|
|
33
|
+
readonly currentLeaderCell: CellHandle<P | null>;
|
|
34
|
+
campaign(now: number, ttl: number): LeaderRoleLabel;
|
|
35
|
+
contend(peer: P, now: number, ttl: number): LeaderRoleLabel;
|
|
36
|
+
tick(now: number): LeaderRoleLabel;
|
|
37
|
+
currentLeader(now: number): P | null;
|
|
38
|
+
role(now: number): LeaderRoleLabel;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class LockCell<P = unknown> {
|
|
42
|
+
constructor(ctx: Context);
|
|
43
|
+
readonly isLockedCell: CellHandle<boolean>;
|
|
44
|
+
acquire(peer: P, now: number, ttl: number): number | null;
|
|
45
|
+
release(peer: P, now: number): void;
|
|
46
|
+
tick(now: number): boolean;
|
|
47
|
+
validate(fence: number): boolean;
|
|
48
|
+
isLocked(now: number): boolean;
|
|
49
|
+
fence(): number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class SemaphoreCore {
|
|
53
|
+
constructor(capacity: number);
|
|
54
|
+
available(): number;
|
|
55
|
+
acquire(): boolean;
|
|
56
|
+
release(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class SemaphoreCell {
|
|
60
|
+
constructor(ctx: Context, capacity: number);
|
|
61
|
+
readonly permitsAvailableCell: CellHandle<number>;
|
|
62
|
+
acquire(): boolean;
|
|
63
|
+
release(): void;
|
|
64
|
+
permitsAvailable(): number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class BarrierCore<P = unknown> {
|
|
68
|
+
constructor(required: number);
|
|
69
|
+
arrive(peer: P): boolean;
|
|
70
|
+
count(): number;
|
|
71
|
+
isOpen(): boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class BarrierCell<P = unknown> {
|
|
75
|
+
constructor(ctx: Context, required: number);
|
|
76
|
+
static quorum<P = unknown>(ctx: Context, total: number): BarrierCell<P>;
|
|
77
|
+
readonly isOpenCell: CellHandle<boolean>;
|
|
78
|
+
arrive(peer: P): boolean;
|
|
79
|
+
count(): number;
|
|
80
|
+
isOpen(): boolean;
|
|
81
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Distributed coordination (#lzcoord) — the JS port.
|
|
2
|
+
//
|
|
3
|
+
// See `lazily-spec/docs/coordination.md` and the formal model
|
|
4
|
+
// `lazily-formal/LazilyFormal/Coordination.lean`. Lease / leader / lock /
|
|
5
|
+
// semaphore / barrier + quorum primitives, each a pure compute core split from a
|
|
6
|
+
// reactive cell projecting the salient reader. Time is the logical clock. A
|
|
7
|
+
// holder of `null` means "no holder".
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Lease + fencing token
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Single-writer lease authority with a monotone fencing token. */
|
|
14
|
+
export class LeaseCore {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.holderPeer = null;
|
|
17
|
+
this.expiry = 0;
|
|
18
|
+
this.fence = 0;
|
|
19
|
+
}
|
|
20
|
+
#isExpired(now) {
|
|
21
|
+
return this.holderPeer !== null && now >= this.expiry;
|
|
22
|
+
}
|
|
23
|
+
isHeld(now) {
|
|
24
|
+
return this.holderPeer !== null && !this.#isExpired(now);
|
|
25
|
+
}
|
|
26
|
+
holder(now) {
|
|
27
|
+
return this.isHeld(now) ? this.holderPeer : null;
|
|
28
|
+
}
|
|
29
|
+
acquire(peer, now, ttl) {
|
|
30
|
+
if (this.holderPeer === null || this.#isExpired(now)) {
|
|
31
|
+
this.fence += 1;
|
|
32
|
+
this.holderPeer = peer;
|
|
33
|
+
this.expiry = now + ttl;
|
|
34
|
+
return this.fence;
|
|
35
|
+
}
|
|
36
|
+
if (this.holderPeer === peer) {
|
|
37
|
+
this.expiry = now + ttl; // renew keeps fence
|
|
38
|
+
return this.fence;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
renew(peer, now, ttl) {
|
|
43
|
+
if (this.isHeld(now) && this.holderPeer === peer) {
|
|
44
|
+
this.expiry = now + ttl;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
release(peer) {
|
|
50
|
+
if (this.holderPeer === peer) this.holderPeer = null;
|
|
51
|
+
}
|
|
52
|
+
tick(now) {
|
|
53
|
+
if (this.#isExpired(now)) {
|
|
54
|
+
this.holderPeer = null;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Reactive lease: projects the holder onto a cell. */
|
|
62
|
+
export class LeaseCell {
|
|
63
|
+
constructor(ctx) {
|
|
64
|
+
this.ctx = ctx;
|
|
65
|
+
this.core = new LeaseCore();
|
|
66
|
+
this.holderCell = ctx.cell(null);
|
|
67
|
+
}
|
|
68
|
+
#refresh(now) {
|
|
69
|
+
this.ctx.setCell(this.holderCell, this.core.holder(now));
|
|
70
|
+
}
|
|
71
|
+
acquire(peer, now, ttl) {
|
|
72
|
+
const r = this.core.acquire(peer, now, ttl);
|
|
73
|
+
this.#refresh(now);
|
|
74
|
+
return r;
|
|
75
|
+
}
|
|
76
|
+
renew(peer, now, ttl) {
|
|
77
|
+
const r = this.core.renew(peer, now, ttl);
|
|
78
|
+
this.#refresh(now);
|
|
79
|
+
return r;
|
|
80
|
+
}
|
|
81
|
+
release(peer, now) {
|
|
82
|
+
this.core.release(peer);
|
|
83
|
+
this.#refresh(now);
|
|
84
|
+
}
|
|
85
|
+
tick(now) {
|
|
86
|
+
const r = this.core.tick(now);
|
|
87
|
+
this.#refresh(now);
|
|
88
|
+
return r;
|
|
89
|
+
}
|
|
90
|
+
holder(now) {
|
|
91
|
+
return this.core.holder(now);
|
|
92
|
+
}
|
|
93
|
+
isHeld(now) {
|
|
94
|
+
return this.core.isHeld(now);
|
|
95
|
+
}
|
|
96
|
+
fence() {
|
|
97
|
+
return this.core.fence;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Leader / follower / candidate
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
export const LeaderRole = Object.freeze({
|
|
106
|
+
Leader: "Leader",
|
|
107
|
+
Follower: "Follower",
|
|
108
|
+
Candidate: "Candidate",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Reactive leadership over a lease from node `me`'s perspective. */
|
|
112
|
+
export class LeaderCell {
|
|
113
|
+
constructor(ctx, me) {
|
|
114
|
+
this.ctx = ctx;
|
|
115
|
+
this.me = me;
|
|
116
|
+
this.core = new LeaseCore();
|
|
117
|
+
this.currentLeaderCell = ctx.cell(null);
|
|
118
|
+
}
|
|
119
|
+
#refresh(now) {
|
|
120
|
+
this.ctx.setCell(this.currentLeaderCell, this.core.holder(now));
|
|
121
|
+
}
|
|
122
|
+
campaign(now, ttl) {
|
|
123
|
+
this.core.acquire(this.me, now, ttl);
|
|
124
|
+
this.#refresh(now);
|
|
125
|
+
return this.role(now);
|
|
126
|
+
}
|
|
127
|
+
contend(peer, now, ttl) {
|
|
128
|
+
this.core.acquire(peer, now, ttl);
|
|
129
|
+
this.#refresh(now);
|
|
130
|
+
return this.role(now);
|
|
131
|
+
}
|
|
132
|
+
tick(now) {
|
|
133
|
+
this.core.tick(now);
|
|
134
|
+
this.#refresh(now);
|
|
135
|
+
return this.role(now);
|
|
136
|
+
}
|
|
137
|
+
currentLeader(now) {
|
|
138
|
+
return this.core.holder(now);
|
|
139
|
+
}
|
|
140
|
+
role(now) {
|
|
141
|
+
const h = this.core.holder(now);
|
|
142
|
+
if (h === null) return LeaderRole.Candidate;
|
|
143
|
+
return h === this.me ? LeaderRole.Leader : LeaderRole.Follower;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Distributed lock + fencing
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
/** Reactive distributed mutex over a lease + fencing token. */
|
|
152
|
+
export class LockCell {
|
|
153
|
+
constructor(ctx) {
|
|
154
|
+
this.ctx = ctx;
|
|
155
|
+
this.core = new LeaseCore();
|
|
156
|
+
this.isLockedCell = ctx.cell(false);
|
|
157
|
+
}
|
|
158
|
+
#refresh(now) {
|
|
159
|
+
this.ctx.setCell(this.isLockedCell, this.core.isHeld(now));
|
|
160
|
+
}
|
|
161
|
+
acquire(peer, now, ttl) {
|
|
162
|
+
const r = this.core.acquire(peer, now, ttl);
|
|
163
|
+
this.#refresh(now);
|
|
164
|
+
return r;
|
|
165
|
+
}
|
|
166
|
+
release(peer, now) {
|
|
167
|
+
this.core.release(peer);
|
|
168
|
+
this.#refresh(now);
|
|
169
|
+
}
|
|
170
|
+
tick(now) {
|
|
171
|
+
const r = this.core.tick(now);
|
|
172
|
+
this.#refresh(now);
|
|
173
|
+
return r;
|
|
174
|
+
}
|
|
175
|
+
/** Whether `fence` is the current (non-stale) fencing token. */
|
|
176
|
+
validate(fence) {
|
|
177
|
+
return this.core.fence === fence;
|
|
178
|
+
}
|
|
179
|
+
isLocked(now) {
|
|
180
|
+
return this.core.isHeld(now);
|
|
181
|
+
}
|
|
182
|
+
fence() {
|
|
183
|
+
return this.core.fence;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Semaphore
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/** Bounded permit pool compute core. */
|
|
192
|
+
export class SemaphoreCore {
|
|
193
|
+
constructor(capacity) {
|
|
194
|
+
this.capacity = capacity;
|
|
195
|
+
this.acquired = 0;
|
|
196
|
+
}
|
|
197
|
+
available() {
|
|
198
|
+
return this.capacity - this.acquired;
|
|
199
|
+
}
|
|
200
|
+
acquire() {
|
|
201
|
+
if (this.acquired < this.capacity) {
|
|
202
|
+
this.acquired += 1;
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
release() {
|
|
208
|
+
if (this.acquired > 0) this.acquired -= 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Reactive semaphore: projects permitsAvailable onto a cell. */
|
|
213
|
+
export class SemaphoreCell {
|
|
214
|
+
constructor(ctx, capacity) {
|
|
215
|
+
this.ctx = ctx;
|
|
216
|
+
this.core = new SemaphoreCore(capacity);
|
|
217
|
+
this.permitsAvailableCell = ctx.cell(capacity);
|
|
218
|
+
}
|
|
219
|
+
#refresh() {
|
|
220
|
+
this.ctx.setCell(this.permitsAvailableCell, this.core.available());
|
|
221
|
+
}
|
|
222
|
+
acquire() {
|
|
223
|
+
const r = this.core.acquire();
|
|
224
|
+
this.#refresh();
|
|
225
|
+
return r;
|
|
226
|
+
}
|
|
227
|
+
release() {
|
|
228
|
+
this.core.release();
|
|
229
|
+
this.#refresh();
|
|
230
|
+
}
|
|
231
|
+
permitsAvailable() {
|
|
232
|
+
return this.ctx.getCell(this.permitsAvailableCell);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Barrier / quorum
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/** Wait-for-N gate over distinct arriving peers. */
|
|
241
|
+
export class BarrierCore {
|
|
242
|
+
constructor(required) {
|
|
243
|
+
this.required = required;
|
|
244
|
+
this.arrived = new Set();
|
|
245
|
+
}
|
|
246
|
+
arrive(peer) {
|
|
247
|
+
this.arrived.add(peer);
|
|
248
|
+
return this.isOpen();
|
|
249
|
+
}
|
|
250
|
+
count() {
|
|
251
|
+
return this.arrived.size;
|
|
252
|
+
}
|
|
253
|
+
isOpen() {
|
|
254
|
+
return this.count() >= this.required;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Reactive wait-for-N gate. A quorum is a barrier with required = total/2 + 1. */
|
|
259
|
+
export class BarrierCell {
|
|
260
|
+
constructor(ctx, required) {
|
|
261
|
+
this.ctx = ctx;
|
|
262
|
+
this.core = new BarrierCore(required);
|
|
263
|
+
this.isOpenCell = ctx.cell(this.core.isOpen());
|
|
264
|
+
}
|
|
265
|
+
/** A quorum gate: opens at strict majority of `total`. */
|
|
266
|
+
static quorum(ctx, total) {
|
|
267
|
+
return new BarrierCell(ctx, Math.floor(total / 2) + 1);
|
|
268
|
+
}
|
|
269
|
+
#refresh() {
|
|
270
|
+
this.ctx.setCell(this.isOpenCell, this.core.isOpen());
|
|
271
|
+
}
|
|
272
|
+
arrive(peer) {
|
|
273
|
+
const r = this.core.arrive(peer);
|
|
274
|
+
this.#refresh();
|
|
275
|
+
return r;
|
|
276
|
+
}
|
|
277
|
+
count() {
|
|
278
|
+
return this.core.count();
|
|
279
|
+
}
|
|
280
|
+
isOpen() {
|
|
281
|
+
return this.ctx.getCell(this.isOpenCell);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Membership + failure detection (#lzmemb) — the JS port.
|
|
2
|
+
// See membership.js and `lazily-spec/docs/membership.md`.
|
|
3
|
+
|
|
4
|
+
import type { Context, CellHandle } from "./reactive.js";
|
|
5
|
+
|
|
6
|
+
export type PeerStateLabel = "Alive" | "Suspect" | "Dead" | "Left";
|
|
7
|
+
export const PeerState: Readonly<{
|
|
8
|
+
Alive: "Alive";
|
|
9
|
+
Suspect: "Suspect";
|
|
10
|
+
Dead: "Dead";
|
|
11
|
+
Left: "Left";
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export interface MembershipConfig {
|
|
15
|
+
phiThreshold: number;
|
|
16
|
+
suspectTimeout: number;
|
|
17
|
+
maxSamples: number;
|
|
18
|
+
minStd: number;
|
|
19
|
+
}
|
|
20
|
+
export const defaultMembershipConfig: Readonly<MembershipConfig>;
|
|
21
|
+
|
|
22
|
+
export type PeerChangeEvent<P> =
|
|
23
|
+
| { type: "Joined"; peer: P }
|
|
24
|
+
| { type: "Left"; peer: P }
|
|
25
|
+
| { type: "StateChanged"; peer: P; from: PeerStateLabel; to: PeerStateLabel };
|
|
26
|
+
|
|
27
|
+
export class PhiAccrual {
|
|
28
|
+
constructor(maxSamples: number, minStd: number);
|
|
29
|
+
heartbeat(now: number): void;
|
|
30
|
+
phi(now: number): number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class MembershipCore<P = unknown> {
|
|
34
|
+
constructor(config?: Partial<MembershipConfig>);
|
|
35
|
+
aliveSet(): P[];
|
|
36
|
+
state(peer: P): PeerStateLabel | null;
|
|
37
|
+
join(peer: P, now: number): PeerChangeEvent<P>[];
|
|
38
|
+
heartbeat(peer: P, now: number): PeerChangeEvent<P>[];
|
|
39
|
+
leave(peer: P, now: number): PeerChangeEvent<P>[];
|
|
40
|
+
tick(now: number): PeerChangeEvent<P>[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class MembershipCell<P = unknown> {
|
|
44
|
+
constructor(ctx: Context, config?: Partial<MembershipConfig>);
|
|
45
|
+
readonly peerSetCell: CellHandle<P[]>;
|
|
46
|
+
join(peer: P, now: number): PeerChangeEvent<P>[];
|
|
47
|
+
heartbeat(peer: P, now: number): PeerChangeEvent<P>[];
|
|
48
|
+
leave(peer: P, now: number): PeerChangeEvent<P>[];
|
|
49
|
+
tick(now: number): PeerChangeEvent<P>[];
|
|
50
|
+
peerSet(): P[];
|
|
51
|
+
state(peer: P): PeerStateLabel | null;
|
|
52
|
+
}
|