@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.
- package/README.md +113 -93
- 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 +227 -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 +78 -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/protocol/flight-protocol.d.ts +94 -0
- package/dist/protocol/flight-protocol.d.ts.map +1 -0
- package/dist/protocol/flight-protocol.js +61 -0
- package/dist/protocol/flight-protocol.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 +43 -9
- package/dist/single-flight.d.ts.map +1 -1
- package/dist/single-flight.js +98 -31
- package/dist/single-flight.js.map +1 -1
- package/package.json +6 -6
- 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 -38
- 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 +0 -3
- package/dist/patiently.d.ts.map +0 -1
- package/dist/patiently.js +0 -31
- 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,61 @@
|
|
|
1
|
+
import { FencingToken } from '@zukhruf/fencing';
|
|
2
|
+
import { isRecord } from "../shared/is-record.js";
|
|
3
|
+
const isToken = (value) => typeof value === 'string' && FencingToken.parse(value) !== null;
|
|
4
|
+
export function isRequestEnvelope(message) {
|
|
5
|
+
return (isRecord(message) &&
|
|
6
|
+
typeof message.op === 'string' &&
|
|
7
|
+
typeof message.id === 'string');
|
|
8
|
+
}
|
|
9
|
+
/** Messages arrive from another process, so their shape is checked before use. */
|
|
10
|
+
export function isFlightRequest(message) {
|
|
11
|
+
if (!isRecord(message) || typeof message.id !== 'string')
|
|
12
|
+
return false;
|
|
13
|
+
switch (message.op) {
|
|
14
|
+
case 'run':
|
|
15
|
+
return (typeof message.key === 'string' &&
|
|
16
|
+
(message.flight === undefined || isToken(message.flight)));
|
|
17
|
+
case 'land':
|
|
18
|
+
return isOutcome(message.outcome);
|
|
19
|
+
case 'cancel':
|
|
20
|
+
return true;
|
|
21
|
+
case 'reassert':
|
|
22
|
+
return typeof message.key === 'string' && isToken(message.token);
|
|
23
|
+
default:
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function isFlightResponse(message) {
|
|
28
|
+
if (!isRecord(message) || typeof message.id !== 'string')
|
|
29
|
+
return false;
|
|
30
|
+
switch (message.op) {
|
|
31
|
+
case 'lead':
|
|
32
|
+
return isToken(message.token);
|
|
33
|
+
case 'joined':
|
|
34
|
+
return isToken(message.flight);
|
|
35
|
+
case 'landed':
|
|
36
|
+
return isOutcome(message.outcome);
|
|
37
|
+
case 'interrupted':
|
|
38
|
+
case 'ack':
|
|
39
|
+
case 'rejected':
|
|
40
|
+
case 'unsupported':
|
|
41
|
+
return true;
|
|
42
|
+
default:
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isOutcome(value) {
|
|
47
|
+
if (!isRecord(value))
|
|
48
|
+
return false;
|
|
49
|
+
if ('value' in value)
|
|
50
|
+
return typeof value.value === 'string';
|
|
51
|
+
return isFailure(value.failure);
|
|
52
|
+
}
|
|
53
|
+
function isFailure(value) {
|
|
54
|
+
return (isRecord(value) &&
|
|
55
|
+
typeof value.name === 'string' &&
|
|
56
|
+
typeof value.message === 'string' &&
|
|
57
|
+
(value.code === undefined ||
|
|
58
|
+
typeof value.code === 'string' ||
|
|
59
|
+
typeof value.code === 'number'));
|
|
60
|
+
}
|
|
61
|
+
//# 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,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAoDlD,MAAM,OAAO,GAAG,CAAC,KAAc,EAAmB,EAAE,CAClD,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;AAElE,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 @@
|
|
|
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,57 @@
|
|
|
1
|
+
import type { FencedLease } from '@zukhruf/fencing';
|
|
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
|
-
*
|
|
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 `LeaseLostError` once the flight is no longer this
|
|
48
|
+
* leader's. After that loss, the leader's call rejects with `LeaseLostError`,
|
|
49
|
+
* also when the work throws its own error. A joiner gets the leader's value
|
|
50
|
+
* through the codec, or rejects with `FlightFailedError` or
|
|
51
|
+
* `FlightInterruptedError`.
|
|
20
52
|
*/
|
|
21
|
-
run(key: string, work: (
|
|
53
|
+
run(key: string, work: (lease: FencedLease) => Promise<T>, { onJoin, signal }?: RunOptions): Promise<FlightValue<T>>;
|
|
54
|
+
/** Disconnects, and stops serving as the coordinator: the other processes elect a successor. */
|
|
55
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
22
56
|
}
|
|
23
57
|
//# 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,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIpD,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;;;;;;;;OAQG;IACG,GAAG,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EACxC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAE,UAAe,GAClC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAmB1B,gGAAgG;IAC1F,CAAC,MAAM,CAAC,YAAY,CAAC;CAmC5B"}
|
package/dist/single-flight.js
CHANGED
|
@@ -1,39 +1,106 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { untilAborted } from '@zukhruf/async';
|
|
2
|
+
import { LeaseLostError } from '@zukhruf/lease';
|
|
3
|
+
import { FlightClient } from "./client/flight-client.js";
|
|
4
|
+
import { ConnectionSupervisor } from "./connection/connection-supervisor.js";
|
|
5
|
+
import { ElectingConnector } from "./connection/electing-connector.js";
|
|
6
|
+
import { LocalDirectoryConnector } from "./connection/local-directory-connector.js";
|
|
7
|
+
import { socketPathFor } from "./connection/socket-path.js";
|
|
8
|
+
import { FlightServer } from "./coordinator/flight-server.js";
|
|
9
|
+
import { LeaderElection } from "./election/leader-election.js";
|
|
10
|
+
import { FlightFailedError, FlightInterruptedError, failureOf, } from "./errors.js";
|
|
11
|
+
/** Milliseconds between two attempts to reach the coordinator or to win its term. */
|
|
12
|
+
const POLL_INTERVAL = 10;
|
|
13
|
+
/**
|
|
14
|
+
* Callers of one key share the flight in progress, in this process and in
|
|
15
|
+
* every process that uses the same directory. One process of the group is
|
|
16
|
+
* elected the coordinator. Each call asks it, in one step, to lead a new
|
|
17
|
+
* flight of the key or to join the flight in progress. The leader runs the
|
|
18
|
+
* work, and the coordinator pushes the outcome to every joiner.
|
|
19
|
+
*/
|
|
3
20
|
export class SingleFlight {
|
|
4
|
-
|
|
5
|
-
#
|
|
21
|
+
#client;
|
|
22
|
+
#codec;
|
|
23
|
+
/** The flight servers this process started; disposing closes them. */
|
|
24
|
+
#servers = new AsyncDisposableStack();
|
|
25
|
+
constructor({ directory, codec, graceWindow = 500 }) {
|
|
26
|
+
this.#codec = codec;
|
|
27
|
+
const socketPath = socketPathFor(directory);
|
|
28
|
+
this.#client = new FlightClient(new ConnectionSupervisor(new LocalDirectoryConnector(directory, new ElectingConnector({
|
|
29
|
+
socketPath,
|
|
30
|
+
election: new LeaderElection(directory, {
|
|
31
|
+
pollInterval: POLL_INTERVAL,
|
|
32
|
+
}),
|
|
33
|
+
pollInterval: POLL_INTERVAL,
|
|
34
|
+
serve: async (leadership) => {
|
|
35
|
+
// Until serving has fully started, a failure closes the new server, so no server outlives its term.
|
|
36
|
+
await using starting = new AsyncDisposableStack();
|
|
37
|
+
starting.adopt(await FlightServer.start(socketPath, leadership, {
|
|
38
|
+
// The first term of a directory has no flights to wait for.
|
|
39
|
+
graceWindow: leadership.epoch > 1n ? graceWindow : 0,
|
|
40
|
+
}), (started) => started.close());
|
|
41
|
+
this.#servers.use(starting.move());
|
|
42
|
+
},
|
|
43
|
+
}))));
|
|
44
|
+
}
|
|
6
45
|
/**
|
|
7
|
-
* Runs `work` for `key
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
46
|
+
* Runs `work` for `key` as the leader of a new flight, or joins the flight
|
|
47
|
+
* of `key` in progress. `work` gets the flight's lease: its token, and a
|
|
48
|
+
* signal that aborts with `LeaseLostError` once the flight is no longer this
|
|
49
|
+
* leader's. After that loss, the leader's call rejects with `LeaseLostError`,
|
|
50
|
+
* also when the work throws its own error. A joiner gets the leader's value
|
|
51
|
+
* through the codec, or rejects with `FlightFailedError` or
|
|
52
|
+
* `FlightInterruptedError`.
|
|
11
53
|
*/
|
|
12
54
|
async run(key, work, { onJoin, signal } = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
55
|
+
const answer = await this.#client.run(key, { signal, onJoin });
|
|
56
|
+
switch (answer.kind) {
|
|
57
|
+
case 'landed':
|
|
58
|
+
return { value: this.#read(key, answer.outcome), joined: true };
|
|
59
|
+
case 'interrupted':
|
|
60
|
+
throw new FlightInterruptedError(key);
|
|
61
|
+
case 'lead': {
|
|
62
|
+
// The signal stops only this caller's wait. The flight runs on for its joiners.
|
|
63
|
+
const settled = await untilAborted(this.#fly(key, work, answer.lead), signal);
|
|
64
|
+
if ('error' in settled)
|
|
65
|
+
throw settled.error;
|
|
66
|
+
return { value: settled.value, joined: false };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Disconnects, and stops serving as the coordinator: the other processes elect a successor. */
|
|
71
|
+
async [Symbol.asyncDispose]() {
|
|
72
|
+
await this.#client.close();
|
|
73
|
+
await this.#servers.disposeAsync();
|
|
74
|
+
}
|
|
75
|
+
/** Runs the work and lands its outcome. Never rejects, so a caller that stopped waiting leaves no rejection behind. */
|
|
76
|
+
async #fly(key, work, lead) {
|
|
77
|
+
const { token, signal: lost } = lead;
|
|
78
|
+
let text;
|
|
79
|
+
try {
|
|
80
|
+
text = this.#codec.encode(await work({ token, signal: lost }));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
lead.land({ failure: failureOf(error) });
|
|
84
|
+
// After a loss, every failure rejects with LeaseLostError: the work did not run alone (ADR 0007).
|
|
85
|
+
if (!lost.aborted || error === lost.reason)
|
|
86
|
+
return { error };
|
|
87
|
+
return { error: new LeaseLostError(key, { cause: error }) };
|
|
88
|
+
}
|
|
89
|
+
// A lost flight's joiners were told that it is interrupted, so this value reaches nobody.
|
|
90
|
+
if (lost.aborted)
|
|
91
|
+
return { error: lost.reason };
|
|
92
|
+
lead.land({ value: text });
|
|
93
|
+
try {
|
|
94
|
+
return { value: this.#codec.decode(text) };
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return { error };
|
|
98
|
+
}
|
|
24
99
|
}
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (this.#flights.get(key) === flight)
|
|
30
|
-
this.#flights.delete(key);
|
|
31
|
-
};
|
|
32
|
-
void flight.ended.then(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;
|
|
100
|
+
#read(key, outcome) {
|
|
101
|
+
if ('failure' in outcome)
|
|
102
|
+
throw new FlightFailedError(key, outcome.failure);
|
|
103
|
+
return this.#codec.decode(outcome.value);
|
|
37
104
|
}
|
|
38
105
|
}
|
|
39
106
|
//# 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":"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"}
|
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.17",
|
|
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",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@zukhruf/async": "^0.3.13"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"@zukhruf/
|
|
44
|
+
"@zukhruf/async": "^0.3.13",
|
|
45
|
+
"@zukhruf/fencing": "^0.3.16",
|
|
46
|
+
"@zukhruf/fs": "^0.3.16",
|
|
47
|
+
"@zukhruf/lease": "^0.3.16"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@zukhruf/mutex": "^0.3.11"
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type Finished, type FlightRecords, type LatestFlight, type Outcome } from './flight-records.ts';
|
|
2
|
-
export interface FileFlightRecordsOptions {
|
|
3
|
-
/**
|
|
4
|
-
* Milliseconds that the outcome of a flight stays readable after the flight
|
|
5
|
-
* ended. A joiner reads it once per poll interval, so keep it at least 20
|
|
6
|
-
* times the `pollInterval` of every joiner. Defaults to 60 000.
|
|
7
|
-
*/
|
|
8
|
-
keepFor?: number;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Keeps the flight records of each key in one JSON file in `directory`. Give
|
|
12
|
-
* the records a directory of their own, never the lock directory of a mutex.
|
|
13
|
-
*/
|
|
14
|
-
export declare class FileFlightRecords implements FlightRecords {
|
|
15
|
-
#private;
|
|
16
|
-
constructor(directory: string, { keepFor }?: FileFlightRecordsOptions);
|
|
17
|
-
begin(key: string): Promise<string>;
|
|
18
|
-
finish(key: string, id: string, outcome: Finished): Promise<void>;
|
|
19
|
-
latest(key: string): Promise<LatestFlight | undefined>;
|
|
20
|
-
outcome(key: string, id: string): Promise<Outcome>;
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=file-flight-records.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file-flight-records.d.ts","sourceRoot":"","sources":["../src/file-flight-records.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,OAAO,EAGb,MAAM,qBAAqB,CAAC;AAyB7B,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,aAAa;;gBAKnD,SAAS,EAAE,MAAM,EACjB,EAAE,OAAgB,EAAE,GAAE,wBAA6B;IAW/C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBnC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAejE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAKtD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CA0DzD"}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { isOutcome, isRecord, } from "./flight-records.js";
|
|
5
|
-
import { patiently } from "./patiently.js";
|
|
6
|
-
const isEntry = (value) => isRecord(value) &&
|
|
7
|
-
typeof value['id'] === 'string' &&
|
|
8
|
-
isOutcome(value['outcome']) &&
|
|
9
|
-
value['outcome'].status !== 'missing' &&
|
|
10
|
-
(value['endedAt'] === undefined || typeof value['endedAt'] === 'number');
|
|
11
|
-
const isFlightFile = (value) => isRecord(value) &&
|
|
12
|
-
Array.isArray(value['flights']) &&
|
|
13
|
-
value['flights'].every(isEntry);
|
|
14
|
-
/**
|
|
15
|
-
* Keeps the flight records of each key in one JSON file in `directory`. Give
|
|
16
|
-
* the records a directory of their own, never the lock directory of a mutex.
|
|
17
|
-
*/
|
|
18
|
-
export class FileFlightRecords {
|
|
19
|
-
#directory;
|
|
20
|
-
#keepFor;
|
|
21
|
-
constructor(directory, { keepFor = 60_000 } = {}) {
|
|
22
|
-
if (!(keepFor >= 0) || !Number.isFinite(keepFor)) {
|
|
23
|
-
throw new RangeError(`keepFor must be a finite number of milliseconds >= 0, got ${keepFor}.`);
|
|
24
|
-
}
|
|
25
|
-
this.#directory = directory;
|
|
26
|
-
this.#keepFor = keepFor;
|
|
27
|
-
}
|
|
28
|
-
async begin(key) {
|
|
29
|
-
const id = randomUUID();
|
|
30
|
-
const endedAt = Date.now();
|
|
31
|
-
const flights = (await this.#read(key)).map((flight) => flight.outcome.status === 'running'
|
|
32
|
-
? {
|
|
33
|
-
id: flight.id,
|
|
34
|
-
outcome: { status: 'interrupted', successor: id },
|
|
35
|
-
endedAt,
|
|
36
|
-
}
|
|
37
|
-
: flight);
|
|
38
|
-
await this.#write(key, [
|
|
39
|
-
...flights,
|
|
40
|
-
{ id, outcome: { status: 'running' } },
|
|
41
|
-
]);
|
|
42
|
-
return id;
|
|
43
|
-
}
|
|
44
|
-
async finish(key, id, outcome) {
|
|
45
|
-
const flights = await this.#read(key);
|
|
46
|
-
const running = flights.some((flight) => flight.id === id && flight.outcome.status === 'running');
|
|
47
|
-
if (!running)
|
|
48
|
-
return;
|
|
49
|
-
const endedAt = Date.now();
|
|
50
|
-
await this.#write(key, flights.map((flight) => flight.id === id ? { id, outcome, endedAt } : flight));
|
|
51
|
-
}
|
|
52
|
-
async latest(key) {
|
|
53
|
-
const last = (await this.#read(key)).at(-1);
|
|
54
|
-
return last && { id: last.id, status: last.outcome.status };
|
|
55
|
-
}
|
|
56
|
-
async outcome(key, id) {
|
|
57
|
-
const flight = (await this.#read(key)).find((flight) => flight.id === id);
|
|
58
|
-
return flight?.outcome ?? { status: 'missing' };
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* A key can hold characters that no file name can, such as `:` on Windows
|
|
62
|
-
* or `/`, and two keys can differ only by case, which a case-insensitive
|
|
63
|
-
* file system ignores. The hex digest has none of these problems. It digests
|
|
64
|
-
* the UTF-16 code units: UTF-8 would turn every lone surrogate into U+FFFD
|
|
65
|
-
* and give such keys one record.
|
|
66
|
-
*/
|
|
67
|
-
#pathOf(key) {
|
|
68
|
-
const name = createHash('sha256').update(key, 'utf16le').digest('hex');
|
|
69
|
-
return join(this.#directory, `${name}.json`);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* A finished flight leaves once it ended `keepFor` ago. The flight that
|
|
73
|
-
* began last always stays, so `latest` still knows it.
|
|
74
|
-
*/
|
|
75
|
-
#kept(flights, now) {
|
|
76
|
-
return flights.filter((flight, index) => index === flights.length - 1 ||
|
|
77
|
-
flight.endedAt === undefined ||
|
|
78
|
-
now - flight.endedAt < this.#keepFor);
|
|
79
|
-
}
|
|
80
|
-
async #read(key) {
|
|
81
|
-
const path = this.#pathOf(key);
|
|
82
|
-
let text;
|
|
83
|
-
try {
|
|
84
|
-
text = await patiently(() => readFile(path, 'utf8'));
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
if (isErrno(error, 'ENOENT'))
|
|
88
|
-
return [];
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
const content = parseJson(text, path);
|
|
92
|
-
if (!isFlightFile(content))
|
|
93
|
-
throw notAFlightFile(path);
|
|
94
|
-
return content.flights;
|
|
95
|
-
}
|
|
96
|
-
/** Replaces the file in one step, so a joiner reads the old records or the new ones, never a part. */
|
|
97
|
-
async #write(key, flights) {
|
|
98
|
-
const path = this.#pathOf(key);
|
|
99
|
-
const draft = `${path}.${randomUUID()}.tmp`;
|
|
100
|
-
const kept = this.#kept(flights, Date.now());
|
|
101
|
-
await mkdir(this.#directory, { recursive: true });
|
|
102
|
-
try {
|
|
103
|
-
await writeFile(draft, JSON.stringify({ key, flights: kept }));
|
|
104
|
-
await patiently(() => rename(draft, path));
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
await rm(draft, { force: true });
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function parseJson(text, path) {
|
|
113
|
-
try {
|
|
114
|
-
return JSON.parse(text);
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
throw notAFlightFile(path, { cause: error });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const notAFlightFile = (path, options) => new Error(`${JSON.stringify(path)} is not a file of flight records. Give the records a directory of their own.`, options);
|
|
121
|
-
function isErrno(error, code) {
|
|
122
|
-
return error instanceof Error && 'code' in error && error.code === code;
|
|
123
|
-
}
|
|
124
|
-
//# sourceMappingURL=file-flight-records.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file-flight-records.js","sourceRoot":"","sources":["../src/file-flight-records.ts"],"names":[],"mappings":"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"}
|
package/dist/flight-records.d.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/** An error as a flight record keeps it: the parts that JSON carries to another process. */
|
|
2
|
-
export interface RecordedError {
|
|
3
|
-
readonly name: string;
|
|
4
|
-
readonly message: string;
|
|
5
|
-
readonly code?: string | number;
|
|
6
|
-
}
|
|
7
|
-
/** Where a flight is: still running, or how it ended. */
|
|
8
|
-
export type Status = 'running' | 'succeeded' | 'failed' | 'interrupted';
|
|
9
|
-
/** How a flight ended, as its leader records it. */
|
|
10
|
-
export type Finished = {
|
|
11
|
-
readonly status: 'succeeded';
|
|
12
|
-
readonly value: unknown;
|
|
13
|
-
} | {
|
|
14
|
-
readonly status: 'failed';
|
|
15
|
-
readonly error: RecordedError;
|
|
16
|
-
} | {
|
|
17
|
-
readonly status: 'interrupted';
|
|
18
|
-
};
|
|
19
|
-
/** What a joiner reads about a flight: running, how it ended, or missing when its record is gone. */
|
|
20
|
-
export type Outcome = {
|
|
21
|
-
readonly status: 'running';
|
|
22
|
-
} | {
|
|
23
|
-
readonly status: 'succeeded';
|
|
24
|
-
readonly value: unknown;
|
|
25
|
-
} | {
|
|
26
|
-
readonly status: 'failed';
|
|
27
|
-
readonly error: RecordedError;
|
|
28
|
-
} | {
|
|
29
|
-
readonly status: 'interrupted';
|
|
30
|
-
readonly successor?: string;
|
|
31
|
-
} | {
|
|
32
|
-
readonly status: 'missing';
|
|
33
|
-
};
|
|
34
|
-
/** The flight that a key began last. */
|
|
35
|
-
export interface LatestFlight {
|
|
36
|
-
readonly id: string;
|
|
37
|
-
readonly status: Status;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Where the outcomes of flights live, so that callers in other processes can
|
|
41
|
-
* join a flight. Only the holder of a key writes the records of the key.
|
|
42
|
-
* Joiners only read them.
|
|
43
|
-
*/
|
|
44
|
-
export interface FlightRecords {
|
|
45
|
-
/**
|
|
46
|
-
* Records a new running flight of `key` and returns its id. A flight of the
|
|
47
|
-
* key that is still running stopped before it had an outcome: it becomes
|
|
48
|
-
* interrupted, with the new flight as its successor.
|
|
49
|
-
*/
|
|
50
|
-
begin(key: string): Promise<string>;
|
|
51
|
-
/** Records how flight `id` ended. Does nothing when the flight is no longer running. */
|
|
52
|
-
finish(key: string, id: string, outcome: Finished): Promise<void>;
|
|
53
|
-
/** The flight that `key` began last, or `undefined` when the key has no record. */
|
|
54
|
-
latest(key: string): Promise<LatestFlight | undefined>;
|
|
55
|
-
/**
|
|
56
|
-
* The outcome of flight `id`. An outcome stays readable for a time after
|
|
57
|
-
* its flight ended, so a joiner that reads it late finds it after newer
|
|
58
|
-
* flights of the key began.
|
|
59
|
-
*/
|
|
60
|
-
outcome(key: string, id: string): Promise<Outcome>;
|
|
61
|
-
}
|
|
62
|
-
/** The parts of a thrown value that a record keeps. A value that is not an error keeps only its text. */
|
|
63
|
-
export declare function recordedError(error: unknown): RecordedError;
|
|
64
|
-
export declare const isRecord: (value: unknown) => value is Readonly<Record<string, unknown>>;
|
|
65
|
-
export declare function isRecordedError(value: unknown): value is RecordedError;
|
|
66
|
-
export declare function isOutcome(value: unknown): value is Outcome;
|
|
67
|
-
//# sourceMappingURL=flight-records.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"flight-records.d.ts","sourceRoot":"","sources":["../src/flight-records.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC;AAED,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;AAExE,oDAAoD;AACpD,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAAC;AAEvC,qGAAqG;AACrG,MAAM,MAAM,OAAO,GACf;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAEnC,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,wFAAwF;IACxF,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,mFAAmF;IACnF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IACvD;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AAED,yGAAyG;AACzG,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,CAQ3D;AAED,eAAO,MAAM,QAAQ,GACnB,OAAO,OAAO,KACb,KAAK,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACC,CAAC;AAE9C,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAQtE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAkB1D"}
|