@smthrs/artifacts 1.0.0-rc.7 → 1.0.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cjs/ArtifactBackupLease.d.ts.map +1 -1
- package/dist/cjs/ArtifactBackupLease.js +21 -13
- package/dist/cjs/ArtifactBackupLease.js.map +2 -2
- package/dist/cjs/ArtifactSweep.d.ts.map +1 -1
- package/dist/cjs/ArtifactSweep.js +9 -1
- package/dist/cjs/ArtifactSweep.js.map +2 -2
- package/dist/cjs/FileLease.d.ts +6 -2
- package/dist/cjs/FileLease.d.ts.map +1 -1
- package/dist/cjs/FileLease.js +7 -0
- package/dist/cjs/FileLease.js.map +2 -2
- package/dist/cjs/RemoteArtifacts.d.ts.map +1 -1
- package/dist/cjs/RemoteArtifacts.js +1 -1
- package/dist/cjs/RemoteArtifacts.js.map +2 -2
- package/dist/esm/ArtifactBackupLease.d.ts.map +1 -1
- package/dist/esm/ArtifactBackupLease.js +26 -17
- package/dist/esm/ArtifactBackupLease.js.map +1 -1
- package/dist/esm/ArtifactSweep.d.ts.map +1 -1
- package/dist/esm/ArtifactSweep.js +9 -1
- package/dist/esm/ArtifactSweep.js.map +1 -1
- package/dist/esm/FileLease.d.ts +6 -2
- package/dist/esm/FileLease.d.ts.map +1 -1
- package/dist/esm/FileLease.js +10 -0
- package/dist/esm/FileLease.js.map +1 -1
- package/dist/esm/RemoteArtifacts.d.ts.map +1 -1
- package/dist/esm/RemoteArtifacts.js +4 -1
- package/dist/esm/RemoteArtifacts.js.map +1 -1
- package/docs/README.md +3 -1
- package/docs/installation.md +5 -1
- package/docs/quickstart.md +2 -1
- package/package.json +2 -2
- package/src/ArtifactBackupLease.ts +24 -14
- package/src/ArtifactSweep.ts +11 -1
- package/src/FileLease.ts +19 -2
- package/src/RemoteArtifacts.ts +6 -1
package/README.md
CHANGED
|
@@ -22,7 +22,9 @@ run in Node.js, in Bun, in a browser tab, and inside a sandbox.
|
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
```bash
|
|
26
|
+
npm install @smthrs/artifacts@next effect
|
|
27
|
+
```
|
|
26
28
|
|
|
27
29
|
`@effect/platform-node` supplies the Node.js implementations of the services
|
|
28
30
|
the store asks for. The filesystem store needs exclusive writable handles,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArtifactBackupLease.d.ts","sourceRoot":"","sources":["../../src/ArtifactBackupLease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAqDvC;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EACnC,IAAI,UAAU,CAAC,UAAU,EACzB,WAAW,MAAM,EACjB,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,EAAE,KAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"ArtifactBackupLease.d.ts","sourceRoot":"","sources":["../../src/ArtifactBackupLease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAqDvC;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EACnC,IAAI,UAAU,CAAC,UAAU,EACzB,WAAW,MAAM,EACjB,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,EAAE,KAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAoHzB,CAAA;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EACtC,IAAI,UAAU,CAAC,UAAU,EACzB,WAAW,MAAM,EACjB,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,SAAS,CAAC,KAAK,EAAE,OAAO,KAAK,EAAE,KAC9B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CASzC,CAAA"}
|
|
@@ -108,9 +108,10 @@ const withLease = (fs, directory, effect, failure) => Effect.gen(function* () {
|
|
|
108
108
|
() => acquire.pipe(Effect.andThen(
|
|
109
109
|
Effect.scoped(Effect.gen(function* () {
|
|
110
110
|
yield* Effect.forkScoped(
|
|
111
|
-
Effect.
|
|
112
|
-
|
|
113
|
-
Effect.
|
|
111
|
+
Effect.gen(function* () {
|
|
112
|
+
while (true) {
|
|
113
|
+
yield* Effect.sleep(heartbeatEvery);
|
|
114
|
+
const reclaimed = yield* gate(
|
|
114
115
|
fs,
|
|
115
116
|
directory,
|
|
116
117
|
fs.readFileString(marker).pipe(
|
|
@@ -120,25 +121,32 @@ const withLease = (fs, directory, effect, failure) => Effect.gen(function* () {
|
|
|
120
121
|
),
|
|
121
122
|
Effect.flatMap((found) => {
|
|
122
123
|
if (Option.getOrUndefined(found) !== owner) {
|
|
123
|
-
return Effect.
|
|
124
|
-
|
|
124
|
+
return Effect.as(
|
|
125
|
+
Effect.logWarning(
|
|
126
|
+
"Artifact backup lease was reclaimed while its backup was still running"
|
|
127
|
+
),
|
|
128
|
+
true
|
|
125
129
|
);
|
|
126
130
|
}
|
|
127
131
|
return Effect.flatMap(Clock.currentTimeMillis, (now) => {
|
|
128
132
|
const timestamp = new Date(now);
|
|
129
|
-
return fs.utimes(marker, timestamp, timestamp);
|
|
133
|
+
return Effect.as(fs.utimes(marker, timestamp, timestamp), false);
|
|
130
134
|
});
|
|
131
135
|
}),
|
|
132
136
|
Effect.mapError(failure)
|
|
133
137
|
),
|
|
134
138
|
failure
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
).pipe(
|
|
140
|
+
// A failed heartbeat is retried on the next tick, but a
|
|
141
|
+
// minute of them lets the marker go stale, so each one is
|
|
142
|
+
// reported rather than dropped.
|
|
143
|
+
Effect.catch(
|
|
144
|
+
(cause) => Effect.as(Effect.logWarning("Artifact backup lease heartbeat failed", cause), false)
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
if (reclaimed) return;
|
|
148
|
+
}
|
|
149
|
+
})
|
|
142
150
|
);
|
|
143
151
|
return yield* effect;
|
|
144
152
|
}))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ArtifactBackupLease.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Cross-process exclusion between a filesystem artifact backup and garbage\n * collection.\n *\n * A backup holds one heartbeat-backed marker for the duration of its database\n * snapshot and blob copy. Sweep deletion checks that marker while holding the\n * same short-lived global gate. Publication remains unconstrained: new blobs\n * may appear during a backup, but a blob already referenced by the frozen\n * database cannot disappear underneath it.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Clock from \"effect/Clock\"\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\"\nimport * as ArtifactLocks from \"./internal/ArtifactLocks.ts\"\n\nconst markerName = \".backup-lease\"\nconst gateName = \".backup-lease-gate\"\nconst heartbeatEvery = \"10 seconds\"\nconst staleAfterMs = 60_000\nconst acquireWithin = \"2 minutes\"\nconst retryEvery = \"25 millis\"\n\nconst isReason = (cause: unknown, tag: PlatformError.SystemErrorTag): boolean =>\n cause instanceof PlatformError.PlatformError && cause.reason._tag === tag\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\nconst gate = <A, E, R, E2>(\n fs: FileSystem.FileSystem,\n directory: string,\n effect: Effect.Effect<A, E, R>,\n failure: (cause: unknown) => E2\n): Effect.Effect<A, E | E2, R> => ArtifactLocks.withDigest(fs, directory, gateName, effect, failure)\n\nconst activeMarker = <E>(\n fs: FileSystem.FileSystem,\n directory: string,\n failure: (cause: unknown) => E\n): Effect.Effect<boolean, E> =>\n Effect.gen(function*() {\n const path = `${directory}/${markerName}`\n const info = yield* fs.stat(path).pipe(\n Effect.map(Option.some),\n Effect.catch((cause): Effect.Effect<Option.Option<FileSystem.File.Info>, E> =>\n isReason(cause, \"NotFound\") ? Effect.succeed(Option.none()) : Effect.fail(failure(cause))\n )\n )\n if (Option.isNone(info)) return false\n const modified = Option.getOrUndefined(info.value.mtime)\n const now = yield* Clock.currentTimeMillis\n if (modified === undefined || now - modified.getTime() <= staleAfterMs) return true\n yield* fs.remove(path).pipe(\n Effect.catch((cause): Effect.Effect<void, E> =>\n isReason(cause, \"NotFound\") ? Effect.void : Effect.fail(failure(cause))\n )\n )\n return false\n })\n\n/**\n * Runs `effect` while sweep deletion is fenced across every process using\n * this objects directory. A crashed holder is recovered after its heartbeat\n * becomes stale.\n *\n * @category combinators\n * @since 1.0.0-rc.0\n */\nexport const withLease = <A, E, R, E2>(\n fs: FileSystem.FileSystem,\n directory: string,\n effect: Effect.Effect<A, E, R>,\n failure: (cause: unknown) => E2\n): Effect.Effect<A, E | E2, R> =>\n Effect.gen(function*() {\n const owner = yield* token\n const marker = `${directory}/${markerName}`\n yield* fs.makeDirectory(directory, { recursive: true }).pipe(Effect.mapError(failure))\n\n let ownsMarker = false\n const acquire = Effect.gen(function*() {\n while (true) {\n const acquired = yield* gate(\n fs,\n directory,\n Effect.gen(function*() {\n if (yield* activeMarker(fs, directory, failure)) return false\n return yield* Effect.uninterruptible(\n fs.writeFileString(marker, owner, { flag: \"wx\", mode: 0o600 }).pipe(\n Effect.andThen(Effect.sync(() => {\n ownsMarker = 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 }),\n failure\n )\n if (acquired) return\n yield* Effect.sleep(retryEvery)\n }\n }).pipe(\n Effect.timeout(acquireWithin),\n Effect.catchTag(\"TimeoutError\", (cause) => Effect.fail(failure(cause)))\n )\n\n // Install marker cleanup before acquisition, including gate release. The\n // marker write and ownership bookkeeping complete in one interruption mask.\n return yield* Effect.acquireUseRelease(\n Effect.void,\n () =>\n acquire.pipe(Effect.andThen(\n Effect.scoped(Effect.gen(function*() {\n yield* Effect.forkScoped(\n Effect.
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Cross-process exclusion between a filesystem artifact backup and garbage\n * collection.\n *\n * A backup holds one heartbeat-backed marker for the duration of its database\n * snapshot and blob copy. Sweep deletion checks that marker while holding the\n * same short-lived global gate. Publication remains unconstrained: new blobs\n * may appear during a backup, but a blob already referenced by the frozen\n * database cannot disappear underneath it.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Clock from \"effect/Clock\"\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\"\nimport * as ArtifactLocks from \"./internal/ArtifactLocks.ts\"\n\nconst markerName = \".backup-lease\"\nconst gateName = \".backup-lease-gate\"\nconst heartbeatEvery = \"10 seconds\"\nconst staleAfterMs = 60_000\nconst acquireWithin = \"2 minutes\"\nconst retryEvery = \"25 millis\"\n\nconst isReason = (cause: unknown, tag: PlatformError.SystemErrorTag): boolean =>\n cause instanceof PlatformError.PlatformError && cause.reason._tag === tag\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\nconst gate = <A, E, R, E2>(\n fs: FileSystem.FileSystem,\n directory: string,\n effect: Effect.Effect<A, E, R>,\n failure: (cause: unknown) => E2\n): Effect.Effect<A, E | E2, R> => ArtifactLocks.withDigest(fs, directory, gateName, effect, failure)\n\nconst activeMarker = <E>(\n fs: FileSystem.FileSystem,\n directory: string,\n failure: (cause: unknown) => E\n): Effect.Effect<boolean, E> =>\n Effect.gen(function*() {\n const path = `${directory}/${markerName}`\n const info = yield* fs.stat(path).pipe(\n Effect.map(Option.some),\n Effect.catch((cause): Effect.Effect<Option.Option<FileSystem.File.Info>, E> =>\n isReason(cause, \"NotFound\") ? Effect.succeed(Option.none()) : Effect.fail(failure(cause))\n )\n )\n if (Option.isNone(info)) return false\n const modified = Option.getOrUndefined(info.value.mtime)\n const now = yield* Clock.currentTimeMillis\n if (modified === undefined || now - modified.getTime() <= staleAfterMs) return true\n yield* fs.remove(path).pipe(\n Effect.catch((cause): Effect.Effect<void, E> =>\n isReason(cause, \"NotFound\") ? Effect.void : Effect.fail(failure(cause))\n )\n )\n return false\n })\n\n/**\n * Runs `effect` while sweep deletion is fenced across every process using\n * this objects directory. A crashed holder is recovered after its heartbeat\n * becomes stale.\n *\n * @category combinators\n * @since 1.0.0-rc.0\n */\nexport const withLease = <A, E, R, E2>(\n fs: FileSystem.FileSystem,\n directory: string,\n effect: Effect.Effect<A, E, R>,\n failure: (cause: unknown) => E2\n): Effect.Effect<A, E | E2, R> =>\n Effect.gen(function*() {\n const owner = yield* token\n const marker = `${directory}/${markerName}`\n yield* fs.makeDirectory(directory, { recursive: true }).pipe(Effect.mapError(failure))\n\n let ownsMarker = false\n const acquire = Effect.gen(function*() {\n while (true) {\n const acquired = yield* gate(\n fs,\n directory,\n Effect.gen(function*() {\n if (yield* activeMarker(fs, directory, failure)) return false\n return yield* Effect.uninterruptible(\n fs.writeFileString(marker, owner, { flag: \"wx\", mode: 0o600 }).pipe(\n Effect.andThen(Effect.sync(() => {\n ownsMarker = 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 }),\n failure\n )\n if (acquired) return\n yield* Effect.sleep(retryEvery)\n }\n }).pipe(\n Effect.timeout(acquireWithin),\n Effect.catchTag(\"TimeoutError\", (cause) => Effect.fail(failure(cause)))\n )\n\n // Install marker cleanup before acquisition, including gate release. The\n // marker write and ownership bookkeeping complete in one interruption mask.\n return yield* Effect.acquireUseRelease(\n Effect.void,\n () =>\n acquire.pipe(Effect.andThen(\n Effect.scoped(Effect.gen(function*() {\n yield* Effect.forkScoped(\n Effect.gen(function*() {\n while (true) {\n yield* Effect.sleep(heartbeatEvery)\n const reclaimed = yield* gate(\n fs,\n directory,\n fs.readFileString(marker).pipe(\n Effect.map(Option.some),\n Effect.catch((cause) =>\n isReason(cause, \"NotFound\") ? Effect.succeed(Option.none<string>()) : Effect.fail(cause)\n ),\n Effect.flatMap((found) => {\n // A missing or foreign marker means a sweep judged this\n // lease stale and reaped it: deletion is no longer\n // fenced while the backup still copies. The sibling\n // `internal/ArtifactLocks.ts` heartbeat reports the same\n // loss the same way. A reaped marker is never handed\n // back, so the loss is reported once and the heartbeat\n // ends instead of repeating it every tick.\n if (Option.getOrUndefined(found) !== owner) {\n return Effect.as(\n Effect.logWarning(\n \"Artifact backup lease was reclaimed while its backup was still running\"\n ),\n true\n )\n }\n return Effect.flatMap(Clock.currentTimeMillis, (now) => {\n const timestamp = new Date(now)\n return Effect.as(fs.utimes(marker, timestamp, timestamp), false)\n })\n }),\n Effect.mapError(failure)\n ),\n failure\n ).pipe(\n // A failed heartbeat is retried on the next tick, but a\n // minute of them lets the marker go stale, so each one is\n // reported rather than dropped.\n Effect.catch((cause) =>\n Effect.as(Effect.logWarning(\"Artifact backup lease heartbeat failed\", cause), false)\n )\n )\n if (reclaimed) return\n }\n })\n )\n return yield* effect\n }))\n )),\n () =>\n ownsMarker\n ? gate(\n fs,\n directory,\n fs.readFileString(marker).pipe(\n Effect.flatMap((found) => found === owner ? fs.remove(marker) : Effect.void),\n // A backup whose heartbeat lapsed has its marker reaped by whoever\n // noticed, so `NotFound` is the ordinary end of a slow lease, not a\n // fault: there is nothing left to release and nothing left to leak.\n // The sibling release in `internal/ArtifactLocks.ts` classifies the\n // same cause the same way.\n Effect.catch((cause): Effect.Effect<void, PlatformError.PlatformError> =>\n isReason(cause, \"NotFound\") ? Effect.void : Effect.fail(cause)\n )\n ),\n failure\n ).pipe(\n Effect.catch((cause) => Effect.logWarning(\"Artifact backup lease release failed\", cause))\n )\n : Effect.void\n )\n })\n\n/**\n * Runs one sweep deletion only when no live backup lease exists. The marker\n * check and `effect` share the global gate, closing the check/delete race.\n * `None` means a backup deliberately fenced the operation.\n *\n * @category combinators\n * @since 1.0.0-rc.0\n */\nexport const unlessActive = <A, E, R, E2>(\n fs: FileSystem.FileSystem,\n directory: string,\n effect: Effect.Effect<A, E, R>,\n failure: (cause: unknown) => E2\n): Effect.Effect<Option.Option<A>, E | E2, R> =>\n gate(\n fs,\n directory,\n Effect.gen(function*() {\n if (yield* activeMarker(fs, directory, failure)) return Option.none<A>()\n return Option.some(yield* effect)\n }),\n failure\n )\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArtifactSweep.d.ts","sourceRoot":"","sources":["../../src/ArtifactSweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAIrC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAInD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAClD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAA;IACpF;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,aAAa,KACpB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAA;CAC9D;;AAED;;;;;;;;;GASG;AACH,qBAAa,aAAc,SAAQ,kBAA4E;CAAG;AAelH;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAA;CAC3D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,cAAc,GACzB,IAAI,UAAU,CAAC,UAAU,EACzB,UAAS,YAAiB,KACzB,
|
|
1
|
+
{"version":3,"file":"ArtifactSweep.d.ts","sourceRoot":"","sources":["../../src/ArtifactSweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAIrC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAInD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAClD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAA;IACpF;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,aAAa,KACpB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAA;CAC9D;;AAED;;;;;;;;;GASG;AACH,qBAAa,aAAc,SAAQ,kBAA4E;CAAG;AAelH;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAA;CAC3D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,cAAc,GACzB,IAAI,UAAU,CAAC,UAAU,EACzB,UAAS,YAAiB,KACzB,OA2HF,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAS,YAAiB,KACzB,KAAK,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,CAC2C,CAAA;AAErG;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,OAI1D,CAAA;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,KAAK,CAAC,KAAK,CAAC,aAAa,CACnC,CAAA"}
|
|
@@ -105,6 +105,15 @@ const makeFileSystem = (fs, options = {}) => {
|
|
|
105
105
|
const remove = Effect.fn("ArtifactSweep.remove")(
|
|
106
106
|
(digest, removeOptions) => Effect.gen(function* () {
|
|
107
107
|
const validated = yield* ArtifactStore.validateDigest(digest);
|
|
108
|
+
const bound = removeOptions?.ifUnmodifiedSinceMs;
|
|
109
|
+
if (bound !== void 0 && (typeof bound !== "number" || Number.isNaN(bound))) {
|
|
110
|
+
return yield* Effect.fail(
|
|
111
|
+
new ArtifactStore.ArtifactStoreError({
|
|
112
|
+
code: "invalid_configuration",
|
|
113
|
+
message: "invalid sweep option: ifUnmodifiedSinceMs"
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
}
|
|
108
117
|
yield* Effect.annotateCurrentSpan({ digest: validated });
|
|
109
118
|
const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure));
|
|
110
119
|
const checkParent = yield* ArtifactPath.guard(fs, `${directory}/${validated.slice(0, 2)}`).pipe(
|
|
@@ -126,7 +135,6 @@ const makeFileSystem = (fs, options = {}) => {
|
|
|
126
135
|
yield* checkRoot.pipe(Effect.mapError(hostFailure));
|
|
127
136
|
yield* checkParent.pipe(Effect.mapError(hostFailure));
|
|
128
137
|
const checkBlob = yield* ArtifactPath.guard(fs, path, "File").pipe(Effect.mapError(hostFailure));
|
|
129
|
-
const bound = removeOptions?.ifUnmodifiedSinceMs;
|
|
130
138
|
if (bound !== void 0) {
|
|
131
139
|
const info = yield* fs.stat(path).pipe(Effect.option);
|
|
132
140
|
if (Option.isNone(info)) return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ArtifactSweep.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * The sweep half of artifact garbage collection: enumerate this host's blobs\n * and delete one, fenced by its age.\n *\n * This surface is deliberately NOT part of `ArtifactStore.Service`. The store\n * contract is shared by memory, filesystem, and network tiers, and a remote\n * tier can neither enumerate its address space nor accept a delete \u2014 Bazel\n * draws the same line: its disk-cache collector\n * (class `DiskCacheGarbageCollector` in\n * `com.google.devtools.build.lib.remote.disk`) walks the local directory, while\n * the remote tier owns its own retention. Only the\n * host-local filesystem store implements this.\n *\n * The *policy* \u2014 which digests are live, how old a dead blob must be before it\n * goes \u2014 belongs to the engine composition, which is the only place the\n * durable roots (attempt rows, cache entries) are visible. This module ships\n * mechanics alone; `@smthrs/engine-store` owns the mark policy and grace\n * period.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as FileSystem from \"effect/FileSystem\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport * as PlatformError from \"effect/PlatformError\"\nimport * as ArtifactBackupLease from \"./ArtifactBackupLease.ts\"\nimport * as ArtifactStore from \"./ArtifactStore.ts\"\nimport * as ArtifactLocks from \"./internal/ArtifactLocks.ts\"\nimport * as ArtifactPath from \"./internal/ArtifactPath.ts\"\n\n/**\n * One enumerated blob: its content address, when it was last written or\n * freshened, and its size.\n *\n * `modifiedAtMs` is the age evidence a sweep fences on \u2014 the same signal git\n * (`git prune --expire`) and Bazel's disk-cache collector read, because it is\n * the one timestamp a filesystem maintains without any bookkeeping of ours.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface BlobStat {\n readonly digest: string\n readonly modifiedAtMs: number\n readonly sizeBytes: number\n}\n\n/**\n * Fencing predicate for a sweep deletion.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface RemoveOptions {\n /**\n * Deletes the blob only while its mtime is still at or before this bound.\n * A blob freshened after the caller computed its live set \u2014 a concurrent\n * `put` re-referencing the same bytes \u2014 fails the fence and survives, the\n * same shape as `CacheStore.evict`'s `ifRecordedBy`: the guard rides in the\n * delete, never in a prior read alone.\n */\n readonly ifUnmodifiedSinceMs?: number | undefined\n}\n\n/**\n * Host-local blob enumeration and fenced deletion.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface Service {\n /**\n * Every content-addressed blob this host's store currently holds. In-flight\n * `.tmp-*` scratch files, foreign files, and blobs whose age cannot be\n * measured are excluded \u2014 what the sweep cannot judge it must not touch.\n */\n readonly inventory: Effect.Effect<Array<BlobStat>, ArtifactStore.ArtifactStoreError>\n /**\n * Deletes the blob at `digest`, returning whether bytes were removed. A\n * missing blob reports `false` rather than failing, so a crashed and\n * re-run sweep converges instead of erroring on its own progress.\n *\n * `false` covers three outcomes a caller cannot tell apart, all of which mean\n * \"nothing was reclaimed, and retrying later is safe\": the blob was already\n * gone, it failed the `ifUnmodifiedSinceMs` fence, or a live backup lease\n * fenced the deletion (see `ArtifactBackupLease`). The last one is not\n * progress \u2014 the blob is still there \u2014 so a collector that counts reclaimed\n * bytes must not count it, and the next pass after the backup finishes\n * removes it.\n */\n readonly remove: (\n digest: string,\n options?: RemoveOptions\n ) => Effect.Effect<boolean, ArtifactStore.ArtifactStoreError>\n}\n\n/**\n * Service tag for host-local artifact sweeping.\n *\n * The identity string equals this module's package path, per the house rule\n * that a new identity is the defining module path.\n *\n * @category services\n * @since 1.0.0-rc.0\n * @slop\n */\nexport class ArtifactSweep extends Context.Service<ArtifactSweep, Service>()(\"@smthrs/artifacts/ArtifactSweep\") {}\n\nconst error = (message: string, cause?: unknown): ArtifactStore.ArtifactStoreError =>\n new ArtifactStore.ArtifactStoreError({\n code: \"unavailable\",\n message,\n ...(cause === undefined ? {} : { cause })\n })\n\nconst hostFailure = (cause: unknown): ArtifactStore.ArtifactStoreError =>\n error(`the host filesystem refused a sweep operation: ${String(cause)}`, cause)\n\nconst isNotFound = (cause: unknown): boolean =>\n cause instanceof PlatformError.PlatformError && cause.reason._tag === \"NotFound\"\n\n/**\n * How the sweep reaches the objects directory.\n *\n * Deliberately not `ArtifactStore.FileSystemOptions`: the sweep never writes a\n * blob, so `durability` would be an accepted argument it ignores. Both fields\n * must match the store built over the same directory \u2014 see `coordination`.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface SweepOptions {\n /**\n * The objects directory to enumerate, the one the paired\n * `ArtifactStore.makeFileSystem` publishes into. Defaults to\n * `ArtifactStore.defaultDirectory`.\n */\n readonly directory?: string | undefined\n /**\n * `required` takes the same per-digest lock file a publishing writer takes,\n * and consults the backup lease, before deleting. It fences nothing unless\n * the paired store was built with `required` too: a store on `process` never\n * takes the lock this sweep waits on, so the fence would read as armed while\n * protecting nothing. `process` is the explicit weaker browser/test mode.\n */\n readonly coordination?: \"required\" | \"process\" | undefined\n}\n\n/**\n * Builds the filesystem-backed sweep surface over the same objects directory\n * an `ArtifactStore.makeFileSystem` publishes into.\n *\n * Enumeration is conservative by construction. Only paths in the store's\n * canonical fanout shape \u2014 `xx/<digest>` with `xx` equal to the digest's\n * two-hex prefix \u2014 are blobs; anything else in the directory (a temp file, a\n * lock file, a foreign file, a fanout directory itself) is skipped, never\n * deleted. A blob that vanishes between listing and stat was removed by someone\n * else and is likewise skipped.\n *\n * Pair it with the store over the SAME `directory` and the SAME `coordination`.\n * Nothing checks the pairing, and a mismatch is silent: a sweep on `required`\n * beside a store on `process` deletes blobs no writer is fenced against.\n *\n * Under `required`, one deletion costs two lock acquisitions \u2014 the per-digest\n * lock, then the workspace-global backup-lease gate \u2014 which is roughly ten\n * filesystem operations and two forked heartbeat fibers per blob. The gate is\n * one file for the whole workspace, so concurrent deletions serialize through\n * it. That is the price of never deleting a blob a running backup already\n * recorded; size a collection window with it in mind.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const makeFileSystem = (\n fs: FileSystem.FileSystem,\n options: SweepOptions = {}\n): Service => {\n const directory = (options.directory ?? ArtifactStore.defaultDirectory).replace(/([^/])\\/+$/, \"$1\")\n const coordination = options.coordination ?? \"required\"\n const blobPath = (digest: string): string => `${directory}/${digest.slice(0, 2)}/${digest}`\n\n const inventory: Service[\"inventory\"] = Effect.gen(function*() {\n // A store that never published anything has no directory; that is an\n // empty inventory, not a failure.\n const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))\n const parents = yield* fs.readDirectory(directory).pipe(\n Effect.catch((cause) => isNotFound(cause) ? Effect.succeed([] as Array<string>) : Effect.fail(hostFailure(cause)))\n )\n const entries: Array<string> = []\n for (const parent of parents) {\n if (!/^[0-9a-f]{2}$/.test(parent)) continue\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n const safe = yield* ArtifactPath.guard(fs, `${directory}/${parent}`).pipe(Effect.option)\n if (Option.isNone(safe)) continue\n const children = yield* fs.readDirectory(`${directory}/${parent}`).pipe(\n Effect.orElseSucceed(() => [] as Array<string>)\n )\n for (const child of children) entries.push(`${parent}/${child}`)\n }\n const candidates = entries.filter((entry) => {\n if (entry.includes(\".tmp-\")) return false\n const separator = entry.indexOf(\"/\")\n const fan = entry.slice(0, separator)\n const digest = entry.slice(separator + 1)\n return /^[0-9a-f]{64}$/.test(digest) && fan === digest.slice(0, 2)\n })\n const blobs = yield* Effect.forEach(candidates, (entry) =>\n Effect.gen(function*() {\n const separator = entry.indexOf(\"/\")\n const fan = entry.slice(0, separator)\n const digest = entry.slice(separator + 1)\n // The stat is per-candidate and tolerant: a blob another process\n // removed between the listing and here simply is not in the\n // inventory, and one whose mtime the host cannot report offers no\n // age evidence, so the sweep must never judge it.\n const info = yield* Effect.gen(function*() {\n yield* checkRoot\n yield* ArtifactPath.guard(fs, `${directory}/${fan}`)\n yield* ArtifactPath.guard(fs, `${directory}/${entry}`, \"File\")\n return yield* fs.stat(`${directory}/${entry}`)\n }).pipe(Effect.option)\n if (Option.isNone(info) || info.value.type !== \"File\") return undefined\n const mtime = Option.getOrUndefined(info.value.mtime)\n if (mtime === undefined) return undefined\n return {\n digest,\n modifiedAtMs: mtime.getTime(),\n sizeBytes: Number(info.value.size)\n }\n }), { concurrency: 16 })\n return blobs.filter((blob) => blob !== undefined)\n })\n\n const remove: Service[\"remove\"] = Effect.fn(\"ArtifactSweep.remove\")((digest, removeOptions) =>\n Effect.gen(function*() {\n const validated = yield* ArtifactStore.validateDigest(digest)\n yield* Effect.annotateCurrentSpan({ digest: validated })\n const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))\n const checkParent = yield* ArtifactPath.guard(fs, `${directory}/${validated.slice(0, 2)}`).pipe(\n Effect.mapError(hostFailure)\n )\n return yield* ArtifactLocks.withDigest(\n fs,\n directory,\n validated,\n coordination === \"process\"\n ? removeBlob()\n : ArtifactBackupLease.unlessActive(fs, directory, removeBlob(), hostFailure).pipe(\n Effect.map(Option.getOrElse(() => false))\n ),\n hostFailure,\n coordination\n )\n\n function removeBlob(): Effect.Effect<boolean, ArtifactStore.ArtifactStoreError> {\n return Effect.gen(function*() {\n const path = blobPath(validated)\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n yield* checkParent.pipe(Effect.mapError(hostFailure))\n const checkBlob = yield* ArtifactPath.guard(fs, path, \"File\").pipe(Effect.mapError(hostFailure))\n const bound = removeOptions?.ifUnmodifiedSinceMs\n if (bound !== undefined) {\n const info = yield* fs.stat(path).pipe(Effect.option)\n // Already gone is a completed deletion, and an unmeasurable mtime is\n // no proof of age: both refuse rather than delete.\n if (Option.isNone(info)) return false\n const mtime = Option.getOrUndefined(info.value.mtime)\n if (mtime === undefined || mtime.getTime() > bound) return false\n }\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n yield* checkParent.pipe(Effect.mapError(hostFailure))\n yield* checkBlob.pipe(Effect.mapError(hostFailure))\n return yield* fs.remove(path).pipe(\n Effect.as(true),\n // The blob may have been removed between the fence and here; only a\n // refusal over bytes that still exist is a host failure.\n Effect.catch((cause) =>\n fs.exists(path).pipe(\n Effect.mapError((probeCause) => hostFailure({ remove: cause, existenceProbe: probeCause })),\n Effect.flatMap((present) => present ? Effect.fail(hostFailure(cause)) : Effect.succeed(false))\n )\n )\n )\n })\n }\n })\n )\n\n return { inventory, remove }\n}\n\n/**\n * Provides the filesystem-backed sweep surface.\n *\n * Give it the same `directory` and `coordination` as the `ArtifactStore` layer\n * it sweeps behind; see {@link makeFileSystem}.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layerFileSystem = (\n options: SweepOptions = {}\n): Layer.Layer<ArtifactSweep, never, FileSystem.FileSystem> =>\n Layer.effect(ArtifactSweep)(Effect.map(FileSystem.FileSystem, (fs) => makeFileSystem(fs, options)))\n\n/**\n * Builds a sweep surface whose every operation fails as unavailable, with\n * per-method overrides.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service => ({\n inventory: Effect.fail(error(\"inventory is unavailable\")),\n remove: Effect.fn(\"ArtifactSweep.remove\")(() => Effect.fail(error(\"remove is unavailable\"))),\n ...overrides\n})\n\n/**\n * Provides a no-op sweep surface.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<ArtifactSweep> =>\n Layer.succeed(ArtifactSweep)(makeNoop(overrides))\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * The sweep half of artifact garbage collection: enumerate this host's blobs\n * and delete one, fenced by its age.\n *\n * This surface is deliberately NOT part of `ArtifactStore.Service`. The store\n * contract is shared by memory, filesystem, and network tiers, and a remote\n * tier can neither enumerate its address space nor accept a delete \u2014 Bazel\n * draws the same line: its disk-cache collector\n * (class `DiskCacheGarbageCollector` in\n * `com.google.devtools.build.lib.remote.disk`) walks the local directory, while\n * the remote tier owns its own retention. Only the\n * host-local filesystem store implements this.\n *\n * The *policy* \u2014 which digests are live, how old a dead blob must be before it\n * goes \u2014 belongs to the engine composition, which is the only place the\n * durable roots (attempt rows, cache entries) are visible. This module ships\n * mechanics alone; `@smthrs/engine-store` owns the mark policy and grace\n * period.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as FileSystem from \"effect/FileSystem\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport * as PlatformError from \"effect/PlatformError\"\nimport * as ArtifactBackupLease from \"./ArtifactBackupLease.ts\"\nimport * as ArtifactStore from \"./ArtifactStore.ts\"\nimport * as ArtifactLocks from \"./internal/ArtifactLocks.ts\"\nimport * as ArtifactPath from \"./internal/ArtifactPath.ts\"\n\n/**\n * One enumerated blob: its content address, when it was last written or\n * freshened, and its size.\n *\n * `modifiedAtMs` is the age evidence a sweep fences on \u2014 the same signal git\n * (`git prune --expire`) and Bazel's disk-cache collector read, because it is\n * the one timestamp a filesystem maintains without any bookkeeping of ours.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface BlobStat {\n readonly digest: string\n readonly modifiedAtMs: number\n readonly sizeBytes: number\n}\n\n/**\n * Fencing predicate for a sweep deletion.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface RemoveOptions {\n /**\n * Deletes the blob only while its mtime is still at or before this bound.\n * A blob freshened after the caller computed its live set \u2014 a concurrent\n * `put` re-referencing the same bytes \u2014 fails the fence and survives, the\n * same shape as `CacheStore.evict`'s `ifRecordedBy`: the guard rides in the\n * delete, never in a prior read alone.\n */\n readonly ifUnmodifiedSinceMs?: number | undefined\n}\n\n/**\n * Host-local blob enumeration and fenced deletion.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface Service {\n /**\n * Every content-addressed blob this host's store currently holds. In-flight\n * `.tmp-*` scratch files, foreign files, and blobs whose age cannot be\n * measured are excluded \u2014 what the sweep cannot judge it must not touch.\n */\n readonly inventory: Effect.Effect<Array<BlobStat>, ArtifactStore.ArtifactStoreError>\n /**\n * Deletes the blob at `digest`, returning whether bytes were removed. A\n * missing blob reports `false` rather than failing, so a crashed and\n * re-run sweep converges instead of erroring on its own progress.\n *\n * `false` covers three outcomes a caller cannot tell apart, all of which mean\n * \"nothing was reclaimed, and retrying later is safe\": the blob was already\n * gone, it failed the `ifUnmodifiedSinceMs` fence, or a live backup lease\n * fenced the deletion (see `ArtifactBackupLease`). The last one is not\n * progress \u2014 the blob is still there \u2014 so a collector that counts reclaimed\n * bytes must not count it, and the next pass after the backup finishes\n * removes it.\n */\n readonly remove: (\n digest: string,\n options?: RemoveOptions\n ) => Effect.Effect<boolean, ArtifactStore.ArtifactStoreError>\n}\n\n/**\n * Service tag for host-local artifact sweeping.\n *\n * The identity string equals this module's package path, per the house rule\n * that a new identity is the defining module path.\n *\n * @category services\n * @since 1.0.0-rc.0\n * @slop\n */\nexport class ArtifactSweep extends Context.Service<ArtifactSweep, Service>()(\"@smthrs/artifacts/ArtifactSweep\") {}\n\nconst error = (message: string, cause?: unknown): ArtifactStore.ArtifactStoreError =>\n new ArtifactStore.ArtifactStoreError({\n code: \"unavailable\",\n message,\n ...(cause === undefined ? {} : { cause })\n })\n\nconst hostFailure = (cause: unknown): ArtifactStore.ArtifactStoreError =>\n error(`the host filesystem refused a sweep operation: ${String(cause)}`, cause)\n\nconst isNotFound = (cause: unknown): boolean =>\n cause instanceof PlatformError.PlatformError && cause.reason._tag === \"NotFound\"\n\n/**\n * How the sweep reaches the objects directory.\n *\n * Deliberately not `ArtifactStore.FileSystemOptions`: the sweep never writes a\n * blob, so `durability` would be an accepted argument it ignores. Both fields\n * must match the store built over the same directory \u2014 see `coordination`.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface SweepOptions {\n /**\n * The objects directory to enumerate, the one the paired\n * `ArtifactStore.makeFileSystem` publishes into. Defaults to\n * `ArtifactStore.defaultDirectory`.\n */\n readonly directory?: string | undefined\n /**\n * `required` takes the same per-digest lock file a publishing writer takes,\n * and consults the backup lease, before deleting. It fences nothing unless\n * the paired store was built with `required` too: a store on `process` never\n * takes the lock this sweep waits on, so the fence would read as armed while\n * protecting nothing. `process` is the explicit weaker browser/test mode.\n */\n readonly coordination?: \"required\" | \"process\" | undefined\n}\n\n/**\n * Builds the filesystem-backed sweep surface over the same objects directory\n * an `ArtifactStore.makeFileSystem` publishes into.\n *\n * Enumeration is conservative by construction. Only paths in the store's\n * canonical fanout shape \u2014 `xx/<digest>` with `xx` equal to the digest's\n * two-hex prefix \u2014 are blobs; anything else in the directory (a temp file, a\n * lock file, a foreign file, a fanout directory itself) is skipped, never\n * deleted. A blob that vanishes between listing and stat was removed by someone\n * else and is likewise skipped.\n *\n * Pair it with the store over the SAME `directory` and the SAME `coordination`.\n * Nothing checks the pairing, and a mismatch is silent: a sweep on `required`\n * beside a store on `process` deletes blobs no writer is fenced against.\n *\n * Under `required`, one deletion costs two lock acquisitions \u2014 the per-digest\n * lock, then the workspace-global backup-lease gate \u2014 which is roughly ten\n * filesystem operations and two forked heartbeat fibers per blob. The gate is\n * one file for the whole workspace, so concurrent deletions serialize through\n * it. That is the price of never deleting a blob a running backup already\n * recorded; size a collection window with it in mind.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const makeFileSystem = (\n fs: FileSystem.FileSystem,\n options: SweepOptions = {}\n): Service => {\n const directory = (options.directory ?? ArtifactStore.defaultDirectory).replace(/([^/])\\/+$/, \"$1\")\n const coordination = options.coordination ?? \"required\"\n const blobPath = (digest: string): string => `${directory}/${digest.slice(0, 2)}/${digest}`\n\n const inventory: Service[\"inventory\"] = Effect.gen(function*() {\n // A store that never published anything has no directory; that is an\n // empty inventory, not a failure.\n const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))\n const parents = yield* fs.readDirectory(directory).pipe(\n Effect.catch((cause) => isNotFound(cause) ? Effect.succeed([] as Array<string>) : Effect.fail(hostFailure(cause)))\n )\n const entries: Array<string> = []\n for (const parent of parents) {\n if (!/^[0-9a-f]{2}$/.test(parent)) continue\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n const safe = yield* ArtifactPath.guard(fs, `${directory}/${parent}`).pipe(Effect.option)\n if (Option.isNone(safe)) continue\n const children = yield* fs.readDirectory(`${directory}/${parent}`).pipe(\n Effect.orElseSucceed(() => [] as Array<string>)\n )\n for (const child of children) entries.push(`${parent}/${child}`)\n }\n const candidates = entries.filter((entry) => {\n if (entry.includes(\".tmp-\")) return false\n const separator = entry.indexOf(\"/\")\n const fan = entry.slice(0, separator)\n const digest = entry.slice(separator + 1)\n return /^[0-9a-f]{64}$/.test(digest) && fan === digest.slice(0, 2)\n })\n const blobs = yield* Effect.forEach(candidates, (entry) =>\n Effect.gen(function*() {\n const separator = entry.indexOf(\"/\")\n const fan = entry.slice(0, separator)\n const digest = entry.slice(separator + 1)\n // The stat is per-candidate and tolerant: a blob another process\n // removed between the listing and here simply is not in the\n // inventory, and one whose mtime the host cannot report offers no\n // age evidence, so the sweep must never judge it.\n const info = yield* Effect.gen(function*() {\n yield* checkRoot\n yield* ArtifactPath.guard(fs, `${directory}/${fan}`)\n yield* ArtifactPath.guard(fs, `${directory}/${entry}`, \"File\")\n return yield* fs.stat(`${directory}/${entry}`)\n }).pipe(Effect.option)\n if (Option.isNone(info) || info.value.type !== \"File\") return undefined\n const mtime = Option.getOrUndefined(info.value.mtime)\n if (mtime === undefined) return undefined\n return {\n digest,\n modifiedAtMs: mtime.getTime(),\n sizeBytes: Number(info.value.size)\n }\n }), { concurrency: 16 })\n return blobs.filter((blob) => blob !== undefined)\n })\n\n const remove: Service[\"remove\"] = Effect.fn(\"ArtifactSweep.remove\")((digest, removeOptions) =>\n Effect.gen(function*() {\n const validated = yield* ArtifactStore.validateDigest(digest)\n const bound = removeOptions?.ifUnmodifiedSinceMs\n // NaN fails every comparison, so the fence below would read it as \"not\n // modified since\" and delete a blob a concurrent put just freshened.\n if (bound !== undefined && (typeof bound !== \"number\" || Number.isNaN(bound))) {\n return yield* Effect.fail(\n new ArtifactStore.ArtifactStoreError({\n code: \"invalid_configuration\",\n message: \"invalid sweep option: ifUnmodifiedSinceMs\"\n })\n )\n }\n yield* Effect.annotateCurrentSpan({ digest: validated })\n const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))\n const checkParent = yield* ArtifactPath.guard(fs, `${directory}/${validated.slice(0, 2)}`).pipe(\n Effect.mapError(hostFailure)\n )\n return yield* ArtifactLocks.withDigest(\n fs,\n directory,\n validated,\n coordination === \"process\"\n ? removeBlob()\n : ArtifactBackupLease.unlessActive(fs, directory, removeBlob(), hostFailure).pipe(\n Effect.map(Option.getOrElse(() => false))\n ),\n hostFailure,\n coordination\n )\n\n function removeBlob(): Effect.Effect<boolean, ArtifactStore.ArtifactStoreError> {\n return Effect.gen(function*() {\n const path = blobPath(validated)\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n yield* checkParent.pipe(Effect.mapError(hostFailure))\n const checkBlob = yield* ArtifactPath.guard(fs, path, \"File\").pipe(Effect.mapError(hostFailure))\n if (bound !== undefined) {\n const info = yield* fs.stat(path).pipe(Effect.option)\n // Already gone is a completed deletion, and an unmeasurable mtime is\n // no proof of age: both refuse rather than delete.\n if (Option.isNone(info)) return false\n const mtime = Option.getOrUndefined(info.value.mtime)\n if (mtime === undefined || mtime.getTime() > bound) return false\n }\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n yield* checkParent.pipe(Effect.mapError(hostFailure))\n yield* checkBlob.pipe(Effect.mapError(hostFailure))\n return yield* fs.remove(path).pipe(\n Effect.as(true),\n // The blob may have been removed between the fence and here; only a\n // refusal over bytes that still exist is a host failure.\n Effect.catch((cause) =>\n fs.exists(path).pipe(\n Effect.mapError((probeCause) => hostFailure({ remove: cause, existenceProbe: probeCause })),\n Effect.flatMap((present) => present ? Effect.fail(hostFailure(cause)) : Effect.succeed(false))\n )\n )\n )\n })\n }\n })\n )\n\n return { inventory, remove }\n}\n\n/**\n * Provides the filesystem-backed sweep surface.\n *\n * Give it the same `directory` and `coordination` as the `ArtifactStore` layer\n * it sweeps behind; see {@link makeFileSystem}.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layerFileSystem = (\n options: SweepOptions = {}\n): Layer.Layer<ArtifactSweep, never, FileSystem.FileSystem> =>\n Layer.effect(ArtifactSweep)(Effect.map(FileSystem.FileSystem, (fs) => makeFileSystem(fs, options)))\n\n/**\n * Builds a sweep surface whose every operation fails as unavailable, with\n * per-method overrides.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service => ({\n inventory: Effect.fail(error(\"inventory is unavailable\")),\n remove: Effect.fn(\"ArtifactSweep.remove\")(() => Effect.fail(error(\"remove is unavailable\"))),\n ...overrides\n})\n\n/**\n * Provides a no-op sweep surface.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<ArtifactSweep> =>\n Layer.succeed(ArtifactSweep)(makeNoop(overrides))\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/cjs/FileLease.d.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*
|
|
35
35
|
* @since 1.0.0-rc.1
|
|
36
36
|
*/
|
|
37
|
-
import
|
|
37
|
+
import * as Duration from "effect/Duration";
|
|
38
38
|
import * as Effect from "effect/Effect";
|
|
39
39
|
import type * as FileSystem from "effect/FileSystem";
|
|
40
40
|
/**
|
|
@@ -50,7 +50,11 @@ export interface Options {
|
|
|
50
50
|
readonly annotations?: Readonly<Record<string, unknown>> | undefined;
|
|
51
51
|
/** How often the holder refreshes the lock's mtime. Defaults to 10 seconds. */
|
|
52
52
|
readonly heartbeatEvery?: Duration.Input | undefined;
|
|
53
|
-
/**
|
|
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
|
+
*/
|
|
54
58
|
readonly staleAfterMs?: number | undefined;
|
|
55
59
|
/** How long acquisition may wait. Defaults to 2 minutes. */
|
|
56
60
|
readonly acquireWithin?: Duration.Input | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileLease.d.ts","sourceRoot":"","sources":["../../src/FileLease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH,OAAO,KAAK,
|
|
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"}
|
package/dist/cjs/FileLease.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(FileLease_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(FileLease_exports);
|
|
37
37
|
var Clock = __toESM(require("effect/Clock"), 1);
|
|
38
38
|
var Deferred = __toESM(require("effect/Deferred"), 1);
|
|
39
|
+
var Duration = __toESM(require("effect/Duration"), 1);
|
|
39
40
|
var Effect = __toESM(require("effect/Effect"), 1);
|
|
40
41
|
var Option = __toESM(require("effect/Option"), 1);
|
|
41
42
|
var PlatformError = __toESM(require("effect/PlatformError"), 1);
|
|
@@ -54,6 +55,12 @@ const hold = (fs, lockPath, effect, failure, options = {}) => Effect.gen(functio
|
|
|
54
55
|
const heartbeatEvery = options.heartbeatEvery ?? "10 seconds";
|
|
55
56
|
const staleAfterMs = options.staleAfterMs ?? defaultStaleAfterMs;
|
|
56
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
|
+
}
|
|
57
64
|
const owner = yield* token;
|
|
58
65
|
let acquired = false;
|
|
59
66
|
const guard = options.guard === void 0 ? Effect.void : options.guard.pipe(Effect.mapError(failure));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 type * 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 /** Age past which a lock or claim is stale. Defaults to 60 000 ms. */\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 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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCA,YAAuB;AACvB,eAA0B;
|
|
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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteArtifacts.d.ts","sourceRoot":"","sources":["../../src/RemoteArtifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAA;AAGpD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,0DAA2D,CAAA;AAEtF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,cAAc,CAAiD,CAAA;AAE5G;;;;;;GAMG;AACH,MAAM,WAAW,OAAQ,SAAQ,aAAa,CAAC,OAAO;IACpD,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;CACxC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,aAAa,CAAC,OAAO,KAAG,cAAc,GAAG,SAGhF,CAAA;AAED;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;IACrD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACrD;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACnD;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACpD;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9C;;;OAGG;IACH,QAAQ,CAAC,2BAA2B,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACzD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,GAAG,SAAS,CAAA;CACrD;AA4FD;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GACf,SAAS,OAAO,KACf,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,kBAAkB,EAAE,UAAU,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"RemoteArtifacts.d.ts","sourceRoot":"","sources":["../../src/RemoteArtifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAA;AAGpD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,0DAA2D,CAAA;AAEtF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,cAAc,CAAiD,CAAA;AAE5G;;;;;;GAMG;AACH,MAAM,WAAW,OAAQ,SAAQ,aAAa,CAAC,OAAO;IACpD,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;CACxC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,aAAa,CAAC,OAAO,KAAG,cAAc,GAAG,SAGhF,CAAA;AAED;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;IACrD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACrD;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACnD;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACpD;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9C;;;OAGG;IACH,QAAQ,CAAC,2BAA2B,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACzD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,GAAG,SAAS,CAAA;CACrD;AA4FD;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GACf,SAAS,OAAO,KACf,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,kBAAkB,EAAE,UAAU,CAAC,UAAU,CAid7E,CAAA;AAEJ;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,KAAK,GAChB,SAAS,OAAO,KACf,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,aAAa,EAAE,aAAa,CAAC,kBAAkB,EAAE,UAAU,CAAC,UAAU,CACzC,CAAA"}
|
|
@@ -137,7 +137,7 @@ const make = (options) => Effect.gen(function* () {
|
|
|
137
137
|
if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && loopback)) {
|
|
138
138
|
return yield* Effect.fail(configurationFailure("remote artifact endpoint must use HTTPS"));
|
|
139
139
|
}
|
|
140
|
-
if (endpoint.username !== "" || endpoint.password !== "" || endpoint.search !== "" || endpoint.hash !== "") {
|
|
140
|
+
if (endpoint.username !== "" || endpoint.password !== "" || endpoint.search !== "" || endpoint.hash !== "" || endpoint.href.includes("?") || endpoint.href.includes("#")) {
|
|
141
141
|
return yield* Effect.fail(
|
|
142
142
|
configurationFailure("remote artifact endpoint must not contain credentials, a query, or a fragment")
|
|
143
143
|
);
|