@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/src/ArtifactSweep.ts
CHANGED
|
@@ -241,6 +241,17 @@ export const makeFileSystem = (
|
|
|
241
241
|
const remove: Service["remove"] = Effect.fn("ArtifactSweep.remove")((digest, removeOptions) =>
|
|
242
242
|
Effect.gen(function*() {
|
|
243
243
|
const validated = yield* ArtifactStore.validateDigest(digest)
|
|
244
|
+
const bound = removeOptions?.ifUnmodifiedSinceMs
|
|
245
|
+
// NaN fails every comparison, so the fence below would read it as "not
|
|
246
|
+
// modified since" and delete a blob a concurrent put just freshened.
|
|
247
|
+
if (bound !== undefined && (typeof bound !== "number" || Number.isNaN(bound))) {
|
|
248
|
+
return yield* Effect.fail(
|
|
249
|
+
new ArtifactStore.ArtifactStoreError({
|
|
250
|
+
code: "invalid_configuration",
|
|
251
|
+
message: "invalid sweep option: ifUnmodifiedSinceMs"
|
|
252
|
+
})
|
|
253
|
+
)
|
|
254
|
+
}
|
|
244
255
|
yield* Effect.annotateCurrentSpan({ digest: validated })
|
|
245
256
|
const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))
|
|
246
257
|
const checkParent = yield* ArtifactPath.guard(fs, `${directory}/${validated.slice(0, 2)}`).pipe(
|
|
@@ -265,7 +276,6 @@ export const makeFileSystem = (
|
|
|
265
276
|
yield* checkRoot.pipe(Effect.mapError(hostFailure))
|
|
266
277
|
yield* checkParent.pipe(Effect.mapError(hostFailure))
|
|
267
278
|
const checkBlob = yield* ArtifactPath.guard(fs, path, "File").pipe(Effect.mapError(hostFailure))
|
|
268
|
-
const bound = removeOptions?.ifUnmodifiedSinceMs
|
|
269
279
|
if (bound !== undefined) {
|
|
270
280
|
const info = yield* fs.stat(path).pipe(Effect.option)
|
|
271
281
|
// Already gone is a completed deletion, and an unmeasurable mtime is
|
package/src/FileLease.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
import * as Clock from "effect/Clock"
|
|
39
39
|
import * as Deferred from "effect/Deferred"
|
|
40
|
-
import
|
|
40
|
+
import * as Duration from "effect/Duration"
|
|
41
41
|
import * as Effect from "effect/Effect"
|
|
42
42
|
import type * as FileSystem from "effect/FileSystem"
|
|
43
43
|
import * as Option from "effect/Option"
|
|
@@ -57,7 +57,11 @@ export interface Options {
|
|
|
57
57
|
readonly annotations?: Readonly<Record<string, unknown>> | undefined
|
|
58
58
|
/** How often the holder refreshes the lock's mtime. Defaults to 10 seconds. */
|
|
59
59
|
readonly heartbeatEvery?: Duration.Input | undefined
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Age past which a lock or claim is stale. Defaults to 60 000 ms. Must be a
|
|
62
|
+
* finite positive number longer than `heartbeatEvery`; anything else fails
|
|
63
|
+
* the lease through `failure` before the lock is touched.
|
|
64
|
+
*/
|
|
61
65
|
readonly staleAfterMs?: number | undefined
|
|
62
66
|
/** How long acquisition may wait. Defaults to 2 minutes. */
|
|
63
67
|
readonly acquireWithin?: Duration.Input | undefined
|
|
@@ -132,6 +136,19 @@ export const hold = <A, E, R, E2>(
|
|
|
132
136
|
const heartbeatEvery = options.heartbeatEvery ?? "10 seconds"
|
|
133
137
|
const staleAfterMs = options.staleAfterMs ?? defaultStaleAfterMs
|
|
134
138
|
const retryEvery = options.retryEvery ?? "25 millis"
|
|
139
|
+
// A holder heartbeats its lock to stay fresh, so a stale bound at or below
|
|
140
|
+
// the heartbeat, or one that is not a positive finite number, would let a
|
|
141
|
+
// contender read a live holder's lock as abandoned and move it, or would
|
|
142
|
+
// never reclaim a dead holder's lock at all.
|
|
143
|
+
const heartbeatMs = Option.map(Duration.fromInput(heartbeatEvery), Duration.toMillis)
|
|
144
|
+
if (
|
|
145
|
+
typeof staleAfterMs !== "number" || !Number.isFinite(staleAfterMs) || staleAfterMs <= 0 ||
|
|
146
|
+
Option.isNone(heartbeatMs) || !(heartbeatMs.value > 0 && heartbeatMs.value < staleAfterMs)
|
|
147
|
+
) {
|
|
148
|
+
return yield* Effect.fail(
|
|
149
|
+
failure(new RangeError(`${label} needs 0 < heartbeatEvery < staleAfterMs, with a finite staleAfterMs`))
|
|
150
|
+
)
|
|
151
|
+
}
|
|
135
152
|
const owner = yield* token
|
|
136
153
|
/**
|
|
137
154
|
* Whether this call created the lock file, and therefore owes a release.
|
package/src/RemoteArtifacts.ts
CHANGED
|
@@ -376,7 +376,12 @@ export const make = (
|
|
|
376
376
|
if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && loopback)) {
|
|
377
377
|
return yield* Effect.fail(configurationFailure("remote artifact endpoint must use HTTPS"))
|
|
378
378
|
}
|
|
379
|
-
|
|
379
|
+
// An empty query or fragment ("https://h/p?") parses as "" yet survives
|
|
380
|
+
// serialization, so the delimiter itself is refused in the href.
|
|
381
|
+
if (
|
|
382
|
+
endpoint.username !== "" || endpoint.password !== "" || endpoint.search !== "" || endpoint.hash !== "" ||
|
|
383
|
+
endpoint.href.includes("?") || endpoint.href.includes("#")
|
|
384
|
+
) {
|
|
380
385
|
return yield* Effect.fail(
|
|
381
386
|
configurationFailure("remote artifact endpoint must not contain credentials, a query, or a fragment")
|
|
382
387
|
)
|