@zukhruf/single-flight 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +113 -93
  2. package/dist/client/flight-client.d.ts +44 -0
  3. package/dist/client/flight-client.d.ts.map +1 -0
  4. package/dist/client/flight-client.js +227 -0
  5. package/dist/client/flight-client.js.map +1 -0
  6. package/dist/codec.d.ts +12 -0
  7. package/dist/codec.d.ts.map +1 -0
  8. package/dist/codec.js +2 -0
  9. package/dist/codec.js.map +1 -0
  10. package/dist/connection/connection-supervisor.d.ts +39 -0
  11. package/dist/connection/connection-supervisor.d.ts.map +1 -0
  12. package/dist/connection/connection-supervisor.js +195 -0
  13. package/dist/connection/connection-supervisor.js.map +1 -0
  14. package/dist/connection/connection.d.ts +21 -0
  15. package/dist/connection/connection.d.ts.map +1 -0
  16. package/dist/connection/connection.js +2 -0
  17. package/dist/connection/connection.js.map +1 -0
  18. package/dist/connection/connector.d.ts +15 -0
  19. package/dist/connection/connector.d.ts.map +1 -0
  20. package/dist/connection/connector.js +2 -0
  21. package/dist/connection/connector.js.map +1 -0
  22. package/dist/connection/electing-connector.d.ts +23 -0
  23. package/dist/connection/electing-connector.d.ts.map +1 -0
  24. package/dist/connection/electing-connector.js +148 -0
  25. package/dist/connection/electing-connector.js.map +1 -0
  26. package/dist/connection/handshake.d.ts +31 -0
  27. package/dist/connection/handshake.d.ts.map +1 -0
  28. package/dist/connection/handshake.js +102 -0
  29. package/dist/connection/handshake.js.map +1 -0
  30. package/dist/connection/local-directory-connector.d.ts +14 -0
  31. package/dist/connection/local-directory-connector.d.ts.map +1 -0
  32. package/dist/connection/local-directory-connector.js +23 -0
  33. package/dist/connection/local-directory-connector.js.map +1 -0
  34. package/dist/connection/protocol-version-error.d.ts +12 -0
  35. package/dist/connection/protocol-version-error.d.ts.map +1 -0
  36. package/dist/connection/protocol-version-error.js +19 -0
  37. package/dist/connection/protocol-version-error.js.map +1 -0
  38. package/dist/connection/socket-connection.d.ts +18 -0
  39. package/dist/connection/socket-connection.d.ts.map +1 -0
  40. package/dist/connection/socket-connection.js +52 -0
  41. package/dist/connection/socket-connection.js.map +1 -0
  42. package/dist/connection/socket-path.d.ts +7 -0
  43. package/dist/connection/socket-path.d.ts.map +1 -0
  44. package/dist/connection/socket-path.js +24 -0
  45. package/dist/connection/socket-path.js.map +1 -0
  46. package/dist/coordinator/flight-coordinator.d.ts +33 -0
  47. package/dist/coordinator/flight-coordinator.d.ts.map +1 -0
  48. package/dist/coordinator/flight-coordinator.js +178 -0
  49. package/dist/coordinator/flight-coordinator.js.map +1 -0
  50. package/dist/coordinator/flight-server.d.ts +24 -0
  51. package/dist/coordinator/flight-server.d.ts.map +1 -0
  52. package/dist/coordinator/flight-server.js +74 -0
  53. package/dist/coordinator/flight-server.js.map +1 -0
  54. package/dist/coordinator/flight.d.ts +20 -0
  55. package/dist/coordinator/flight.d.ts.map +1 -0
  56. package/dist/coordinator/flight.js +39 -0
  57. package/dist/coordinator/flight.js.map +1 -0
  58. package/dist/coordinator/party.d.ts +38 -0
  59. package/dist/coordinator/party.d.ts.map +1 -0
  60. package/dist/coordinator/party.js +43 -0
  61. package/dist/coordinator/party.js.map +1 -0
  62. package/dist/coordinator/session.d.ts +16 -0
  63. package/dist/coordinator/session.d.ts.map +1 -0
  64. package/dist/coordinator/session.js +103 -0
  65. package/dist/coordinator/session.js.map +1 -0
  66. package/dist/election/leader-election.d.ts +24 -0
  67. package/dist/election/leader-election.d.ts.map +1 -0
  68. package/dist/election/leader-election.js +78 -0
  69. package/dist/election/leader-election.js.map +1 -0
  70. package/dist/election/leadership.d.ts +14 -0
  71. package/dist/election/leadership.d.ts.map +1 -0
  72. package/dist/election/leadership.js +23 -0
  73. package/dist/election/leadership.js.map +1 -0
  74. package/dist/errors.d.ts +9 -12
  75. package/dist/errors.d.ts.map +1 -1
  76. package/dist/errors.js +13 -11
  77. package/dist/errors.js.map +1 -1
  78. package/dist/index.d.ts +6 -5
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +3 -3
  81. package/dist/index.js.map +1 -1
  82. package/dist/protocol/flight-protocol.d.ts +94 -0
  83. package/dist/protocol/flight-protocol.d.ts.map +1 -0
  84. package/dist/protocol/flight-protocol.js +61 -0
  85. package/dist/protocol/flight-protocol.js.map +1 -0
  86. package/dist/shared/is-record.d.ts +3 -0
  87. package/dist/shared/is-record.d.ts.map +1 -0
  88. package/dist/shared/is-record.js +3 -0
  89. package/dist/shared/is-record.js.map +1 -0
  90. package/dist/shared/sqlite/is-busy.d.ts +3 -0
  91. package/dist/shared/sqlite/is-busy.d.ts.map +1 -0
  92. package/dist/shared/sqlite/is-busy.js +10 -0
  93. package/dist/shared/sqlite/is-busy.js.map +1 -0
  94. package/dist/single-flight.d.ts +43 -9
  95. package/dist/single-flight.d.ts.map +1 -1
  96. package/dist/single-flight.js +98 -31
  97. package/dist/single-flight.js.map +1 -1
  98. package/package.json +6 -6
  99. package/dist/file-flight-records.d.ts +0 -22
  100. package/dist/file-flight-records.d.ts.map +0 -1
  101. package/dist/file-flight-records.js +0 -124
  102. package/dist/file-flight-records.js.map +0 -1
  103. package/dist/flight-records.d.ts +0 -67
  104. package/dist/flight-records.d.ts.map +0 -1
  105. package/dist/flight-records.js +0 -38
  106. package/dist/flight-records.js.map +0 -1
  107. package/dist/flight.d.ts +0 -12
  108. package/dist/flight.d.ts.map +0 -1
  109. package/dist/flight.js +0 -38
  110. package/dist/flight.js.map +0 -1
  111. package/dist/follow.d.ts +0 -31
  112. package/dist/follow.d.ts.map +0 -1
  113. package/dist/follow.js +0 -60
  114. package/dist/follow.js.map +0 -1
  115. package/dist/patiently.d.ts +0 -3
  116. package/dist/patiently.d.ts.map +0 -1
  117. package/dist/patiently.js +0 -31
  118. package/dist/patiently.js.map +0 -1
  119. package/dist/shared-flight.d.ts +0 -25
  120. package/dist/shared-flight.d.ts.map +0 -1
  121. package/dist/shared-flight.js +0 -89
  122. package/dist/shared-flight.js.map +0 -1
@@ -0,0 +1,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,3 @@
1
+ /** Narrows a parsed or received value to an object whose fields can be checked one by one. */
2
+ export declare const isRecord: (value: unknown) => value is Readonly<Record<string, unknown>>;
3
+ //# sourceMappingURL=is-record.d.ts.map
@@ -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,3 @@
1
+ /** Narrows a parsed or received value to an object whose fields can be checked one by one. */
2
+ export const isRecord = (value) => typeof value === 'object' && value !== null;
3
+ //# sourceMappingURL=is-record.js.map
@@ -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,3 @@
1
+ /** Whether `error` means another connection holds a conflicting lock right now. */
2
+ export declare function isBusy(error: unknown): boolean;
3
+ //# sourceMappingURL=is-busy.d.ts.map
@@ -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"}
@@ -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, before this caller starts to wait for a flight that another caller started. */
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 started. */
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
- /** Callers of one key in this process share the flight in progress. */
13
- export declare class SingleFlight<T> {
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`, or joins the flight of `key` in progress. `work`
17
- * gets a signal that aborts only when every caller of the flight cancelled:
18
- * work that can stop early may read it. Work that ignores it runs on, and
19
- * the next call starts a new flight either way.
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: (abandoned: AbortSignal) => Promise<T>, { onJoin, signal }?: RunOptions): Promise<FlightValue<T>>;
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":"AAEA,MAAM,WAAW,UAAU;IACzB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAClC,uGAAuG;IACvG,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAClC;AAED,uHAAuH;AACvH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,uEAAuE;AACvE,qBAAa,YAAY,CAAC,CAAC;;IAIzB;;;;;OAKG;IACG,GAAG,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAC5C,EAAE,MAAM,EAAE,MAAM,EAAE,GAAE,UAAe,GAClC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;CAyB3B"}
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"}
@@ -1,39 +1,106 @@
1
- import { Flight } from "./flight.js";
2
- /** Callers of one key in this process share the flight in progress. */
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
- /** Only the flights in progress: a flight that ended leaves, so the next call starts a new one. */
5
- #flights = new Map();
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`, or joins the flight of `key` in progress. `work`
8
- * gets a signal that aborts only when every caller of the flight cancelled:
9
- * work that can stop early may read it. Work that ignores it runs on, and
10
- * the next call starts a new flight either way.
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
- // A caller that already cancelled neither starts nor joins a flight.
14
- signal?.throwIfAborted();
15
- // No await before the flight is in the map: a second caller must find it.
16
- const inProgress = this.#flights.get(key);
17
- const flight = inProgress ?? this.#start(key, work);
18
- const joined = inProgress !== undefined;
19
- if (joined)
20
- onJoin?.();
21
- // The signal stops only this caller's wait. The work never sees it, so
22
- // the other callers still get the value.
23
- return { value: await flight.wait(signal), joined };
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
- #start(key, work) {
26
- const flight = new Flight(work);
27
- this.#flights.set(key, flight);
28
- const leave = () => {
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,aAAa,CAAC;AAerC,uEAAuE;AACvE,MAAM,OAAO,YAAY;IACvB,mGAAmG;IAC1F,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAEjD;;;;;OAKG;IACH,KAAK,CAAC,GAAG,CACP,GAAW,EACX,IAA4C,EAC5C,EAAE,MAAM,EAAE,MAAM,KAAiB,EAAE;QAEnC,qEAAqE;QACrE,MAAM,EAAE,cAAc,EAAE,CAAC;QACzB,0EAA0E;QAC1E,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,UAAU,KAAK,SAAS,CAAC;QACxC,IAAI,MAAM;YAAE,MAAM,EAAE,EAAE,CAAC;QACvB,uEAAuE;QACvE,yCAAyC;QACzC,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IACtD,CAAC;IAED,MAAM,CAAC,GAAW,EAAE,IAA4C;QAC9D,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,MAAM;gBAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnE,CAAC,CAAC;QACF,KAAK,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,yEAAyE;QACzE,mEAAmE;QACnE,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
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.15",
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
- "peerDependencies": {
47
- "@zukhruf/mutex": "^0.3.11"
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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"}
@@ -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"}