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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +175 -2
  4. package/dist/cjs/ArtifactBackupLease.d.ts +34 -0
  5. package/dist/cjs/ArtifactBackupLease.d.ts.map +1 -0
  6. package/dist/cjs/ArtifactBackupLease.js +183 -0
  7. package/dist/cjs/ArtifactBackupLease.js.map +7 -0
  8. package/dist/cjs/ArtifactStore.d.ts +129 -0
  9. package/dist/cjs/ArtifactStore.d.ts.map +1 -0
  10. package/dist/cjs/ArtifactStore.js +73 -0
  11. package/dist/cjs/ArtifactStore.js.map +7 -0
  12. package/dist/cjs/ArtifactStoreError.d.ts +82 -0
  13. package/dist/cjs/ArtifactStoreError.d.ts.map +1 -0
  14. package/dist/cjs/ArtifactStoreError.js +72 -0
  15. package/dist/cjs/ArtifactStoreError.js.map +7 -0
  16. package/dist/cjs/ArtifactStoreMetrics.d.ts +70 -0
  17. package/dist/cjs/ArtifactStoreMetrics.d.ts.map +1 -0
  18. package/dist/cjs/ArtifactStoreMetrics.js +51 -0
  19. package/dist/cjs/ArtifactStoreMetrics.js.map +7 -0
  20. package/dist/cjs/ArtifactSweep.d.ts +185 -0
  21. package/dist/cjs/ArtifactSweep.d.ts.map +1 -0
  22. package/dist/cjs/ArtifactSweep.js +171 -0
  23. package/dist/cjs/ArtifactSweep.js.map +7 -0
  24. package/dist/cjs/CombinedArtifacts.d.ts +103 -0
  25. package/dist/cjs/CombinedArtifacts.d.ts.map +1 -0
  26. package/dist/cjs/CombinedArtifacts.js +189 -0
  27. package/dist/cjs/CombinedArtifacts.js.map +7 -0
  28. package/dist/cjs/FileLease.d.ts +117 -0
  29. package/dist/cjs/FileLease.d.ts.map +1 -0
  30. package/dist/cjs/FileLease.js +208 -0
  31. package/dist/cjs/FileLease.js.map +7 -0
  32. package/dist/cjs/FileSystemArtifactStore.d.ts +93 -0
  33. package/dist/cjs/FileSystemArtifactStore.d.ts.map +1 -0
  34. package/dist/cjs/FileSystemArtifactStore.js +276 -0
  35. package/dist/cjs/FileSystemArtifactStore.js.map +7 -0
  36. package/dist/cjs/MemoryArtifactStore.d.ts +25 -0
  37. package/dist/cjs/MemoryArtifactStore.d.ts.map +1 -0
  38. package/dist/cjs/MemoryArtifactStore.js +84 -0
  39. package/dist/cjs/MemoryArtifactStore.js.map +7 -0
  40. package/dist/cjs/NoopArtifactStore.d.ts +17 -0
  41. package/dist/cjs/NoopArtifactStore.d.ts.map +1 -0
  42. package/dist/cjs/NoopArtifactStore.js +46 -0
  43. package/dist/cjs/NoopArtifactStore.js.map +7 -0
  44. package/dist/cjs/RemoteArtifacts.d.ts +237 -0
  45. package/dist/cjs/RemoteArtifacts.d.ts.map +1 -0
  46. package/dist/cjs/RemoteArtifacts.js +405 -0
  47. package/dist/cjs/RemoteArtifacts.js.map +7 -0
  48. package/dist/cjs/index.d.ts +65 -0
  49. package/dist/cjs/index.d.ts.map +1 -0
  50. package/dist/cjs/index.js +47 -0
  51. package/dist/cjs/index.js.map +7 -0
  52. package/dist/cjs/internal/ArtifactLocks.d.ts +30 -0
  53. package/dist/cjs/internal/ArtifactLocks.d.ts.map +1 -0
  54. package/dist/cjs/internal/ArtifactLocks.js +93 -0
  55. package/dist/cjs/internal/ArtifactLocks.js.map +7 -0
  56. package/dist/cjs/internal/ArtifactPath.d.ts +19 -0
  57. package/dist/cjs/internal/ArtifactPath.d.ts.map +1 -0
  58. package/dist/cjs/internal/ArtifactPath.js +74 -0
  59. package/dist/cjs/internal/ArtifactPath.js.map +7 -0
  60. package/dist/cjs/measureBytes.d.ts +18 -0
  61. package/dist/cjs/measureBytes.d.ts.map +1 -0
  62. package/dist/cjs/measureBytes.js +43 -0
  63. package/dist/cjs/measureBytes.js.map +7 -0
  64. package/dist/cjs/package.json +1 -0
  65. package/dist/cjs/snapshotBytes.d.ts +19 -0
  66. package/dist/cjs/snapshotBytes.d.ts.map +1 -0
  67. package/dist/cjs/snapshotBytes.js +41 -0
  68. package/dist/cjs/snapshotBytes.js.map +7 -0
  69. package/dist/cjs/validateDigest.d.ts +22 -0
  70. package/dist/cjs/validateDigest.d.ts.map +1 -0
  71. package/dist/cjs/validateDigest.js +42 -0
  72. package/dist/cjs/validateDigest.js.map +7 -0
  73. package/dist/esm/ArtifactBackupLease.d.ts +34 -0
  74. package/dist/esm/ArtifactBackupLease.d.ts.map +1 -0
  75. package/dist/esm/ArtifactBackupLease.js +126 -0
  76. package/dist/esm/ArtifactBackupLease.js.map +1 -0
  77. package/dist/esm/ArtifactStore.d.ts +129 -0
  78. package/dist/esm/ArtifactStore.d.ts.map +1 -0
  79. package/dist/esm/ArtifactStore.js +83 -0
  80. package/dist/esm/ArtifactStore.js.map +1 -0
  81. package/dist/esm/ArtifactStoreError.d.ts +82 -0
  82. package/dist/esm/ArtifactStoreError.d.ts.map +1 -0
  83. package/dist/esm/ArtifactStoreError.js +77 -0
  84. package/dist/esm/ArtifactStoreError.js.map +1 -0
  85. package/dist/esm/ArtifactStoreMetrics.d.ts +70 -0
  86. package/dist/esm/ArtifactStoreMetrics.d.ts.map +1 -0
  87. package/dist/esm/ArtifactStoreMetrics.js +77 -0
  88. package/dist/esm/ArtifactStoreMetrics.js.map +1 -0
  89. package/dist/esm/ArtifactSweep.d.ts +185 -0
  90. package/dist/esm/ArtifactSweep.d.ts.map +1 -0
  91. package/dist/esm/ArtifactSweep.js +210 -0
  92. package/dist/esm/ArtifactSweep.js.map +1 -0
  93. package/dist/esm/CombinedArtifacts.d.ts +103 -0
  94. package/dist/esm/CombinedArtifacts.d.ts.map +1 -0
  95. package/dist/esm/CombinedArtifacts.js +214 -0
  96. package/dist/esm/CombinedArtifacts.js.map +1 -0
  97. package/dist/esm/FileLease.d.ts +117 -0
  98. package/dist/esm/FileLease.d.ts.map +1 -0
  99. package/dist/esm/FileLease.js +249 -0
  100. package/dist/esm/FileLease.js.map +1 -0
  101. package/dist/esm/FileSystemArtifactStore.d.ts +93 -0
  102. package/dist/esm/FileSystemArtifactStore.d.ts.map +1 -0
  103. package/dist/esm/FileSystemArtifactStore.js +336 -0
  104. package/dist/esm/FileSystemArtifactStore.js.map +1 -0
  105. package/dist/esm/MemoryArtifactStore.d.ts +25 -0
  106. package/dist/esm/MemoryArtifactStore.d.ts.map +1 -0
  107. package/dist/esm/MemoryArtifactStore.js +67 -0
  108. package/dist/esm/MemoryArtifactStore.js.map +1 -0
  109. package/dist/esm/NoopArtifactStore.d.ts +17 -0
  110. package/dist/esm/NoopArtifactStore.d.ts.map +1 -0
  111. package/dist/esm/NoopArtifactStore.js +27 -0
  112. package/dist/esm/NoopArtifactStore.js.map +1 -0
  113. package/dist/esm/RemoteArtifacts.d.ts +237 -0
  114. package/dist/esm/RemoteArtifacts.d.ts.map +1 -0
  115. package/dist/esm/RemoteArtifacts.js +557 -0
  116. package/dist/esm/RemoteArtifacts.js.map +1 -0
  117. package/dist/esm/index.d.ts +65 -0
  118. package/dist/esm/index.d.ts.map +1 -0
  119. package/dist/esm/index.js +65 -0
  120. package/dist/esm/index.js.map +1 -0
  121. package/dist/esm/internal/ArtifactLocks.d.ts +30 -0
  122. package/dist/esm/internal/ArtifactLocks.d.ts.map +1 -0
  123. package/dist/esm/internal/ArtifactLocks.js +86 -0
  124. package/dist/esm/internal/ArtifactLocks.js.map +1 -0
  125. package/dist/esm/internal/ArtifactPath.d.ts +19 -0
  126. package/dist/esm/internal/ArtifactPath.d.ts.map +1 -0
  127. package/dist/esm/internal/ArtifactPath.js +59 -0
  128. package/dist/esm/internal/ArtifactPath.js.map +1 -0
  129. package/dist/esm/measureBytes.d.ts +18 -0
  130. package/dist/esm/measureBytes.d.ts.map +1 -0
  131. package/dist/esm/measureBytes.js +22 -0
  132. package/dist/esm/measureBytes.js.map +1 -0
  133. package/dist/esm/snapshotBytes.d.ts +19 -0
  134. package/dist/esm/snapshotBytes.d.ts.map +1 -0
  135. package/dist/esm/snapshotBytes.js +30 -0
  136. package/dist/esm/snapshotBytes.js.map +1 -0
  137. package/dist/esm/validateDigest.d.ts +22 -0
  138. package/dist/esm/validateDigest.d.ts.map +1 -0
  139. package/dist/esm/validateDigest.js +26 -0
  140. package/dist/esm/validateDigest.js.map +1 -0
  141. package/docs/README.md +202 -0
  142. package/docs/api.md +484 -0
  143. package/docs/concepts/content-addressing.md +174 -0
  144. package/docs/concepts/coordination.md +141 -0
  145. package/docs/concepts/tiers.md +147 -0
  146. package/docs/guides/fence-a-backup.md +123 -0
  147. package/docs/guides/reclaim-disk-space.md +143 -0
  148. package/docs/guides/serve-the-artifact-protocol.md +140 -0
  149. package/docs/guides/share-artifacts-across-machines.md +180 -0
  150. package/docs/guides/test-against-an-artifact-store.md +158 -0
  151. package/docs/installation.md +102 -0
  152. package/docs/quickstart.md +183 -0
  153. package/docs/troubleshooting.md +232 -0
  154. package/package.json +179 -3
  155. package/src/ArtifactBackupLease.ts +223 -0
  156. package/src/ArtifactStore.ts +152 -0
  157. package/src/ArtifactStoreError.ts +96 -0
  158. package/src/ArtifactStoreMetrics.ts +83 -0
  159. package/src/ArtifactSweep.ts +346 -0
  160. package/src/CombinedArtifacts.ts +369 -0
  161. package/src/FileLease.ts +397 -0
  162. package/src/FileSystemArtifactStore.ts +459 -0
  163. package/src/MemoryArtifactStore.ts +83 -0
  164. package/src/NoopArtifactStore.ts +30 -0
  165. package/src/RemoteArtifacts.ts +814 -0
  166. package/src/index.ts +71 -0
  167. package/src/internal/ArtifactLocks.ts +110 -0
  168. package/src/internal/ArtifactPath.ts +72 -0
  169. package/src/measureBytes.ts +28 -0
  170. package/src/snapshotBytes.ts +34 -0
  171. package/src/validateDigest.ts +31 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # @smthrs/artifacts
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Fixed
6
+
7
+ - The filesystem store syncs the real ancestors of a Windows drive directory.
8
+ Breaking: `makeFileSystem(fs, path, options)` takes the host `Path`, and
9
+ `layerFileSystem` requires `Path.Path` (`NodePath.layer` on Node) (#2302).
10
+ - The filesystem store's `get`, `has`, and `findMissing` refuse a symlinked
11
+ objects root, fanout, or blob as `unavailable` instead of following it.
12
+ - Artifact locks refuse a symlinked `.locks` directory before and after
13
+ creating it and recheck its identity before every lock-file mutation;
14
+ `FileLease.hold` takes a `guard` option for that check.
15
+ - `RemoteArtifacts` never follows redirects (fetch runs with
16
+ `redirect: "manual"`), and allows plain `http:` only for the literal
17
+ loopback hosts `localhost`, `127.0.0.1`, and `[::1]`. Breaking: an
18
+ `http://*.localhost` endpoint is refused.
19
+
20
+ ## [1.0.0-rc.0] - 2026-08-31
21
+
22
+ ### Added
23
+
24
+ - New package. The content-addressed artifact store extracted out of
25
+ `@smthrs/engine-store`'s `StepBoundary`: `ArtifactStore` (contract,
26
+ filesystem, memory, and no-op implementations), `RemoteArtifacts` (the
27
+ dumb-HTTP CAS client), and `CombinedArtifacts` (local-first, remote-second
28
+ read-through with local write-back).
29
+ - Two improvements taken while moving, both from Bazel's `DiskCacheClient`: a
30
+ two-hex-prefix fanout directory layout, and an fsync of the temp file before
31
+ the rename that publishes it. There is no compatibility shim for the old flat
32
+ layout.
33
+
34
+ ### Changed
35
+
36
+ - Snapshotted all caller-owned byte arrays at Effect start and copied every
37
+ read result.
38
+ - Added strict SHA-256 address validation, typed configuration and crypto
39
+ failures, finite remote deadlines and body limits, and deterministic
40
+ `findMissing` deduplication.
41
+ - Added workspace-wide writer/sweeper coordination with crash recovery, and
42
+ reclaimed crash-orphaned lock files in the same conservative scratch sweep
43
+ that reclaims `.tmp-*` payloads.
44
+ - Made download policy immutable and consistent across constructors and layers.
45
+ - Made `CombinedArtifacts.get`'s local write-back opportunistic: a local tier
46
+ that cannot store bytes the shared tier already served costs the next read a
47
+ round trip instead of failing this one. `minimal` now repairs a corrupt local
48
+ address, because an address the local tier already claims must be one it can
49
+ serve.
50
+ - Applied caller-configured headers before the protocol's own, so a configured
51
+ `content-range`, `content-type`, or `content-length` can no longer overwrite
52
+ a chunked upload's computed headers.
53
+ - Reported a refused byte snapshot as `unavailable`, the host-refusal code,
54
+ rather than as a crypto `digest_failed`.
55
+ - Ignored a resume prefix past the safe integer range instead of trusting a
56
+ rounded offset.
57
+ - Gave `ArtifactSweep` its own `SweepOptions` rather than the store's options,
58
+ which carried a `durability` field it ignored, and exported
59
+ `ArtifactStore.defaultDirectory` so the store and its sweep name one
60
+ directory.
61
+ - Classified a backup-lease marker another process already reaped as a clean
62
+ release rather than logging a release failure.
63
+ - Stopped a lock heartbeat from freshening a lock file it no longer owns. A
64
+ holder reaped as stale and replaced kept touching that path every 10 seconds,
65
+ which held the replacement fresh past every stale bound, so a replacement that
66
+ was then hard-killed blocked the digest for as long as the reaped holder ran.
67
+ A lock that is gone or now names another owner ends the heartbeat; a read the
68
+ host refused for any other reason skips one beat and retries, so a transient
69
+ refusal cannot retire a lock still held.
70
+ - Made per-digest lock bookkeeping per execution rather than per construction.
71
+ A lock effect built once and run repeatedly, which is how the backup lease
72
+ heartbeats, retired its entry underneath a live holder, and the next caller
73
+ minted a second in-process semaphore for the same digest.
74
+ - Documented that reclaiming a stale lock measures and then removes in separate
75
+ steps, so simultaneous reclaims of one stale lock can both proceed, and that
76
+ `ifUnmodifiedSinceMs` and the backup lease are the fences that do not depend
77
+ on it.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 William Cory and the Smithers Flows contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,176 @@
1
- # Temporary Holding Version
1
+ # `@smthrs/artifacts`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Release candidate scope, host requirements and compatibility review are defined in the [library support policy](https://github.com/smithersai/smithers/blob/main/RELEASE_SUPPORT.md).
4
+
5
+ This package declares `effect` as a `^4.0.4` peer dependency. Install one
6
+ `effect` release in that range so all Smithers packages share one Effect
7
+ runtime.
8
+
9
+ **Documentation:** https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/artifacts/docs
10
+
11
+ The content-addressed artifact store: bytes addressed by their own SHA-256
12
+ digest. You hand it a `Uint8Array`, it hands back a 64 character address, and
13
+ any store holding those bytes returns them for that address after checking that
14
+ they still hash to it.
15
+
16
+ Reach for it when you cache or ship large byte payloads, a compiled bundle, a
17
+ build log, a model transcript, a tarball, and you care that a read either
18
+ returns the exact bytes that were published or fails. It opens no file and no
19
+ socket by itself: the filesystem and the network arrive as Effect's
20
+ `FileSystem` and `HttpClient` services, which is what lets the same store code
21
+ run in Node.js, in Bun, in a browser tab, and inside a sandbox.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install @smthrs/artifacts@next effect
27
+ ```
28
+
29
+ `@effect/platform-node` supplies the Node.js implementations of the services
30
+ the store asks for. The filesystem store needs exclusive writable handles,
31
+ symlink inspection, and (by default) file and directory syncing. Compose it
32
+ with the trusted host filesystem before applying the capability kernel to
33
+ workflow services; `NodeRuntime.layerHost` and `BunRuntime.layerHost` do this.
34
+ The guarded native filesystem refuses open handles and cannot back this store,
35
+ even with `durability: "best-effort"`. Use `layerMemory` on hosts without those
36
+ filesystem capabilities.
37
+
38
+ ## Publish bytes and read them back
39
+
40
+ ```ts
41
+ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
42
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
43
+ import * as NodePath from "@effect/platform-node/NodePath"
44
+ import * as ArtifactStore from "@smthrs/artifacts/ArtifactStore"
45
+ import * as Effect from "effect/Effect"
46
+ import * as Layer from "effect/Layer"
47
+
48
+ const layer = ArtifactStore.layerFileSystem({ directory: ".flows/objects" }).pipe(
49
+ Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer, NodeCrypto.layer))
50
+ )
51
+
52
+ const program = Effect.gen(function*() {
53
+ const store = yield* ArtifactStore.ArtifactStore
54
+ const bytes = new TextEncoder().encode("the bytes a build step produced")
55
+
56
+ const address = yield* store.put(bytes)
57
+ const again = yield* store.put(bytes)
58
+
59
+ console.log(address)
60
+ console.log(again === address)
61
+ console.log(new TextDecoder().decode(yield* store.get(address)))
62
+ })
63
+
64
+ await Effect.runPromise(program.pipe(Effect.provide(layer)))
65
+ ```
66
+
67
+ ```text
68
+ 6bb29e0869012afcfc246886c647422236e0b7d3419d3dc4ded8da758a4dfeb3
69
+ true
70
+ the bytes a build step produced
71
+ ```
72
+
73
+ One blob landed on disk, under the first two characters of its own address. The
74
+ second `put` measured the bytes, took the digest's lock, verified the blob
75
+ already there, freshened its modification time, and synced it, then returned the
76
+ same address without rewriting the payload.
77
+
78
+ ## The stores you can compose
79
+
80
+ Every implementation is the same four operations, `put`, `get`, `has`, and
81
+ `findMissing`, so a composition swaps one for another without touching a
82
+ caller:
83
+
84
+ | Constructor | What backs it | Use it for |
85
+ | ------------------------------ | ------------------------------------------- | ------------------------------------------ |
86
+ | `ArtifactStore.makeFileSystem` | A directory, reached through `FileSystem` | The durable local tier |
87
+ | `ArtifactStore.makeMemory` | A private `Map` | Tests, and hosts with no durable disk |
88
+ | `ArtifactStore.makeNoop` | Nothing, with per-method overrides | Declaring a tier honestly unavailable |
89
+ | `RemoteArtifacts.make` | An HTTP cache, reached through `HttpClient` | The tier several machines share |
90
+ | `CombinedArtifacts.make` | A local store in front of a shared one | Read local first, fall through, write back |
91
+
92
+ `ArtifactSweep` enumerates an objects directory and deletes one blob behind a
93
+ modification time fence, and `ArtifactBackupLease` keeps a sweep from deleting
94
+ a blob a running backup is still copying.
95
+
96
+ Every export, option, and error code is in the
97
+ [API reference](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/api.md).
98
+
99
+ ## What it guarantees
100
+
101
+ - **Every read is digest verified.** A truncated blob left by a crashing
102
+ writer, a corrupted disk, or a mis-serving shared tier is refused with
103
+ `ArtifactCorruption`, never handed back as the recorded artifact. The memory
104
+ store is the one deliberate exception: its address space is a private `Map`
105
+ keyed by the digest it measured, so the address and the content cannot
106
+ disagree.
107
+ - **Publication is atomic.** Bytes land at a temp path in the destination
108
+ directory, are fsynced, and are renamed into place. Temp names fold a random
109
+ per-instance token, so two processes publishing the same digest into one
110
+ workspace never share a scratch path.
111
+ - **An existing blob is verified on every `put`.** The objects directory is
112
+ shared, so a remembered proof could outlive the bytes it proved. A mismatch
113
+ or failing read falls through to the atomic rewrite and heals the address.
114
+ - **The endpoint and its credentials are a capability, never an input.** They
115
+ arrive as layer construction options, so they are never hashed into a cache
116
+ key and never recorded. `RemoteArtifacts` refuses a non-HTTPS endpoint, and
117
+ any endpoint carrying credentials, a query, or a fragment, at construction.
118
+ The sanitized message names only the violated rule, so the failure text never
119
+ echoes the endpoint.
120
+
121
+ Every `put` snapshots its bytes when the Effect begins. Stores never retain a
122
+ caller-owned buffer, and every successful `get` returns a new byte array.
123
+
124
+ ## Defaults
125
+
126
+ | Boundary | Default | Guarantee |
127
+ | ----------------------------------------- | ----------------------- | ---------------------------------------------------------------------------- |
128
+ | Filesystem directory | `.flows/objects` | Two-hex fanout, atomic publication, digest verification, required fsync |
129
+ | Cross-process coordination | `required` | Writers and sweepers share per-digest lock files; stale owners are recovered |
130
+ | Remote request, upload, download deadline | 60 seconds each | No remote exchange can wait forever |
131
+ | Maximum downloaded blob | 256 MiB | The body is rejected before or while buffering past the limit |
132
+ | `findMissing` batch and response | 1,000 digests / 256 KiB | Inputs are validated and deduplicated in first-seen order |
133
+
134
+ `invalid_configuration` and `invalid_digest` are permanent caller failures.
135
+ `ArtifactMissing` is an ordinary miss, and `ArtifactCorruption` is an integrity
136
+ failure. `digest_failed`, `unavailable`, and `transport_failed` describe host
137
+ or transport failures whose retryability depends on the host and operation.
138
+ [Troubleshooting](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/troubleshooting.md) lists each
139
+ one with what to change.
140
+
141
+ ## Prior art
142
+
143
+ The contract's ergonomics follow Effect's own `KeyValueStore`: one small set of
144
+ total operations over one address space, so memory, filesystem, and network
145
+ implementations are the same shape. Everything else follows Bazel's
146
+ remote-cache classes: `findMissing` is
147
+ [`MissingDigestsFinder`](https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/remote/common/MissingDigestsFinder.java),
148
+ one batched probe whose result is a subset of its input; the two-hex fanout and
149
+ the fsync before the rename are
150
+ [`DiskCacheClient`](https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheClient.java);
151
+ and the wire protocol, CAS blobs under `/cas/base16-key`, is
152
+ [`HttpCacheClient`](https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/remote/http/HttpCacheClient.java).
153
+ Where this package deviates, and why, is in
154
+ [content addressing](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/docs/concepts/content-addressing.md).
155
+
156
+ ## Where it fits
157
+
158
+ This package is one piece of the Smithers durable flow engine, whose whole
159
+ surface is re-exported by [`@smthrs/flows`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/docs). Inside
160
+ that engine the artifact store is the byte half of the result cache:
161
+ [`@smthrs/step-cache`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/step-cache/docs) records what a step
162
+ returned, and any part of that result too large to sit inline is spilled here
163
+ and referenced by digest.
164
+
165
+ Install `@smthrs/artifacts` on its own when a content-addressed byte store is
166
+ all you want. Nothing in it knows what a flow, a step, or a run is.
167
+
168
+ Reclaiming published artifacts is an explicit operation, never a side effect of
169
+ a store call. The `.tmp-*` sweep in `layerFileSystem` reclaims crash orphans
170
+ only. `ArtifactSweep` is the deletion surface, and the mark phase that decides
171
+ what is live belongs to
172
+ [`@smthrs/engine-store`](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/engine-store/docs).
173
+
174
+ ## License
175
+
176
+ MIT
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Cross-process exclusion between a filesystem artifact backup and garbage
3
+ * collection.
4
+ *
5
+ * A backup holds one heartbeat-backed marker for the duration of its database
6
+ * snapshot and blob copy. Sweep deletion checks that marker while holding the
7
+ * same short-lived global gate. Publication remains unconstrained: new blobs
8
+ * may appear during a backup, but a blob already referenced by the frozen
9
+ * database cannot disappear underneath it.
10
+ *
11
+ * @since 1.0.0-rc.0
12
+ */
13
+ import * as Effect from "effect/Effect";
14
+ import type * as FileSystem from "effect/FileSystem";
15
+ import * as Option from "effect/Option";
16
+ /**
17
+ * Runs `effect` while sweep deletion is fenced across every process using
18
+ * this objects directory. A crashed holder is recovered after its heartbeat
19
+ * becomes stale.
20
+ *
21
+ * @category combinators
22
+ * @since 1.0.0-rc.0
23
+ */
24
+ export declare const withLease: <A, E, R, E2>(fs: FileSystem.FileSystem, directory: string, effect: Effect.Effect<A, E, R>, failure: (cause: unknown) => E2) => Effect.Effect<A, E | E2, R>;
25
+ /**
26
+ * Runs one sweep deletion only when no live backup lease exists. The marker
27
+ * check and `effect` share the global gate, closing the check/delete race.
28
+ * `None` means a backup deliberately fenced the operation.
29
+ *
30
+ * @category combinators
31
+ * @since 1.0.0-rc.0
32
+ */
33
+ export declare const unlessActive: <A, E, R, E2>(fs: FileSystem.FileSystem, directory: string, effect: Effect.Effect<A, E, R>, failure: (cause: unknown) => E2) => Effect.Effect<Option.Option<A>, E | E2, R>;
34
+ //# sourceMappingURL=ArtifactBackupLease.d.ts.map
@@ -0,0 +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,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"}
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var ArtifactBackupLease_exports = {};
30
+ __export(ArtifactBackupLease_exports, {
31
+ unlessActive: () => unlessActive,
32
+ withLease: () => withLease
33
+ });
34
+ module.exports = __toCommonJS(ArtifactBackupLease_exports);
35
+ var Clock = __toESM(require("effect/Clock"), 1);
36
+ var Effect = __toESM(require("effect/Effect"), 1);
37
+ var Option = __toESM(require("effect/Option"), 1);
38
+ var PlatformError = __toESM(require("effect/PlatformError"), 1);
39
+ var Random = __toESM(require("effect/Random"), 1);
40
+ var ArtifactLocks = __toESM(require("./internal/ArtifactLocks.js"), 1);
41
+ const markerName = ".backup-lease";
42
+ const gateName = ".backup-lease-gate";
43
+ const heartbeatEvery = "10 seconds";
44
+ const staleAfterMs = 6e4;
45
+ const acquireWithin = "2 minutes";
46
+ const retryEvery = "25 millis";
47
+ const isReason = (cause, tag) => cause instanceof PlatformError.PlatformError && cause.reason._tag === tag;
48
+ const token = Effect.gen(function* () {
49
+ const first = yield* Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER, { halfOpen: true });
50
+ const second = yield* Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER, { halfOpen: true });
51
+ return `${first.toString(36)}-${second.toString(36)}-${yield* Clock.currentTimeMillis}`;
52
+ });
53
+ const gate = (fs, directory, effect, failure) => ArtifactLocks.withDigest(fs, directory, gateName, effect, failure);
54
+ const activeMarker = (fs, directory, failure) => Effect.gen(function* () {
55
+ const path = `${directory}/${markerName}`;
56
+ const info = yield* fs.stat(path).pipe(
57
+ Effect.map(Option.some),
58
+ Effect.catch(
59
+ (cause) => isReason(cause, "NotFound") ? Effect.succeed(Option.none()) : Effect.fail(failure(cause))
60
+ )
61
+ );
62
+ if (Option.isNone(info)) return false;
63
+ const modified = Option.getOrUndefined(info.value.mtime);
64
+ const now = yield* Clock.currentTimeMillis;
65
+ if (modified === void 0 || now - modified.getTime() <= staleAfterMs) return true;
66
+ yield* fs.remove(path).pipe(
67
+ Effect.catch(
68
+ (cause) => isReason(cause, "NotFound") ? Effect.void : Effect.fail(failure(cause))
69
+ )
70
+ );
71
+ return false;
72
+ });
73
+ const withLease = (fs, directory, effect, failure) => Effect.gen(function* () {
74
+ const owner = yield* token;
75
+ const marker = `${directory}/${markerName}`;
76
+ yield* fs.makeDirectory(directory, { recursive: true }).pipe(Effect.mapError(failure));
77
+ let ownsMarker = false;
78
+ const acquire = Effect.gen(function* () {
79
+ while (true) {
80
+ const acquired = yield* gate(
81
+ fs,
82
+ directory,
83
+ Effect.gen(function* () {
84
+ if (yield* activeMarker(fs, directory, failure)) return false;
85
+ return yield* Effect.uninterruptible(
86
+ fs.writeFileString(marker, owner, { flag: "wx", mode: 384 }).pipe(
87
+ Effect.andThen(Effect.sync(() => {
88
+ ownsMarker = true;
89
+ return true;
90
+ })),
91
+ Effect.catch(
92
+ (cause) => isReason(cause, "AlreadyExists") ? Effect.succeed(false) : Effect.fail(failure(cause))
93
+ )
94
+ )
95
+ );
96
+ }),
97
+ failure
98
+ );
99
+ if (acquired) return;
100
+ yield* Effect.sleep(retryEvery);
101
+ }
102
+ }).pipe(
103
+ Effect.timeout(acquireWithin),
104
+ Effect.catchTag("TimeoutError", (cause) => Effect.fail(failure(cause)))
105
+ );
106
+ return yield* Effect.acquireUseRelease(
107
+ Effect.void,
108
+ () => acquire.pipe(Effect.andThen(
109
+ Effect.scoped(Effect.gen(function* () {
110
+ yield* Effect.forkScoped(
111
+ Effect.gen(function* () {
112
+ while (true) {
113
+ yield* Effect.sleep(heartbeatEvery);
114
+ const reclaimed = yield* gate(
115
+ fs,
116
+ directory,
117
+ fs.readFileString(marker).pipe(
118
+ Effect.map(Option.some),
119
+ Effect.catch(
120
+ (cause) => isReason(cause, "NotFound") ? Effect.succeed(Option.none()) : Effect.fail(cause)
121
+ ),
122
+ Effect.flatMap((found) => {
123
+ if (Option.getOrUndefined(found) !== owner) {
124
+ return Effect.as(
125
+ Effect.logWarning(
126
+ "Artifact backup lease was reclaimed while its backup was still running"
127
+ ),
128
+ true
129
+ );
130
+ }
131
+ return Effect.flatMap(Clock.currentTimeMillis, (now) => {
132
+ const timestamp = new Date(now);
133
+ return Effect.as(fs.utimes(marker, timestamp, timestamp), false);
134
+ });
135
+ }),
136
+ Effect.mapError(failure)
137
+ ),
138
+ failure
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
+ })
150
+ );
151
+ return yield* effect;
152
+ }))
153
+ )),
154
+ () => ownsMarker ? gate(
155
+ fs,
156
+ directory,
157
+ fs.readFileString(marker).pipe(
158
+ Effect.flatMap((found) => found === owner ? fs.remove(marker) : Effect.void),
159
+ // A backup whose heartbeat lapsed has its marker reaped by whoever
160
+ // noticed, so `NotFound` is the ordinary end of a slow lease, not a
161
+ // fault: there is nothing left to release and nothing left to leak.
162
+ // The sibling release in `internal/ArtifactLocks.ts` classifies the
163
+ // same cause the same way.
164
+ Effect.catch(
165
+ (cause) => isReason(cause, "NotFound") ? Effect.void : Effect.fail(cause)
166
+ )
167
+ ),
168
+ failure
169
+ ).pipe(
170
+ Effect.catch((cause) => Effect.logWarning("Artifact backup lease release failed", cause))
171
+ ) : Effect.void
172
+ );
173
+ });
174
+ const unlessActive = (fs, directory, effect, failure) => gate(
175
+ fs,
176
+ directory,
177
+ Effect.gen(function* () {
178
+ if (yield* activeMarker(fs, directory, failure)) return Option.none();
179
+ return Option.some(yield* effect);
180
+ }),
181
+ failure
182
+ );
183
+ //# sourceMappingURL=ArtifactBackupLease.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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.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
+ "names": []
7
+ }