@zukhruf/single-flight 0.3.15 → 0.3.17

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 (122) hide show
  1. package/README.md +113 -93
  2. package/dist/client/flight-client.d.ts +44 -0
  3. package/dist/client/flight-client.d.ts.map +1 -0
  4. package/dist/client/flight-client.js +227 -0
  5. package/dist/client/flight-client.js.map +1 -0
  6. package/dist/codec.d.ts +12 -0
  7. package/dist/codec.d.ts.map +1 -0
  8. package/dist/codec.js +2 -0
  9. package/dist/codec.js.map +1 -0
  10. package/dist/connection/connection-supervisor.d.ts +39 -0
  11. package/dist/connection/connection-supervisor.d.ts.map +1 -0
  12. package/dist/connection/connection-supervisor.js +195 -0
  13. package/dist/connection/connection-supervisor.js.map +1 -0
  14. package/dist/connection/connection.d.ts +21 -0
  15. package/dist/connection/connection.d.ts.map +1 -0
  16. package/dist/connection/connection.js +2 -0
  17. package/dist/connection/connection.js.map +1 -0
  18. package/dist/connection/connector.d.ts +15 -0
  19. package/dist/connection/connector.d.ts.map +1 -0
  20. package/dist/connection/connector.js +2 -0
  21. package/dist/connection/connector.js.map +1 -0
  22. package/dist/connection/electing-connector.d.ts +23 -0
  23. package/dist/connection/electing-connector.d.ts.map +1 -0
  24. package/dist/connection/electing-connector.js +148 -0
  25. package/dist/connection/electing-connector.js.map +1 -0
  26. package/dist/connection/handshake.d.ts +31 -0
  27. package/dist/connection/handshake.d.ts.map +1 -0
  28. package/dist/connection/handshake.js +102 -0
  29. package/dist/connection/handshake.js.map +1 -0
  30. package/dist/connection/local-directory-connector.d.ts +14 -0
  31. package/dist/connection/local-directory-connector.d.ts.map +1 -0
  32. package/dist/connection/local-directory-connector.js +23 -0
  33. package/dist/connection/local-directory-connector.js.map +1 -0
  34. package/dist/connection/protocol-version-error.d.ts +12 -0
  35. package/dist/connection/protocol-version-error.d.ts.map +1 -0
  36. package/dist/connection/protocol-version-error.js +19 -0
  37. package/dist/connection/protocol-version-error.js.map +1 -0
  38. package/dist/connection/socket-connection.d.ts +18 -0
  39. package/dist/connection/socket-connection.d.ts.map +1 -0
  40. package/dist/connection/socket-connection.js +52 -0
  41. package/dist/connection/socket-connection.js.map +1 -0
  42. package/dist/connection/socket-path.d.ts +7 -0
  43. package/dist/connection/socket-path.d.ts.map +1 -0
  44. package/dist/connection/socket-path.js +24 -0
  45. package/dist/connection/socket-path.js.map +1 -0
  46. package/dist/coordinator/flight-coordinator.d.ts +33 -0
  47. package/dist/coordinator/flight-coordinator.d.ts.map +1 -0
  48. package/dist/coordinator/flight-coordinator.js +178 -0
  49. package/dist/coordinator/flight-coordinator.js.map +1 -0
  50. package/dist/coordinator/flight-server.d.ts +24 -0
  51. package/dist/coordinator/flight-server.d.ts.map +1 -0
  52. package/dist/coordinator/flight-server.js +74 -0
  53. package/dist/coordinator/flight-server.js.map +1 -0
  54. package/dist/coordinator/flight.d.ts +20 -0
  55. package/dist/coordinator/flight.d.ts.map +1 -0
  56. package/dist/coordinator/flight.js +39 -0
  57. package/dist/coordinator/flight.js.map +1 -0
  58. package/dist/coordinator/party.d.ts +38 -0
  59. package/dist/coordinator/party.d.ts.map +1 -0
  60. package/dist/coordinator/party.js +43 -0
  61. package/dist/coordinator/party.js.map +1 -0
  62. package/dist/coordinator/session.d.ts +16 -0
  63. package/dist/coordinator/session.d.ts.map +1 -0
  64. package/dist/coordinator/session.js +103 -0
  65. package/dist/coordinator/session.js.map +1 -0
  66. package/dist/election/leader-election.d.ts +24 -0
  67. package/dist/election/leader-election.d.ts.map +1 -0
  68. package/dist/election/leader-election.js +78 -0
  69. package/dist/election/leader-election.js.map +1 -0
  70. package/dist/election/leadership.d.ts +14 -0
  71. package/dist/election/leadership.d.ts.map +1 -0
  72. package/dist/election/leadership.js +23 -0
  73. package/dist/election/leadership.js.map +1 -0
  74. package/dist/errors.d.ts +9 -12
  75. package/dist/errors.d.ts.map +1 -1
  76. package/dist/errors.js +13 -11
  77. package/dist/errors.js.map +1 -1
  78. package/dist/index.d.ts +6 -5
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +3 -3
  81. package/dist/index.js.map +1 -1
  82. package/dist/protocol/flight-protocol.d.ts +94 -0
  83. package/dist/protocol/flight-protocol.d.ts.map +1 -0
  84. package/dist/protocol/flight-protocol.js +61 -0
  85. package/dist/protocol/flight-protocol.js.map +1 -0
  86. package/dist/shared/is-record.d.ts +3 -0
  87. package/dist/shared/is-record.d.ts.map +1 -0
  88. package/dist/shared/is-record.js +3 -0
  89. package/dist/shared/is-record.js.map +1 -0
  90. package/dist/shared/sqlite/is-busy.d.ts +3 -0
  91. package/dist/shared/sqlite/is-busy.d.ts.map +1 -0
  92. package/dist/shared/sqlite/is-busy.js +10 -0
  93. package/dist/shared/sqlite/is-busy.js.map +1 -0
  94. package/dist/single-flight.d.ts +43 -9
  95. package/dist/single-flight.d.ts.map +1 -1
  96. package/dist/single-flight.js +98 -31
  97. package/dist/single-flight.js.map +1 -1
  98. package/package.json +6 -6
  99. package/dist/file-flight-records.d.ts +0 -22
  100. package/dist/file-flight-records.d.ts.map +0 -1
  101. package/dist/file-flight-records.js +0 -124
  102. package/dist/file-flight-records.js.map +0 -1
  103. package/dist/flight-records.d.ts +0 -67
  104. package/dist/flight-records.d.ts.map +0 -1
  105. package/dist/flight-records.js +0 -38
  106. package/dist/flight-records.js.map +0 -1
  107. package/dist/flight.d.ts +0 -12
  108. package/dist/flight.d.ts.map +0 -1
  109. package/dist/flight.js +0 -38
  110. package/dist/flight.js.map +0 -1
  111. package/dist/follow.d.ts +0 -31
  112. package/dist/follow.d.ts.map +0 -1
  113. package/dist/follow.js +0 -60
  114. package/dist/follow.js.map +0 -1
  115. package/dist/patiently.d.ts +0 -3
  116. package/dist/patiently.d.ts.map +0 -1
  117. package/dist/patiently.js +0 -31
  118. package/dist/patiently.js.map +0 -1
  119. package/dist/shared-flight.d.ts +0 -25
  120. package/dist/shared-flight.d.ts.map +0 -1
  121. package/dist/shared-flight.js +0 -89
  122. package/dist/shared-flight.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-directory-connector.d.ts","sourceRoot":"","sources":["../../src/connection/local-directory-connector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAExE;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,eAAe;;gBAIjD,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe;IAKnD,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;CAM1E"}
@@ -0,0 +1,23 @@
1
+ import { assertLocalDirectory } from '@zukhruf/fs';
2
+ /**
3
+ * Refuses a flight directory on a network file system before the first
4
+ * connection exists. Every connect asks, but a directory that passed once is
5
+ * remembered, so a failover never fails here and never costs a flight that
6
+ * this process leads. Runs keep their call order, because the client queues
7
+ * them before it connects.
8
+ */
9
+ export class LocalDirectoryConnector {
10
+ #directory;
11
+ #connector;
12
+ constructor(directory, connector) {
13
+ this.#directory = directory;
14
+ this.#connector = connector;
15
+ }
16
+ async connect(signal) {
17
+ signal.throwIfAborted();
18
+ await assertLocalDirectory(this.#directory);
19
+ signal.throwIfAborted();
20
+ return this.#connector.connect(signal);
21
+ }
22
+ }
23
+ //# sourceMappingURL=local-directory-connector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-directory-connector.js","sourceRoot":"","sources":["../../src/connection/local-directory-connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAInD;;;;;;GAMG;AACH,MAAM,OAAO,uBAAuB;IACzB,UAAU,CAAS;IACnB,UAAU,CAAkB;IAErC,YAAY,SAAiB,EAAE,SAA0B;QACvD,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,MAAmB;QAC/B,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The single flight's coordinator speaks another protocol version, so this
3
+ * process cannot use it. Run one version of the protocol per flight directory.
4
+ */
5
+ export declare class ProtocolVersionError extends Error {
6
+ /** The protocol this process speaks. */
7
+ readonly ours: number;
8
+ /** The protocol the coordinator speaks, or `undefined` when it closed the handshake without saying. */
9
+ readonly theirs: number | undefined;
10
+ constructor(ours: number, theirs: number | undefined);
11
+ }
12
+ //# sourceMappingURL=protocol-version-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol-version-error.d.ts","sourceRoot":"","sources":["../../src/connection/protocol-version-error.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uGAAuG;IACvG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;gBAExB,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS;CAUrD"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The single flight's coordinator speaks another protocol version, so this
3
+ * process cannot use it. Run one version of the protocol per flight directory.
4
+ */
5
+ export class ProtocolVersionError extends Error {
6
+ /** The protocol this process speaks. */
7
+ ours;
8
+ /** The protocol the coordinator speaks, or `undefined` when it closed the handshake without saying. */
9
+ theirs;
10
+ constructor(ours, theirs) {
11
+ super(theirs === undefined
12
+ ? `The single flight's coordinator closed the handshake without an answer while it still coordinates, so it does not speak protocol version ${ours}.`
13
+ : `The single flight's coordinator speaks protocol version ${theirs}, and this process speaks version ${ours}. Run one version per flight directory.`);
14
+ this.name = 'ProtocolVersionError';
15
+ this.ours = ours;
16
+ this.theirs = theirs;
17
+ }
18
+ }
19
+ //# sourceMappingURL=protocol-version-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol-version-error.js","sourceRoot":"","sources":["../../src/connection/protocol-version-error.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,wCAAwC;IAC/B,IAAI,CAAS;IACtB,uGAAuG;IAC9F,MAAM,CAAqB;IAEpC,YAAY,IAAY,EAAE,MAA0B;QAClD,KAAK,CACH,MAAM,KAAK,SAAS;YAClB,CAAC,CAAC,4IAA4I,IAAI,GAAG;YACrJ,CAAC,CAAC,2DAA2D,MAAM,qCAAqC,IAAI,yCAAyC,CACxJ,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Socket } from 'node:net';
3
+ import type { Connection, ConnectionEvents } from './connection.ts';
4
+ /**
5
+ * Newline-delimited JSON over a stream socket. A stream has no message
6
+ * boundaries (writes arrive merged and split), and JSON escapes newlines inside
7
+ * strings, so one line is always one message. `isIncoming` checks each message
8
+ * from the peer; a line that is not one closes the connection.
9
+ */
10
+ export declare class SocketConnection<Outgoing, Incoming> extends EventEmitter<ConnectionEvents<Incoming>> implements Connection<Outgoing, Incoming> {
11
+ #private;
12
+ constructor(socket: Socket, isIncoming: (message: unknown) => message is Incoming);
13
+ send(message: Outgoing): Promise<void>;
14
+ ref(): void;
15
+ unref(): void;
16
+ close(): void;
17
+ }
18
+ //# sourceMappingURL=socket-connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-connection.d.ts","sourceRoot":"","sources":["../../src/connection/socket-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGvC,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEpE;;;;;GAKG;AACH,qBAAa,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAC9C,SAAQ,YAAY,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAC/C,YAAW,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC;;gBAKvC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,IAAI,QAAQ;IAqBvD,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAYtC,GAAG;IAIH,KAAK;IAIL,KAAK;CAGN"}
@@ -0,0 +1,52 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { createInterface } from 'node:readline';
3
+ /**
4
+ * Newline-delimited JSON over a stream socket. A stream has no message
5
+ * boundaries (writes arrive merged and split), and JSON escapes newlines inside
6
+ * strings, so one line is always one message. `isIncoming` checks each message
7
+ * from the peer; a line that is not one closes the connection.
8
+ */
9
+ export class SocketConnection extends EventEmitter {
10
+ #socket;
11
+ constructor(socket, isIncoming) {
12
+ super();
13
+ this.#socket = socket;
14
+ // Every error is followed by `close`, which is where the peer's loss is handled.
15
+ socket.on('error', () => { });
16
+ createInterface({ input: socket, crlfDelay: Infinity })
17
+ .on('line', (line) => {
18
+ try {
19
+ const parsed = JSON.parse(line);
20
+ if (isIncoming(parsed))
21
+ this.emit('message', parsed);
22
+ else
23
+ this.close();
24
+ }
25
+ catch {
26
+ this.close();
27
+ }
28
+ })
29
+ // readline re-emits socket errors (e.g. EPIPE when the peer died); `close` reports the loss.
30
+ .on('error', () => { });
31
+ socket.once('close', () => this.emit('close'));
32
+ }
33
+ send(message) {
34
+ return new Promise((resolve, reject) => {
35
+ if (!this.#socket.writable) {
36
+ reject(new Error('The socket is closed.'));
37
+ return;
38
+ }
39
+ this.#socket.write(`${JSON.stringify(message)}\n`, (error) => error ? reject(error) : resolve());
40
+ });
41
+ }
42
+ ref() {
43
+ this.#socket.ref();
44
+ }
45
+ unref() {
46
+ this.#socket.unref();
47
+ }
48
+ close() {
49
+ this.#socket.destroy();
50
+ }
51
+ }
52
+ //# sourceMappingURL=socket-connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-connection.js","sourceRoot":"","sources":["../../src/connection/socket-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAIhD;;;;;GAKG;AACH,MAAM,OAAO,gBACX,SAAQ,YAAwC;IAGvC,OAAO,CAAS;IAEzB,YACE,MAAc,EACd,UAAqD;QAErD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,iFAAiF;QACjF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC7B,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;aACpD,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACnB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACzC,IAAI,UAAU,CAAC,MAAM,CAAC;oBAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;;oBAChD,IAAI,CAAC,KAAK,EAAE,CAAC;YACpB,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,CAAC;QACH,CAAC,CAAC;YACF,6FAA6F;aAC5F,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,CAAC,OAAiB;QACpB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC,CAAC;gBAC3C,OAAO;YACT,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,CAC3D,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAClC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,GAAG;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;IACrB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A Unix socket file in the directory on macOS and Linux. On Windows, a named
3
+ * pipe: pipe names are global, so the name comes from the directory, and
4
+ * Windows paths ignore case, so the path is lowercased first.
5
+ */
6
+ export declare function socketPathFor(directory: string): string;
7
+ //# sourceMappingURL=socket-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-path.d.ts","sourceRoot":"","sources":["../../src/connection/socket-path.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAevD"}
@@ -0,0 +1,24 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { join, resolve } from 'node:path';
3
+ /** macOS allows 104 bytes including the terminating NUL; Linux allows 108. */
4
+ const SOCKET_PATH_LIMIT = 103;
5
+ /**
6
+ * A Unix socket file in the directory on macOS and Linux. On Windows, a named
7
+ * pipe: pipe names are global, so the name comes from the directory, and
8
+ * Windows paths ignore case, so the path is lowercased first.
9
+ */
10
+ export function socketPathFor(directory) {
11
+ if (process.platform === 'win32') {
12
+ const id = createHash('sha256')
13
+ .update(resolve(directory).toLowerCase())
14
+ .digest('hex')
15
+ .slice(0, 32);
16
+ return `\\\\.\\pipe\\single-flight-${id}`;
17
+ }
18
+ const socketPath = join(directory, 'flight.sock');
19
+ if (Buffer.byteLength(socketPath) > SOCKET_PATH_LIMIT) {
20
+ throw new RangeError(`The socket path ${socketPath} exceeds ${SOCKET_PATH_LIMIT} bytes; choose a shorter directory.`);
21
+ }
22
+ return socketPath;
23
+ }
24
+ //# sourceMappingURL=socket-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-path.js","sourceRoot":"","sources":["../../src/connection/socket-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;aAC5B,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;aACxC,MAAM,CAAC,KAAK,CAAC;aACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChB,OAAO,8BAA8B,EAAE,EAAE,CAAC;IAC5C,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAClD,IAAI,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,iBAAiB,EAAE,CAAC;QACtD,MAAM,IAAI,UAAU,CAClB,mBAAmB,UAAU,YAAY,iBAAiB,qCAAqC,CAChG,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { FencingToken, TokenSource } from '@zukhruf/fencing';
2
+ import type { Connection } from '../connection/connection.ts';
3
+ import type { FlightResponse, Outcome, RequestEnvelope } from '../protocol/flight-protocol.ts';
4
+ import { Flight } from './flight.ts';
5
+ import type { Party } from './party.ts';
6
+ export interface FlightCoordinatorOptions {
7
+ /** Mints the lease token of each flight this coordinator starts. */
8
+ tokens: TokenSource;
9
+ /**
10
+ * Milliseconds after start during which no flight starts, so leaders from a
11
+ * previous coordinator can reassert their flights first. Zero for a
12
+ * coordinator that never replaces another.
13
+ */
14
+ graceWindow: number;
15
+ }
16
+ /**
17
+ * Coordinates the flights of one term. A run leads a new flight of its key, or
18
+ * joins the flight of that key in progress, in one step, so two runs never
19
+ * both lead. The outcome is pushed to every joiner as soon as it lands.
20
+ */
21
+ export declare class FlightCoordinator {
22
+ #private;
23
+ constructor({ tokens, graceWindow }: FlightCoordinatorOptions);
24
+ serve(connection: Connection<FlightResponse, RequestEnvelope>): void;
25
+ run(party: Party, key: string): Promise<void>;
26
+ /** A joiner that lost its connection, back for the flight it joined: it never leads, so its work never runs twice. */
27
+ rejoin(party: Party, key: string, flight: string): Promise<void>;
28
+ reassert(party: Party, key: string, token: FencingToken): void;
29
+ land(flight: Flight, outcome: Outcome): void;
30
+ /** The leader withdrew or its connection is gone. */
31
+ interrupt(flight: Flight): void;
32
+ }
33
+ //# sourceMappingURL=flight-coordinator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-coordinator.d.ts","sourceRoot":"","sources":["../../src/coordinator/flight-coordinator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,eAAe,EAChB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAUD;;;;GAIG;AACH,qBAAa,iBAAiB;;gBAGhB,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,wBAAwB;IAgB7D,KAAK,CAAC,UAAU,EAAE,UAAU,CAAC,cAAc,EAAE,eAAe,CAAC,GAAG,IAAI;IAIpE,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,sHAAsH;IACtH,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI;IAI9D,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAI5C,qDAAqD;IACrD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAGhC"}
@@ -0,0 +1,178 @@
1
+ import { Latch } from '@zukhruf/async';
2
+ import { Flight } from "./flight.js";
3
+ import { Session } from "./session.js";
4
+ /**
5
+ * Coordinates the flights of one term. A run leads a new flight of its key, or
6
+ * joins the flight of that key in progress, in one step, so two runs never
7
+ * both lead. The outcome is pushed to every joiner as soon as it lands.
8
+ */
9
+ export class FlightCoordinator {
10
+ #phase;
11
+ constructor({ tokens, graceWindow }) {
12
+ const flights = new Map();
13
+ const coordinating = new Coordinating(flights, tokens);
14
+ if (graceWindow > 0) {
15
+ const over = new Latch();
16
+ this.#phase = new GraceWindow(flights, over, coordinating);
17
+ setTimeout(() => {
18
+ // Coordinating is current before the latch lets the waiting runs resume.
19
+ this.#phase = coordinating;
20
+ over.open();
21
+ }, graceWindow).unref();
22
+ }
23
+ else {
24
+ this.#phase = coordinating;
25
+ }
26
+ }
27
+ serve(connection) {
28
+ new Session(this, connection);
29
+ }
30
+ run(party, key) {
31
+ return this.#phase.run(party, key);
32
+ }
33
+ /** A joiner that lost its connection, back for the flight it joined: it never leads, so its work never runs twice. */
34
+ rejoin(party, key, flight) {
35
+ return this.#phase.rejoin(party, key, flight);
36
+ }
37
+ reassert(party, key, token) {
38
+ this.#phase.reassert(party, key, token);
39
+ }
40
+ land(flight, outcome) {
41
+ this.#phase.land(flight, outcome);
42
+ }
43
+ /** The leader withdrew or its connection is gone. */
44
+ interrupt(flight) {
45
+ this.#phase.interrupt(flight);
46
+ }
47
+ }
48
+ /** The only phase that starts flights. A leader that reasserts now is too late. */
49
+ class Coordinating {
50
+ #flights;
51
+ #tokens;
52
+ constructor(flights, tokens) {
53
+ this.#flights = flights;
54
+ this.#tokens = tokens;
55
+ }
56
+ async run(party, key) {
57
+ // A token that goes unused is harmless: tokens only need to grow.
58
+ const token = await this.#tokens.next(key);
59
+ // The client may have withdrawn the run while the token was minted.
60
+ if (party.role.kind !== 'waiting')
61
+ return;
62
+ const inProgress = this.#flights.get(key);
63
+ if (inProgress)
64
+ return inProgress.add(party);
65
+ const flight = new Flight(key, token, party);
66
+ this.#flights.set(key, flight);
67
+ party.lead(flight);
68
+ }
69
+ /** A rejoin joins only its own flight; any other answer could be a second run of the work. */
70
+ async rejoin(party, key, flight) {
71
+ if (party.role.kind !== 'waiting')
72
+ return;
73
+ const inProgress = this.#flights.get(key);
74
+ if (inProgress?.is(flight))
75
+ return inProgress.add(party);
76
+ party.end({ op: 'interrupted', id: party.id });
77
+ }
78
+ reassert(party) {
79
+ party.end({ op: 'rejected', id: party.id });
80
+ }
81
+ land(flight, outcome) {
82
+ this.#forget(flight);
83
+ flight.land(outcome);
84
+ }
85
+ interrupt(flight) {
86
+ this.#forget(flight);
87
+ flight.interrupt();
88
+ }
89
+ #forget(flight) {
90
+ if (this.#flights.get(flight.key) === flight) {
91
+ this.#flights.delete(flight.key);
92
+ }
93
+ }
94
+ }
95
+ /** Starts no flight, so leaders from a previous coordinator can reassert theirs first. */
96
+ class GraceWindow {
97
+ #flights;
98
+ #over;
99
+ #next;
100
+ #endings = new Map();
101
+ /** Wakes the rejoins that wait for a reassertion of their key. */
102
+ #wakers = new Map();
103
+ constructor(flights, over, next) {
104
+ this.#flights = flights;
105
+ this.#over = over;
106
+ this.#next = next;
107
+ }
108
+ /** A fresh run leads or joins only once every leader had its chance to reassert. */
109
+ async run(party, key) {
110
+ await this.#over.wait();
111
+ return this.#next.run(party, key);
112
+ }
113
+ /**
114
+ * A rejoin gets its flight's outcome when the flight's leader reasserted it
115
+ * and it ends during the window, and joins it while it is in progress. Once
116
+ * the window ends, it is answered as in any other phase.
117
+ */
118
+ async rejoin(party, key, flight) {
119
+ const over = this.#over.wait().then(() => 'over');
120
+ for (;;) {
121
+ if (party.role.kind !== 'waiting')
122
+ return;
123
+ const inProgress = this.#flights.get(key);
124
+ if (inProgress?.is(flight))
125
+ return inProgress.add(party);
126
+ const ending = this.#endings.get(key);
127
+ if (ending?.token === flight) {
128
+ return party.end('landed' in ending
129
+ ? { op: 'landed', id: party.id, outcome: ending.landed }
130
+ : { op: 'interrupted', id: party.id });
131
+ }
132
+ const woken = await Promise.race([over, this.#reassertionOf(key)]);
133
+ if (woken === 'over')
134
+ return this.#next.rejoin(party, key, flight);
135
+ }
136
+ }
137
+ /** For two claims on one key, the newer token wins, and the older flight is lost. */
138
+ reassert(party, key, token) {
139
+ const claimed = this.#flights.get(key);
140
+ if (claimed && !token.isNewerThan(claimed.token)) {
141
+ return party.end({ op: 'rejected', id: party.id });
142
+ }
143
+ if (claimed) {
144
+ claimed.leader.end({ op: 'rejected', id: claimed.leader.id });
145
+ this.interrupt(claimed);
146
+ }
147
+ const flight = new Flight(key, token, party);
148
+ this.#flights.set(key, flight);
149
+ party.resume(flight);
150
+ const wakers = this.#wakers.get(key);
151
+ this.#wakers.delete(key);
152
+ for (const wake of wakers ?? [])
153
+ wake();
154
+ }
155
+ land(flight, outcome) {
156
+ this.#endings.set(flight.key, {
157
+ token: flight.token.toString(),
158
+ landed: outcome,
159
+ });
160
+ this.#next.land(flight, outcome);
161
+ }
162
+ interrupt(flight) {
163
+ this.#endings.set(flight.key, {
164
+ token: flight.token.toString(),
165
+ interrupted: true,
166
+ });
167
+ this.#next.interrupt(flight);
168
+ }
169
+ #reassertionOf(key) {
170
+ const { promise, resolve } = Promise.withResolvers();
171
+ let wakers = this.#wakers.get(key);
172
+ if (!wakers)
173
+ this.#wakers.set(key, (wakers = new Set()));
174
+ wakers.add(() => resolve('reasserted'));
175
+ return promise;
176
+ }
177
+ }
178
+ //# sourceMappingURL=flight-coordinator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-coordinator.js","sourceRoot":"","sources":["../../src/coordinator/flight-coordinator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { Leadership } from '../election/leadership.ts';
2
+ export interface FlightServerOptions {
3
+ /** Milliseconds the new term starts no flight, so leaders from the last term can reassert theirs. */
4
+ graceWindow: number;
5
+ }
6
+ /**
7
+ * Serves a `FlightCoordinator` on a Unix socket for one term. Lease tokens
8
+ * carry the term's epoch, so they outrank every token of earlier terms.
9
+ */
10
+ export declare class FlightServer {
11
+ #private;
12
+ private constructor();
13
+ static start(socketPath: string, leadership: Leadership, { graceWindow }: FlightServerOptions): Promise<FlightServer>;
14
+ /**
15
+ * Hands the term over. Ending every connection tells the other processes to
16
+ * elect a successor and reassert their flights during its grace window;
17
+ * `close` alone would wait for clients that never disconnect on their own.
18
+ * Each connection ends only after what was written to it, such as an
19
+ * outcome for a joiner, is sent. The term ends only after the socket is
20
+ * gone: in the other order, this close could remove a successor's socket file.
21
+ */
22
+ close(): Promise<void>;
23
+ }
24
+ //# sourceMappingURL=flight-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-server.d.ts","sourceRoot":"","sources":["../../src/coordinator/flight-server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAQ5D,MAAM,WAAW,mBAAmB;IAClC,qGAAqG;IACrG,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,YAAY;;IAKvB,OAAO;WAUM,KAAK,CAChB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,UAAU,EACtB,EAAE,WAAW,EAAE,EAAE,mBAAmB,GACnC,OAAO,CAAC,YAAY,CAAC;IAwCxB;;;;;;;OAOG;IACG,KAAK;CAQZ"}
@@ -0,0 +1,74 @@
1
+ import { unlink } from 'node:fs/promises';
2
+ import { createServer } from 'node:net';
3
+ import { EpochTokenSource } from '@zukhruf/fencing';
4
+ import { isErrno } from '@zukhruf/fs';
5
+ import { welcome } from "../connection/handshake.js";
6
+ import { SocketConnection } from "../connection/socket-connection.js";
7
+ import { isRequestEnvelope, } from "../protocol/flight-protocol.js";
8
+ import { FlightCoordinator } from "./flight-coordinator.js";
9
+ /**
10
+ * Serves a `FlightCoordinator` on a Unix socket for one term. Lease tokens
11
+ * carry the term's epoch, so they outrank every token of earlier terms.
12
+ */
13
+ export class FlightServer {
14
+ #server;
15
+ #connections;
16
+ #leadership;
17
+ constructor(server, connections, leadership) {
18
+ this.#server = server;
19
+ this.#connections = connections;
20
+ this.#leadership = leadership;
21
+ }
22
+ static async start(socketPath, leadership, { graceWindow }) {
23
+ // Only the coordinator gets here, so removing a dead coordinator's socket file cannot race another server.
24
+ // Windows removes a named pipe when its process stops, so there is no file to remove.
25
+ if (process.platform !== 'win32') {
26
+ await unlink(socketPath).catch((error) => {
27
+ if (!isErrno(error, 'ENOENT'))
28
+ throw error;
29
+ });
30
+ }
31
+ const coordinator = new FlightCoordinator({
32
+ tokens: new EpochTokenSource(leadership.epoch),
33
+ graceWindow,
34
+ });
35
+ const connections = new Set();
36
+ // Serving others never keeps the coordinator alive: when its own work ends, it exits and another process takes over.
37
+ const server = createServer((socket) => {
38
+ socket.unref();
39
+ connections.add(socket);
40
+ socket.once('close', () => connections.delete(socket));
41
+ // Only a process that speaks this protocol is served, so this coordinator never reads a message it would misread.
42
+ void welcome(socket).then((speaksOurs) => {
43
+ if (!speaksOurs)
44
+ return;
45
+ coordinator.serve(new SocketConnection(socket, isRequestEnvelope));
46
+ });
47
+ });
48
+ await new Promise((resolve, reject) => {
49
+ server.once('error', reject);
50
+ server.listen(socketPath, () => {
51
+ server.off('error', reject);
52
+ resolve();
53
+ });
54
+ });
55
+ server.unref();
56
+ return new FlightServer(server, connections, leadership);
57
+ }
58
+ /**
59
+ * Hands the term over. Ending every connection tells the other processes to
60
+ * elect a successor and reassert their flights during its grace window;
61
+ * `close` alone would wait for clients that never disconnect on their own.
62
+ * Each connection ends only after what was written to it, such as an
63
+ * outcome for a joiner, is sent. The term ends only after the socket is
64
+ * gone: in the other order, this close could remove a successor's socket file.
65
+ */
66
+ async close() {
67
+ const closed = new Promise((resolve) => this.#server.close(() => resolve()));
68
+ for (const socket of this.#connections)
69
+ socket.destroySoon();
70
+ await closed;
71
+ await this.#leadership.resign();
72
+ }
73
+ }
74
+ //# sourceMappingURL=flight-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-server.js","sourceRoot":"","sources":["../../src/coordinator/flight-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { FencingToken } from '@zukhruf/fencing';
2
+ import type { Outcome } from '../protocol/flight-protocol.ts';
3
+ import type { Party } from './party.ts';
4
+ /** A flight in progress: the key, the leader that runs the work, its lease token, and the parties that joined. */
5
+ export declare class Flight {
6
+ #private;
7
+ readonly key: string;
8
+ readonly token: FencingToken;
9
+ readonly leader: Party;
10
+ constructor(key: string, token: FencingToken, leader: Party);
11
+ /** Whether this is the flight with the token `flight`, which a rejoin names. */
12
+ is(flight: string): boolean;
13
+ add(joiner: Party): void;
14
+ remove(joiner: Party): void;
15
+ /** Pushes the outcome to every joiner, then tells the leader that it was handed over. */
16
+ land(outcome: Outcome): void;
17
+ /** Ends without an outcome: every joiner learns that its flight is lost, and none runs the work again. */
18
+ interrupt(): void;
19
+ }
20
+ //# sourceMappingURL=flight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight.d.ts","sourceRoot":"","sources":["../../src/coordinator/flight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,kHAAkH;AAClH,qBAAa,MAAM;;IACjB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;gBAGX,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK;IAM3D,gFAAgF;IAChF,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAI3B,GAAG,CAAC,MAAM,EAAE,KAAK;IAKjB,MAAM,CAAC,MAAM,EAAE,KAAK;IAIpB,yFAAyF;IACzF,IAAI,CAAC,OAAO,EAAE,OAAO;IAQrB,0GAA0G;IAC1G,SAAS;CAMV"}
@@ -0,0 +1,39 @@
1
+ /** A flight in progress: the key, the leader that runs the work, its lease token, and the parties that joined. */
2
+ export class Flight {
3
+ key;
4
+ token;
5
+ leader;
6
+ #joiners = new Set();
7
+ constructor(key, token, leader) {
8
+ this.key = key;
9
+ this.token = token;
10
+ this.leader = leader;
11
+ }
12
+ /** Whether this is the flight with the token `flight`, which a rejoin names. */
13
+ is(flight) {
14
+ return flight === this.token.toString();
15
+ }
16
+ add(joiner) {
17
+ this.#joiners.add(joiner);
18
+ joiner.join(this);
19
+ }
20
+ remove(joiner) {
21
+ this.#joiners.delete(joiner);
22
+ }
23
+ /** Pushes the outcome to every joiner, then tells the leader that it was handed over. */
24
+ land(outcome) {
25
+ for (const joiner of this.#joiners) {
26
+ joiner.end({ op: 'landed', id: joiner.id, outcome });
27
+ }
28
+ this.#joiners.clear();
29
+ this.leader.end({ op: 'ack', id: this.leader.id });
30
+ }
31
+ /** Ends without an outcome: every joiner learns that its flight is lost, and none runs the work again. */
32
+ interrupt() {
33
+ for (const joiner of this.#joiners) {
34
+ joiner.end({ op: 'interrupted', id: joiner.id });
35
+ }
36
+ this.#joiners.clear();
37
+ }
38
+ }
39
+ //# sourceMappingURL=flight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight.js","sourceRoot":"","sources":["../../src/coordinator/flight.ts"],"names":[],"mappings":"AAKA,kHAAkH;AAClH,MAAM,OAAO,MAAM;IACR,GAAG,CAAS;IACZ,KAAK,CAAe;IACpB,MAAM,CAAQ;IACd,QAAQ,GAAG,IAAI,GAAG,EAAS,CAAC;IAErC,YAAY,GAAW,EAAE,KAAmB,EAAE,MAAa;QACzD,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,gFAAgF;IAChF,EAAE,CAAC,MAAc;QACf,OAAO,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1C,CAAC;IAED,GAAG,CAAC,MAAa;QACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,CAAC,MAAa;QAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,yFAAyF;IACzF,IAAI,CAAC,OAAgB;QACnB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACvD,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,0GAA0G;IAC1G,SAAS;QACP,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF"}