@zukhruf/single-flight 0.3.14 → 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.
- package/README.md +110 -91
- package/dist/client/flight-client.d.ts +44 -0
- package/dist/client/flight-client.d.ts.map +1 -0
- package/dist/client/flight-client.js +222 -0
- package/dist/client/flight-client.js.map +1 -0
- package/dist/codec.d.ts +12 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +2 -0
- package/dist/codec.js.map +1 -0
- package/dist/connection/connection-supervisor.d.ts +39 -0
- package/dist/connection/connection-supervisor.d.ts.map +1 -0
- package/dist/connection/connection-supervisor.js +195 -0
- package/dist/connection/connection-supervisor.js.map +1 -0
- package/dist/connection/connection.d.ts +21 -0
- package/dist/connection/connection.d.ts.map +1 -0
- package/dist/connection/connection.js +2 -0
- package/dist/connection/connection.js.map +1 -0
- package/dist/connection/connector.d.ts +15 -0
- package/dist/connection/connector.d.ts.map +1 -0
- package/dist/connection/connector.js +2 -0
- package/dist/connection/connector.js.map +1 -0
- package/dist/connection/electing-connector.d.ts +23 -0
- package/dist/connection/electing-connector.d.ts.map +1 -0
- package/dist/connection/electing-connector.js +148 -0
- package/dist/connection/electing-connector.js.map +1 -0
- package/dist/connection/handshake.d.ts +31 -0
- package/dist/connection/handshake.d.ts.map +1 -0
- package/dist/connection/handshake.js +102 -0
- package/dist/connection/handshake.js.map +1 -0
- package/dist/connection/local-directory-connector.d.ts +14 -0
- package/dist/connection/local-directory-connector.d.ts.map +1 -0
- package/dist/connection/local-directory-connector.js +23 -0
- package/dist/connection/local-directory-connector.js.map +1 -0
- package/dist/connection/protocol-version-error.d.ts +12 -0
- package/dist/connection/protocol-version-error.d.ts.map +1 -0
- package/dist/connection/protocol-version-error.js +19 -0
- package/dist/connection/protocol-version-error.js.map +1 -0
- package/dist/connection/socket-connection.d.ts +18 -0
- package/dist/connection/socket-connection.d.ts.map +1 -0
- package/dist/connection/socket-connection.js +52 -0
- package/dist/connection/socket-connection.js.map +1 -0
- package/dist/connection/socket-path.d.ts +7 -0
- package/dist/connection/socket-path.d.ts.map +1 -0
- package/dist/connection/socket-path.js +24 -0
- package/dist/connection/socket-path.js.map +1 -0
- package/dist/coordinator/flight-coordinator.d.ts +33 -0
- package/dist/coordinator/flight-coordinator.d.ts.map +1 -0
- package/dist/coordinator/flight-coordinator.js +178 -0
- package/dist/coordinator/flight-coordinator.js.map +1 -0
- package/dist/coordinator/flight-server.d.ts +24 -0
- package/dist/coordinator/flight-server.d.ts.map +1 -0
- package/dist/coordinator/flight-server.js +74 -0
- package/dist/coordinator/flight-server.js.map +1 -0
- package/dist/coordinator/flight.d.ts +20 -0
- package/dist/coordinator/flight.d.ts.map +1 -0
- package/dist/coordinator/flight.js +39 -0
- package/dist/coordinator/flight.js.map +1 -0
- package/dist/coordinator/party.d.ts +38 -0
- package/dist/coordinator/party.d.ts.map +1 -0
- package/dist/coordinator/party.js +43 -0
- package/dist/coordinator/party.js.map +1 -0
- package/dist/coordinator/session.d.ts +16 -0
- package/dist/coordinator/session.d.ts.map +1 -0
- package/dist/coordinator/session.js +103 -0
- package/dist/coordinator/session.js.map +1 -0
- package/dist/election/leader-election.d.ts +24 -0
- package/dist/election/leader-election.d.ts.map +1 -0
- package/dist/election/leader-election.js +80 -0
- package/dist/election/leader-election.js.map +1 -0
- package/dist/election/leadership.d.ts +14 -0
- package/dist/election/leadership.d.ts.map +1 -0
- package/dist/election/leadership.js +23 -0
- package/dist/election/leadership.js.map +1 -0
- package/dist/errors.d.ts +9 -12
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -11
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/local-directory/local-directory.d.ts +7 -0
- package/dist/local-directory/local-directory.d.ts.map +1 -0
- package/dist/local-directory/local-directory.js +75 -0
- package/dist/local-directory/local-directory.js.map +1 -0
- package/dist/local-directory/network-directory-error.d.ts +8 -0
- package/dist/local-directory/network-directory-error.d.ts.map +1 -0
- package/dist/local-directory/network-directory-error.js +13 -0
- package/dist/local-directory/network-directory-error.js.map +1 -0
- package/dist/protocol/flight-protocol.d.ts +94 -0
- package/dist/protocol/flight-protocol.d.ts.map +1 -0
- package/dist/protocol/flight-protocol.js +60 -0
- package/dist/protocol/flight-protocol.js.map +1 -0
- package/dist/shared/fs/durable-write.d.ts +7 -0
- package/dist/shared/fs/durable-write.d.ts.map +1 -0
- package/dist/shared/fs/durable-write.js +47 -0
- package/dist/shared/fs/durable-write.js.map +1 -0
- package/dist/shared/fs/errno.d.ts +2 -0
- package/dist/shared/fs/errno.d.ts.map +1 -0
- package/dist/shared/fs/errno.js +4 -0
- package/dist/shared/fs/errno.js.map +1 -0
- package/dist/{patiently.d.ts → shared/fs/patiently.d.ts} +1 -1
- package/dist/shared/fs/patiently.d.ts.map +1 -0
- package/dist/{patiently.js → shared/fs/patiently.js} +7 -8
- package/dist/shared/fs/patiently.js.map +1 -0
- package/dist/shared/fs/replace-file.d.ts +8 -0
- package/dist/shared/fs/replace-file.d.ts.map +1 -0
- package/dist/shared/fs/replace-file.js +12 -0
- package/dist/shared/fs/replace-file.js.map +1 -0
- package/dist/shared/is-record.d.ts +3 -0
- package/dist/shared/is-record.d.ts.map +1 -0
- package/dist/shared/is-record.js +3 -0
- package/dist/shared/is-record.js.map +1 -0
- package/dist/shared/sqlite/is-busy.d.ts +3 -0
- package/dist/shared/sqlite/is-busy.d.ts.map +1 -0
- package/dist/shared/sqlite/is-busy.js +10 -0
- package/dist/shared/sqlite/is-busy.js.map +1 -0
- package/dist/single-flight.d.ts +41 -9
- package/dist/single-flight.d.ts.map +1 -1
- package/dist/single-flight.js +91 -31
- package/dist/single-flight.js.map +1 -1
- package/package.json +2 -2
- package/dist/file-flight-records.d.ts +0 -22
- package/dist/file-flight-records.d.ts.map +0 -1
- package/dist/file-flight-records.js +0 -124
- package/dist/file-flight-records.js.map +0 -1
- package/dist/flight-records.d.ts +0 -67
- package/dist/flight-records.d.ts.map +0 -1
- package/dist/flight-records.js +0 -38
- package/dist/flight-records.js.map +0 -1
- package/dist/flight.d.ts +0 -12
- package/dist/flight.d.ts.map +0 -1
- package/dist/flight.js +0 -33
- package/dist/flight.js.map +0 -1
- package/dist/follow.d.ts +0 -31
- package/dist/follow.d.ts.map +0 -1
- package/dist/follow.js +0 -60
- package/dist/follow.js.map +0 -1
- package/dist/patiently.d.ts.map +0 -1
- package/dist/patiently.js.map +0 -1
- package/dist/shared-flight.d.ts +0 -25
- package/dist/shared-flight.d.ts.map +0 -1
- package/dist/shared-flight.js +0 -89
- package/dist/shared-flight.js.map +0 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The flight directory is on a file system that other machines share, where its election does not hold. */
|
|
2
|
+
export class NetworkDirectoryError extends Error {
|
|
3
|
+
directory;
|
|
4
|
+
/** The network file system the directory is on, for example `NFS`. */
|
|
5
|
+
fileSystem;
|
|
6
|
+
constructor(directory, fileSystem) {
|
|
7
|
+
super(`The flight directory ${JSON.stringify(directory)} is on a network file system (${fileSystem}), where its election is not shared reliably between machines. Use a local directory.`);
|
|
8
|
+
this.name = 'NetworkDirectoryError';
|
|
9
|
+
this.directory = directory;
|
|
10
|
+
this.fileSystem = fileSystem;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=network-directory-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-directory-error.js","sourceRoot":"","sources":["../../src/local-directory/network-directory-error.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAC5G,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IACrC,SAAS,CAAS;IAC3B,sEAAsE;IAC7D,UAAU,CAAS;IAE5B,YAAY,SAAiB,EAAE,UAAkB;QAC/C,KAAK,CACH,wBAAwB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,iCAAiC,UAAU,uFAAuF,CACpL,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** A leader's error as text, because an Error object cannot cross a process boundary. */
|
|
2
|
+
export interface Failure {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
/** The error's `code`, when it has one, as Node.js errors do. */
|
|
6
|
+
readonly code?: string | number;
|
|
7
|
+
}
|
|
8
|
+
/** How a flight landed: the value as the codec encoded it, or the leader's failure. */
|
|
9
|
+
export type Outcome = {
|
|
10
|
+
value: string;
|
|
11
|
+
} | {
|
|
12
|
+
failure: Failure;
|
|
13
|
+
};
|
|
14
|
+
/** Tokens and flights travel as decimal strings because JSON has no bigint. */
|
|
15
|
+
export type FlightRequest =
|
|
16
|
+
/**
|
|
17
|
+
* Leads the flight of `key` when none is in progress, or joins the flight in
|
|
18
|
+
* progress. After a lost connection, a run that the coordinator had told
|
|
19
|
+
* which flight it joined is sent again with that flight's token: such a
|
|
20
|
+
* rejoin never leads, so a joiner whose leader died never runs the work
|
|
21
|
+
* again. A run that got no answer has joined nothing, and goes again as a
|
|
22
|
+
* plain run.
|
|
23
|
+
*/
|
|
24
|
+
{
|
|
25
|
+
op: 'run';
|
|
26
|
+
id: string;
|
|
27
|
+
key: string;
|
|
28
|
+
flight?: string;
|
|
29
|
+
}
|
|
30
|
+
/** The leader of `id` ends its flight with `outcome`. Kept by the client until the coordinator acknowledges it. */
|
|
31
|
+
| {
|
|
32
|
+
op: 'land';
|
|
33
|
+
id: string;
|
|
34
|
+
outcome: Outcome;
|
|
35
|
+
}
|
|
36
|
+
/** Withdraws a run that is not answered yet, or a joiner from its flight. */
|
|
37
|
+
| {
|
|
38
|
+
op: 'cancel';
|
|
39
|
+
id: string;
|
|
40
|
+
}
|
|
41
|
+
/** A leader from before a failover claims its flight again, during the new coordinator's grace window. */
|
|
42
|
+
| {
|
|
43
|
+
op: 'reassert';
|
|
44
|
+
id: string;
|
|
45
|
+
key: string;
|
|
46
|
+
token: string;
|
|
47
|
+
};
|
|
48
|
+
export type FlightResponse =
|
|
49
|
+
/** The run leads a new flight; `token` is its lease's fencing token. */
|
|
50
|
+
{
|
|
51
|
+
op: 'lead';
|
|
52
|
+
id: string;
|
|
53
|
+
token: string;
|
|
54
|
+
}
|
|
55
|
+
/** The run joined the flight in progress, whose leader holds the token `flight`. */
|
|
56
|
+
| {
|
|
57
|
+
op: 'joined';
|
|
58
|
+
id: string;
|
|
59
|
+
flight: string;
|
|
60
|
+
} | {
|
|
61
|
+
op: 'landed';
|
|
62
|
+
id: string;
|
|
63
|
+
outcome: Outcome;
|
|
64
|
+
}
|
|
65
|
+
/** The flight that the run joined ended without an outcome: its leader stopped or lost its lease. */
|
|
66
|
+
| {
|
|
67
|
+
op: 'interrupted';
|
|
68
|
+
id: string;
|
|
69
|
+
}
|
|
70
|
+
/** The coordinator handed a landing to the joiners. */
|
|
71
|
+
| {
|
|
72
|
+
op: 'ack';
|
|
73
|
+
id: string;
|
|
74
|
+
}
|
|
75
|
+
/** The flight of `id` is no longer its leader's: a reassertion or a landing came too late. */
|
|
76
|
+
| {
|
|
77
|
+
op: 'rejected';
|
|
78
|
+
id: string;
|
|
79
|
+
}
|
|
80
|
+
/** The coordinator does not know the request: it runs an older version. */
|
|
81
|
+
| {
|
|
82
|
+
op: 'unsupported';
|
|
83
|
+
id: string;
|
|
84
|
+
};
|
|
85
|
+
/** A request whose `op` this process may not know. A coordinator answers one it does not know with `unsupported`. */
|
|
86
|
+
export interface RequestEnvelope {
|
|
87
|
+
op: string;
|
|
88
|
+
id: string;
|
|
89
|
+
}
|
|
90
|
+
export declare function isRequestEnvelope(message: unknown): message is RequestEnvelope;
|
|
91
|
+
/** Messages arrive from another process, so their shape is checked before use. */
|
|
92
|
+
export declare function isFlightRequest(message: unknown): message is FlightRequest;
|
|
93
|
+
export declare function isFlightResponse(message: unknown): message is FlightResponse;
|
|
94
|
+
//# sourceMappingURL=flight-protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-protocol.d.ts","sourceRoot":"","sources":["../../src/protocol/flight-protocol.ts"],"names":[],"mappings":"AAEA,yFAAyF;AACzF,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC;AAED,uFAAuF;AACvF,MAAM,MAAM,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/D,+EAA+E;AAC/E,MAAM,MAAM,aAAa;AACvB;;;;;;;GAOG;AACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AACzD,mHAAmH;GACjH;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE;AAC9C,6EAA6E;GAC3E;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AAC9B,0GAA0G;GACxG;IAAE,EAAE,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/D,MAAM,MAAM,cAAc;AACxB,wEAAwE;AACtE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC3C,oFAAoF;GAClF;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE;AAChD,qGAAqG;GACnG;IAAE,EAAE,EAAE,aAAa,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AACnC,uDAAuD;GACrD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AAC3B,8FAA8F;GAC5F;IAAE,EAAE,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AAChC,2EAA2E;GACzE;IAAE,EAAE,EAAE,aAAa,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,qHAAqH;AACrH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAKD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,eAAe,CAM5B;AAED,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,aAAa,CAiB1E;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,cAAc,CAiB5E"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isRecord } from "../shared/is-record.js";
|
|
2
|
+
const isToken = (value) => typeof value === 'string' && /^\d+$/.test(value);
|
|
3
|
+
export function isRequestEnvelope(message) {
|
|
4
|
+
return (isRecord(message) &&
|
|
5
|
+
typeof message.op === 'string' &&
|
|
6
|
+
typeof message.id === 'string');
|
|
7
|
+
}
|
|
8
|
+
/** Messages arrive from another process, so their shape is checked before use. */
|
|
9
|
+
export function isFlightRequest(message) {
|
|
10
|
+
if (!isRecord(message) || typeof message.id !== 'string')
|
|
11
|
+
return false;
|
|
12
|
+
switch (message.op) {
|
|
13
|
+
case 'run':
|
|
14
|
+
return (typeof message.key === 'string' &&
|
|
15
|
+
(message.flight === undefined || isToken(message.flight)));
|
|
16
|
+
case 'land':
|
|
17
|
+
return isOutcome(message.outcome);
|
|
18
|
+
case 'cancel':
|
|
19
|
+
return true;
|
|
20
|
+
case 'reassert':
|
|
21
|
+
return typeof message.key === 'string' && isToken(message.token);
|
|
22
|
+
default:
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function isFlightResponse(message) {
|
|
27
|
+
if (!isRecord(message) || typeof message.id !== 'string')
|
|
28
|
+
return false;
|
|
29
|
+
switch (message.op) {
|
|
30
|
+
case 'lead':
|
|
31
|
+
return isToken(message.token);
|
|
32
|
+
case 'joined':
|
|
33
|
+
return isToken(message.flight);
|
|
34
|
+
case 'landed':
|
|
35
|
+
return isOutcome(message.outcome);
|
|
36
|
+
case 'interrupted':
|
|
37
|
+
case 'ack':
|
|
38
|
+
case 'rejected':
|
|
39
|
+
case 'unsupported':
|
|
40
|
+
return true;
|
|
41
|
+
default:
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isOutcome(value) {
|
|
46
|
+
if (!isRecord(value))
|
|
47
|
+
return false;
|
|
48
|
+
if ('value' in value)
|
|
49
|
+
return typeof value.value === 'string';
|
|
50
|
+
return isFailure(value.failure);
|
|
51
|
+
}
|
|
52
|
+
function isFailure(value) {
|
|
53
|
+
return (isRecord(value) &&
|
|
54
|
+
typeof value.name === 'string' &&
|
|
55
|
+
typeof value.message === 'string' &&
|
|
56
|
+
(value.code === undefined ||
|
|
57
|
+
typeof value.code === 'string' ||
|
|
58
|
+
typeof value.code === 'number'));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=flight-protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-protocol.js","sourceRoot":"","sources":["../../src/protocol/flight-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAoDlD,MAAM,OAAO,GAAG,CAAC,KAAc,EAAmB,EAAE,CAClD,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAEnD,MAAM,UAAU,iBAAiB,CAC/B,OAAgB;IAEhB,OAAO,CACL,QAAQ,CAAC,OAAO,CAAC;QACjB,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ;QAC9B,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ,CAC/B,CAAC;AACJ,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACvE,QAAQ,OAAO,CAAC,EAAE,EAAE,CAAC;QACnB,KAAK,KAAK;YACR,OAAO,CACL,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ;gBAC/B,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAC1D,CAAC;QACJ,KAAK,MAAM;YACT,OAAO,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACpC,KAAK,QAAQ;YACX,OAAO,IAAI,CAAC;QACd,KAAK,UAAU;YACb,OAAO,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACnE;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACvE,QAAQ,OAAO,CAAC,EAAE,EAAE,CAAC;QACnB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAChC,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,QAAQ;YACX,OAAO,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACpC,KAAK,aAAa,CAAC;QACnB,KAAK,KAAK,CAAC;QACX,KAAK,UAAU,CAAC;QAChB,KAAK,aAAa;YAChB,OAAO,IAAI,CAAC;QACd;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,OAAO,IAAI,KAAK;QAAE,OAAO,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC;IAC7D,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAC9B,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QACjC,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS;YACvB,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;YAC9B,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAClC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces `path` in one step like `atomicWrite`, and returns only once both
|
|
3
|
+
* the new content and the replacement survive a power loss: a counter that
|
|
4
|
+
* goes back after a crash would hand out a token twice.
|
|
5
|
+
*/
|
|
6
|
+
export declare function durableWrite(path: string, content: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=durable-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-write.d.ts","sourceRoot":"","sources":["../../../src/shared/fs/durable-write.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAW/D"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { open, rm } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { isErrno } from "./errno.js";
|
|
5
|
+
import { replaceFile } from "./replace-file.js";
|
|
6
|
+
const draftOf = (path) => `${path}.${randomUUID()}.tmp`;
|
|
7
|
+
/**
|
|
8
|
+
* Replaces `path` in one step like `atomicWrite`, and returns only once both
|
|
9
|
+
* the new content and the replacement survive a power loss: a counter that
|
|
10
|
+
* goes back after a crash would hand out a token twice.
|
|
11
|
+
*/
|
|
12
|
+
export async function durableWrite(path, content) {
|
|
13
|
+
const draft = draftOf(path);
|
|
14
|
+
try {
|
|
15
|
+
await writeSynced(draft, content);
|
|
16
|
+
await replaceFile(draft, path);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// A draft that never replaced `path` would only pile up, one per failed write.
|
|
20
|
+
await rm(draft, { force: true });
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
await syncDirectory(dirname(path));
|
|
24
|
+
}
|
|
25
|
+
async function writeSynced(path, content) {
|
|
26
|
+
await using handle = await open(path, 'wx');
|
|
27
|
+
await handle.writeFile(content);
|
|
28
|
+
await handle.sync();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A rename lives in its directory, so the directory must reach the disk too.
|
|
32
|
+
* Windows cannot open a directory to sync it, and some file systems refuse to
|
|
33
|
+
* sync one; there the rename is as durable as that system makes it.
|
|
34
|
+
*/
|
|
35
|
+
async function syncDirectory(directory) {
|
|
36
|
+
if (process.platform === 'win32')
|
|
37
|
+
return;
|
|
38
|
+
await using handle = await open(directory, 'r');
|
|
39
|
+
try {
|
|
40
|
+
await handle.sync();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!isErrno(error, 'EINVAL') && !isErrno(error, 'ENOTSUP'))
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=durable-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-write.js","sourceRoot":"","sources":["../../../src/shared/fs/durable-write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,MAAM,CAAC;AAEhE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,OAAe;IAC9D,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC;QACH,MAAM,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAClC,MAAM,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,+EAA+E;QAC/E,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACjC,MAAM,KAAK,CAAC;IACd,CAAC;IACD,MAAM,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,OAAe;IACtD,YAAY,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC5C,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAChC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,aAAa,CAAC,SAAiB;IAC5C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO;IACzC,YAAY,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC;YAAE,MAAM,KAAK,CAAC;IAC3E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errno.d.ts","sourceRoot":"","sources":["../../../src/shared/fs/errno.ts"],"names":[],"mappings":"AAAA,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errno.js","sourceRoot":"","sources":["../../../src/shared/fs/errno.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,OAAO,CAAC,KAAc,EAAE,IAAY;IAClD,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;AAC1E,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/** Runs `operation`, and runs it again while Windows refuses the file for a moment. */
|
|
1
|
+
/** Runs `operation`, and runs it again while Windows refuses the file for a moment; after that, the error reaches the caller. */
|
|
2
2
|
export declare function patiently<T>(operation: () => Promise<T>): Promise<T>;
|
|
3
3
|
//# sourceMappingURL=patiently.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patiently.d.ts","sourceRoot":"","sources":["../../../src/shared/fs/patiently.ts"],"names":[],"mappings":"AAoBA,iIAAiI;AACjI,wBAAsB,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAc1E"}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { isErrno } from "./errno.js";
|
|
2
3
|
/** How long to retry a file operation that Windows refuses for a moment. */
|
|
3
4
|
const WINDOWS_PATIENCE = 1000;
|
|
4
5
|
/**
|
|
5
|
-
* Windows refuses a file for a moment while another
|
|
6
|
-
*
|
|
7
|
-
* error code is the same as for a real permission denial, so a
|
|
8
|
-
* retried only for a limited time.
|
|
6
|
+
* Windows refuses a file for a moment while another program holds it open
|
|
7
|
+
* with no sharing, as a virus scanner can, or while a delete of it is in
|
|
8
|
+
* progress. The error code is the same as for a real permission denial, so a
|
|
9
|
+
* refusal is retried only for a limited time.
|
|
9
10
|
*/
|
|
10
11
|
function isRefusedForNow(error) {
|
|
11
12
|
return (process.platform === 'win32' &&
|
|
12
|
-
|
|
13
|
-
'code' in error &&
|
|
14
|
-
['EPERM', 'EACCES', 'EBUSY'].includes(String(error.code)));
|
|
13
|
+
['EPERM', 'EACCES', 'EBUSY'].some((code) => isErrno(error, code)));
|
|
15
14
|
}
|
|
16
|
-
/** Runs `operation`, and runs it again while Windows refuses the file for a moment. */
|
|
15
|
+
/** Runs `operation`, and runs it again while Windows refuses the file for a moment; after that, the error reaches the caller. */
|
|
17
16
|
export async function patiently(operation) {
|
|
18
17
|
const started = performance.now();
|
|
19
18
|
for (let attempt = 0;; attempt++) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patiently.js","sourceRoot":"","sources":["../../../src/shared/fs/patiently.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,4EAA4E;AAC5E,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B;;;;;GAKG;AACH,SAAS,eAAe,CAAC,KAAc;IACrC,OAAO,CACL,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC5B,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAClE,CAAC;AACJ,CAAC;AAED,iIAAiI;AACjI,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,SAA2B;IAC5D,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAClC,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,OAAO,MAAM,SAAS,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IACE,CAAC,eAAe,CAAC,KAAK,CAAC;gBACvB,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,gBAAgB;gBAE9C,MAAM,KAAK,CAAC;YACd,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moves `draft` over `path` in one step. On Windows, a rename over a file
|
|
3
|
+
* fails while another process has that file open, for example a waiter that
|
|
4
|
+
* reads it at that moment. The refusal is brief, so the rename is tried again;
|
|
5
|
+
* it stays the single commit point.
|
|
6
|
+
*/
|
|
7
|
+
export declare function replaceFile(draft: string, path: string): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=replace-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../../../src/shared/fs/replace-file.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,iBAEtD"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { rename } from 'node:fs/promises';
|
|
2
|
+
import { patiently } from "./patiently.js";
|
|
3
|
+
/**
|
|
4
|
+
* Moves `draft` over `path` in one step. On Windows, a rename over a file
|
|
5
|
+
* fails while another process has that file open, for example a waiter that
|
|
6
|
+
* reads it at that moment. The refusal is brief, so the rename is tried again;
|
|
7
|
+
* it stays the single commit point.
|
|
8
|
+
*/
|
|
9
|
+
export function replaceFile(draft, path) {
|
|
10
|
+
return patiently(() => rename(draft, path));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=replace-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file.js","sourceRoot":"","sources":["../../../src/shared/fs/replace-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,IAAY;IACrD,OAAO,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-record.d.ts","sourceRoot":"","sources":["../../src/shared/is-record.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,eAAO,MAAM,QAAQ,GACnB,OAAO,OAAO,KACb,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-record.js","sourceRoot":"","sources":["../../src/shared/is-record.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,KAAc,EAC8B,EAAE,CAC9C,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-busy.d.ts","sourceRoot":"","sources":["../../../src/shared/sqlite/is-busy.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQ9C"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const SQLITE_BUSY = 5;
|
|
2
|
+
/** Whether `error` means another connection holds a conflicting lock right now. */
|
|
3
|
+
export function isBusy(error) {
|
|
4
|
+
return (error instanceof Error &&
|
|
5
|
+
'errcode' in error &&
|
|
6
|
+
typeof error.errcode === 'number' &&
|
|
7
|
+
// The low byte is the primary result code, covering SQLITE_BUSY_* variants.
|
|
8
|
+
(error.errcode & 0xff) === SQLITE_BUSY);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=is-busy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-busy.js","sourceRoot":"","sources":["../../../src/shared/sqlite/is-busy.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB,mFAAmF;AACnF,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,OAAO,CACL,KAAK,YAAY,KAAK;QACtB,SAAS,IAAI,KAAK;QAClB,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QACjC,4EAA4E;QAC5E,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,WAAW,CACvC,CAAC;AACJ,CAAC"}
|
package/dist/single-flight.d.ts
CHANGED
|
@@ -1,23 +1,55 @@
|
|
|
1
|
+
import type { Lease } from '@zukhruf/mutex';
|
|
2
|
+
import type { Codec } from './codec.ts';
|
|
3
|
+
export interface SingleFlightOptions<T> {
|
|
4
|
+
/**
|
|
5
|
+
* The local folder where the processes that share flights meet. They keep
|
|
6
|
+
* `flight.lock`, `flight.epoch` and `flight.sock` there, so every process
|
|
7
|
+
* that names the same folder is in one group, and callers of one key share
|
|
8
|
+
* a flight across that group. There is no default: one shared default would
|
|
9
|
+
* put every application on the host into one group, and unrelated
|
|
10
|
+
* applications that use the same key would join each other's flights. It
|
|
11
|
+
* must be on a local file system.
|
|
12
|
+
*/
|
|
13
|
+
directory: string;
|
|
14
|
+
/** Turns the flight's value into text for the processes that joined it, and back. */
|
|
15
|
+
codec: Codec<T>;
|
|
16
|
+
/**
|
|
17
|
+
* Milliseconds that a new coordinator starts no flight after the previous
|
|
18
|
+
* one stopped, so the leaders of flights in progress can claim them again.
|
|
19
|
+
* Must exceed how long a process takes to reconnect. Defaults to 500.
|
|
20
|
+
*/
|
|
21
|
+
graceWindow?: number;
|
|
22
|
+
}
|
|
1
23
|
export interface RunOptions {
|
|
2
|
-
/** Called once,
|
|
24
|
+
/** Called once, when this caller joins a flight that another caller leads. */
|
|
3
25
|
onJoin?: (() => void) | undefined;
|
|
4
26
|
/** Stops the wait of this caller only, which then rejects with `signal.reason`. The flight runs on. */
|
|
5
27
|
signal?: AbortSignal | undefined;
|
|
6
28
|
}
|
|
7
|
-
/** What a caller gets: the value of the flight, and whether the caller joined a flight that another caller
|
|
29
|
+
/** What a caller gets: the value of the flight, and whether the caller joined a flight that another caller led. */
|
|
8
30
|
export interface FlightValue<T> {
|
|
9
31
|
readonly value: T;
|
|
10
32
|
readonly joined: boolean;
|
|
11
33
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Callers of one key share the flight in progress, in this process and in
|
|
36
|
+
* every process that uses the same directory. One process of the group is
|
|
37
|
+
* elected the coordinator. Each call asks it, in one step, to lead a new
|
|
38
|
+
* flight of the key or to join the flight in progress. The leader runs the
|
|
39
|
+
* work, and the coordinator pushes the outcome to every joiner.
|
|
40
|
+
*/
|
|
41
|
+
export declare class SingleFlight<T> implements AsyncDisposable {
|
|
14
42
|
#private;
|
|
43
|
+
constructor({ directory, codec, graceWindow }: SingleFlightOptions<T>);
|
|
15
44
|
/**
|
|
16
|
-
* Runs `work` for `key
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the
|
|
45
|
+
* Runs `work` for `key` as the leader of a new flight, or joins the flight
|
|
46
|
+
* of `key` in progress. `work` gets the flight's lease: its token, and a
|
|
47
|
+
* signal that aborts with `LockLostError` once the flight is no longer this
|
|
48
|
+
* leader's. A joiner gets the leader's value through the codec, or rejects
|
|
49
|
+
* with `FlightFailedError` or `FlightInterruptedError`.
|
|
20
50
|
*/
|
|
21
|
-
run(key: string, work: (
|
|
51
|
+
run(key: string, work: (lease: Lease) => Promise<T>, { onJoin, signal }?: RunOptions): Promise<FlightValue<T>>;
|
|
52
|
+
/** Disconnects, and stops serving as the coordinator: the other processes elect a successor. */
|
|
53
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
22
54
|
}
|
|
23
55
|
//# sourceMappingURL=single-flight.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"single-flight.d.ts","sourceRoot":"","sources":["../src/single-flight.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"single-flight.d.ts","sourceRoot":"","sources":["../src/single-flight.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAiBxC,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC;;;;;;;;OAQG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAClC,uGAAuG;IACvG,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;AAED,mHAAmH;AACnH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAKD;;;;;;GAMG;AACH,qBAAa,YAAY,CAAC,CAAC,CAAE,YAAW,eAAe;;gBAMzC,EAAE,SAAS,EAAE,KAAK,EAAE,WAAiB,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC;IA+B3E;;;;;;OAMG;IACG,GAAG,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,EAClC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAE,UAAe,GAClC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAmB1B,gGAAgG;IAC1F,CAAC,MAAM,CAAC,YAAY,CAAC;CAiC5B"}
|
package/dist/single-flight.js
CHANGED
|
@@ -1,39 +1,99 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { untilAborted } from '@zukhruf/async';
|
|
2
|
+
import { FlightClient } from "./client/flight-client.js";
|
|
3
|
+
import { ConnectionSupervisor } from "./connection/connection-supervisor.js";
|
|
4
|
+
import { ElectingConnector } from "./connection/electing-connector.js";
|
|
5
|
+
import { LocalDirectoryConnector } from "./connection/local-directory-connector.js";
|
|
6
|
+
import { socketPathFor } from "./connection/socket-path.js";
|
|
7
|
+
import { FlightServer } from "./coordinator/flight-server.js";
|
|
8
|
+
import { LeaderElection } from "./election/leader-election.js";
|
|
9
|
+
import { FlightFailedError, FlightInterruptedError, failureOf, } from "./errors.js";
|
|
10
|
+
/** Milliseconds between two attempts to reach the coordinator or to win its term. */
|
|
11
|
+
const POLL_INTERVAL = 10;
|
|
12
|
+
/**
|
|
13
|
+
* Callers of one key share the flight in progress, in this process and in
|
|
14
|
+
* every process that uses the same directory. One process of the group is
|
|
15
|
+
* elected the coordinator. Each call asks it, in one step, to lead a new
|
|
16
|
+
* flight of the key or to join the flight in progress. The leader runs the
|
|
17
|
+
* work, and the coordinator pushes the outcome to every joiner.
|
|
18
|
+
*/
|
|
3
19
|
export class SingleFlight {
|
|
4
|
-
|
|
5
|
-
#
|
|
20
|
+
#client;
|
|
21
|
+
#codec;
|
|
22
|
+
/** The flight servers this process started; disposing closes them. */
|
|
23
|
+
#servers = new AsyncDisposableStack();
|
|
24
|
+
constructor({ directory, codec, graceWindow = 500 }) {
|
|
25
|
+
this.#codec = codec;
|
|
26
|
+
const socketPath = socketPathFor(directory);
|
|
27
|
+
this.#client = new FlightClient(new ConnectionSupervisor(new LocalDirectoryConnector(directory, new ElectingConnector({
|
|
28
|
+
socketPath,
|
|
29
|
+
election: new LeaderElection(directory, {
|
|
30
|
+
pollInterval: POLL_INTERVAL,
|
|
31
|
+
}),
|
|
32
|
+
pollInterval: POLL_INTERVAL,
|
|
33
|
+
serve: async (leadership) => {
|
|
34
|
+
// Until serving has fully started, a failure closes the new server, so no server outlives its term.
|
|
35
|
+
await using starting = new AsyncDisposableStack();
|
|
36
|
+
starting.adopt(await FlightServer.start(socketPath, leadership, {
|
|
37
|
+
// The first term of a directory has no flights to wait for.
|
|
38
|
+
graceWindow: leadership.epoch > 1n ? graceWindow : 0,
|
|
39
|
+
}), (started) => started.close());
|
|
40
|
+
this.#servers.use(starting.move());
|
|
41
|
+
},
|
|
42
|
+
}))));
|
|
43
|
+
}
|
|
6
44
|
/**
|
|
7
|
-
* Runs `work` for `key
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
45
|
+
* Runs `work` for `key` as the leader of a new flight, or joins the flight
|
|
46
|
+
* of `key` in progress. `work` gets the flight's lease: its token, and a
|
|
47
|
+
* signal that aborts with `LockLostError` once the flight is no longer this
|
|
48
|
+
* leader's. A joiner gets the leader's value through the codec, or rejects
|
|
49
|
+
* with `FlightFailedError` or `FlightInterruptedError`.
|
|
11
50
|
*/
|
|
12
51
|
async run(key, work, { onJoin, signal } = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
const answer = await this.#client.run(key, { signal, onJoin });
|
|
53
|
+
switch (answer.kind) {
|
|
54
|
+
case 'landed':
|
|
55
|
+
return { value: this.#read(key, answer.outcome), joined: true };
|
|
56
|
+
case 'interrupted':
|
|
57
|
+
throw new FlightInterruptedError(key);
|
|
58
|
+
case 'lead': {
|
|
59
|
+
// The signal stops only this caller's wait. The flight runs on for its joiners.
|
|
60
|
+
const settled = await untilAborted(this.#fly(work, answer.lead), signal);
|
|
61
|
+
if ('error' in settled)
|
|
62
|
+
throw settled.error;
|
|
63
|
+
return { value: settled.value, joined: false };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Disconnects, and stops serving as the coordinator: the other processes elect a successor. */
|
|
68
|
+
async [Symbol.asyncDispose]() {
|
|
69
|
+
await this.#client.close();
|
|
70
|
+
await this.#servers.disposeAsync();
|
|
71
|
+
}
|
|
72
|
+
/** Runs the work and lands its outcome. Never rejects, so a caller that stopped waiting leaves no rejection behind. */
|
|
73
|
+
async #fly(work, lead) {
|
|
74
|
+
let text;
|
|
75
|
+
try {
|
|
76
|
+
text = this.#codec.encode(await work({ token: lead.token, signal: lead.signal }));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
lead.land({ failure: failureOf(error) });
|
|
80
|
+
return { error };
|
|
81
|
+
}
|
|
82
|
+
// A lost flight's joiners were told that it is interrupted, so this value reaches nobody.
|
|
83
|
+
if (lead.signal.aborted)
|
|
84
|
+
return { error: lead.signal.reason };
|
|
85
|
+
lead.land({ value: text });
|
|
86
|
+
try {
|
|
87
|
+
return { value: this.#codec.decode(text) };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return { error };
|
|
91
|
+
}
|
|
24
92
|
}
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (this.#flights.get(key) === flight)
|
|
30
|
-
this.#flights.delete(key);
|
|
31
|
-
};
|
|
32
|
-
flight.ended.then(leave, leave);
|
|
33
|
-
// In the same step as the abort, so no caller joins a flight that nobody
|
|
34
|
-
// waits for (Go issue 22724: a new lookup joined a cancelled one).
|
|
35
|
-
flight.abandoned.addEventListener('abort', leave, { once: true });
|
|
36
|
-
return flight;
|
|
93
|
+
#read(key, outcome) {
|
|
94
|
+
if ('failure' in outcome)
|
|
95
|
+
throw new FlightFailedError(key, outcome.failure);
|
|
96
|
+
return this.#codec.decode(outcome.value);
|
|
37
97
|
}
|
|
38
98
|
}
|
|
39
99
|
//# sourceMappingURL=single-flight.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"single-flight.js","sourceRoot":"","sources":["../src/single-flight.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"single-flight.js","sourceRoot":"","sources":["../src/single-flight.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,OAAO,EAAE,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAEpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2CAA2C,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,SAAS,GACV,MAAM,aAAa,CAAC;AAGrB,qFAAqF;AACrF,MAAM,aAAa,GAAG,EAAE,CAAC;AAuCzB;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IACd,OAAO,CAAe;IACtB,MAAM,CAAW;IAC1B,sEAAsE;IAC7D,QAAQ,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE/C,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,GAAG,GAAG,EAA0B;QACzE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,GAAG,IAAI,YAAY,CAC7B,IAAI,oBAAoB,CACtB,IAAI,uBAAuB,CACzB,SAAS,EACT,IAAI,iBAAiB,CAAC;YACpB,UAAU;YACV,QAAQ,EAAE,IAAI,cAAc,CAAC,SAAS,EAAE;gBACtC,YAAY,EAAE,aAAa;aAC5B,CAAC;YACF,YAAY,EAAE,aAAa;YAC3B,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;gBAC1B,oGAAoG;gBACpG,YAAY,QAAQ,GAAG,IAAI,oBAAoB,EAAE,CAAC;gBAClD,QAAQ,CAAC,KAAK,CACZ,MAAM,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE;oBAC/C,4DAA4D;oBAC5D,WAAW,EAAE,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;iBACrD,CAAC,EACF,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAC7B,CAAC;gBACF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;YACrC,CAAC;SACF,CAAC,CACH,CACF,CACF,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,CACP,GAAW,EACX,IAAkC,EAClC,EAAE,MAAM,EAAE,MAAM,KAAiB,EAAE;QAEnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAC/D,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,QAAQ;gBACX,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YAClE,KAAK,aAAa;gBAChB,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;YACxC,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,gFAAgF;gBAChF,MAAM,OAAO,GAAG,MAAM,YAAY,CAChC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAC5B,MAAM,CACP,CAAC;gBACF,IAAI,OAAO,IAAI,OAAO;oBAAE,MAAM,OAAO,CAAC,KAAK,CAAC;gBAC5C,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;YACjD,CAAC;QACH,CAAC;IACH,CAAC;IAED,gGAAgG;IAChG,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QACzB,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC3B,MAAM,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC;IACrC,CAAC;IAED,uHAAuH;IACvH,KAAK,CAAC,IAAI,CACR,IAAkC,EAClC,IAAU;QAEV,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CACvB,MAAM,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CACvD,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACzC,OAAO,EAAE,KAAK,EAAE,CAAC;QACnB,CAAC;QACD,0FAA0F;QAC1F,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QAC9D,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC;YACH,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,KAAK,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAW,EAAE,OAAgB;QACjC,IAAI,SAAS,IAAI,OAAO;YAAE,MAAM,IAAI,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5E,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zukhruf/single-flight",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Callers of one key share the run already in flight, in one process or across processes, and get its outcome instead of a busy error.",
|
|
3
|
+
"version": "0.3.16",
|
|
4
|
+
"description": "Callers of one key share the run already in flight, in one process or across the processes that use one directory, and get its outcome instead of a busy error.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|