lunibee 0.3.0 → 0.3.1

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 (38) hide show
  1. package/dist/builders/embed.d.ts +1 -0
  2. package/dist/builders/index.js +6 -2
  3. package/dist/builders/index.js.map +4 -4
  4. package/dist/core/index.js +66 -15
  5. package/dist/core/index.js.map +8 -8
  6. package/dist/index.js +2345 -199
  7. package/dist/index.js.map +22 -14
  8. package/dist/managers/index.js +2 -2
  9. package/dist/managers/index.js.map +3 -3
  10. package/dist/rest/errors.d.ts +8 -0
  11. package/dist/rest/index.d.ts +2 -0
  12. package/dist/rest/index.js +65 -14
  13. package/dist/rest/index.js.map +5 -5
  14. package/dist/rest/transport.d.ts +9 -0
  15. package/dist/sharding/bus.d.ts +8 -0
  16. package/dist/sharding/cluster.d.ts +224 -14
  17. package/dist/sharding/context.d.ts +41 -0
  18. package/dist/sharding/gateway-bot.d.ts +20 -0
  19. package/dist/sharding/health.d.ts +94 -0
  20. package/dist/sharding/host.d.ts +70 -0
  21. package/dist/sharding/identify.d.ts +62 -0
  22. package/dist/sharding/index.d.ts +56 -12
  23. package/dist/sharding/index.js +2255 -164
  24. package/dist/sharding/index.js.map +18 -10
  25. package/dist/sharding/protocol.d.ts +47 -0
  26. package/dist/sharding/remote-protocol.d.ts +100 -0
  27. package/dist/sharding/remote.d.ts +118 -0
  28. package/dist/sharding/requests.d.ts +54 -0
  29. package/dist/sharding/worker.d.ts +45 -0
  30. package/dist/structures/index.js +2 -2
  31. package/dist/structures/index.js.map +3 -3
  32. package/dist/types/gateway.d.ts +3 -0
  33. package/dist/types/index.js.map +1 -1
  34. package/dist/ws/index.d.ts +1 -1
  35. package/dist/ws/index.js +2 -2
  36. package/dist/ws/index.js.map +5 -5
  37. package/dist/ws/send-budget.d.ts +1 -1
  38. package/package.json +18 -16
@@ -0,0 +1,47 @@
1
+ import type { WorkerReport } from "./health.js";
2
+ /** Marks a cluster control frame on the IPC channel, next to the shard-bus frames. */
3
+ export declare const CLUSTER_FRAME = "__lunibeeCluster";
4
+ /** The `source` and reply `target` of the parent process on the shard bus. */
5
+ export declare const PARENT_ID = -1;
6
+ /** Built-in cluster request: the worker answers with a {@link WorkerReport}. */
7
+ export declare const REQUEST_HEALTH = "__lunibee:health";
8
+ /** Built-in cluster request: the worker starts the given shards (recovery). */
9
+ export declare const REQUEST_ASSIGN = "__lunibee:assign";
10
+ /** Frames a worker sends to the parent. */
11
+ export type ChildFrame = {
12
+ kind: "ready";
13
+ shardIds: number[];
14
+ } | {
15
+ kind: "identify";
16
+ id: number;
17
+ shardId: number;
18
+ } | {
19
+ kind: "identify-done";
20
+ shardId: number;
21
+ } | {
22
+ kind: "reply";
23
+ id: string;
24
+ result?: unknown;
25
+ error?: string;
26
+ } | {
27
+ kind: "health";
28
+ report: WorkerReport;
29
+ };
30
+ /** Frames the parent sends to a worker. */
31
+ export type ParentFrame = {
32
+ kind: "identify-grant";
33
+ id: number;
34
+ } | {
35
+ kind: "identify-deny";
36
+ id: number;
37
+ error: string;
38
+ } | {
39
+ kind: "request";
40
+ id: string;
41
+ type: string;
42
+ data: unknown;
43
+ };
44
+ /** Validates the body of a control frame from a worker; `null` when it is malformed. */
45
+ export declare function parseChildFrame(value: unknown): ChildFrame | null;
46
+ /** Validates the body of a control frame from the parent; `null` when it is malformed. */
47
+ export declare function parseParentFrame(value: unknown): ParentFrame | null;
@@ -0,0 +1,100 @@
1
+ /** Version of the coordinator ↔ host wire protocol. Both sides must agree. */
2
+ export declare const REMOTE_PROTOCOL_VERSION = 1;
3
+ /** Largest text frame either side accepts, in UTF-8 bytes. */
4
+ export declare const MAX_REMOTE_FRAME_BYTES: number;
5
+ /** WebSocket close codes used between a coordinator and a host. */
6
+ export declare const RemoteClose: {
7
+ readonly BadRequest: 4400;
8
+ readonly AuthFailed: 4401;
9
+ readonly HelloTimeout: 4408;
10
+ readonly DuplicateHost: 4409;
11
+ readonly TooManyConnections: 4429;
12
+ readonly UnknownHost: 4410;
13
+ readonly VersionMismatch: 4426;
14
+ readonly HostLost: 4504;
15
+ readonly Shutdown: 4500;
16
+ };
17
+ /**
18
+ * The only environment variables a coordinator may set in a remote worker.
19
+ * Everything else comes from the host's own environment, so a coordinator
20
+ * cannot inject `NODE_OPTIONS`, `LD_PRELOAD` or a token.
21
+ */
22
+ export declare const SPAWN_ENV_KEYS: ReadonlySet<string>;
23
+ /** Signals a coordinator may send to a remote worker. */
24
+ export declare const KILL_SIGNALS: ReadonlySet<string>;
25
+ /** Whether a host name is this machine, where an unencrypted link is acceptable. */
26
+ export declare function isLoopbackHost(hostname: string): boolean;
27
+ /** A host ID: letters, digits, `.`, `_` and `-`, up to 64 characters. */
28
+ export declare const HOST_ID: RegExp;
29
+ /** Frames the coordinator sends to a host. */
30
+ export type ToHost = {
31
+ t: "challenge";
32
+ nonce: string;
33
+ version: number;
34
+ } | {
35
+ t: "welcome";
36
+ leaseMs: number;
37
+ pingMs: number;
38
+ } | {
39
+ t: "ping";
40
+ n: number;
41
+ } | {
42
+ t: "spawn";
43
+ cluster: number;
44
+ epoch: number;
45
+ env: Record<string, string>;
46
+ } | {
47
+ t: "frame";
48
+ cluster: number;
49
+ epoch: number;
50
+ frame: unknown;
51
+ } | {
52
+ t: "kill";
53
+ cluster: number;
54
+ epoch: number;
55
+ signal: string;
56
+ } | {
57
+ t: "bye";
58
+ reason: string;
59
+ };
60
+ /** Frames a host sends to the coordinator. */
61
+ export type ToCoordinator = {
62
+ t: "hello";
63
+ id: string;
64
+ mac: string;
65
+ version: number;
66
+ } | {
67
+ t: "pong";
68
+ n: number;
69
+ } | {
70
+ t: "spawned";
71
+ cluster: number;
72
+ epoch: number;
73
+ pid?: number;
74
+ } | {
75
+ t: "frame";
76
+ cluster: number;
77
+ epoch: number;
78
+ frame: unknown;
79
+ } | {
80
+ t: "exit";
81
+ cluster: number;
82
+ epoch: number;
83
+ code: number | null;
84
+ signal: string | null;
85
+ } | {
86
+ t: "error";
87
+ cluster: number;
88
+ epoch: number;
89
+ message: string;
90
+ };
91
+ /** The size of a string on the wire, in UTF-8 bytes. */
92
+ export declare function utf8Bytes(text: string): number;
93
+ /** The proof a host sends: an HMAC of the coordinator's nonce and the host ID, keyed by the shared secret. The secret itself never travels. */
94
+ export declare function signChallenge(secret: string, nonce: string, hostId: string): string;
95
+ /** Checks a host's proof in constant time. */
96
+ export declare function verifyChallenge(secret: string, nonce: string, hostId: string, mac: unknown): boolean;
97
+ /** Validates a text frame from the coordinator; `null` when malformed. */
98
+ export declare function parseToHost(text: unknown, maxBytes?: number): ToHost | null;
99
+ /** Validates a text frame from a host; `null` when malformed. */
100
+ export declare function parseToCoordinator(text: unknown, maxBytes?: number): ToCoordinator | null;
@@ -0,0 +1,118 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { ChildProcess } from "node:child_process";
3
+ /** Where, and how, a coordinator listens for hosts. */
4
+ export interface RemoteOptions {
5
+ /** TCP port. `0` picks a free one (read it from `ClusterManager#remotePort`). */
6
+ port: number;
7
+ /**
8
+ * Interface to bind. Default `127.0.0.1`, which only this machine can
9
+ * reach. Any other address needs `tls` or `allowInsecure`.
10
+ */
11
+ hostname?: string;
12
+ /** Shared secret, at least 16 characters. Hosts prove they know it; it never crosses the wire. */
13
+ secret: string;
14
+ /** TLS for `wss://`. Required to listen on a non-loopback address unless `allowInsecure` is set. */
15
+ tls?: {
16
+ cert: string;
17
+ key: string;
18
+ };
19
+ /**
20
+ * Listen on a non-loopback address without TLS: frames (shard messages
21
+ * and worker output) travel unencrypted and unsigned. Only for a network
22
+ * you fully trust, or when a TLS proxy in front terminates the connection.
23
+ * Default false.
24
+ */
25
+ allowInsecure?: boolean;
26
+ /**
27
+ * The host IDs that make up the deployment. Clusters are placed on them in
28
+ * this order, and an unlisted host is refused. Without it, the hosts that
29
+ * have joined when `spawn()` starts are used, sorted by ID.
30
+ */
31
+ hosts?: string[];
32
+ /** How many hosts must join before clusters are placed when `hosts` is not set. Default 1. */
33
+ minHosts?: number;
34
+ /** Milliseconds to wait for the hosts to join before `spawn()` fails. Default 60,000. */
35
+ joinTimeout?: number;
36
+ /**
37
+ * Fencing lease in milliseconds. A host that hears nothing from the
38
+ * coordinator for this long kills its workers; the coordinator waits twice
39
+ * as long before it treats the host as lost and restarts its clusters, so
40
+ * a shard never runs in two places. Default 15,000.
41
+ */
42
+ lease?: number;
43
+ /** Milliseconds a new connection has to authenticate before it is closed. Default 10,000. */
44
+ helloTimeout?: number;
45
+ /** Most connections that may be waiting to authenticate at once; more are closed. Default 16. */
46
+ maxUnauthenticated?: number;
47
+ /** Also run clusters on the coordinator's own machine. Default false: clusters run on the hosts only. */
48
+ local?: boolean;
49
+ }
50
+ /** A host as the coordinator sees it. */
51
+ export interface HostStatus {
52
+ id: string;
53
+ /** Whether its socket is open and answering. */
54
+ connected: boolean;
55
+ /** When it last sent anything (epoch ms). */
56
+ lastSeen: number;
57
+ /** Clusters currently running on it. */
58
+ clusters: number[];
59
+ }
60
+ type Kill = (signal: string) => boolean;
61
+ /** What the coordinator wants to hear about its hosts. */
62
+ export interface RemoteHooks {
63
+ join(id: string): void;
64
+ leave(id: string, reason: string): void;
65
+ error(error: Error): void;
66
+ }
67
+ /**
68
+ * A worker process on another host, shaped like the `ChildProcess` that
69
+ * `ClusterManager` supervises. `send()` and `kill()` travel to the host's
70
+ * agent; its messages and exit travel back, so the manager's restart,
71
+ * IDENTIFY, routing and health logic needs no special case for it.
72
+ */
73
+ export declare class RemoteChild extends EventEmitter {
74
+ #private;
75
+ readonly host: string;
76
+ readonly cluster: number;
77
+ readonly epoch: number;
78
+ pid: number | undefined;
79
+ exitCode: number | null;
80
+ signalCode: NodeJS.Signals | null;
81
+ connected: boolean;
82
+ constructor(host: string, cluster: number, epoch: number, send: (frame: unknown) => boolean, kill: Kill);
83
+ /** Sends an IPC frame to the worker. */
84
+ send(frame: unknown): boolean;
85
+ /** Asks the host to signal the worker; the exit arrives as an event. */
86
+ kill(signal?: string): boolean;
87
+ /** The agent says the worker started. */
88
+ spawned(pid: number | undefined): void;
89
+ /** The worker sent a message. */
90
+ deliver(frame: unknown): void;
91
+ /** The worker exited, or its host was lost. Emits `exit` once. */
92
+ finish(code: number | null, signal: string | null): void;
93
+ }
94
+ /** The coordinator's listener and the hosts that joined it. */
95
+ export declare class RemoteHosts {
96
+ #private;
97
+ constructor(options: RemoteOptions, hooks: RemoteHooks);
98
+ /** The port in use, once started. */
99
+ get port(): number | undefined;
100
+ /** Host IDs that are connected now, sorted. */
101
+ connected(): string[];
102
+ /** Opens the listener. Safe to call twice. */
103
+ start(): void;
104
+ /** Closes the listener and every host connection. */
105
+ stop(): void;
106
+ /** Resolves when the given hosts (or `count` hosts) are connected. @throws {Error} On timeout. */
107
+ waitFor(hosts: readonly string[] | undefined, count: number, timeoutMs: number): Promise<void>;
108
+ /** Fails everything waiting for hosts to join (a shutdown). */
109
+ abortWaits(): void;
110
+ /** Hosts and the clusters running on them. */
111
+ status(declared?: readonly string[]): HostStatus[];
112
+ /**
113
+ * Starts a worker on `hostId`. If the host is not connected the returned
114
+ * child exits at once, which `ClusterManager` handles like a crash.
115
+ */
116
+ createChild(hostId: string, cluster: number, env: Record<string, string>): ChildProcess;
117
+ }
118
+ export {};
@@ -0,0 +1,54 @@
1
+ /** One answer collected for a request. */
2
+ export interface Settled<R = unknown> {
3
+ key: string;
4
+ result?: R;
5
+ error?: string;
6
+ }
7
+ /** What a finished request collected. */
8
+ export interface Collected<R = unknown> {
9
+ /** Answers by key, in arrival order. */
10
+ replies: Settled<R>[];
11
+ /** Keys that never answered (timeout, abort or shutdown). */
12
+ missing: string[];
13
+ }
14
+ /**
15
+ * Pending requests of the parent process. Each request waits for answers from
16
+ * a set of keys (a shard, a cluster, or every shard of the live clusters) and
17
+ * settles when all arrived, the timeout passed, or the caller aborted.
18
+ *
19
+ * Answers are accepted only from the owner of the key, once per key; late,
20
+ * duplicate and unknown answers are ignored. The number of waiting requests is
21
+ * bounded, and a timer per request guarantees the entry is removed.
22
+ */
23
+ export declare class RequestTracker {
24
+ #private;
25
+ constructor(prefix?: string, maxPending?: number);
26
+ /** Requests waiting for answers. */
27
+ get size(): number;
28
+ /**
29
+ * Starts waiting for answers.
30
+ * @param keys Each key that should answer, with the cluster that owns it.
31
+ * @throws {Error} If `maxPending` requests are already waiting.
32
+ */
33
+ begin<R>(options: {
34
+ keys: ReadonlyMap<string, number>;
35
+ timeoutMs: number;
36
+ signal?: AbortSignal;
37
+ }): {
38
+ id: string;
39
+ done: Promise<Collected<R>>;
40
+ };
41
+ /**
42
+ * Records one answer.
43
+ * @param from The cluster the answer came from; it must own `key`.
44
+ * @returns Whether the answer was accepted.
45
+ */
46
+ answer(id: string, key: string, from: number, value: {
47
+ result?: unknown;
48
+ error?: string;
49
+ }): boolean;
50
+ /** A cluster went away: every key it owns that has not answered settles with `error`. */
51
+ failOwner(owner: number, error: string): void;
52
+ /** Settles every waiting request with what it has (shutdown). */
53
+ close(): void;
54
+ }
@@ -0,0 +1,45 @@
1
+ import { type ClusterContext } from "./context.js";
2
+ import type { WorkerReport } from "./health.js";
3
+ /** Asks for the right to IDENTIFY, and gives it back. */
4
+ export interface IdentifyGate {
5
+ /** Resolves when `shardId` may IDENTIFY now. Rejects if the parent refuses or goes away. */
6
+ acquire(shardId: number): Promise<void>;
7
+ /** `shardId` has sent its IDENTIFY. */
8
+ release(shardId: number): void;
9
+ }
10
+ /** The part of `process` a worker uses. */
11
+ export interface WorkerEndpoint {
12
+ send?(message: unknown): unknown;
13
+ on(event: string, listener: (...args: never[]) => void): unknown;
14
+ off(event: string, listener: (...args: never[]) => void): unknown;
15
+ }
16
+ type RequestHandler = (data: unknown) => unknown;
17
+ /**
18
+ * The worker side of a cluster. It tells the parent when the process is
19
+ * ready, answers the parent's requests, and asks the parent for IDENTIFY
20
+ * slots. `ShardManager` uses it automatically inside a cluster; use it
21
+ * directly when you run your own Gateways.
22
+ */
23
+ export declare class ClusterWorker {
24
+ #private;
25
+ /** The worker of this process, or `undefined` when it was not forked by a `ClusterManager`. */
26
+ static current(): ClusterWorker | undefined;
27
+ /** Forgets the process-wide worker (tests). */
28
+ static reset(): void;
29
+ readonly context: ClusterContext;
30
+ constructor(context: ClusterContext, endpoint?: WorkerEndpoint);
31
+ /** The gate to use when the parent coordinates IDENTIFY, otherwise `undefined`. */
32
+ get identifyGate(): IdentifyGate | undefined;
33
+ /** Tells the parent every shard of this process finished its handshake. */
34
+ ready(shardIds?: readonly number[]): void;
35
+ /** Pushes a health report to the parent without being asked. */
36
+ reportHealth(report: WorkerReport): void;
37
+ /**
38
+ * Answers requests of `type` sent with `ClusterManager.requestCluster()`.
39
+ * The return value (or thrown error) goes back to the caller.
40
+ */
41
+ onRequest(type: string, handler: RequestHandler): this;
42
+ /** Stops listening and rejects pending IDENTIFY requests. */
43
+ close(): void;
44
+ }
45
+ export {};
@@ -779,7 +779,7 @@ var mapHas = Map.prototype.has;
779
779
  // packages/rest/package.json
780
780
  var package_default = {
781
781
  name: "@lunibee/rest",
782
- version: "0.3.0",
782
+ version: "0.3.1",
783
783
  type: "module",
784
784
  module: "./src/index.ts",
785
785
  types: "./src/index.ts",
@@ -2417,4 +2417,4 @@ export {
2417
2417
  stickerURL
2418
2418
  };
2419
2419
 
2420
- //# debugId=CCB1FE6D82188C4D64756E2164756E21
2420
+ //# debugId=1F2A9DC9E180B64C64756E2164756E21