@zukhruf/single-flight 0.3.15 → 0.3.16

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 (144) hide show
  1. package/README.md +110 -91
  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 +222 -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 +80 -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/local-directory/local-directory.d.ts +7 -0
  83. package/dist/local-directory/local-directory.d.ts.map +1 -0
  84. package/dist/local-directory/local-directory.js +75 -0
  85. package/dist/local-directory/local-directory.js.map +1 -0
  86. package/dist/local-directory/network-directory-error.d.ts +8 -0
  87. package/dist/local-directory/network-directory-error.d.ts.map +1 -0
  88. package/dist/local-directory/network-directory-error.js +13 -0
  89. package/dist/local-directory/network-directory-error.js.map +1 -0
  90. package/dist/protocol/flight-protocol.d.ts +94 -0
  91. package/dist/protocol/flight-protocol.d.ts.map +1 -0
  92. package/dist/protocol/flight-protocol.js +60 -0
  93. package/dist/protocol/flight-protocol.js.map +1 -0
  94. package/dist/shared/fs/durable-write.d.ts +7 -0
  95. package/dist/shared/fs/durable-write.d.ts.map +1 -0
  96. package/dist/shared/fs/durable-write.js +47 -0
  97. package/dist/shared/fs/durable-write.js.map +1 -0
  98. package/dist/shared/fs/errno.d.ts +2 -0
  99. package/dist/shared/fs/errno.d.ts.map +1 -0
  100. package/dist/shared/fs/errno.js +4 -0
  101. package/dist/shared/fs/errno.js.map +1 -0
  102. package/dist/{patiently.d.ts → shared/fs/patiently.d.ts} +1 -1
  103. package/dist/shared/fs/patiently.d.ts.map +1 -0
  104. package/dist/{patiently.js → shared/fs/patiently.js} +7 -8
  105. package/dist/shared/fs/patiently.js.map +1 -0
  106. package/dist/shared/fs/replace-file.d.ts +8 -0
  107. package/dist/shared/fs/replace-file.d.ts.map +1 -0
  108. package/dist/shared/fs/replace-file.js +12 -0
  109. package/dist/shared/fs/replace-file.js.map +1 -0
  110. package/dist/shared/is-record.d.ts +3 -0
  111. package/dist/shared/is-record.d.ts.map +1 -0
  112. package/dist/shared/is-record.js +3 -0
  113. package/dist/shared/is-record.js.map +1 -0
  114. package/dist/shared/sqlite/is-busy.d.ts +3 -0
  115. package/dist/shared/sqlite/is-busy.d.ts.map +1 -0
  116. package/dist/shared/sqlite/is-busy.js +10 -0
  117. package/dist/shared/sqlite/is-busy.js.map +1 -0
  118. package/dist/single-flight.d.ts +41 -9
  119. package/dist/single-flight.d.ts.map +1 -1
  120. package/dist/single-flight.js +91 -31
  121. package/dist/single-flight.js.map +1 -1
  122. package/package.json +2 -2
  123. package/dist/file-flight-records.d.ts +0 -22
  124. package/dist/file-flight-records.d.ts.map +0 -1
  125. package/dist/file-flight-records.js +0 -124
  126. package/dist/file-flight-records.js.map +0 -1
  127. package/dist/flight-records.d.ts +0 -67
  128. package/dist/flight-records.d.ts.map +0 -1
  129. package/dist/flight-records.js +0 -38
  130. package/dist/flight-records.js.map +0 -1
  131. package/dist/flight.d.ts +0 -12
  132. package/dist/flight.d.ts.map +0 -1
  133. package/dist/flight.js +0 -38
  134. package/dist/flight.js.map +0 -1
  135. package/dist/follow.d.ts +0 -31
  136. package/dist/follow.d.ts.map +0 -1
  137. package/dist/follow.js +0 -60
  138. package/dist/follow.js.map +0 -1
  139. package/dist/patiently.d.ts.map +0 -1
  140. package/dist/patiently.js.map +0 -1
  141. package/dist/shared-flight.d.ts +0 -25
  142. package/dist/shared-flight.d.ts.map +0 -1
  143. package/dist/shared-flight.js +0 -89
  144. package/dist/shared-flight.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zukhruf/single-flight
2
2
 
3
- Callers of one key share the flight in progress, in one process or across the processes of a host. A caller that comes while a flight runs does not start a second flight and does not fail with a "busy" error. It joins the flight and gets its outcome. This pattern is also known as single-flight or request coalescing, as in Go's `golang.org/x/sync/singleflight`.
3
+ Callers of one key share the flight in progress, in one process and across all the processes that use one directory. A caller that comes while a flight runs does not start a second flight and does not fail with a "busy" error. It joins the flight and gets its outcome. This pattern is also known as single-flight or request coalescing, as in Go's `golang.org/x/sync/singleflight`.
4
4
 
5
5
  The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
6
6
 
@@ -14,11 +14,18 @@ A user runs `sync`. A second `sync` starts while the first one runs. The mutex o
14
14
  The user wanted a sync to happen. Thus the second `sync` must tell the user that a sync runs, wait for it, and report its outcome. That is a join:
15
15
 
16
16
  ```ts
17
+ import { mkdtemp, rm } from 'node:fs/promises';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
17
20
  import { setTimeout as delay } from 'node:timers/promises';
18
21
 
19
22
  import { SingleFlight } from '@zukhruf/single-flight';
20
23
 
21
- const flights = new SingleFlight<string>();
24
+ const directory = await mkdtemp(join(tmpdir(), 'reports-'));
25
+ const flights = new SingleFlight<string>({
26
+ directory,
27
+ codec: { encode: (report) => report, decode: (text) => text },
28
+ });
22
29
  let builds = 0;
23
30
 
24
31
  async function buildReport() {
@@ -33,6 +40,8 @@ const calls = [1, 2, 3].map(() =>
33
40
  }),
34
41
  );
35
42
  console.log(await Promise.all(calls), { builds });
43
+ await flights[Symbol.asyncDispose]();
44
+ await rm(directory, { recursive: true, force: true });
36
45
  ```
37
46
 
38
47
  Output:
@@ -47,7 +56,7 @@ A report is being built. Waiting for it…
47
56
  ] { builds: 1 }
48
57
  ```
49
58
 
50
- The first caller is the leader: its call runs the flight. The other two callers join it. `onJoin` runs once for each joiner, before it waits. A call after the flight ended starts a new flight: a flight is not a cache. To keep a value after its flight, see the mutex recipe [Compute a value once and share it](../mutex/docs/recipes/compute-once-and-share-it.md).
59
+ The first caller is the leader: its call runs the work of the flight. The other two callers join it. `onJoin` runs once for each joiner, before it waits. A call after the flight ended starts a new flight: a flight is not a cache. To keep a value after its flight, see the mutex recipe [Compute a value once and share it](../mutex/docs/recipes/compute-once-and-share-it.md).
51
60
 
52
61
  ## Use it
53
62
 
@@ -57,126 +66,133 @@ This project is an experiment.
57
66
  npm install @zukhruf/single-flight @zukhruf/mutex
58
67
  ```
59
68
 
60
- `@zukhruf/mutex` is a peer dependency. `SharedFlight` uses its mutex, and `SingleFlight` does not.
69
+ `@zukhruf/mutex` is a peer dependency. The lease that the work gets, and its token, come from it.
70
+
71
+ ## The directory
72
+
73
+ The directory is the local folder where the processes that share flights meet. Each process that uses a `SingleFlight` keeps three files there: `flight.lock`, `flight.epoch` and `flight.sock`. All the processes that name the same directory are one group, and the callers of one key share a flight in that group only.
61
74
 
62
- ## In one process: `SingleFlight`
75
+ There is no default directory. One default would put each application on the host into one group, and unrelated applications that use the same key would join each other's flights. Give each group its own directory, for example a folder of your application's data.
76
+
77
+ - The directory must be on a local file system. On a network file system, a call rejects with `NetworkDirectoryError`.
78
+ - On macOS and Linux, the path of `flight.sock` must have 103 bytes or fewer. A longer path throws a `RangeError` when you create the `SingleFlight`. On Windows, the socket is a named pipe, and the length of the directory does not matter.
79
+ - A single flight and a socket lock store of `@zukhruf/mutex` can use one directory. They use different files, and they never meet.
80
+
81
+ ## Run a flight
63
82
 
64
83
  `flights.run(key, work, { onJoin, signal })` resolves with `{ value, joined }`. `joined` is `false` for the leader and `true` for each joiner.
65
84
 
66
- - A failed flight rejects each caller with the original error of the work.
67
- - `work` gets a signal that aborts when the flight is abandoned. Read it only when the work can stop early.
85
+ All the processes of the directory take part in one election. The winner is the coordinator. Each call asks the coordinator, in one step, to lead a new flight of the key or to join the flight in progress. When the work of the leader ends, the coordinator pushes the outcome to each joiner. Callers in the same process go through the coordinator too, also in worker threads. [ADR 0003](./docs/adr/0003-a-caller-leads-or-joins-in-one-request-to-an-elected-coordinator.md) tells why.
86
+
87
+ The work gets the lease of the flight:
88
+
89
+ - `token`: a fencing token. Its high 32 bits are the epoch of the coordinator's term, so a newer term always gives higher tokens.
90
+ - `signal`: it aborts with `LockLostError` when the flight is no longer the leader's. This occurs when the leader misses the grace window of a new coordinator.
68
91
 
69
- ## Across processes: `SharedFlight`
92
+ | Option | What it does |
93
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `directory` | The local folder where the processes of one group meet. See [The directory](#the-directory). |
95
+ | `codec` | Turns the value of a flight into text for the joiners, and back. See [The codec](#the-codec). |
96
+ | `graceWindow` | Milliseconds that a new coordinator starts no flight, so the leaders of the flights in progress can reassert them. It must be longer than a process takes to connect again. Defaults to `500`. |
70
97
 
71
- A `SharedFlight` makes a flight visible to the other processes of the host. It uses three parts:
98
+ `flights[Symbol.asyncDispose]()` waits until each landing of this process reached a coordinator, and then it disconnects. When this process is the coordinator, the other processes elect a new one.
72
99
 
73
- - A `SingleFlight`: the callers in one process share one flight first.
74
- - The mutex: the holder of the key is the leader. The other processes learn that the key is busy.
75
- - The records: the leader writes the flight record, and joiners in other processes read it.
100
+ ## The codec
101
+
102
+ The value of a flight goes to the joiners as text. The codec has two functions: `encode` turns a value into text, and `decode` turns the text back into a value. The coordinator only carries the text. Thus the format is yours: JSON, or a format that keeps what JSON drops, such as `undefined` and `Map`. The leader gets `decode(encode(value))` too, so each caller gets the value in the same shape.
76
103
 
77
104
  ```ts
78
105
  import { mkdtemp, rm } from 'node:fs/promises';
79
106
  import { tmpdir } from 'node:os';
80
107
  import { join } from 'node:path';
81
108
 
82
- import { Mutex, SqliteStore } from '@zukhruf/mutex';
83
- import { FileFlightRecords, SharedFlight } from '@zukhruf/single-flight';
84
-
85
- const directory = await mkdtemp(join(tmpdir(), 'app-'));
109
+ import { type Codec, SingleFlight } from '@zukhruf/single-flight';
86
110
 
87
- const flights = new SharedFlight({
88
- mutex: new Mutex(new SqliteStore(join(directory, 'locks'))),
89
- records: new FileFlightRecords(join(directory, 'flights')),
90
- parse: (value) => {
91
- if (typeof value !== 'string') throw new TypeError('Not a sync summary.');
92
- return value;
93
- },
94
- });
111
+ interface Sync {
112
+ at: Date;
113
+ files: number;
114
+ }
95
115
 
96
- const { value, joined } = await flights.run(
97
- 'sync',
98
- async () => 'synced 42 files',
99
- {
100
- onJoin: () => console.log('A sync is running. Waiting for it…'),
116
+ const syncs: Codec<Sync> = {
117
+ encode: (sync) => JSON.stringify(sync),
118
+ decode: (text) => {
119
+ const { at, files } = JSON.parse(text);
120
+ return { at: new Date(at), files };
101
121
  },
102
- );
103
- console.log({ value, joined });
122
+ };
123
+
124
+ const directory = await mkdtemp(join(tmpdir(), 'syncs-'));
125
+ const flights = new SingleFlight({ directory, codec: syncs });
126
+ const { value } = await flights.run('sync', async () => ({
127
+ at: new Date('2026-10-09T12:00:00Z'),
128
+ files: 42,
129
+ }));
130
+ console.log(value.at instanceof Date, value.files);
131
+ await flights[Symbol.asyncDispose]();
104
132
  await rm(directory, { recursive: true, force: true });
105
133
  ```
106
134
 
107
135
  Output:
108
136
 
109
137
  ```
110
- { value: 'synced 42 files', joined: false }
138
+ true 42
111
139
  ```
112
140
 
113
- The program with three processes is in the recipe [Join a run that is still in flight](./docs/recipes/join-a-run-in-flight.md).
114
-
115
- | Option | What it does |
116
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
- | `mutex` | Decides which caller is the leader. All processes must use one lock directory, with a host lock store such as `SqliteStore`. |
118
- | `records` | Keeps the flight records. All processes must use the same records. Give the records a directory of their own, never the lock directory. |
119
- | `parse` | Turns a value read from a flight record into your type. The value travels as JSON, so a `Date` arrives as a string. The leader also gets `parse` of the JSON value. |
120
- | `pollInterval` | Milliseconds between two reads of a flight record while a caller joins. Defaults to `100`. |
121
-
122
- The work gets the lease of the key. A leader whose lease is lost records the flight as interrupted, and the call rejects with the reason of the lease.
123
-
124
- A joiner never acquires the key. It reads the flight record until the flight has an outcome, and it asks the mutex only whether a running flight still has a holder. [ADR 0001](./docs/adr/0001-a-joiner-follows-its-flight-record-without-acquiring-the-key.md) tells why.
141
+ A codec that cannot encode the value fails the flight for all callers. A codec that cannot decode the text fails only the call that decodes it.
125
142
 
126
143
  ## Errors
127
144
 
128
- | The caller is | The flight | The call rejects with |
129
- | -------------------------------------- | ------------------------------------------------------ | ------------------------------ |
130
- | The leader, or a joiner in its process | Failed | The original error of the work |
131
- | A joiner in another process | Failed | `FlightFailedError` |
132
- | A joiner in another process | Interrupted: the holder stopped, or its lease was lost | `FlightInterruptedError` |
133
- | A joiner in another process | Its flight record is gone, before the joiner read it | `FlightOutcomeLostError` |
134
- | The leader, or a joiner in its process | Ended, and the records refused its outcome | The error of the records |
135
- | A joiner in another process | Ended, and the records refused its outcome | `FlightInterruptedError` |
136
-
137
- `FlightFailedError` has the `key` and the `failure`: the `name`, the `message` and the `code` of the error. A thrown value that is not an error keeps only its text. A value that JSON cannot carry, such as a `BigInt`, fails the flight.
145
+ | The caller is | The flight | The call rejects with |
146
+ | ------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- |
147
+ | The leader | Its work threw | The original error of the work |
148
+ | A joiner | Its work threw | `FlightFailedError` |
149
+ | A joiner | Interrupted: the leader's process stopped, or it lost the lease | `FlightInterruptedError` |
150
+ | The leader | It lost the lease | The reason of the lease's signal: `LockLostError` |
151
+ | Each caller | `encode` threw in the leader | The leader: the error of `encode`. A joiner: `FlightFailedError` |
152
+ | One caller | Its own `decode` threw | The error of `decode`, for that caller only |
153
+ | Each caller | The coordinator speaks another protocol version | `ProtocolVersionError` |
154
+ | Each caller | The directory is on a network file system | `NetworkDirectoryError` |
155
+ | Each caller | The `SingleFlight` is disposed | `Error('This single flight is closed.')` |
138
156
 
139
- When the holder of a flight stops and the next holder begins a new flight, the new flight is the successor. A joiner of the stopped flight continues with the successor and gets its outcome.
157
+ A joiner in the leader's own process is a joiner too: it gets `FlightFailedError`, not the original error. `FlightFailedError` has the `key` and the `failure`: the `name`, the `message` and the `code` of the error. A thrown value that is not an error keeps only its text.
140
158
 
141
159
  ## Cancel a wait
142
160
 
143
- Give a signal to stop the wait of one caller: `flights.run(key, work, { signal: AbortSignal.timeout(30_000) })`. The call rejects with the reason of the signal. The flight continues for the other callers, also when the leader's caller cancels.
161
+ Give a signal to stop the wait of one caller: `flights.run(key, work, { signal: AbortSignal.timeout(30_000) })`. The call rejects with the reason of the signal. The flight continues for the other callers. When the caller of the leader cancels, the work continues and lands for the joiners. A call whose signal aborted before the call rejects at once: it never leads and never joins. [ADR 0004](./docs/adr/0004-a-cancel-withdraws-only-its-caller.md) tells why.
144
162
 
145
- When all callers of a flight cancel, the flight is abandoned:
163
+ ## When a process stops
146
164
 
147
- - The next call of the key starts a new flight.
148
- - In `SingleFlight`, the signal of the work aborts.
149
- - In `SharedFlight`, a process that joins stops to read the flight record, and it never starts a flight for callers that left. A flight that a process leads continues to its end, because joiners in other processes can wait for its outcome.
165
+ - **The process of a leader stops.** The coordinator tells each joiner that the flight is interrupted, and each joiner rejects with `FlightInterruptedError` at once. No joiner runs the work again. The next call of the key leads a new flight.
166
+ - **The coordinator stops.** The other processes elect a new coordinator. Its term starts with a grace window. In the grace window:
167
+ - Each leader reasserts its flight, and sends its landing again when it has one.
168
+ - Each joiner rejoins its flight by the flight's token, and it gets the outcome of that flight.
169
+ - A new call waits for the grace window to end. Then it leads or joins.
170
+ - **A leader misses the grace window**, for example because its process was frozen. Its lease is lost: the signal of the lease aborts with `LockLostError`, and its call rejects with that error. Its joiners get `FlightInterruptedError`.
171
+ - **A joiner's leader and the coordinator stop together.** The joiner gets `FlightInterruptedError` at the end of the grace window. It never runs the work again.
150
172
 
151
- [ADR 0002](./docs/adr/0002-a-flight-that-all-callers-left-is-abandoned.md) tells how other tools solve this.
173
+ A call that was on its way to a coordinator that stopped joined nothing. It goes again to the next coordinator as a new call. [ADR 0005](./docs/adr/0005-a-joiner-rejoins-its-flight-by-its-token.md) tells why.
152
174
 
153
- ## Records
175
+ ## Changes from 0.3.x
154
176
 
155
- `new FileFlightRecords(directory, { keepFor })` keeps one JSON file for each key. The name of the file is the SHA-256 of the key, so any key is a valid file name. The leader replaces the file in one step, so a joiner never reads a part of it.
156
-
157
- `keepFor` is the keep window, in milliseconds. It defaults to `60_000`. Keep it at least 20 times the `pollInterval` of each joiner. A joiner that reads the flight record after the keep window gets `FlightOutcomeLostError`.
158
-
159
- To keep the flight records in another place, for example a table of your database, write your own `FlightRecords`:
160
-
161
- ```ts
162
- interface FlightRecords {
163
- begin(key: string): Promise<string>;
164
- finish(key: string, id: string, outcome: Finished): Promise<void>;
165
- latest(key: string): Promise<LatestFlight | undefined>;
166
- outcome(key: string, id: string): Promise<Outcome>;
167
- }
168
- ```
177
+ Version 0.3.13 to 0.3.15 had a pull design. These parts changed:
169
178
 
170
- - `begin` records a new running flight and returns a new id. A flight of the key that is still running becomes interrupted, and the new flight is its successor.
171
- - `finish` records the outcome. It does nothing when the flight is not running.
172
- - `outcome` returns `running`, the outcome, or `missing` when the flight record is gone.
173
- - Only the holder of the key calls `begin` and `finish`. Joiners call only `latest` and `outcome`.
179
+ - `SharedFlight`, `FlightRecords` and `FileFlightRecords` are gone. Each `SingleFlight` now works across processes. Give it a `directory` and a `codec`.
180
+ - `parse` is gone. The `codec` replaces it.
181
+ - The records, `keepFor` and `pollInterval` are gone. The coordinator pushes each outcome, so nothing is kept and nothing is read again. `FlightOutcomeLostError` is gone too.
182
+ - A joiner in the leader's own process now gets `FlightFailedError`, not the original error of the work. Each caller goes through the coordinator.
183
+ - The signal that told the work that all its callers left is gone. The work gets the lease of the flight.
184
+ - A caller whose signal aborted before the call rejects at once.
185
+ - A joiner of an interrupted flight no longer continues with the next flight of the key. It gets `FlightInterruptedError`.
174
186
 
175
187
  ## Documentation
176
188
 
177
189
  - [Join a run that is still in flight](./docs/recipes/join-a-run-in-flight.md): three processes, one sync.
178
- - [ADR 0001: A joiner follows its flight record without acquiring the key](./docs/adr/0001-a-joiner-follows-its-flight-record-without-acquiring-the-key.md)
179
- - [ADR 0002: A flight that all callers left is abandoned](./docs/adr/0002-a-flight-that-all-callers-left-is-abandoned.md)
190
+ - [ADR 0003: A caller leads or joins in one request to an elected coordinator](./docs/adr/0003-a-caller-leads-or-joins-in-one-request-to-an-elected-coordinator.md)
191
+ - [ADR 0004: A cancel withdraws only its caller](./docs/adr/0004-a-cancel-withdraws-only-its-caller.md)
192
+ - [ADR 0005: A joiner rejoins its flight by its token](./docs/adr/0005-a-joiner-rejoins-its-flight-by-its-token.md)
193
+ - [ADR 0006: The election and the connection are a copy of the mutex code](./docs/adr/0006-the-election-and-the-connection-are-a-copy-of-the-mutex-code.md)
194
+ - [Code copied from the mutex](./docs/copied-from-mutex.md): each copied file, what changed, and why.
195
+ - Superseded: [ADR 0001](./docs/adr/0001-a-joiner-follows-its-flight-record-without-acquiring-the-key.md) and [ADR 0002](./docs/adr/0002-a-flight-that-all-callers-left-is-abandoned.md).
180
196
 
181
197
  ## Development
182
198
 
@@ -189,16 +205,19 @@ npx nx run single-flight:typecheck # formats, lints, then type checks
189
205
  npx nx run single-flight:build # compiles src/ to dist/
190
206
  ```
191
207
 
192
- The tests run from `src/`, not from `dist/`. The tests across processes start `src/testing/caller.fixture.ts` in child processes.
208
+ The tests run from `src/`, not from `dist/`. The tests across processes start the script of `src/testing/caller.ts` in child processes.
193
209
 
194
210
  ```
195
211
  src/
196
- single-flight.ts SingleFlight: the flights of one process
197
- flight.ts one flight of this process and its callers
198
- shared-flight.ts SharedFlight: the leader, and joiners across processes
199
- follow.ts how a joiner finds and follows a flight record
200
- flight-records.ts the FlightRecords interface and the outcomes
201
- file-flight-records.ts FileFlightRecords: one JSON file for each key
202
- errors.ts FlightFailedError, FlightInterruptedError, FlightOutcomeLostError
203
- testing/ the caller process that the tests start
212
+ single-flight.ts SingleFlight: each call goes to the coordinator
213
+ codec.ts the Codec interface
214
+ errors.ts FlightFailedError, FlightInterruptedError
215
+ client/ the requests of one process: runs, leads, landings
216
+ coordinator/ the coordinator of one term: flights, sessions, its server
217
+ protocol/ the messages between a process and its coordinator
218
+ election/ the election in a directory (a copy of the mutex code)
219
+ connection/ the socket, the handshake and the supervisor (a copy)
220
+ local-directory/ the check for a network file system (a copy)
221
+ shared/ file and SQLite helpers (a copy)
222
+ testing/ the caller process that the tests start
204
223
  ```
@@ -0,0 +1,44 @@
1
+ import { FencingToken } from '@zukhruf/mutex';
2
+ import type { ConnectionSupervisor } from '../connection/connection-supervisor.ts';
3
+ import type { FlightRequest, FlightResponse, Outcome } from '../protocol/flight-protocol.ts';
4
+ /** This process leads a flight: it runs the work and lands the outcome. */
5
+ export interface Lead {
6
+ readonly token: FencingToken;
7
+ /** Aborts with a `LockLostError` once the flight is no longer this leader's. */
8
+ readonly signal: AbortSignal;
9
+ /** Ends the flight with `outcome` for every joiner. Kept and sent again until a coordinator acknowledges it. */
10
+ land(outcome: Outcome): void;
11
+ }
12
+ /** How the coordinator answered a run for good. */
13
+ export type Answer = {
14
+ kind: 'lead';
15
+ lead: Lead;
16
+ } | {
17
+ kind: 'landed';
18
+ outcome: Outcome;
19
+ } | {
20
+ kind: 'interrupted';
21
+ };
22
+ export interface RunRequest {
23
+ signal?: AbortSignal | undefined;
24
+ onJoin?: (() => void) | undefined;
25
+ }
26
+ /**
27
+ * Sends runs to the coordinator over a supervised connection. When the
28
+ * connection is replaced, it reasserts the flights this process leads, sends
29
+ * their landings again, and sends again the runs that wait for an answer. A
30
+ * run that had joined a flight rejoins that flight only, so it never leads; a
31
+ * run that got no answer had joined nothing, and goes again as a plain run.
32
+ */
33
+ export declare class FlightClient {
34
+ #private;
35
+ constructor(link: ConnectionSupervisor<FlightRequest, FlightResponse>);
36
+ run(key: string, { signal, onJoin }: RunRequest): Promise<Answer>;
37
+ /**
38
+ * Disconnects for good, once every landing reached a coordinator: a landing
39
+ * is the outcome its joiners wait for. The coordinator then interrupts the
40
+ * flights this process still leads without an outcome.
41
+ */
42
+ close(): Promise<void>;
43
+ }
44
+ //# sourceMappingURL=flight-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-client.d.ts","sourceRoot":"","sources":["../../src/client/flight-client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAiB,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AACnF,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,OAAO,EACR,MAAM,gCAAgC,CAAC;AAExC,2EAA2E;AAC3E,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,gHAAgH;IAChH,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAED,mDAAmD;AACnD,MAAM,MAAM,MAAM,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAE5B,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CACnC;AAuBD;;;;;;GAMG;AACH,qBAAa,YAAY;;gBAOX,IAAI,EAAE,oBAAoB,CAAC,aAAa,EAAE,cAAc,CAAC;IAQ/D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BvE;;;;OAIG;IACG,KAAK;CAiKZ"}
@@ -0,0 +1,222 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { untilAborted } from '@zukhruf/async';
3
+ import { FencingToken, LockLostError } from '@zukhruf/mutex';
4
+ /**
5
+ * Sends runs to the coordinator over a supervised connection. When the
6
+ * connection is replaced, it reasserts the flights this process leads, sends
7
+ * their landings again, and sends again the runs that wait for an answer. A
8
+ * run that had joined a flight rejoins that flight only, so it never leads; a
9
+ * run that got no answer had joined nothing, and goes again as a plain run.
10
+ */
11
+ export class FlightClient {
12
+ #link;
13
+ #pending = new Map();
14
+ #held = new Map();
15
+ /** Wakes `close` each time a flight this process leads is acknowledged or lost. */
16
+ #released = new Set();
17
+ constructor(link) {
18
+ this.#link = link;
19
+ link.on('connected', () => this.#resume());
20
+ link.on('message', (response) => this.#receive(response));
21
+ link.on('disconnected', () => this.#interrupt());
22
+ link.on('failed', (error) => this.#fail(error));
23
+ }
24
+ async run(key, { signal, onJoin }) {
25
+ signal?.throwIfAborted();
26
+ if (this.#link.status === 'closed') {
27
+ throw new Error('This single flight is closed.');
28
+ }
29
+ const id = randomUUID();
30
+ const { promise, resolve, reject } = Promise.withResolvers();
31
+ const pending = {
32
+ key,
33
+ onJoin,
34
+ delivery: 'queued',
35
+ flight: undefined,
36
+ resolve,
37
+ reject,
38
+ };
39
+ this.#pending.set(id, pending);
40
+ this.#updateRef();
41
+ this.#link.open();
42
+ this.#dispatch(id, pending);
43
+ try {
44
+ return await untilAborted(promise, signal);
45
+ }
46
+ catch (error) {
47
+ if (signal?.aborted)
48
+ this.#withdraw(id);
49
+ throw error;
50
+ }
51
+ }
52
+ /**
53
+ * Disconnects for good, once every landing reached a coordinator: a landing
54
+ * is the outcome its joiners wait for. The coordinator then interrupts the
55
+ * flights this process still leads without an outcome.
56
+ */
57
+ async close() {
58
+ while ([...this.#held.values()].some(({ landing }) => landing)) {
59
+ await new Promise((resolve) => this.#released.add(resolve));
60
+ }
61
+ const closing = this.#link.close();
62
+ for (const id of [...this.#pending.keys()]) {
63
+ this.#take(id)?.reject(new Error('This single flight is closed.'));
64
+ }
65
+ await closing;
66
+ }
67
+ #dispatch(id, pending) {
68
+ const { key, flight } = pending;
69
+ const request = flight === undefined
70
+ ? { op: 'run', id, key }
71
+ : { op: 'run', id, key, flight };
72
+ if (this.#link.send(request))
73
+ pending.delivery = 'sent';
74
+ }
75
+ /**
76
+ * The caller gave up. A run the coordinator may have heard is withdrawn;
77
+ * one that already leads is not pending any more, so its flight runs on.
78
+ */
79
+ #withdraw(id) {
80
+ const pending = this.#take(id);
81
+ if (pending?.delivery === 'sent')
82
+ this.#link.send({ op: 'cancel', id });
83
+ }
84
+ #hold(id, key, token) {
85
+ const held = {
86
+ key,
87
+ token,
88
+ lost: new AbortController(),
89
+ landing: undefined,
90
+ };
91
+ this.#held.set(id, held);
92
+ return {
93
+ token,
94
+ signal: held.lost.signal,
95
+ land: (outcome) => {
96
+ // A lost flight is no longer this leader's to land.
97
+ if (this.#held.get(id) !== held || held.landing)
98
+ return;
99
+ held.landing = outcome;
100
+ this.#updateRef();
101
+ this.#link.send({ op: 'land', id, outcome });
102
+ },
103
+ };
104
+ }
105
+ /** The flight of `id` is no longer this leader's, so its lease is told and nothing reasserts it. */
106
+ #lose(id) {
107
+ const held = this.#held.get(id);
108
+ if (!held)
109
+ return;
110
+ this.#release(id);
111
+ held.lost.abort(new LockLostError(held.key));
112
+ }
113
+ /** The flight of `id` needs nothing more from this process. */
114
+ #release(id) {
115
+ this.#held.delete(id);
116
+ this.#updateRef();
117
+ const waiting = [...this.#released];
118
+ this.#released.clear();
119
+ for (const wake of waiting)
120
+ wake();
121
+ }
122
+ #receive(response) {
123
+ switch (response.op) {
124
+ case 'lead': {
125
+ // A run withdrawn meanwhile sent `cancel`, which interrupts the flight at the coordinator.
126
+ const pending = this.#take(response.id);
127
+ if (!pending)
128
+ return;
129
+ const token = new FencingToken(BigInt(response.token));
130
+ pending.resolve({
131
+ kind: 'lead',
132
+ lead: this.#hold(response.id, pending.key, token),
133
+ });
134
+ return;
135
+ }
136
+ case 'joined': {
137
+ const pending = this.#pending.get(response.id);
138
+ if (!pending)
139
+ return;
140
+ const first = pending.flight === undefined;
141
+ pending.flight = response.flight;
142
+ if (first)
143
+ this.#tellJoined(response.id, pending);
144
+ return;
145
+ }
146
+ case 'landed':
147
+ this.#take(response.id)?.resolve({
148
+ kind: 'landed',
149
+ outcome: response.outcome,
150
+ });
151
+ return;
152
+ case 'interrupted':
153
+ this.#take(response.id)?.resolve({ kind: 'interrupted' });
154
+ return;
155
+ case 'ack':
156
+ // The joiners have the outcome, so there is nothing left to reassert.
157
+ if (this.#held.get(response.id)?.landing)
158
+ this.#release(response.id);
159
+ return;
160
+ case 'rejected':
161
+ this.#lose(response.id);
162
+ return;
163
+ case 'unsupported':
164
+ this.#take(response.id)?.reject(new Error("The single flight's coordinator does not know a run."));
165
+ return;
166
+ }
167
+ }
168
+ /** The caller's `onJoin` runs once per run; a throw from it fails that run only. */
169
+ #tellJoined(id, pending) {
170
+ try {
171
+ pending.onJoin?.();
172
+ }
173
+ catch (error) {
174
+ this.#withdraw(id);
175
+ pending.reject(error);
176
+ }
177
+ }
178
+ /** A new connection: reassert the flights this process leads and land them again, then send what waits. */
179
+ #resume() {
180
+ // A new connection starts with its adapter's default, which may not match whether this client waits.
181
+ this.#updateRef();
182
+ for (const [id, { key, token, landing }] of this.#held) {
183
+ this.#link.send({ op: 'reassert', id, key, token: token.toString() });
184
+ if (landing)
185
+ this.#link.send({ op: 'land', id, outcome: landing });
186
+ }
187
+ for (const [id, pending] of this.#pending) {
188
+ if (pending.delivery === 'queued')
189
+ this.#dispatch(id, pending);
190
+ }
191
+ }
192
+ /** Nothing sent on the lost connection will be answered, so every run goes again on the next one. */
193
+ #interrupt() {
194
+ for (const pending of this.#pending.values())
195
+ pending.delivery = 'queued';
196
+ }
197
+ /** No coordinator heard the reassertions, so the flights this process leads may be interrupted already. */
198
+ #fail(error) {
199
+ for (const id of [...this.#held.keys()])
200
+ this.#lose(id);
201
+ for (const id of [...this.#pending.keys()])
202
+ this.#take(id)?.reject(error);
203
+ }
204
+ /** Removes a run that is about to be answered, so a late answer for it is ignored. */
205
+ #take(id) {
206
+ const pending = this.#pending.get(id);
207
+ if (!pending)
208
+ return undefined;
209
+ this.#pending.delete(id);
210
+ this.#updateRef();
211
+ return pending;
212
+ }
213
+ /** A run waiting for its answer, or a landing not yet acknowledged, keeps the process alive. */
214
+ #updateRef() {
215
+ const landing = [...this.#held.values()].some(({ landing }) => landing);
216
+ if (this.#pending.size > 0 || landing)
217
+ this.#link.ref();
218
+ else
219
+ this.#link.unref();
220
+ }
221
+ }
222
+ //# sourceMappingURL=flight-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flight-client.js","sourceRoot":"","sources":["../../src/client/flight-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Turns a flight's value into text and back. The value crosses to the
3
+ * processes whose callers joined the flight, and the coordinator only carries
4
+ * the text, so the format is the caller's: JSON, or any other that keeps what
5
+ * JSON would drop. The leader gets `decode(encode(value))` too, so every
6
+ * caller sees the value in the same shape.
7
+ */
8
+ export interface Codec<T> {
9
+ encode(value: T): string;
10
+ decode(text: string): T;
11
+ }
12
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC;IACzB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;CACzB"}
package/dist/codec.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.js","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":""}