@smthrs/artifacts 0.0.0-stage → 1.0.0-rc.12

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 (171) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +175 -2
  4. package/dist/cjs/ArtifactBackupLease.d.ts +34 -0
  5. package/dist/cjs/ArtifactBackupLease.d.ts.map +1 -0
  6. package/dist/cjs/ArtifactBackupLease.js +183 -0
  7. package/dist/cjs/ArtifactBackupLease.js.map +7 -0
  8. package/dist/cjs/ArtifactStore.d.ts +129 -0
  9. package/dist/cjs/ArtifactStore.d.ts.map +1 -0
  10. package/dist/cjs/ArtifactStore.js +73 -0
  11. package/dist/cjs/ArtifactStore.js.map +7 -0
  12. package/dist/cjs/ArtifactStoreError.d.ts +82 -0
  13. package/dist/cjs/ArtifactStoreError.d.ts.map +1 -0
  14. package/dist/cjs/ArtifactStoreError.js +72 -0
  15. package/dist/cjs/ArtifactStoreError.js.map +7 -0
  16. package/dist/cjs/ArtifactStoreMetrics.d.ts +70 -0
  17. package/dist/cjs/ArtifactStoreMetrics.d.ts.map +1 -0
  18. package/dist/cjs/ArtifactStoreMetrics.js +51 -0
  19. package/dist/cjs/ArtifactStoreMetrics.js.map +7 -0
  20. package/dist/cjs/ArtifactSweep.d.ts +185 -0
  21. package/dist/cjs/ArtifactSweep.d.ts.map +1 -0
  22. package/dist/cjs/ArtifactSweep.js +171 -0
  23. package/dist/cjs/ArtifactSweep.js.map +7 -0
  24. package/dist/cjs/CombinedArtifacts.d.ts +103 -0
  25. package/dist/cjs/CombinedArtifacts.d.ts.map +1 -0
  26. package/dist/cjs/CombinedArtifacts.js +189 -0
  27. package/dist/cjs/CombinedArtifacts.js.map +7 -0
  28. package/dist/cjs/FileLease.d.ts +117 -0
  29. package/dist/cjs/FileLease.d.ts.map +1 -0
  30. package/dist/cjs/FileLease.js +208 -0
  31. package/dist/cjs/FileLease.js.map +7 -0
  32. package/dist/cjs/FileSystemArtifactStore.d.ts +93 -0
  33. package/dist/cjs/FileSystemArtifactStore.d.ts.map +1 -0
  34. package/dist/cjs/FileSystemArtifactStore.js +276 -0
  35. package/dist/cjs/FileSystemArtifactStore.js.map +7 -0
  36. package/dist/cjs/MemoryArtifactStore.d.ts +25 -0
  37. package/dist/cjs/MemoryArtifactStore.d.ts.map +1 -0
  38. package/dist/cjs/MemoryArtifactStore.js +84 -0
  39. package/dist/cjs/MemoryArtifactStore.js.map +7 -0
  40. package/dist/cjs/NoopArtifactStore.d.ts +17 -0
  41. package/dist/cjs/NoopArtifactStore.d.ts.map +1 -0
  42. package/dist/cjs/NoopArtifactStore.js +46 -0
  43. package/dist/cjs/NoopArtifactStore.js.map +7 -0
  44. package/dist/cjs/RemoteArtifacts.d.ts +237 -0
  45. package/dist/cjs/RemoteArtifacts.d.ts.map +1 -0
  46. package/dist/cjs/RemoteArtifacts.js +405 -0
  47. package/dist/cjs/RemoteArtifacts.js.map +7 -0
  48. package/dist/cjs/index.d.ts +65 -0
  49. package/dist/cjs/index.d.ts.map +1 -0
  50. package/dist/cjs/index.js +47 -0
  51. package/dist/cjs/index.js.map +7 -0
  52. package/dist/cjs/internal/ArtifactLocks.d.ts +30 -0
  53. package/dist/cjs/internal/ArtifactLocks.d.ts.map +1 -0
  54. package/dist/cjs/internal/ArtifactLocks.js +93 -0
  55. package/dist/cjs/internal/ArtifactLocks.js.map +7 -0
  56. package/dist/cjs/internal/ArtifactPath.d.ts +19 -0
  57. package/dist/cjs/internal/ArtifactPath.d.ts.map +1 -0
  58. package/dist/cjs/internal/ArtifactPath.js +74 -0
  59. package/dist/cjs/internal/ArtifactPath.js.map +7 -0
  60. package/dist/cjs/measureBytes.d.ts +18 -0
  61. package/dist/cjs/measureBytes.d.ts.map +1 -0
  62. package/dist/cjs/measureBytes.js +43 -0
  63. package/dist/cjs/measureBytes.js.map +7 -0
  64. package/dist/cjs/package.json +1 -0
  65. package/dist/cjs/snapshotBytes.d.ts +19 -0
  66. package/dist/cjs/snapshotBytes.d.ts.map +1 -0
  67. package/dist/cjs/snapshotBytes.js +41 -0
  68. package/dist/cjs/snapshotBytes.js.map +7 -0
  69. package/dist/cjs/validateDigest.d.ts +22 -0
  70. package/dist/cjs/validateDigest.d.ts.map +1 -0
  71. package/dist/cjs/validateDigest.js +42 -0
  72. package/dist/cjs/validateDigest.js.map +7 -0
  73. package/dist/esm/ArtifactBackupLease.d.ts +34 -0
  74. package/dist/esm/ArtifactBackupLease.d.ts.map +1 -0
  75. package/dist/esm/ArtifactBackupLease.js +126 -0
  76. package/dist/esm/ArtifactBackupLease.js.map +1 -0
  77. package/dist/esm/ArtifactStore.d.ts +129 -0
  78. package/dist/esm/ArtifactStore.d.ts.map +1 -0
  79. package/dist/esm/ArtifactStore.js +83 -0
  80. package/dist/esm/ArtifactStore.js.map +1 -0
  81. package/dist/esm/ArtifactStoreError.d.ts +82 -0
  82. package/dist/esm/ArtifactStoreError.d.ts.map +1 -0
  83. package/dist/esm/ArtifactStoreError.js +77 -0
  84. package/dist/esm/ArtifactStoreError.js.map +1 -0
  85. package/dist/esm/ArtifactStoreMetrics.d.ts +70 -0
  86. package/dist/esm/ArtifactStoreMetrics.d.ts.map +1 -0
  87. package/dist/esm/ArtifactStoreMetrics.js +77 -0
  88. package/dist/esm/ArtifactStoreMetrics.js.map +1 -0
  89. package/dist/esm/ArtifactSweep.d.ts +185 -0
  90. package/dist/esm/ArtifactSweep.d.ts.map +1 -0
  91. package/dist/esm/ArtifactSweep.js +210 -0
  92. package/dist/esm/ArtifactSweep.js.map +1 -0
  93. package/dist/esm/CombinedArtifacts.d.ts +103 -0
  94. package/dist/esm/CombinedArtifacts.d.ts.map +1 -0
  95. package/dist/esm/CombinedArtifacts.js +214 -0
  96. package/dist/esm/CombinedArtifacts.js.map +1 -0
  97. package/dist/esm/FileLease.d.ts +117 -0
  98. package/dist/esm/FileLease.d.ts.map +1 -0
  99. package/dist/esm/FileLease.js +249 -0
  100. package/dist/esm/FileLease.js.map +1 -0
  101. package/dist/esm/FileSystemArtifactStore.d.ts +93 -0
  102. package/dist/esm/FileSystemArtifactStore.d.ts.map +1 -0
  103. package/dist/esm/FileSystemArtifactStore.js +336 -0
  104. package/dist/esm/FileSystemArtifactStore.js.map +1 -0
  105. package/dist/esm/MemoryArtifactStore.d.ts +25 -0
  106. package/dist/esm/MemoryArtifactStore.d.ts.map +1 -0
  107. package/dist/esm/MemoryArtifactStore.js +67 -0
  108. package/dist/esm/MemoryArtifactStore.js.map +1 -0
  109. package/dist/esm/NoopArtifactStore.d.ts +17 -0
  110. package/dist/esm/NoopArtifactStore.d.ts.map +1 -0
  111. package/dist/esm/NoopArtifactStore.js +27 -0
  112. package/dist/esm/NoopArtifactStore.js.map +1 -0
  113. package/dist/esm/RemoteArtifacts.d.ts +237 -0
  114. package/dist/esm/RemoteArtifacts.d.ts.map +1 -0
  115. package/dist/esm/RemoteArtifacts.js +557 -0
  116. package/dist/esm/RemoteArtifacts.js.map +1 -0
  117. package/dist/esm/index.d.ts +65 -0
  118. package/dist/esm/index.d.ts.map +1 -0
  119. package/dist/esm/index.js +65 -0
  120. package/dist/esm/index.js.map +1 -0
  121. package/dist/esm/internal/ArtifactLocks.d.ts +30 -0
  122. package/dist/esm/internal/ArtifactLocks.d.ts.map +1 -0
  123. package/dist/esm/internal/ArtifactLocks.js +86 -0
  124. package/dist/esm/internal/ArtifactLocks.js.map +1 -0
  125. package/dist/esm/internal/ArtifactPath.d.ts +19 -0
  126. package/dist/esm/internal/ArtifactPath.d.ts.map +1 -0
  127. package/dist/esm/internal/ArtifactPath.js +59 -0
  128. package/dist/esm/internal/ArtifactPath.js.map +1 -0
  129. package/dist/esm/measureBytes.d.ts +18 -0
  130. package/dist/esm/measureBytes.d.ts.map +1 -0
  131. package/dist/esm/measureBytes.js +22 -0
  132. package/dist/esm/measureBytes.js.map +1 -0
  133. package/dist/esm/snapshotBytes.d.ts +19 -0
  134. package/dist/esm/snapshotBytes.d.ts.map +1 -0
  135. package/dist/esm/snapshotBytes.js +30 -0
  136. package/dist/esm/snapshotBytes.js.map +1 -0
  137. package/dist/esm/validateDigest.d.ts +22 -0
  138. package/dist/esm/validateDigest.d.ts.map +1 -0
  139. package/dist/esm/validateDigest.js +26 -0
  140. package/dist/esm/validateDigest.js.map +1 -0
  141. package/docs/README.md +202 -0
  142. package/docs/api.md +484 -0
  143. package/docs/concepts/content-addressing.md +174 -0
  144. package/docs/concepts/coordination.md +141 -0
  145. package/docs/concepts/tiers.md +147 -0
  146. package/docs/guides/fence-a-backup.md +123 -0
  147. package/docs/guides/reclaim-disk-space.md +143 -0
  148. package/docs/guides/serve-the-artifact-protocol.md +140 -0
  149. package/docs/guides/share-artifacts-across-machines.md +180 -0
  150. package/docs/guides/test-against-an-artifact-store.md +158 -0
  151. package/docs/installation.md +102 -0
  152. package/docs/quickstart.md +183 -0
  153. package/docs/troubleshooting.md +232 -0
  154. package/package.json +179 -3
  155. package/src/ArtifactBackupLease.ts +223 -0
  156. package/src/ArtifactStore.ts +152 -0
  157. package/src/ArtifactStoreError.ts +96 -0
  158. package/src/ArtifactStoreMetrics.ts +83 -0
  159. package/src/ArtifactSweep.ts +346 -0
  160. package/src/CombinedArtifacts.ts +369 -0
  161. package/src/FileLease.ts +397 -0
  162. package/src/FileSystemArtifactStore.ts +459 -0
  163. package/src/MemoryArtifactStore.ts +83 -0
  164. package/src/NoopArtifactStore.ts +30 -0
  165. package/src/RemoteArtifacts.ts +814 -0
  166. package/src/index.ts +71 -0
  167. package/src/internal/ArtifactLocks.ts +110 -0
  168. package/src/internal/ArtifactPath.ts +72 -0
  169. package/src/measureBytes.ts +28 -0
  170. package/src/snapshotBytes.ts +34 -0
  171. package/src/validateDigest.ts +31 -0
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var CombinedArtifacts_exports = {};
30
+ __export(CombinedArtifacts_exports, {
31
+ layer: () => layer,
32
+ make: () => make
33
+ });
34
+ module.exports = __toCommonJS(CombinedArtifacts_exports);
35
+ var Cause = __toESM(require("effect/Cause"), 1);
36
+ var Clock = __toESM(require("effect/Clock"), 1);
37
+ var Deferred = __toESM(require("effect/Deferred"), 1);
38
+ var Duration = __toESM(require("effect/Duration"), 1);
39
+ var Effect = __toESM(require("effect/Effect"), 1);
40
+ var Exit = __toESM(require("effect/Exit"), 1);
41
+ var Layer = __toESM(require("effect/Layer"), 1);
42
+ var Metric = __toESM(require("effect/Metric"), 1);
43
+ var Option = __toESM(require("effect/Option"), 1);
44
+ var Schema = __toESM(require("effect/Schema"), 1);
45
+ var ArtifactStore = __toESM(require("./ArtifactStore.js"), 1);
46
+ var ArtifactStoreMetrics = __toESM(require("./ArtifactStoreMetrics.js"), 1);
47
+ var RemoteArtifacts = __toESM(require("./RemoteArtifacts.js"), 1);
48
+ const defaultUploadTimeout = Duration.seconds(60);
49
+ const droppedWarningIntervalMs = 6e4;
50
+ const reasonOf = (error) => Cause.isTimeoutError(error) ? error._tag : error.code;
51
+ const make = (options) => Effect.gen(function* () {
52
+ const { local, remote } = options;
53
+ const parsedUploadTimeout = Duration.fromInput(options.uploadTimeout ?? defaultUploadTimeout);
54
+ if (Option.isNone(parsedUploadTimeout) || !Number.isFinite(Duration.toMillis(parsedUploadTimeout.value)) || Duration.toMillis(parsedUploadTimeout.value) <= 0) {
55
+ return yield* Effect.fail(
56
+ new ArtifactStore.ArtifactStoreError({
57
+ code: "invalid_configuration",
58
+ message: "invalid combined artifact option: uploadTimeout"
59
+ })
60
+ );
61
+ }
62
+ const uploadTimeout = parsedUploadTimeout.value;
63
+ const parsedWriteBackTimeout = Duration.fromInput(options.writeBackTimeout ?? defaultUploadTimeout);
64
+ if (Option.isNone(parsedWriteBackTimeout) || !Number.isFinite(Duration.toMillis(parsedWriteBackTimeout.value)) || Duration.toMillis(parsedWriteBackTimeout.value) <= 0) {
65
+ return yield* Effect.fail(
66
+ new ArtifactStore.ArtifactStoreError({
67
+ code: "invalid_configuration",
68
+ message: "invalid combined artifact option: writeBackTimeout"
69
+ })
70
+ );
71
+ }
72
+ const writeBackTimeout = parsedWriteBackTimeout.value;
73
+ const downloadPolicy = options.downloadPolicy ?? RemoteArtifacts.downloadPolicyOf(remote) ?? "all";
74
+ if (!Schema.is(RemoteArtifacts.DownloadPolicy)(downloadPolicy)) {
75
+ return yield* Effect.fail(
76
+ new ArtifactStore.ArtifactStoreError({
77
+ code: "invalid_configuration",
78
+ message: "invalid combined artifact option: downloadPolicy"
79
+ })
80
+ );
81
+ }
82
+ const uploads = /* @__PURE__ */ new Map();
83
+ const lastWarnedAt = /* @__PURE__ */ new Map();
84
+ const dropped = (operation) => (error) => Effect.gen(function* () {
85
+ yield* Metric.update(ArtifactStoreMetrics.remoteFailure[operation], 1);
86
+ const now = yield* Clock.currentTimeMillis;
87
+ const last = lastWarnedAt.get(operation);
88
+ if (last !== void 0 && now - last < droppedWarningIntervalMs) return;
89
+ lastWarnedAt.set(operation, now);
90
+ yield* Effect.logWarning("Combined artifact transfer dropped").pipe(
91
+ Effect.annotateLogs({ operation, reason: reasonOf(error) })
92
+ );
93
+ });
94
+ const uploadInterrupted = () => new ArtifactStore.ArtifactStoreError({
95
+ code: "unavailable",
96
+ message: "the shared upload was interrupted before it settled"
97
+ });
98
+ const uploadOnce = (digest, bytes) => Effect.suspend(() => {
99
+ const joined = uploads.get(digest);
100
+ if (joined !== void 0) return Deferred.await(joined);
101
+ return Effect.uninterruptibleMask(
102
+ (restore) => Effect.gen(function* () {
103
+ const deferred = yield* Deferred.make();
104
+ uploads.set(digest, deferred);
105
+ return yield* restore(remote.put(bytes)).pipe(
106
+ Effect.onExit((exit) => {
107
+ uploads.delete(digest);
108
+ return Exit.hasInterrupts(exit) ? Deferred.fail(deferred, uploadInterrupted()) : Deferred.done(deferred, exit);
109
+ })
110
+ );
111
+ })
112
+ );
113
+ });
114
+ const put = Effect.fn("CombinedArtifacts.put")(
115
+ (bytes) => Effect.flatMap(ArtifactStore.snapshotBytes(bytes), (snapshot) => Effect.gen(function* () {
116
+ const digest = yield* local.put(snapshot);
117
+ yield* Effect.annotateCurrentSpan({ digest });
118
+ yield* uploadOnce(digest, snapshot).pipe(
119
+ Effect.timeout(uploadTimeout),
120
+ Effect.asVoid,
121
+ Effect.catch(dropped("put"))
122
+ );
123
+ return digest;
124
+ }))
125
+ );
126
+ const get = Effect.fn("CombinedArtifacts.get")(
127
+ (digest) => Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) => Effect.gen(function* () {
128
+ yield* Effect.annotateCurrentSpan({ digest: validated });
129
+ const cached = yield* local.get(validated).pipe(
130
+ Effect.map((bytes) => ({ _tag: "hit", bytes })),
131
+ Effect.catchTags({
132
+ "@smthrs/artifacts/ArtifactMissing": () => Effect.succeed({ _tag: "miss" }),
133
+ "@smthrs/artifacts/ArtifactCorruption": () => Effect.succeed({ _tag: "corrupt" })
134
+ })
135
+ );
136
+ if (cached._tag === "hit") return cached.bytes;
137
+ const fetched = yield* remote.get(validated);
138
+ if (downloadPolicy !== "minimal" || cached._tag === "corrupt") {
139
+ yield* local.put(fetched).pipe(
140
+ Effect.timeout(writeBackTimeout),
141
+ Effect.asVoid,
142
+ Effect.catch(dropped("write_back"))
143
+ );
144
+ }
145
+ return fetched;
146
+ }))
147
+ );
148
+ const has = Effect.fn("CombinedArtifacts.has")(
149
+ (digest) => Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) => Effect.annotateCurrentSpan({ digest: validated }).pipe(
150
+ Effect.andThen(
151
+ Effect.flatMap(
152
+ local.has(validated),
153
+ (present) => present ? Effect.succeed(true) : remote.has(validated)
154
+ )
155
+ )
156
+ ))
157
+ );
158
+ const findMissing = Effect.fn("CombinedArtifacts.findMissing")(
159
+ (digests) => Effect.gen(function* () {
160
+ const requested = [...new Set(digests)];
161
+ const validated = [];
162
+ for (const digest of requested) {
163
+ validated.push(yield* ArtifactStore.validateDigest(digest));
164
+ }
165
+ return yield* Effect.annotateCurrentSpan({ count: validated.length }).pipe(
166
+ Effect.andThen(
167
+ Effect.flatMap(
168
+ local.findMissing(validated),
169
+ (missingLocally) => missingLocally.length === 0 ? Effect.succeed(missingLocally) : remote.findMissing(missingLocally)
170
+ )
171
+ )
172
+ );
173
+ })
174
+ );
175
+ return { put, get, has, findMissing, downloadPolicy };
176
+ });
177
+ const layer = (options) => Layer.effect(ArtifactStore.ArtifactStore)(
178
+ Effect.flatMap(
179
+ Effect.all({ local: options.local, remote: options.remote }),
180
+ ({ local, remote }) => make({
181
+ local,
182
+ remote,
183
+ uploadTimeout: options.uploadTimeout,
184
+ writeBackTimeout: options.writeBackTimeout,
185
+ downloadPolicy: options.downloadPolicy
186
+ })
187
+ )
188
+ );
189
+ //# sourceMappingURL=CombinedArtifacts.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/CombinedArtifacts.ts"],
4
+ "sourcesContent": ["/**\n * Two artifact tiers composed into one: local first, remote second, with\n * write-back into the local tier.\n *\n * This is the shape of Bazel's `CombinedCache`\n * (`com.google.devtools.build.lib.remote.CombinedCache`): a read consults the\n * disk cache, falls back to the remote cache only on a miss, and *uploads what it\n * found back into the disk cache* so the next read is local\n * (`downloadActionResultFromRemote`, lines 230-303). A write goes to both.\n *\n * Deviation from Bazel: policy is declared once by the remote tier rather than\n * threaded through every call. `downloadPolicy` controls prefetch and local\n * materialization; composing only the local tier opts out of shared storage.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Cause from \"effect/Cause\"\nimport * as Clock from \"effect/Clock\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Layer from \"effect/Layer\"\nimport * as Metric from \"effect/Metric\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as ArtifactStore from \"./ArtifactStore.ts\"\nimport * as ArtifactStoreMetrics from \"./ArtifactStoreMetrics.ts\"\nimport * as RemoteArtifacts from \"./RemoteArtifacts.ts\"\n\n/**\n * The two tiers to compose.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface Options {\n /** The fast, machine-local tier. Every read tries this one first. */\n readonly local: ArtifactStore.Service\n /** The shared tier. Consulted only on a local miss; written through on put. */\n readonly remote: ArtifactStore.Service\n /**\n * How long a `put` waits for its opportunistic upload to the shared tier\n * before abandoning it. The local digest is already in hand when the upload\n * starts, so the deadline bounds only how long a stalled remote can delay\n * the answer \u2014 an abandoned upload is dropped exactly like a refused one.\n * Defaults to 60 seconds, Bazel's `--remote_timeout` default\n * (its `RemoteOptions`).\n */\n readonly uploadTimeout?: Duration.Input | undefined\n /**\n * How long a `get` waits for opportunistic local write-back before\n * interrupting it and returning the verified remote bytes. Defaults to\n * 60 seconds, matching the default upload deadline.\n */\n readonly writeBackTimeout?: Duration.Input | undefined\n /**\n * How eagerly a read materializes a blob into the local tier. Defaults to\n * the policy the remote tier declares (`RemoteArtifacts.Options.downloadPolicy`),\n * and to `all` for a remote tier that declares none.\n *\n * `all` and `toplevel` both write a fetched blob back into the local tier, so\n * the second read is local; they differ only in whether\n * `@smthrs/engine-store`'s `ArtifactSync.hydrate` prefetches. `minimal`\n * serves the bytes without writing them back, so a host that must not\n * accumulate other machines' artifacts never does. `minimal` still repairs a\n * *corrupt* local address, because a rewrite of an address the local tier\n * already claims is repair rather than growth.\n *\n * No policy makes the write-back load-bearing: a local tier that refuses it\n * costs the next read a network round trip, never this read's answer.\n */\n readonly downloadPolicy?: RemoteArtifacts.DownloadPolicy | undefined\n}\n\n/**\n * The default deadline on the opportunistic upload. 60 seconds is Bazel's\n * `--remote_timeout` default for its remote cache calls.\n */\nconst defaultUploadTimeout = Duration.seconds(60)\n\n/** At most one dropped-transfer warning per operation in this window. */\nconst droppedWarningIntervalMs = 60_000\n\n/** The credential-free reason a dropped transfer failed: its code, never its bytes. */\nconst reasonOf = (error: ArtifactStore.ArtifactStoreError | Cause.TimeoutError): string =>\n Cause.isTimeoutError(error) ? error._tag : error.code\n\n/**\n * What the local tier answered a read with. A miss and a corrupt address both\n * fall through to the shared tier, but they are not the same fact: only\n * corruption earns a write-back the `minimal` policy would otherwise skip.\n */\ntype LocalRead =\n | { readonly _tag: \"hit\"; readonly bytes: Uint8Array }\n | { readonly _tag: \"miss\" }\n | { readonly _tag: \"corrupt\" }\n\n/**\n * Composes a local and a remote artifact store.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const make = (\n options: Options\n): Effect.Effect<RemoteArtifacts.Service, ArtifactStore.ArtifactStoreError> =>\n Effect.gen(function*() {\n const { local, remote } = options\n const parsedUploadTimeout = Duration.fromInput(options.uploadTimeout ?? defaultUploadTimeout)\n if (\n Option.isNone(parsedUploadTimeout) ||\n !Number.isFinite(Duration.toMillis(parsedUploadTimeout.value)) ||\n Duration.toMillis(parsedUploadTimeout.value) <= 0\n ) {\n return yield* Effect.fail(\n new ArtifactStore.ArtifactStoreError({\n code: \"invalid_configuration\",\n message: \"invalid combined artifact option: uploadTimeout\"\n })\n )\n }\n const uploadTimeout = parsedUploadTimeout.value\n const parsedWriteBackTimeout = Duration.fromInput(options.writeBackTimeout ?? defaultUploadTimeout)\n if (\n Option.isNone(parsedWriteBackTimeout) ||\n !Number.isFinite(Duration.toMillis(parsedWriteBackTimeout.value)) ||\n Duration.toMillis(parsedWriteBackTimeout.value) <= 0\n ) {\n return yield* Effect.fail(\n new ArtifactStore.ArtifactStoreError({\n code: \"invalid_configuration\",\n message: \"invalid combined artifact option: writeBackTimeout\"\n })\n )\n }\n const writeBackTimeout = parsedWriteBackTimeout.value\n const downloadPolicy = options.downloadPolicy ?? RemoteArtifacts.downloadPolicyOf(remote) ?? \"all\"\n if (!Schema.is(RemoteArtifacts.DownloadPolicy)(downloadPolicy)) {\n return yield* Effect.fail(\n new ArtifactStore.ArtifactStoreError({\n code: \"invalid_configuration\",\n message: \"invalid combined artifact option: downloadPolicy\"\n })\n )\n }\n /**\n * In-flight uploads, keyed by digest. Two settles in one process that spill\n * the same artifact would otherwise both push the same bytes over the\n * network; the second joins the first's `Deferred` instead. The map holds\n * only in-flight work \u2014 the entry is removed before the deferred is\n * completed, so a later `put` of the same digest starts a fresh upload\n * rather than replaying a stale outcome.\n */\n const uploads = new Map<string, Deferred.Deferred<ArtifactStore.Digest, ArtifactStore.ArtifactStoreError>>()\n /**\n * Counts a dropped opportunistic transfer and warns at most once per\n * operation per {@link droppedWarningIntervalMs}, so a shared tier that\n * refuses every request is visible without one log line per artifact.\n */\n const lastWarnedAt = new Map<\"put\" | \"write_back\", number>()\n const dropped =\n (operation: \"put\" | \"write_back\") => (error: ArtifactStore.ArtifactStoreError | Cause.TimeoutError) =>\n Effect.gen(function*() {\n yield* Metric.update(ArtifactStoreMetrics.remoteFailure[operation], 1)\n const now = yield* Clock.currentTimeMillis\n const last = lastWarnedAt.get(operation)\n if (last !== undefined && now - last < droppedWarningIntervalMs) return\n lastWarnedAt.set(operation, now)\n yield* Effect.logWarning(\"Combined artifact transfer dropped\").pipe(\n Effect.annotateLogs({ operation, reason: reasonOf(error) })\n )\n })\n const uploadInterrupted = (): ArtifactStore.ArtifactStoreError =>\n new ArtifactStore.ArtifactStoreError({\n code: \"unavailable\",\n message: \"the shared upload was interrupted before it settled\"\n })\n const uploadOnce = (digest: ArtifactStore.Digest, bytes: Uint8Array) =>\n Effect.suspend(() => {\n const joined = uploads.get(digest)\n if (joined !== undefined) return Deferred.await(joined)\n // Registration and settlement are atomic against interruption. The\n // upload itself stays interruptible \u2014 that is how the deadline in `put`\n // cuts it short \u2014 but everything around it runs masked: interruption\n // striking between registering the deferred and resolving it would\n // otherwise orphan the entry, and every later `put` of the digest would\n // join a deferred nobody will ever complete.\n return Effect.uninterruptibleMask((restore) =>\n Effect.gen(function*() {\n const deferred = yield* Deferred.make<ArtifactStore.Digest, ArtifactStore.ArtifactStoreError>()\n uploads.set(digest, deferred)\n return yield* restore(remote.put(bytes)).pipe(\n Effect.onExit((exit) => {\n uploads.delete(digest)\n // An interrupted upload resolves the deferred with a typed\n // failure, never with the interruption itself: interruption\n // would tear down every innocent waiter, while a typed refusal\n // is exactly the outcome `put` already drops. The map entry is\n // gone either way, so the next `put` of the digest retries\n // with a fresh upload.\n return Exit.hasInterrupts(exit)\n ? Deferred.fail(deferred, uploadInterrupted())\n : Deferred.done(deferred, exit)\n })\n )\n })\n )\n })\n\n const put: ArtifactStore.Service[\"put\"] = Effect.fn(\"CombinedArtifacts.put\")((bytes: Uint8Array) =>\n Effect.flatMap(ArtifactStore.snapshotBytes(bytes), (snapshot) =>\n Effect.gen(function*() {\n // Local first, and its digest is the answer: the local tier is the one\n // this machine's replays resolve against, so a remote tier that is down\n // must not stop an artifact from being recorded locally.\n const digest = yield* local.put(snapshot)\n yield* Effect.annotateCurrentSpan({ digest })\n // Which means the upload is opportunistic, and a refusal is dropped\n // rather than propagated. Failing here would fail whatever produced the\n // bytes \u2014 a step's `settle`, say \u2014 because a *cache* was unreachable,\n // which is the opposite of the line above. Nothing depends on this\n // upload: what actually guarantees a shared cache entry's blobs are\n // durable is the publication protocol's `findMissing` \u2192 upload \u2192\n // confirm, run before the entry is published. A dropped upload here\n // costs that protocol one re-upload, never correctness. The deadline\n // keeps it opportunistic in time as well: a remote that stalls instead\n // of refusing must not hold the local answer hostage, so the upload is\n // interrupted after `uploadTimeout` and abandoned like any refusal.\n yield* uploadOnce(digest, snapshot).pipe(\n Effect.timeout(uploadTimeout),\n Effect.asVoid,\n Effect.catch(dropped(\"put\"))\n )\n return digest\n }))\n )\n\n const get: ArtifactStore.Service[\"get\"] = Effect.fn(\"CombinedArtifacts.get\")((digest: string) =>\n Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) =>\n Effect.gen(function*() {\n yield* Effect.annotateCurrentSpan({ digest: validated })\n // A local miss AND local corruption fall through to the remote tier,\n // and they are kept apart because they earn different write-backs. A\n // local `ArtifactStoreError` deliberately does NOT fall through: a\n // host that refused the read has not answered the question, and\n // silently paying the network for it would hide a broken local tier\n // behind a working shared one.\n const cached = yield* local.get(validated).pipe(\n Effect.map((bytes): LocalRead => ({ _tag: \"hit\", bytes })),\n Effect.catchTags({\n \"@smthrs/artifacts/ArtifactMissing\": () => Effect.succeed<LocalRead>({ _tag: \"miss\" }),\n \"@smthrs/artifacts/ArtifactCorruption\": () => Effect.succeed<LocalRead>({ _tag: \"corrupt\" })\n })\n )\n if (cached._tag === \"hit\") return cached.bytes\n const fetched = yield* remote.get(validated)\n // `minimal` reads through without materializing: the caller gets the\n // bytes, the local tier stays exactly as small as it was, and the next\n // read pays the network again. Corruption is the exception every\n // policy makes. Handing the correct bytes to `local.put` lets its own\n // digest verification rewrite the mismatched blob, and an address the\n // local tier already claims \u2014 `has` and `findMissing` both report it\n // as present \u2014 must be one it can serve, or the publication protocol\n // is told a lie no later read can correct.\n if (downloadPolicy !== \"minimal\" || cached._tag === \"corrupt\") {\n // Opportunistic, exactly like `put`'s upload: the answer is already\n // in hand, so a full disk, a read-only mount, or a refused sync must\n // cost the next read a round trip rather than fail this one. A\n // stalled write is interrupted at the deadline so it cannot hold\n // the verified remote answer indefinitely.\n yield* local.put(fetched).pipe(\n Effect.timeout(writeBackTimeout),\n Effect.asVoid,\n Effect.catch(dropped(\"write_back\"))\n )\n }\n return fetched\n }))\n )\n\n const has: ArtifactStore.Service[\"has\"] = Effect.fn(\"CombinedArtifacts.has\")((digest: string) =>\n Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) =>\n Effect.annotateCurrentSpan({ digest: validated }).pipe(\n Effect.andThen(\n Effect.flatMap(\n local.has(validated),\n (present) => present ? Effect.succeed(true) : remote.has(validated)\n )\n )\n ))\n )\n\n const findMissing: ArtifactStore.Service[\"findMissing\"] = Effect.fn(\"CombinedArtifacts.findMissing\")(\n (digests: Iterable<string>) =>\n Effect.gen(function*() {\n // The iterable is materialized once: it may be single-pass, and both the\n // annotation and the local probe need it.\n const requested = [...new Set(digests)]\n const validated: Array<ArtifactStore.Digest> = []\n for (const digest of requested) {\n validated.push(yield* ArtifactStore.validateDigest(digest))\n }\n // Missing means missing from BOTH tiers, and the remote probe is asked\n // only about what the local tier could not answer \u2014 one network round\n // trip, over the smallest possible set. The result stays a subset of the\n // input because each stage filters the previous stage's output.\n return yield* Effect.annotateCurrentSpan({ count: validated.length }).pipe(\n Effect.andThen(\n Effect.flatMap(\n local.findMissing(validated),\n (missingLocally) =>\n missingLocally.length === 0 ? Effect.succeed(missingLocally) : remote.findMissing(missingLocally)\n )\n )\n )\n })\n )\n\n return { put, get, has, findMissing, downloadPolicy }\n })\n\n/**\n * The two tiers as effects, plus the options {@link make} takes.\n *\n * Both tiers are supplied as *effects* rather than layers because they inhabit\n * the same tag: composing two `Layer<ArtifactStore>` would just shadow one\n * with the other. Pair `ArtifactStore.makeFileSystem` (wrapped in\n * `Effect.sync`) or `Effect.map(FileSystem.FileSystem, ...)` with\n * `RemoteArtifacts.make`.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface LayerOptions<EL, RL, ER, RR> {\n readonly local: Effect.Effect<ArtifactStore.Service, EL, RL>\n readonly remote: Effect.Effect<ArtifactStore.Service, ER, RR>\n readonly uploadTimeout?: Duration.Input | undefined\n readonly writeBackTimeout?: Duration.Input | undefined\n readonly downloadPolicy?: RemoteArtifacts.DownloadPolicy | undefined\n}\n\n/**\n * Provides a local-first artifact store, backed by local and remote effects, as\n * the `ArtifactStore` tag.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layer = <EL, RL, ER, RR>(\n options: LayerOptions<EL, RL, ER, RR>\n): Layer.Layer<ArtifactStore.ArtifactStore, EL | ER | ArtifactStore.ArtifactStoreError, RL | RR> =>\n Layer.effect(ArtifactStore.ArtifactStore)(\n Effect.flatMap(\n Effect.all({ local: options.local, remote: options.remote }),\n ({ local, remote }) =>\n make({\n local,\n remote,\n uploadTimeout: options.uploadTimeout,\n writeBackTimeout: options.writeBackTimeout,\n downloadPolicy: options.downloadPolicy\n })\n )\n )\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A cross-process lease on one lock file.
3
+ *
4
+ * The lock is a file created with `wx` (exclusive create) that holds a unique
5
+ * owner token. The holder refreshes its mtime every `heartbeatEvery`; a lock
6
+ * whose mtime is older than `staleAfterMs` belongs to a crashed or hard-killed
7
+ * owner and is reclaimed. Reclaimers of one lock generation race for a `wx`
8
+ * claim file first, so a stale generation is moved away at most once and a
9
+ * fresh lock is never displaced. Acquisition is bounded by `acquireWithin`,
10
+ * so a waiter fails with a typed error instead of waiting forever on a lock
11
+ * nothing will release.
12
+ *
13
+ * | Setting | Default |
14
+ * | ---------------- | ----------- |
15
+ * | `heartbeatEvery` | 10 seconds |
16
+ * | `staleAfterMs` | 60 000 ms |
17
+ * | `acquireWithin` | 2 minutes |
18
+ * | `retryEvery` | 25 millis |
19
+ *
20
+ * The fence is bounded, not absolute: a holder stalled past `staleAfterMs`
21
+ * (a suspended laptop, a stopped debugger) is reaped while it still runs. Its
22
+ * heartbeat then logs `<label> was reclaimed while its holder was still
23
+ * running`, and the protected effect continues unfenced.
24
+ *
25
+ * A directory at the lock path, left by a release that used an exclusive
26
+ * `makeDirectory` as its lock, is treated as a lock with no readable owner:
27
+ * it is reclaimed once its mtime is past `staleAfterMs`, and waited on
28
+ * before that.
29
+ *
30
+ * The host needs exclusive `wx` file creation, `readFileString`, `stat` with
31
+ * an mtime, `utimes`, `rename`, and `remove`. The claim file and the tombstone
32
+ * a reclaim renames the stale lock to are siblings of the lock:
33
+ * `<lockPath>.reclaim-<token>` and `<lockPath>.stale-<token>`.
34
+ *
35
+ * @since 1.0.0-rc.1
36
+ */
37
+ import * as Duration from "effect/Duration";
38
+ import * as Effect from "effect/Effect";
39
+ import type * as FileSystem from "effect/FileSystem";
40
+ /**
41
+ * Tuning for one lease.
42
+ *
43
+ * @category models
44
+ * @since 1.0.0-rc.1
45
+ */
46
+ export interface Options {
47
+ /** Names the lock in the reclaimed-holder warning. Defaults to `"File lease"`. */
48
+ readonly label?: string | undefined;
49
+ /** Extra fields logged with the reclaimed-holder warning. */
50
+ readonly annotations?: Readonly<Record<string, unknown>> | undefined;
51
+ /** How often the holder refreshes the lock's mtime. Defaults to 10 seconds. */
52
+ readonly heartbeatEvery?: Duration.Input | undefined;
53
+ /**
54
+ * Age past which a lock or claim is stale. Defaults to 60 000 ms. Must be a
55
+ * finite positive number longer than `heartbeatEvery`; anything else fails
56
+ * the lease through `failure` before the lock is touched.
57
+ */
58
+ readonly staleAfterMs?: number | undefined;
59
+ /** How long acquisition may wait. Defaults to 2 minutes. */
60
+ readonly acquireWithin?: Duration.Input | undefined;
61
+ /** How long a waiter sleeps between attempts. Defaults to 25 millis. */
62
+ readonly retryEvery?: Duration.Input | undefined;
63
+ }
64
+ /**
65
+ * {@link Options} for {@link hold}, which leaves the deadline to its caller.
66
+ *
67
+ * @category models
68
+ * @since 1.0.0-rc.1
69
+ */
70
+ export interface HoldOptions extends Omit<Options, "acquireWithin"> {
71
+ /** Runs once, the moment the lease is held and before `effect` starts. */
72
+ readonly onAcquired?: Effect.Effect<void> | undefined;
73
+ /**
74
+ * Runs before every create, heartbeat, rename, and remove of the lock, its
75
+ * claim, or its tombstone. A failure refuses that step through `failure`,
76
+ * so a caller can re-check that the lock directory was not replaced (for
77
+ * example by a symlink) after it was created.
78
+ */
79
+ readonly guard?: Effect.Effect<void, unknown> | undefined;
80
+ }
81
+ /**
82
+ * The default acquisition deadline.
83
+ *
84
+ * @category constants
85
+ * @since 1.0.0-rc.1
86
+ */
87
+ export declare const defaultAcquireWithin: Duration.Input;
88
+ /**
89
+ * The default stale bound, in milliseconds.
90
+ *
91
+ * @category constants
92
+ * @since 1.0.0-rc.1
93
+ */
94
+ export declare const defaultStaleAfterMs = 60000;
95
+ /**
96
+ * Runs `effect` while holding the lease at `lockPath`, with no acquisition
97
+ * deadline. Use it when the caller already bounds a wider wait (an
98
+ * in-process semaphore plus this lease) with its own deadline; otherwise use
99
+ * {@link withLease}.
100
+ *
101
+ * @category combinators
102
+ * @since 1.0.0-rc.1
103
+ */
104
+ export declare const hold: <A, E, R, E2>(fs: FileSystem.FileSystem, lockPath: string, effect: Effect.Effect<A, E, R>, failure: (cause: unknown) => E2, options?: HoldOptions) => Effect.Effect<A, E | E2, R>;
105
+ /**
106
+ * Runs `effect` while holding the lease at `lockPath`.
107
+ *
108
+ * Acquisition fails with `failure(TimeoutError)` once `acquireWithin` passes
109
+ * without the lease; every other host refusal reaches `failure` with the
110
+ * refusing `PlatformError`. The deadline ends when the lease is held: the
111
+ * protected effect and its release are never timed out.
112
+ *
113
+ * @category combinators
114
+ * @since 1.0.0-rc.1
115
+ */
116
+ export declare const withLease: <A, E, R, E2>(fs: FileSystem.FileSystem, lockPath: string, effect: Effect.Effect<A, E, R>, failure: (cause: unknown) => E2, options?: Options) => Effect.Effect<A, E | E2, R>;
117
+ //# sourceMappingURL=FileLease.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileLease.d.ts","sourceRoot":"","sources":["../../src/FileLease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAKpD;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAA;IACpE,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACpD;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACnD,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;CACjD;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC;IACjE,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;CAC1D;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,KAAmB,CAAA;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAA;AAczC;;;;;;;;GAQG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAC9B,IAAI,UAAU,CAAC,UAAU,EACzB,UAAU,MAAM,EAChB,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,EAAE,EAC/B,UAAS,WAAgB,KACxB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAyOzB,CAAA;AAEJ;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EACnC,IAAI,UAAU,CAAC,UAAU,EACzB,UAAU,MAAM,EAChB,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,EAAE,EAC/B,UAAS,OAAY,KACpB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAYzB,CAAA"}
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var FileLease_exports = {};
30
+ __export(FileLease_exports, {
31
+ defaultAcquireWithin: () => defaultAcquireWithin,
32
+ defaultStaleAfterMs: () => defaultStaleAfterMs,
33
+ hold: () => hold,
34
+ withLease: () => withLease
35
+ });
36
+ module.exports = __toCommonJS(FileLease_exports);
37
+ var Clock = __toESM(require("effect/Clock"), 1);
38
+ var Deferred = __toESM(require("effect/Deferred"), 1);
39
+ var Duration = __toESM(require("effect/Duration"), 1);
40
+ var Effect = __toESM(require("effect/Effect"), 1);
41
+ var Option = __toESM(require("effect/Option"), 1);
42
+ var PlatformError = __toESM(require("effect/PlatformError"), 1);
43
+ var Random = __toESM(require("effect/Random"), 1);
44
+ const defaultAcquireWithin = "2 minutes";
45
+ const defaultStaleAfterMs = 6e4;
46
+ const isReason = (cause, tag) => cause instanceof PlatformError.PlatformError && cause.reason._tag === tag;
47
+ const directoryGeneration = "legacy-directory";
48
+ const token = Effect.gen(function* () {
49
+ const first = yield* Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER, { halfOpen: true });
50
+ const second = yield* Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER, { halfOpen: true });
51
+ return `${first.toString(36)}-${second.toString(36)}-${yield* Clock.currentTimeMillis}`;
52
+ });
53
+ const hold = (fs, lockPath, effect, failure, options = {}) => Effect.gen(function* () {
54
+ const label = options.label ?? "File lease";
55
+ const heartbeatEvery = options.heartbeatEvery ?? "10 seconds";
56
+ const staleAfterMs = options.staleAfterMs ?? defaultStaleAfterMs;
57
+ const retryEvery = options.retryEvery ?? "25 millis";
58
+ const heartbeatMs = Option.map(Duration.fromInput(heartbeatEvery), Duration.toMillis);
59
+ if (typeof staleAfterMs !== "number" || !Number.isFinite(staleAfterMs) || staleAfterMs <= 0 || Option.isNone(heartbeatMs) || !(heartbeatMs.value > 0 && heartbeatMs.value < staleAfterMs)) {
60
+ return yield* Effect.fail(
61
+ failure(new RangeError(`${label} needs 0 < heartbeatEvery < staleAfterMs, with a finite staleAfterMs`))
62
+ );
63
+ }
64
+ const owner = yield* token;
65
+ let acquired = false;
66
+ const guard = options.guard === void 0 ? Effect.void : options.guard.pipe(Effect.mapError(failure));
67
+ const guarded = (mutation) => Effect.andThen(guard, mutation);
68
+ const statIfPresent = (path) => fs.stat(path).pipe(
69
+ Effect.map(Option.some),
70
+ Effect.catch(
71
+ (cause) => isReason(cause, "NotFound") ? Effect.succeed(Option.none()) : Effect.fail(failure(cause))
72
+ )
73
+ );
74
+ const refuse = (cause) => Effect.fail(failure(cause));
75
+ const readOwner = (path, onRefused = refuse) => fs.readFileString(path).pipe(
76
+ Effect.map(Option.some),
77
+ Effect.catch((cause) => isReason(cause, "NotFound") ? Effect.succeed(Option.none()) : onRefused(cause))
78
+ );
79
+ const observe = (path) => readOwner(path, (cause) => fs.stat(path).pipe(
80
+ Effect.matchEffect({
81
+ onFailure: () => refuse(cause),
82
+ onSuccess: (info) => info.type === "Directory" ? Effect.succeed(Option.some(directoryGeneration)) : refuse(cause)
83
+ })
84
+ ));
85
+ const isStale = (path) => Effect.gen(function* () {
86
+ const info = yield* statIfPresent(path);
87
+ const modified = Option.isSome(info) ? Option.getOrUndefined(info.value.mtime) : void 0;
88
+ return modified !== void 0 && (yield* Clock.currentTimeMillis) - modified.getTime() > staleAfterMs;
89
+ });
90
+ const removeIfOwnedBy = (path, expected) => fs.readFileString(path).pipe(
91
+ Effect.flatMap((found) => found === expected ? guarded(fs.remove(path)) : Effect.void),
92
+ Effect.ignore
93
+ );
94
+ const acquire = Effect.gen(function* () {
95
+ while (true) {
96
+ const created = yield* Effect.uninterruptible(
97
+ guarded(fs.writeFileString(lockPath, owner, { flag: "wx", mode: 384 })).pipe(
98
+ Effect.andThen(Effect.sync(() => {
99
+ acquired = true;
100
+ return true;
101
+ })),
102
+ Effect.catch(
103
+ (cause) => isReason(cause, "AlreadyExists") ? Effect.succeed(false) : Effect.fail(failure(cause))
104
+ )
105
+ )
106
+ );
107
+ if (created) return;
108
+ const observed = yield* observe(lockPath);
109
+ if (Option.isNone(observed)) continue;
110
+ if (yield* isStale(lockPath)) {
111
+ const reclaimed = yield* reclaim(observed.value);
112
+ if (reclaimed) continue;
113
+ }
114
+ yield* Effect.sleep(retryEvery);
115
+ }
116
+ });
117
+ const reclaim = (observed) => {
118
+ const claimPath = `${lockPath}.reclaim-${observed.replace(/[^0-9A-Za-z-]/g, "_").slice(0, 96)}`;
119
+ return Effect.acquireUseRelease(
120
+ guarded(fs.writeFileString(claimPath, owner, { flag: "wx", mode: 384 })).pipe(
121
+ Effect.as(true),
122
+ Effect.catch(
123
+ (cause) => isReason(cause, "AlreadyExists") ? Effect.succeed(false) : Effect.fail(failure(cause))
124
+ )
125
+ ),
126
+ (claimed) => Effect.gen(function* () {
127
+ if (!claimed) {
128
+ const holder = yield* readOwner(claimPath);
129
+ if (Option.isSome(holder) && (yield* isStale(claimPath))) {
130
+ yield* removeIfOwnedBy(claimPath, holder.value);
131
+ }
132
+ return false;
133
+ }
134
+ const current = yield* observe(lockPath);
135
+ if (Option.isNone(current) || current.value !== observed || !(yield* isStale(lockPath))) return true;
136
+ const tombstone = `${lockPath}.stale-${owner}`;
137
+ const moved = yield* guarded(fs.rename(lockPath, tombstone)).pipe(
138
+ Effect.as(true),
139
+ Effect.catch(
140
+ (cause) => isReason(cause, "NotFound") ? Effect.succeed(false) : Effect.fail(failure(cause))
141
+ )
142
+ );
143
+ if (!moved) return true;
144
+ const displaced = yield* observe(tombstone);
145
+ if (Option.isSome(displaced) && displaced.value !== observed && displaced.value !== directoryGeneration) {
146
+ yield* guarded(fs.writeFileString(lockPath, displaced.value, { flag: "wx", mode: 384 })).pipe(
147
+ Effect.ignore
148
+ );
149
+ }
150
+ yield* guarded(fs.remove(tombstone, { recursive: true })).pipe(Effect.ignore);
151
+ return true;
152
+ }),
153
+ (claimed) => claimed ? removeIfOwnedBy(claimPath, owner) : Effect.void
154
+ );
155
+ };
156
+ const release = fs.readFileString(lockPath).pipe(
157
+ Effect.flatMap((found) => found === owner ? guarded(fs.remove(lockPath)) : Effect.void),
158
+ // A concurrent stale-lock reaper can win release. `NotFound`
159
+ // means no lock remains for this owner to release or leak.
160
+ Effect.catch(
161
+ (cause) => isReason(cause, "NotFound") ? Effect.void : Effect.fail(failure(cause))
162
+ )
163
+ );
164
+ return yield* acquire.pipe(
165
+ Effect.andThen(options.onAcquired ?? Effect.void),
166
+ Effect.andThen(Effect.scoped(Effect.gen(function* () {
167
+ yield* Effect.forkScoped(
168
+ Effect.gen(function* () {
169
+ while (true) {
170
+ yield* Effect.sleep(heartbeatEvery);
171
+ const beat = yield* fs.readFileString(lockPath).pipe(
172
+ Effect.map((found) => found === owner ? "own" : "foreign"),
173
+ Effect.catch(
174
+ (cause) => Effect.succeed(isReason(cause, "NotFound") ? "gone" : "unreadable")
175
+ )
176
+ );
177
+ if (beat === "own") {
178
+ const timestamp = new Date(yield* Clock.currentTimeMillis);
179
+ yield* Effect.ignore(guarded(fs.utimes(lockPath, timestamp, timestamp)));
180
+ continue;
181
+ }
182
+ if (beat === "unreadable") continue;
183
+ yield* Effect.logWarning(
184
+ `${label} was reclaimed while its holder was still running`,
185
+ { ...options.annotations, state: beat }
186
+ );
187
+ return;
188
+ }
189
+ })
190
+ );
191
+ return yield* effect;
192
+ }))),
193
+ Effect.onExit(() => acquired ? release : Effect.void)
194
+ );
195
+ });
196
+ const withLease = (fs, lockPath, effect, failure, options = {}) => Effect.suspend(() => {
197
+ const ready = Deferred.makeUnsafe();
198
+ const deadline = Deferred.await(ready).pipe(
199
+ Effect.timeout(options.acquireWithin ?? defaultAcquireWithin),
200
+ Effect.catchTag("TimeoutError", (cause) => Effect.fail(failure(cause))),
201
+ Effect.andThen(Effect.never)
202
+ );
203
+ return hold(fs, lockPath, effect, failure, {
204
+ ...options,
205
+ onAcquired: Deferred.succeed(ready, void 0).pipe(Effect.asVoid)
206
+ }).pipe(Effect.raceFirst(deadline));
207
+ });
208
+ //# sourceMappingURL=FileLease.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/FileLease.ts"],
4
+ "sourcesContent": ["/**\n * A cross-process lease on one lock file.\n *\n * The lock is a file created with `wx` (exclusive create) that holds a unique\n * owner token. The holder refreshes its mtime every `heartbeatEvery`; a lock\n * whose mtime is older than `staleAfterMs` belongs to a crashed or hard-killed\n * owner and is reclaimed. Reclaimers of one lock generation race for a `wx`\n * claim file first, so a stale generation is moved away at most once and a\n * fresh lock is never displaced. Acquisition is bounded by `acquireWithin`,\n * so a waiter fails with a typed error instead of waiting forever on a lock\n * nothing will release.\n *\n * | Setting | Default |\n * | ---------------- | ----------- |\n * | `heartbeatEvery` | 10 seconds |\n * | `staleAfterMs` | 60 000 ms |\n * | `acquireWithin` | 2 minutes |\n * | `retryEvery` | 25 millis |\n *\n * The fence is bounded, not absolute: a holder stalled past `staleAfterMs`\n * (a suspended laptop, a stopped debugger) is reaped while it still runs. Its\n * heartbeat then logs `<label> was reclaimed while its holder was still\n * running`, and the protected effect continues unfenced.\n *\n * A directory at the lock path, left by a release that used an exclusive\n * `makeDirectory` as its lock, is treated as a lock with no readable owner:\n * it is reclaimed once its mtime is past `staleAfterMs`, and waited on\n * before that.\n *\n * The host needs exclusive `wx` file creation, `readFileString`, `stat` with\n * an mtime, `utimes`, `rename`, and `remove`. The claim file and the tombstone\n * a reclaim renames the stale lock to are siblings of the lock:\n * `<lockPath>.reclaim-<token>` and `<lockPath>.stale-<token>`.\n *\n * @since 1.0.0-rc.1\n */\n\nimport * as Clock from \"effect/Clock\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport type * as FileSystem from \"effect/FileSystem\"\nimport * as Option from \"effect/Option\"\nimport * as PlatformError from \"effect/PlatformError\"\nimport * as Random from \"effect/Random\"\n\n/**\n * Tuning for one lease.\n *\n * @category models\n * @since 1.0.0-rc.1\n */\nexport interface Options {\n /** Names the lock in the reclaimed-holder warning. Defaults to `\"File lease\"`. */\n readonly label?: string | undefined\n /** Extra fields logged with the reclaimed-holder warning. */\n readonly annotations?: Readonly<Record<string, unknown>> | undefined\n /** How often the holder refreshes the lock's mtime. Defaults to 10 seconds. */\n readonly heartbeatEvery?: Duration.Input | undefined\n /**\n * Age past which a lock or claim is stale. Defaults to 60 000 ms. Must be a\n * finite positive number longer than `heartbeatEvery`; anything else fails\n * the lease through `failure` before the lock is touched.\n */\n readonly staleAfterMs?: number | undefined\n /** How long acquisition may wait. Defaults to 2 minutes. */\n readonly acquireWithin?: Duration.Input | undefined\n /** How long a waiter sleeps between attempts. Defaults to 25 millis. */\n readonly retryEvery?: Duration.Input | undefined\n}\n\n/**\n * {@link Options} for {@link hold}, which leaves the deadline to its caller.\n *\n * @category models\n * @since 1.0.0-rc.1\n */\nexport interface HoldOptions extends Omit<Options, \"acquireWithin\"> {\n /** Runs once, the moment the lease is held and before `effect` starts. */\n readonly onAcquired?: Effect.Effect<void> | undefined\n /**\n * Runs before every create, heartbeat, rename, and remove of the lock, its\n * claim, or its tombstone. A failure refuses that step through `failure`,\n * so a caller can re-check that the lock directory was not replaced (for\n * example by a symlink) after it was created.\n */\n readonly guard?: Effect.Effect<void, unknown> | undefined\n}\n\n/**\n * The default acquisition deadline.\n *\n * @category constants\n * @since 1.0.0-rc.1\n */\nexport const defaultAcquireWithin: Duration.Input = \"2 minutes\"\n\n/**\n * The default stale bound, in milliseconds.\n *\n * @category constants\n * @since 1.0.0-rc.1\n */\nexport const defaultStaleAfterMs = 60_000\n\nconst isReason = (cause: unknown, tag: PlatformError.SystemErrorTag): boolean =>\n cause instanceof PlatformError.PlatformError && cause.reason._tag === tag\n\n/** The generation name of a lock with no readable owner: a directory. */\nconst directoryGeneration = \"legacy-directory\"\n\nconst token = Effect.gen(function*() {\n const first = yield* Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER, { halfOpen: true })\n const second = yield* Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER, { halfOpen: true })\n return `${first.toString(36)}-${second.toString(36)}-${yield* Clock.currentTimeMillis}`\n})\n\n/**\n * Runs `effect` while holding the lease at `lockPath`, with no acquisition\n * deadline. Use it when the caller already bounds a wider wait (an\n * in-process semaphore plus this lease) with its own deadline; otherwise use\n * {@link withLease}.\n *\n * @category combinators\n * @since 1.0.0-rc.1\n */\nexport const hold = <A, E, R, E2>(\n fs: FileSystem.FileSystem,\n lockPath: string,\n effect: Effect.Effect<A, E, R>,\n failure: (cause: unknown) => E2,\n options: HoldOptions = {}\n): Effect.Effect<A, E | E2, R> =>\n Effect.gen(function*() {\n const label = options.label ?? \"File lease\"\n const heartbeatEvery = options.heartbeatEvery ?? \"10 seconds\"\n const staleAfterMs = options.staleAfterMs ?? defaultStaleAfterMs\n const retryEvery = options.retryEvery ?? \"25 millis\"\n // A holder heartbeats its lock to stay fresh, so a stale bound at or below\n // the heartbeat, or one that is not a positive finite number, would let a\n // contender read a live holder's lock as abandoned and move it, or would\n // never reclaim a dead holder's lock at all.\n const heartbeatMs = Option.map(Duration.fromInput(heartbeatEvery), Duration.toMillis)\n if (\n typeof staleAfterMs !== \"number\" || !Number.isFinite(staleAfterMs) || staleAfterMs <= 0 ||\n Option.isNone(heartbeatMs) || !(heartbeatMs.value > 0 && heartbeatMs.value < staleAfterMs)\n ) {\n return yield* Effect.fail(\n failure(new RangeError(`${label} needs 0 < heartbeatEvery < staleAfterMs, with a finite staleAfterMs`))\n )\n }\n const owner = yield* token\n /**\n * Whether this call created the lock file, and therefore owes a release.\n * It is set in the same uninterruptible step that creates the file, so\n * interruption striking between the two cannot leak a lock nothing\n * releases \u2014 the only party that would ever reclaim it is a later\n * acquirer of the same path, which may never come.\n */\n let acquired = false\n\n const guard = options.guard === undefined ? Effect.void : options.guard.pipe(Effect.mapError(failure))\n const guarded = <B, F, S>(mutation: Effect.Effect<B, F, S>): Effect.Effect<B, F | E2, S> =>\n Effect.andThen(guard, mutation)\n\n const statIfPresent = (path: string): Effect.Effect<Option.Option<FileSystem.File.Info>, E2> =>\n fs.stat(path).pipe(\n Effect.map(Option.some),\n Effect.catch((cause): Effect.Effect<Option.Option<FileSystem.File.Info>, E2> =>\n isReason(cause, \"NotFound\") ? Effect.succeed(Option.none()) : Effect.fail(failure(cause))\n )\n )\n\n const refuse = (cause: unknown): Effect.Effect<never, E2> => Effect.fail(failure(cause))\n\n const readOwner = (\n path: string,\n onRefused: (cause: unknown) => Effect.Effect<Option.Option<string>, E2> = refuse\n ): Effect.Effect<Option.Option<string>, E2> =>\n fs.readFileString(path).pipe(\n Effect.map(Option.some),\n Effect.catch((cause) => isReason(cause, \"NotFound\") ? Effect.succeed(Option.none()) : onRefused(cause))\n )\n\n /**\n * Names the lock generation at `path`: its owner token, or\n * `directoryGeneration` for a directory, which has no readable owner.\n * A read refused for any other reason is propagated, not guessed at.\n */\n const observe = (path: string): Effect.Effect<Option.Option<string>, E2> =>\n readOwner(path, (cause) =>\n fs.stat(path).pipe(\n Effect.matchEffect({\n onFailure: () => refuse(cause),\n onSuccess: (info) =>\n info.type === \"Directory\" ? Effect.succeed(Option.some(directoryGeneration)) : refuse(cause)\n })\n ))\n\n const isStale = (path: string): Effect.Effect<boolean, E2> =>\n Effect.gen(function*() {\n const info = yield* statIfPresent(path)\n const modified = Option.isSome(info) ? Option.getOrUndefined(info.value.mtime) : undefined\n return modified !== undefined && (yield* Clock.currentTimeMillis) - modified.getTime() > staleAfterMs\n })\n\n const removeIfOwnedBy = (path: string, expected: string): Effect.Effect<void> =>\n fs.readFileString(path).pipe(\n Effect.flatMap((found) => found === expected ? guarded(fs.remove(path)) : Effect.void),\n Effect.ignore\n )\n\n const acquire = Effect.gen(function*() {\n while (true) {\n const created = yield* Effect.uninterruptible(\n guarded(fs.writeFileString(lockPath, owner, { flag: \"wx\", mode: 0o600 })).pipe(\n Effect.andThen(Effect.sync(() => {\n acquired = true\n return true\n })),\n Effect.catch((cause): Effect.Effect<boolean, E2> =>\n isReason(cause, \"AlreadyExists\") ? Effect.succeed(false) : Effect.fail(failure(cause))\n )\n )\n )\n if (created) return\n\n // The owner is read before the age, so a stale verdict always\n // belongs to the lock generation this read named.\n const observed = yield* observe(lockPath)\n if (Option.isNone(observed)) continue\n if (yield* isStale(lockPath)) {\n const reclaimed = yield* reclaim(observed.value)\n if (reclaimed) continue\n }\n yield* Effect.sleep(retryEvery)\n }\n })\n\n /**\n * Moves away the stale lock generation `observed` names, at most once.\n *\n * Measuring a lock and renaming it are two steps, so two contenders\n * that both measure the same generation as stale would otherwise both\n * rename, and the second would move away the fresh lock the first just\n * took. Reclaimers of one generation therefore race for a `wx` claim\n * named after its owner token first. Only the winner renames, and only\n * after re-reading that the path still holds that stale generation. A\n * loser, or a winner whose generation is already gone, moves nothing.\n * Owner tokens are unique per acquisition, so a claim never outlives\n * the generation it names in any way that matters.\n */\n const reclaim = (observed: string): Effect.Effect<boolean, E2> => {\n const claimPath = `${lockPath}.reclaim-${observed.replace(/[^0-9A-Za-z-]/g, \"_\").slice(0, 96)}`\n return Effect.acquireUseRelease(\n guarded(fs.writeFileString(claimPath, owner, { flag: \"wx\", mode: 0o600 })).pipe(\n Effect.as(true),\n Effect.catch((cause): Effect.Effect<boolean, E2> =>\n isReason(cause, \"AlreadyExists\") ? Effect.succeed(false) : Effect.fail(failure(cause))\n )\n ),\n (claimed) =>\n Effect.gen(function*() {\n if (!claimed) {\n // A claimant that died mid-reclaim leaves its claim behind.\n // It ages out on the same bound as a lock.\n const holder = yield* readOwner(claimPath)\n if (Option.isSome(holder) && (yield* isStale(claimPath))) {\n yield* removeIfOwnedBy(claimPath, holder.value)\n }\n return false\n }\n const current = yield* observe(lockPath)\n if (Option.isNone(current) || current.value !== observed || !(yield* isStale(lockPath))) return true\n const tombstone = `${lockPath}.stale-${owner}`\n const moved = yield* guarded(fs.rename(lockPath, tombstone)).pipe(\n Effect.as(true),\n Effect.catch((cause): Effect.Effect<boolean, E2> =>\n isReason(cause, \"NotFound\") ? Effect.succeed(false) : Effect.fail(failure(cause))\n )\n )\n if (!moved) return true\n // Only a stalled owner releasing between the re-read and the\n // rename, and a new owner acquiring in that gap, puts a live\n // lock here. Hand it back with an atomic create, which cannot\n // displace anyone who took the path since.\n const displaced = yield* observe(tombstone)\n if (\n Option.isSome(displaced) && displaced.value !== observed && displaced.value !== directoryGeneration\n ) {\n yield* guarded(fs.writeFileString(lockPath, displaced.value, { flag: \"wx\", mode: 0o600 })).pipe(\n Effect.ignore\n )\n }\n yield* guarded(fs.remove(tombstone, { recursive: true })).pipe(Effect.ignore)\n return true\n }),\n (claimed) => claimed ? removeIfOwnedBy(claimPath, owner) : Effect.void\n )\n }\n\n const release = fs.readFileString(lockPath).pipe(\n Effect.flatMap((found) => found === owner ? guarded(fs.remove(lockPath)) : Effect.void),\n // A concurrent stale-lock reaper can win release. `NotFound`\n // means no lock remains for this owner to release or leak.\n Effect.catch((cause): Effect.Effect<void, E2> =>\n isReason(cause, \"NotFound\") ? Effect.void : Effect.fail(failure(cause))\n )\n )\n\n // The finalizer is attached around acquisition itself, not just around\n // the protected effect, so a lock created moments before an interruption\n // is still released. A call that never created one owes nothing and must\n // not touch a file another owner holds.\n return yield* acquire.pipe(\n Effect.andThen(options.onAcquired ?? Effect.void),\n Effect.andThen(Effect.scoped(Effect.gen(function*() {\n yield* Effect.forkScoped(\n Effect.gen(function*() {\n while (true) {\n yield* Effect.sleep(heartbeatEvery)\n // Freshen only a lock this call still owns, the same comparison\n // the release below makes. A holder stalled past the stale\n // bound has already been reaped and replaced, and touching that\n // pathname anyway would hold a stranger's lock fresh forever: a\n // replacement that is then hard-killed could never be judged\n // stale while this zombie ran, so every later acquirer would\n // burn the full acquisition deadline and fail.\n const beat = yield* fs.readFileString(lockPath).pipe(\n Effect.map((found) => found === owner ? \"own\" as const : \"foreign\" as const),\n Effect.catch((cause) =>\n Effect.succeed(isReason(cause, \"NotFound\") ? \"gone\" as const : \"unreadable\" as const)\n )\n )\n if (beat === \"own\") {\n const timestamp = new Date(yield* Clock.currentTimeMillis)\n yield* Effect.ignore(guarded(fs.utimes(lockPath, timestamp, timestamp)))\n continue\n }\n // A read the host refused for any other reason is no evidence\n // either way, so this beat is skipped and the next one retries.\n // Ending the heartbeat on it would retire a lock this call\n // still holds: the file goes stale within the minute, another\n // process reaps it, and this holder keeps working unfenced.\n if (beat === \"unreadable\") continue\n // `gone` and `foreign` are the two states worth an operator\n // signal: this call's lock was reaped, and possibly already\n // replaced, while the effect it fences is still running, so\n // the rest of that effect proceeds unfenced against every other\n // process. The heartbeat is the only party that ever observes\n // it \u2014 the protected effect is not interrupted and the caller\n // is not failed \u2014 so retiring the fiber silently would leave no\n // trace at all.\n yield* Effect.logWarning(\n `${label} was reclaimed while its holder was still running`,\n { ...options.annotations, state: beat }\n )\n return\n }\n })\n )\n return yield* effect\n }))),\n Effect.onExit(() => acquired ? release : Effect.void)\n )\n })\n\n/**\n * Runs `effect` while holding the lease at `lockPath`.\n *\n * Acquisition fails with `failure(TimeoutError)` once `acquireWithin` passes\n * without the lease; every other host refusal reaches `failure` with the\n * refusing `PlatformError`. The deadline ends when the lease is held: the\n * protected effect and its release are never timed out.\n *\n * @category combinators\n * @since 1.0.0-rc.1\n */\nexport const withLease = <A, E, R, E2>(\n fs: FileSystem.FileSystem,\n lockPath: string,\n effect: Effect.Effect<A, E, R>,\n failure: (cause: unknown) => E2,\n options: Options = {}\n): Effect.Effect<A, E | E2, R> =>\n Effect.suspend(() => {\n const ready = Deferred.makeUnsafe<void>()\n const deadline = Deferred.await(ready).pipe(\n Effect.timeout(options.acquireWithin ?? defaultAcquireWithin),\n Effect.catchTag(\"TimeoutError\", (cause) => Effect.fail(failure(cause))),\n Effect.andThen(Effect.never)\n )\n return hold(fs, lockPath, effect, failure, {\n ...options,\n onAcquired: Deferred.succeed(ready, undefined).pipe(Effect.asVoid)\n }).pipe(Effect.raceFirst(deadline))\n })\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }