@einaraglen/quorum 1.0.0 → 1.0.3
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 +66 -45
- package/dist/core/bully.d.ts +56 -0
- package/dist/core/bully.d.ts.map +1 -0
- package/dist/core/bully.js +140 -0
- package/dist/core/forum.d.ts +41 -0
- package/dist/core/forum.d.ts.map +1 -0
- package/dist/core/forum.js +152 -0
- package/dist/core/logger.d.ts +3 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +0 -0
- package/dist/core/quorum.d.ts +51 -0
- package/dist/core/quorum.d.ts.map +1 -0
- package/dist/core/quorum.js +100 -0
- package/dist/core/sharding.d.ts +100 -0
- package/dist/core/sharding.d.ts.map +1 -0
- package/dist/core/sharding.js +264 -0
- package/dist/core/transport.d.ts +29 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +83 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/package.json +49 -43
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Discovery } from "./bully";
|
|
2
|
+
import type { Logger } from "./logger";
|
|
3
|
+
export type QuorumOptions<TId = string> = {
|
|
4
|
+
discovery: Discovery;
|
|
5
|
+
ids: TId[];
|
|
6
|
+
fetchFn?: typeof fetch;
|
|
7
|
+
internalPort?: number;
|
|
8
|
+
internalHost?: string;
|
|
9
|
+
requestTimeoutMs?: number;
|
|
10
|
+
coordinatorWaitMs?: number;
|
|
11
|
+
heartbeatIntervalMs?: number;
|
|
12
|
+
reportTimeoutMs?: number;
|
|
13
|
+
rebalanceIntervalMs?: number;
|
|
14
|
+
expectedClusterSize?: number;
|
|
15
|
+
quorumFailureThreshold?: number;
|
|
16
|
+
onSustainedQuorumLoss?: () => void;
|
|
17
|
+
/** Injectable log sink; must implement `info`/`warn`/`error`/`debug`. Defaults to `console`. */
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
};
|
|
20
|
+
export declare class Quorum<TId = string> implements Disposable {
|
|
21
|
+
private readonly transport;
|
|
22
|
+
private readonly bully;
|
|
23
|
+
private readonly forum;
|
|
24
|
+
private readonly shard;
|
|
25
|
+
constructor(opts: QuorumOptions<TId>);
|
|
26
|
+
isLeader(): boolean;
|
|
27
|
+
getStatus(): {
|
|
28
|
+
self: string | undefined;
|
|
29
|
+
leader: string | undefined;
|
|
30
|
+
isLeader: boolean;
|
|
31
|
+
};
|
|
32
|
+
getPodRoles(): Promise<{
|
|
33
|
+
name: string | undefined;
|
|
34
|
+
host: string | undefined;
|
|
35
|
+
role: string;
|
|
36
|
+
}[]>;
|
|
37
|
+
subscribe(id: TId, onEvent: (payload: unknown) => void): () => void;
|
|
38
|
+
publish(id: TId, payload: unknown): void;
|
|
39
|
+
getOwner(id: TId): string | undefined;
|
|
40
|
+
getOwnership(): Map<TId, string>;
|
|
41
|
+
getHeldIds(): TId[];
|
|
42
|
+
updateIds(ids: TId[]): Promise<void>;
|
|
43
|
+
/** Fires with the ids just gained whenever this pod's held ids grow. Returns an unsubscribe function. */
|
|
44
|
+
onAssigned(onEvent: (ids: TId[]) => void): () => void;
|
|
45
|
+
/** Fires with the ids just lost whenever this pod's held ids shrink. Returns an unsubscribe function. */
|
|
46
|
+
onReleased(onEvent: (ids: TId[]) => void): () => void;
|
|
47
|
+
start(): void;
|
|
48
|
+
stop(): void;
|
|
49
|
+
[Symbol.dispose](): void;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=quorum.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quorum.d.ts","sourceRoot":"","sources":["../../src/core/quorum.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAGhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,aAAa,CAAC,GAAG,GAAG,MAAM,IAAI;IACxC,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,GAAG,EAAE,CAAC;IACX,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,gGAAgG;IAChG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,MAAM,CAAC,GAAG,GAAG,MAAM,CAAE,YAAW,UAAU;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IAEtC,YAAY,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,EAiCnC;IAEM,QAAQ,IAAI,OAAO,CAEzB;IACM,SAAS;;;;MAEf;IACY,WAAW;;;;SAEvB;IAEM,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAEzE;IACM,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAE9C;IACM,QAAQ,CAAC,EAAE,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAE3C;IACM,YAAY,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAEtC;IACM,UAAU,IAAI,GAAG,EAAE,CAEzB;IACY,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhD;IAED,yGAAyG;IAClG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAG3D;IACD,yGAAyG;IAClG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAG3D;IAEM,KAAK,IAAI,IAAI,CAUnB;IAEM,IAAI,IAAI,IAAI,CAIlB;IAEM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAE9B;CACF"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Transport } from "./transport";
|
|
2
|
+
import { Bully } from "./bully";
|
|
3
|
+
import { Forum } from "./forum";
|
|
4
|
+
import { Sharding } from "./sharding";
|
|
5
|
+
export class Quorum {
|
|
6
|
+
transport;
|
|
7
|
+
bully;
|
|
8
|
+
forum;
|
|
9
|
+
shard;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.transport = new Transport({
|
|
12
|
+
fetchFn: opts.fetchFn,
|
|
13
|
+
internalPort: opts.internalPort,
|
|
14
|
+
internalHost: opts.internalHost,
|
|
15
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
16
|
+
logger: opts.logger,
|
|
17
|
+
});
|
|
18
|
+
this.bully = new Bully({
|
|
19
|
+
discovery: opts.discovery,
|
|
20
|
+
transport: this.transport,
|
|
21
|
+
coordinatorWaitMs: opts.coordinatorWaitMs,
|
|
22
|
+
heartbeatIntervalMs: opts.heartbeatIntervalMs,
|
|
23
|
+
logger: opts.logger,
|
|
24
|
+
});
|
|
25
|
+
this.forum = new Forum({
|
|
26
|
+
bully: this.bully,
|
|
27
|
+
transport: this.transport,
|
|
28
|
+
discovery: opts.discovery,
|
|
29
|
+
fetchFn: opts.fetchFn,
|
|
30
|
+
logger: opts.logger,
|
|
31
|
+
});
|
|
32
|
+
this.shard = new Sharding({
|
|
33
|
+
bully: this.bully,
|
|
34
|
+
forum: this.forum,
|
|
35
|
+
ids: opts.ids,
|
|
36
|
+
reportTimeoutMs: opts.reportTimeoutMs,
|
|
37
|
+
rebalanceIntervalMs: opts.rebalanceIntervalMs,
|
|
38
|
+
expectedClusterSize: opts.expectedClusterSize,
|
|
39
|
+
quorumFailureThreshold: opts.quorumFailureThreshold,
|
|
40
|
+
onSustainedQuorumLoss: opts.onSustainedQuorumLoss,
|
|
41
|
+
logger: opts.logger,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
isLeader() {
|
|
45
|
+
return this.bully.isLeader();
|
|
46
|
+
}
|
|
47
|
+
getStatus() {
|
|
48
|
+
return this.bully.getStatus();
|
|
49
|
+
}
|
|
50
|
+
async getPodRoles() {
|
|
51
|
+
return this.bully.getPodRoles();
|
|
52
|
+
}
|
|
53
|
+
subscribe(id, onEvent) {
|
|
54
|
+
return this.shard.subscribe(id, onEvent);
|
|
55
|
+
}
|
|
56
|
+
publish(id, payload) {
|
|
57
|
+
this.shard.publish(id, payload);
|
|
58
|
+
}
|
|
59
|
+
getOwner(id) {
|
|
60
|
+
return this.shard.getOwner(id);
|
|
61
|
+
}
|
|
62
|
+
getOwnership() {
|
|
63
|
+
return this.shard.getOwnership();
|
|
64
|
+
}
|
|
65
|
+
getHeldIds() {
|
|
66
|
+
return this.shard.getHeldIds();
|
|
67
|
+
}
|
|
68
|
+
async updateIds(ids) {
|
|
69
|
+
return this.shard.updateIds(ids);
|
|
70
|
+
}
|
|
71
|
+
/** Fires with the ids just gained whenever this pod's held ids grow. Returns an unsubscribe function. */
|
|
72
|
+
onAssigned(onEvent) {
|
|
73
|
+
this.shard.lifecycle.on("assigned", onEvent);
|
|
74
|
+
return () => this.shard.lifecycle.off("assigned", onEvent);
|
|
75
|
+
}
|
|
76
|
+
/** Fires with the ids just lost whenever this pod's held ids shrink. Returns an unsubscribe function. */
|
|
77
|
+
onReleased(onEvent) {
|
|
78
|
+
this.shard.lifecycle.on("released", onEvent);
|
|
79
|
+
return () => this.shard.lifecycle.off("released", onEvent);
|
|
80
|
+
}
|
|
81
|
+
start() {
|
|
82
|
+
this.transport.start({
|
|
83
|
+
onElectionMessage: (id) => this.bully.onElectionMessage(id),
|
|
84
|
+
onCoordinatorMessage: (id) => this.bully.onCoordinatorMessage(id),
|
|
85
|
+
onMessage: (event, payload) => this.bully.onMessage(event, payload),
|
|
86
|
+
channel: this.bully.channel,
|
|
87
|
+
});
|
|
88
|
+
this.shard.start();
|
|
89
|
+
this.bully.start();
|
|
90
|
+
this.bully.startElection();
|
|
91
|
+
}
|
|
92
|
+
stop() {
|
|
93
|
+
this.shard.stop();
|
|
94
|
+
this.bully.stop();
|
|
95
|
+
this.transport.stop();
|
|
96
|
+
}
|
|
97
|
+
[Symbol.dispose]() {
|
|
98
|
+
this.stop();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import type { Bully } from "./bully";
|
|
3
|
+
import type { Forum } from "./forum";
|
|
4
|
+
import type { Logger } from "./logger";
|
|
5
|
+
export type ShardingOptions<TId> = {
|
|
6
|
+
bully: Bully;
|
|
7
|
+
forum: Forum;
|
|
8
|
+
ids: TId[];
|
|
9
|
+
reportTimeoutMs?: number;
|
|
10
|
+
rebalanceIntervalMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* The cluster's intended total pod count (e.g. the Deployment's `replicas`), used as a
|
|
13
|
+
* quorum guard: reconcile() refuses to act unless more than half of this count actually
|
|
14
|
+
* reported in. Left unset, quorum checking is disabled entirely — safe default for tests
|
|
15
|
+
* and small ad-hoc clusters that don't care about split-brain protection.
|
|
16
|
+
*/
|
|
17
|
+
expectedClusterSize?: number;
|
|
18
|
+
/**
|
|
19
|
+
* How many *consecutive* quorum failures to tolerate before treating this pod as sustainably
|
|
20
|
+
* isolated (as opposed to one slow reconcile pass on an otherwise-healthy network) and
|
|
21
|
+
* invoking onSustainedQuorumLoss. Only relevant when expectedClusterSize is set.
|
|
22
|
+
*/
|
|
23
|
+
quorumFailureThreshold?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Called once quorumFailureThreshold consecutive quorum failures are hit. Defaults to
|
|
26
|
+
* `process.exit(1)` — fail loudly and let Kubernetes restart the pod, rather than keep
|
|
27
|
+
* serving increasingly stale data while believing it's still leader. Injectable so tests
|
|
28
|
+
* (and anyone who wants different behavior) don't have to actually kill the process.
|
|
29
|
+
*/
|
|
30
|
+
onSustainedQuorumLoss?: () => void;
|
|
31
|
+
logger?: Logger;
|
|
32
|
+
};
|
|
33
|
+
export declare class Sharding<TId = string> implements Disposable {
|
|
34
|
+
/** Emits `"assigned"` / `"released"` (each with the delta of ids, not the full held set) whenever this pod's held ids change. */
|
|
35
|
+
readonly lifecycle: EventEmitter<[never]>;
|
|
36
|
+
private bully;
|
|
37
|
+
private forum;
|
|
38
|
+
private allIds;
|
|
39
|
+
private reportTimeoutMs;
|
|
40
|
+
private rebalanceIntervalMs;
|
|
41
|
+
private expectedClusterSize?;
|
|
42
|
+
private quorumFailureThreshold;
|
|
43
|
+
private sustainedQuorumLossCallback;
|
|
44
|
+
private logger;
|
|
45
|
+
private consecutiveQuorumFailures;
|
|
46
|
+
/** Ensures sustainedQuorumLossCallback fires once per episode, not on every failure past threshold. */
|
|
47
|
+
private hasTriggeredSustainedLoss;
|
|
48
|
+
/** Guards against a periodic rebalance tick overlapping an explicit reconcile() already in flight. */
|
|
49
|
+
private reconcileInFlight;
|
|
50
|
+
private heldIds;
|
|
51
|
+
private ownership;
|
|
52
|
+
private rebalanceInterval?;
|
|
53
|
+
/** Local-only: fires `<id>` whenever getOwner(id) changes, to drive subscribe() reconnects. */
|
|
54
|
+
private ownershipEvents;
|
|
55
|
+
/** Safety net: any subscribe() the caller never explicitly closed gets torn down in stop(). */
|
|
56
|
+
private activeSubscriptions;
|
|
57
|
+
constructor(opts: ShardingOptions<TId>);
|
|
58
|
+
getHeldIds(): TId[];
|
|
59
|
+
/** Local, instant lookup — no network round-trip — of which peer currently owns an id. */
|
|
60
|
+
getOwner(id: TId): string | undefined;
|
|
61
|
+
/** Local, cluster-wide view of every id's current owner — every peer as of the last reconcile. */
|
|
62
|
+
getOwnership(): Map<TId, string>;
|
|
63
|
+
/**
|
|
64
|
+
* Live-subscribe to events published for `id`, wherever it currently lives. Returns an
|
|
65
|
+
* unsubscribe function. Reconnects automatically if ownership of this specific id changes
|
|
66
|
+
* while the subscription is active, driven by the same ownership map reconcile() already
|
|
67
|
+
* keeps fresh — no message from the old owner required, no subscriber bookkeeping on the
|
|
68
|
+
* owner's side at all. Under the hood this is just an SSE connection (via
|
|
69
|
+
* bully.subscribeToPeer); closing it is the entire unsubscribe.
|
|
70
|
+
*/
|
|
71
|
+
subscribe(id: TId, onEvent: (payload: unknown) => void): () => void;
|
|
72
|
+
/** Called by the app whenever it has a new event for one of the ids this pod owns. */
|
|
73
|
+
publish(id: TId, payload: unknown): void;
|
|
74
|
+
private onAssign;
|
|
75
|
+
private onRelease;
|
|
76
|
+
private onReportRequest;
|
|
77
|
+
private onUpdateIds;
|
|
78
|
+
private onOwnershipMap;
|
|
79
|
+
private onElected;
|
|
80
|
+
private onDemoted;
|
|
81
|
+
start(): void;
|
|
82
|
+
stop(): void;
|
|
83
|
+
[Symbol.dispose](): void;
|
|
84
|
+
/**
|
|
85
|
+
* Leader-only: change the full set of ids that should be distributed across the cluster —
|
|
86
|
+
* e.g. after re-querying the database this bundle came from and finding it grew or shrank.
|
|
87
|
+
* Broadcasts the new set to every peer (so whichever pod is leader next already has it) and
|
|
88
|
+
* immediately reconciles, rather than waiting for the next periodic tick.
|
|
89
|
+
*/
|
|
90
|
+
updateIds(ids: TId[]): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Gathers what every reachable peer currently holds, computes the fair target split of all
|
|
93
|
+
* ids across those peers, broadcasts the full id→peer map so any pod can look up an owner
|
|
94
|
+
* locally via getOwner() without asking around, and sends each peer exactly the
|
|
95
|
+
* assign/release messages needed to converge on that target.
|
|
96
|
+
*/
|
|
97
|
+
reconcile(): Promise<void>;
|
|
98
|
+
private doReconcile;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=sharding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharding.d.ts","sourceRoot":"","sources":["../../src/core/sharding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,eAAe,CAAC,GAAG,IAAI;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,GAAG,EAAE,CAAC;IACX,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAKF,qBAAa,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAE,YAAW,UAAU;IACvD,iIAAiI;IACjI,SAAgB,SAAS,wBAAsB;IAE/C,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,mBAAmB,CAAC,CAAS;IACrC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,2BAA2B,CAAa;IAChD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,yBAAyB,CAAK;IACtC,uGAAuG;IACvG,OAAO,CAAC,yBAAyB,CAAS;IAC1C,sGAAsG;IACtG,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAiB;IAC3C,+FAA+F;IAC/F,OAAO,CAAC,eAAe,CAAsB;IAC7C,+FAA+F;IAC/F,OAAO,CAAC,mBAAmB,CAAyB;IAEpD,YAAY,IAAI,EAAE,eAAe,CAAC,GAAG,CAAC,EAUrC;IAEM,UAAU,UAEhB;IAED,0FAA0F;IACnF,QAAQ,CAAC,EAAE,EAAE,GAAG,sBAEtB;IAED,kGAAkG;IAC3F,YAAY,qBAElB;IAED;;;;;;;OAOG;IACI,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CA+BzE;IAED,sFAAsF;IAC/E,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,QAMvC;IAED,OAAO,CAAC,QAAQ,CAId;IAEF,OAAO,CAAC,SAAS,CAIf;IAEF,OAAO,CAAC,eAAe,CAIrB;IAEF,OAAO,CAAC,WAAW,CAEjB;IAEF,OAAO,CAAC,cAAc,CAUpB;IAEF,OAAO,CAAC,SAAS,CAGf;IAEF,OAAO,CAAC,SAAS,CAKf;IAEK,KAAK,SAQX;IAEM,IAAI,SAWV;IAEM,CAAC,MAAM,CAAC,OAAO,CAAC,SAEtB;IAED;;;;;OAKG;IACU,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,iBAQhC;IAED;;;;;OAKG;IACU,SAAS,kBASrB;YAEa,WAAW;CA2F1B"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
export class Sharding {
|
|
3
|
+
/** Emits `"assigned"` / `"released"` (each with the delta of ids, not the full held set) whenever this pod's held ids change. */
|
|
4
|
+
lifecycle = new EventEmitter();
|
|
5
|
+
bully;
|
|
6
|
+
forum;
|
|
7
|
+
allIds;
|
|
8
|
+
reportTimeoutMs;
|
|
9
|
+
rebalanceIntervalMs;
|
|
10
|
+
expectedClusterSize;
|
|
11
|
+
quorumFailureThreshold;
|
|
12
|
+
sustainedQuorumLossCallback;
|
|
13
|
+
logger;
|
|
14
|
+
consecutiveQuorumFailures = 0;
|
|
15
|
+
/** Ensures sustainedQuorumLossCallback fires once per episode, not on every failure past threshold. */
|
|
16
|
+
hasTriggeredSustainedLoss = false;
|
|
17
|
+
/** Guards against a periodic rebalance tick overlapping an explicit reconcile() already in flight. */
|
|
18
|
+
reconcileInFlight = false;
|
|
19
|
+
heldIds = new Set();
|
|
20
|
+
ownership = new Map();
|
|
21
|
+
rebalanceInterval;
|
|
22
|
+
/** Local-only: fires `<id>` whenever getOwner(id) changes, to drive subscribe() reconnects. */
|
|
23
|
+
ownershipEvents = new EventEmitter();
|
|
24
|
+
/** Safety net: any subscribe() the caller never explicitly closed gets torn down in stop(). */
|
|
25
|
+
activeSubscriptions = new Set();
|
|
26
|
+
constructor(opts) {
|
|
27
|
+
this.bully = opts.bully;
|
|
28
|
+
this.forum = opts.forum;
|
|
29
|
+
this.allIds = opts.ids;
|
|
30
|
+
this.reportTimeoutMs = opts.reportTimeoutMs ?? 2000;
|
|
31
|
+
this.rebalanceIntervalMs = opts.rebalanceIntervalMs ?? 10000;
|
|
32
|
+
this.expectedClusterSize = opts.expectedClusterSize;
|
|
33
|
+
this.quorumFailureThreshold = opts.quorumFailureThreshold ?? 3;
|
|
34
|
+
this.sustainedQuorumLossCallback = opts.onSustainedQuorumLoss ?? (() => process.exit(1));
|
|
35
|
+
this.logger = opts.logger ?? console;
|
|
36
|
+
}
|
|
37
|
+
getHeldIds() {
|
|
38
|
+
return [...this.heldIds];
|
|
39
|
+
}
|
|
40
|
+
/** Local, instant lookup — no network round-trip — of which peer currently owns an id. */
|
|
41
|
+
getOwner(id) {
|
|
42
|
+
return this.ownership.get(id);
|
|
43
|
+
}
|
|
44
|
+
/** Local, cluster-wide view of every id's current owner — every peer as of the last reconcile. */
|
|
45
|
+
getOwnership() {
|
|
46
|
+
return new Map(this.ownership);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Live-subscribe to events published for `id`, wherever it currently lives. Returns an
|
|
50
|
+
* unsubscribe function. Reconnects automatically if ownership of this specific id changes
|
|
51
|
+
* while the subscription is active, driven by the same ownership map reconcile() already
|
|
52
|
+
* keeps fresh — no message from the old owner required, no subscriber bookkeeping on the
|
|
53
|
+
* owner's side at all. Under the hood this is just an SSE connection (via
|
|
54
|
+
* bully.subscribeToPeer); closing it is the entire unsubscribe.
|
|
55
|
+
*/
|
|
56
|
+
subscribe(id, onEvent) {
|
|
57
|
+
let close;
|
|
58
|
+
const connect = () => {
|
|
59
|
+
const owner = this.getOwner(id);
|
|
60
|
+
if (!owner) {
|
|
61
|
+
this.logger.warn(`subscribe(${id}) could not resolve an owner`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
close = this.forum.subscribeToPeer(owner, "connection-event", (event) => {
|
|
65
|
+
const { id: eventId, payload } = event;
|
|
66
|
+
if (eventId === id)
|
|
67
|
+
onEvent(payload);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const onOwnerChanged = () => {
|
|
71
|
+
close?.();
|
|
72
|
+
connect();
|
|
73
|
+
};
|
|
74
|
+
connect();
|
|
75
|
+
this.ownershipEvents.on(String(id), onOwnerChanged);
|
|
76
|
+
const unsubscribe = () => {
|
|
77
|
+
this.ownershipEvents.off(String(id), onOwnerChanged);
|
|
78
|
+
close?.();
|
|
79
|
+
this.activeSubscriptions.delete(unsubscribe);
|
|
80
|
+
};
|
|
81
|
+
this.activeSubscriptions.add(unsubscribe);
|
|
82
|
+
return unsubscribe;
|
|
83
|
+
}
|
|
84
|
+
/** Called by the app whenever it has a new event for one of the ids this pod owns. */
|
|
85
|
+
publish(id, payload) {
|
|
86
|
+
if (!this.heldIds.has(id)) {
|
|
87
|
+
this.logger.warn(`publish(${id}) called but this pod doesn't own that id, dropping`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.bully.channel.emit("connection-event", { id, payload });
|
|
91
|
+
}
|
|
92
|
+
onAssign = (ids) => {
|
|
93
|
+
for (const id of ids)
|
|
94
|
+
this.heldIds.add(id);
|
|
95
|
+
this.logger.debug(`Now holding ${this.heldIds.size} id(s)`);
|
|
96
|
+
this.lifecycle.emit("assigned", ids);
|
|
97
|
+
};
|
|
98
|
+
onRelease = (ids) => {
|
|
99
|
+
for (const id of ids)
|
|
100
|
+
this.heldIds.delete(id);
|
|
101
|
+
this.logger.debug(`Released ${ids.length} id(s), now holding ${this.heldIds.size}`);
|
|
102
|
+
this.lifecycle.emit("released", ids);
|
|
103
|
+
};
|
|
104
|
+
onReportRequest = () => {
|
|
105
|
+
const { self } = this.bully.getStatus();
|
|
106
|
+
if (!self)
|
|
107
|
+
return;
|
|
108
|
+
this.forum.send("holdings-report", { peer: self, ids: this.getHeldIds() });
|
|
109
|
+
};
|
|
110
|
+
onUpdateIds = (ids) => {
|
|
111
|
+
this.allIds = ids;
|
|
112
|
+
};
|
|
113
|
+
onOwnershipMap = (entries) => {
|
|
114
|
+
const next = new Map(entries);
|
|
115
|
+
const previous = this.ownership;
|
|
116
|
+
// Swapped in before emitting: subscribe()'s reconnect handler calls getOwner() synchronously
|
|
117
|
+
// in reaction to this event, so it must already see the new value, not the one it's replacing.
|
|
118
|
+
this.ownership = next;
|
|
119
|
+
for (const [id, peer] of next) {
|
|
120
|
+
if (previous.get(id) !== peer)
|
|
121
|
+
this.ownershipEvents.emit(String(id), peer);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
onElected = () => {
|
|
125
|
+
this.reconcile();
|
|
126
|
+
this.rebalanceInterval = setInterval(() => this.reconcile(), this.rebalanceIntervalMs);
|
|
127
|
+
};
|
|
128
|
+
onDemoted = () => {
|
|
129
|
+
if (this.rebalanceInterval)
|
|
130
|
+
clearInterval(this.rebalanceInterval);
|
|
131
|
+
this.rebalanceInterval = undefined;
|
|
132
|
+
this.consecutiveQuorumFailures = 0;
|
|
133
|
+
this.hasTriggeredSustainedLoss = false;
|
|
134
|
+
};
|
|
135
|
+
start() {
|
|
136
|
+
this.bully.channel.on("assign-connections", this.onAssign);
|
|
137
|
+
this.bully.channel.on("release-connections", this.onRelease);
|
|
138
|
+
this.bully.channel.on("report-holdings-request", this.onReportRequest);
|
|
139
|
+
this.bully.channel.on("update-ids", this.onUpdateIds);
|
|
140
|
+
this.bully.channel.on("ownership-map", this.onOwnershipMap);
|
|
141
|
+
this.bully.lifecycle.on("elected", this.onElected);
|
|
142
|
+
this.bully.lifecycle.on("demoted", this.onDemoted);
|
|
143
|
+
}
|
|
144
|
+
stop() {
|
|
145
|
+
this.bully.channel.off("assign-connections", this.onAssign);
|
|
146
|
+
this.bully.channel.off("release-connections", this.onRelease);
|
|
147
|
+
this.bully.channel.off("report-holdings-request", this.onReportRequest);
|
|
148
|
+
this.bully.channel.off("update-ids", this.onUpdateIds);
|
|
149
|
+
this.bully.channel.off("ownership-map", this.onOwnershipMap);
|
|
150
|
+
this.bully.lifecycle.off("elected", this.onElected);
|
|
151
|
+
this.bully.lifecycle.off("demoted", this.onDemoted);
|
|
152
|
+
this.onDemoted();
|
|
153
|
+
for (const unsubscribe of this.activeSubscriptions)
|
|
154
|
+
unsubscribe();
|
|
155
|
+
}
|
|
156
|
+
[Symbol.dispose]() {
|
|
157
|
+
this.stop();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Leader-only: change the full set of ids that should be distributed across the cluster —
|
|
161
|
+
* e.g. after re-querying the database this bundle came from and finding it grew or shrank.
|
|
162
|
+
* Broadcasts the new set to every peer (so whichever pod is leader next already has it) and
|
|
163
|
+
* immediately reconciles, rather than waiting for the next periodic tick.
|
|
164
|
+
*/
|
|
165
|
+
async updateIds(ids) {
|
|
166
|
+
if (!this.bully.isLeader()) {
|
|
167
|
+
this.logger.warn("updateIds() called while not leader, ignoring");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await this.forum.broadcast("update-ids", ids);
|
|
171
|
+
await this.reconcile();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Gathers what every reachable peer currently holds, computes the fair target split of all
|
|
175
|
+
* ids across those peers, broadcasts the full id→peer map so any pod can look up an owner
|
|
176
|
+
* locally via getOwner() without asking around, and sends each peer exactly the
|
|
177
|
+
* assign/release messages needed to converge on that target.
|
|
178
|
+
*/
|
|
179
|
+
async reconcile() {
|
|
180
|
+
if (this.reconcileInFlight)
|
|
181
|
+
return;
|
|
182
|
+
this.reconcileInFlight = true;
|
|
183
|
+
try {
|
|
184
|
+
await this.doReconcile();
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
this.reconcileInFlight = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async doReconcile() {
|
|
191
|
+
const reports = new Map();
|
|
192
|
+
const { self } = this.bully.getStatus();
|
|
193
|
+
if (self)
|
|
194
|
+
reports.set(self, this.getHeldIds());
|
|
195
|
+
const collector = (payload) => {
|
|
196
|
+
reports.set(payload.peer, payload.ids);
|
|
197
|
+
};
|
|
198
|
+
this.bully.channel.on("holdings-report", collector);
|
|
199
|
+
await this.forum.broadcast("report-holdings-request");
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, this.reportTimeoutMs));
|
|
201
|
+
this.bully.channel.off("holdings-report", collector);
|
|
202
|
+
const reporters = [...reports.keys()].sort();
|
|
203
|
+
if (reporters.length === 0) {
|
|
204
|
+
this.logger.warn("Reconciled: no peer reported in, nothing we can do");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (this.expectedClusterSize) {
|
|
208
|
+
const majority = Math.floor(this.expectedClusterSize / 2) + 1;
|
|
209
|
+
if (reporters.length < majority) {
|
|
210
|
+
this.consecutiveQuorumFailures++;
|
|
211
|
+
this.logger.warn(`Reconciled: only ${reporters.length}/${this.expectedClusterSize} peer(s) reachable ` +
|
|
212
|
+
`(need ${majority} for quorum), refusing to reconcile ` +
|
|
213
|
+
`[${this.consecutiveQuorumFailures}/${this.quorumFailureThreshold} consecutive failures]`);
|
|
214
|
+
if (this.consecutiveQuorumFailures >= this.quorumFailureThreshold && !this.hasTriggeredSustainedLoss) {
|
|
215
|
+
this.hasTriggeredSustainedLoss = true;
|
|
216
|
+
this.logger.error(`Sustained quorum loss after ${this.consecutiveQuorumFailures} consecutive attempts, ` +
|
|
217
|
+
`invoking onSustainedQuorumLoss`);
|
|
218
|
+
this.sustainedQuorumLossCallback();
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.consecutiveQuorumFailures = 0;
|
|
224
|
+
this.hasTriggeredSustainedLoss = false;
|
|
225
|
+
const fairShare = Math.floor(this.allIds.length / reporters.length);
|
|
226
|
+
const remainder = this.allIds.length % reporters.length;
|
|
227
|
+
let cursor = 0;
|
|
228
|
+
const targets = new Map();
|
|
229
|
+
reporters.forEach((peer, index) => {
|
|
230
|
+
const count = fairShare + (index < remainder ? 1 : 0);
|
|
231
|
+
targets.set(peer, new Set(this.allIds.slice(cursor, cursor + count)));
|
|
232
|
+
cursor += count;
|
|
233
|
+
});
|
|
234
|
+
// Broadcast unconditionally, even on a pass where nothing moves — a peer's cached map is
|
|
235
|
+
// otherwise only as fresh as the last pass that happened to change something.
|
|
236
|
+
const ownershipEntries = [];
|
|
237
|
+
for (const [peer, ids] of targets)
|
|
238
|
+
for (const id of ids)
|
|
239
|
+
ownershipEntries.push([id, peer]);
|
|
240
|
+
await this.forum.broadcast("ownership-map", ownershipEntries);
|
|
241
|
+
const assignments = new Map();
|
|
242
|
+
const releases = new Map();
|
|
243
|
+
for (const peer of reporters) {
|
|
244
|
+
const current = new Set(reports.get(peer));
|
|
245
|
+
const target = targets.get(peer);
|
|
246
|
+
const toAdd = [...target].filter((id) => !current.has(id));
|
|
247
|
+
const toRemove = [...current].filter((id) => !target.has(id));
|
|
248
|
+
if (toAdd.length > 0)
|
|
249
|
+
assignments.set(peer, toAdd);
|
|
250
|
+
if (toRemove.length > 0)
|
|
251
|
+
releases.set(peer, toRemove);
|
|
252
|
+
}
|
|
253
|
+
if (assignments.size === 0 && releases.size === 0) {
|
|
254
|
+
this.logger.debug("Reconciled: already balanced, nothing to do");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.logger.info(`Reconciled: converging ${reporters.length} peer(s) on a fair split ` +
|
|
258
|
+
`(${assignments.size} gaining ids, ${releases.size} releasing ids)`);
|
|
259
|
+
await Promise.all([
|
|
260
|
+
...[...assignments.entries()].map(([peer, ids]) => this.forum.tell(peer, "assign-connections", ids)),
|
|
261
|
+
...[...releases.entries()].map(([peer, ids]) => this.forum.tell(peer, "release-connections", ids)),
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import type { Logger } from "./logger";
|
|
3
|
+
export type TransportOptions = {
|
|
4
|
+
fetchFn?: typeof fetch;
|
|
5
|
+
internalPort?: number;
|
|
6
|
+
internalHost?: string;
|
|
7
|
+
requestTimeoutMs?: number;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
};
|
|
10
|
+
export type TransportCallbacks = {
|
|
11
|
+
onElectionMessage: (fromId: string) => void;
|
|
12
|
+
onCoordinatorMessage: (id: string) => void;
|
|
13
|
+
onMessage: (event: string, payload: unknown) => void;
|
|
14
|
+
channel: EventEmitter;
|
|
15
|
+
};
|
|
16
|
+
export declare class Transport {
|
|
17
|
+
readonly fetchFn: typeof fetch;
|
|
18
|
+
readonly port: number;
|
|
19
|
+
private readonly internalHost?;
|
|
20
|
+
private readonly requestTimeoutMs;
|
|
21
|
+
private readonly logger;
|
|
22
|
+
private server?;
|
|
23
|
+
constructor(opts?: TransportOptions);
|
|
24
|
+
postToPeer(host: string, path: string, body: unknown): Promise<boolean>;
|
|
25
|
+
pingPeer(host: string): Promise<boolean>;
|
|
26
|
+
start(cb: TransportCallbacks): void;
|
|
27
|
+
stop(): void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/core/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,oBAAoB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,OAAO,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,qBAAa,SAAS;IACpB,SAAgB,OAAO,EAAE,OAAO,KAAK,CAAC;IACtC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,MAAM,CAAC,CAAS;IAExB,YAAY,IAAI,GAAE,gBAAqB,EAMtC;IAEY,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAYnF;IAEY,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASpD;IAEM,KAAK,CAAC,EAAE,EAAE,kBAAkB,GAAG,IAAI,CA4CzC;IAEM,IAAI,IAAI,IAAI,CAElB;CACF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
export class Transport {
|
|
3
|
+
fetchFn;
|
|
4
|
+
port;
|
|
5
|
+
internalHost;
|
|
6
|
+
requestTimeoutMs;
|
|
7
|
+
logger;
|
|
8
|
+
server;
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.fetchFn = opts.fetchFn ?? fetch;
|
|
11
|
+
this.port = opts.internalPort ?? Number(process.env.INTERNAL_PORT || 4001);
|
|
12
|
+
this.internalHost = opts.internalHost;
|
|
13
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? 2000;
|
|
14
|
+
this.logger = opts.logger ?? console;
|
|
15
|
+
}
|
|
16
|
+
async postToPeer(host, path, body) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await this.fetchFn(`http://${host}:${this.port}${path}`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
23
|
+
});
|
|
24
|
+
return res.ok;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async pingPeer(host) {
|
|
31
|
+
try {
|
|
32
|
+
const res = await this.fetchFn(`http://${host}:${this.port}/health`, {
|
|
33
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
34
|
+
});
|
|
35
|
+
return res.ok;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
start(cb) {
|
|
42
|
+
const app = express();
|
|
43
|
+
app.use(express.json());
|
|
44
|
+
app.get("/health", (_, res) => {
|
|
45
|
+
res.json({ status: "OK", timestamp: new Date().toISOString() });
|
|
46
|
+
});
|
|
47
|
+
app.post("/bully/election", (req, res) => {
|
|
48
|
+
cb.onElectionMessage(req.body?.id);
|
|
49
|
+
res.sendStatus(200);
|
|
50
|
+
});
|
|
51
|
+
app.post("/bully/coordinator", (req, res) => {
|
|
52
|
+
cb.onCoordinatorMessage(req.body?.id);
|
|
53
|
+
res.sendStatus(200);
|
|
54
|
+
});
|
|
55
|
+
app.post("/bully/message", (req, res) => {
|
|
56
|
+
cb.onMessage(req.body?.event, req.body?.payload);
|
|
57
|
+
res.sendStatus(200);
|
|
58
|
+
});
|
|
59
|
+
app.get("/channel/stream/:event", (req, res) => {
|
|
60
|
+
const eventName = req.params.event;
|
|
61
|
+
res.writeHead(200, {
|
|
62
|
+
"Content-Type": "text/event-stream",
|
|
63
|
+
"Cache-Control": "no-cache",
|
|
64
|
+
Connection: "keep-alive",
|
|
65
|
+
});
|
|
66
|
+
const forward = (payload) => res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
67
|
+
cb.channel.on(eventName, forward);
|
|
68
|
+
req.on("close", () => cb.channel.off(eventName, forward));
|
|
69
|
+
});
|
|
70
|
+
app.use((err, _req, res, _next) => {
|
|
71
|
+
this.logger.error(err);
|
|
72
|
+
res.status(500).json({ status: "ERROR", error: err?.message ?? String(err) });
|
|
73
|
+
});
|
|
74
|
+
const onListening = () => this.logger.info(`Internal coordination server listening on port ${this.port}`);
|
|
75
|
+
this.server = this.internalHost
|
|
76
|
+
? app.listen(this.port, this.internalHost, onListening)
|
|
77
|
+
: app.listen(this.port, onListening);
|
|
78
|
+
}
|
|
79
|
+
stop() {
|
|
80
|
+
if (this.server)
|
|
81
|
+
this.server.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Quorum } from "./core/quorum";
|
|
2
|
+
export type { QuorumOptions } from "./core/quorum";
|
|
3
|
+
export { Bully } from "./core/bully";
|
|
4
|
+
export type { BullyPeer, BullyCluster, Discovery, BullyOptions } from "./core/bully";
|
|
5
|
+
export { Forum } from "./core/forum";
|
|
6
|
+
export type { ForumOptions } from "./core/forum";
|
|
7
|
+
export { Transport } from "./core/transport";
|
|
8
|
+
export type { TransportOptions, TransportCallbacks } from "./core/transport";
|
|
9
|
+
export { Sharding } from "./core/sharding";
|
|
10
|
+
export type { ShardingOptions } from "./core/sharding";
|
|
11
|
+
export type { Logger } from "./core/logger";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErF,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE7E,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC"}
|