Skip to content

Release 7.0.0

Latest

Choose a tag to compare

@nashysolutions-bot nashysolutions-bot released this 08 Oct 13:55
e08eb02

7.0.0 is a major release. FileSystemCache now writes to disk with no setup and stores entries in a new layout scoped by item type, and both caches read the current time through @Dependency(\.date). Read Upgrading first. Some of the changes there produce no compiler error.

Upgrading

Tests that use either cache must now set the time. Both caches read the current time from @Dependency(\.date) when they set an item, look up or sweep. swift-dependencies declares no test value for \.date, so a test that stashes, looks up or sweeps a VolatileCache or a FileSystemCache without overriding \.date now records "@Dependency(\.date) has no test implementation". Nothing reports this at compile time; it appears when the tests run. Override it in a test with the .dependency(\.date.now, someFixedDate) trait from DependenciesTestSupport, or with withDependencies, as QuickStart's "Controlling time" section shows. Compilation, previews and shipping apps are unaffected. (#56)

Delete your copy of the live file system conformance. Earlier versions of QuickStart asked you to write a @retroactive DependencyKey conformance for FileSystemResourceClientKey by hand. This package now declares that conformance, so a copy in your own code fails to compile with redundant conformance of 'FileSystemResourceClientKey' to protocol 'DependencyKey'. Delete yours. If you also wrote a FileSystemClientKey conformance, this package never read it. Keep it only if something else in your app does. (#39)

Your own conformances to Cache. A type outside this package that conforms to Cache needs two changes:

  • Item and Item.ID must both be Sendable. A conformance whose Item or Item.ID is not Sendable no longer compiles. (#45)
  • Implement removeExpired(). Until you do, the conformance compiles with a deprecation warning and picks up a default that removes nothing and throws CocoaError(.featureUnsupported). That default is removed in 8.0.0. (#42, #53)

Code that passes a FileSystemDirectory value. FileSystemCache now takes a CacheDirectory. Calls written with a leading dot, such as FileSystemCache<Item>(.caches), need no change. A call that passes a FileSystemDirectory value still compiles, with a deprecation warning. Pass the CacheDirectory case of the same name instead: it names the same directory, so entries already on disk are still found. If Files was imported only to name the directory, that import can go. (#57)

Code that uses Resource or CodableResource. Both still compile, with a deprecation warning. Nothing in the package accepts or returns either. Pass the item to setItem(_:expiry:) directly, or declare your own type to pair an item with an expiry date. (#53)

Subfolders that contain ... A FileSystemCache subfolder with a .. component is now refused at run time, not at compile time. Use a path below the base directory without ... See "Subfolders stay inside the base directory" below. (#54)

Package requirements. foundation-dependencies is now required at "6.0.0"..<"7.1.0", where 6.0.0 required .upToNextMinor(from: "5.0.1") (#11, #47). files is now declared directly, at .upToNextMinor(from: "3.0.0"); 6.0.0's Package.resolved pinned 2.2.1, reached through another dependency (#39). swift-dependencies is unchanged at .upToNextMinor(from: "1.8.1"). A package graph that requires versions outside these ranges cannot be resolved.

The renamed operations. stash(_:duration:) is now setItem(_:expiry:), resource(for:) is item(for:), removeResource(for:) is removeItem(for:), and reset() is removeAll(). Expiry.custom(_:) is now the case at(_:), and .short, .medium and .long are presets of a new case, after(_:), which takes a Duration. Calls of the old names still compile, each with a deprecation warning and a fix-it. So does a conformance to Cache that implements only the old names, with a warning on each operation naming the method to implement. Two things stop compiling:

  • A case .custom(let date) pattern. Write case .at(let date).
  • An exhaustive switch over Expiry. Cover .after and .at, or add a default.

A third change may not show at compile time. If a conformance of your own already has a method with the signature of a new name, such as a helper func removeAll() async, that method now implements the requirement. Where it is less visible than the type, the conformance stops compiling. Where it is not, it compiles with no warning, and a caller of the new name reaches that method instead of your implementation of the old name. Rename the method.

A conformance that implements neither the old nor the new name of an operation compiles with only a warning, and a call to that operation never returns: memory grows until the system terminates the process. If you declared Expiry Equatable or Hashable yourself, delete that conformance, which now produces a warning. (#61)

Behaviour changes

These need no code change.

Caches start cold after upgrading. Entries are now written to <base>/[<subfolder>/]cache-v2/<type>/<digest>.cache, where <type> is a SHA-256 digest of the item type's fully qualified name. Entries written by 6.0.0 and earlier are not read, migrated or deleted. This package cannot tell them apart from your own files, so it leaves them on disk. To reclaim the space, delete them yourself: with subfolder: nil they sit directly in the base directory beside the cache-v2 folder, and with a subfolder, directly inside it. The "On-disk format" article describes the layout. (#30, #35, #39)

Caches over different item types no longer collide. This affected only consumers on 6.0.0 who registered their own file system client. For them, two FileSystemCache instances over different Item types in the same directory derived the same filename for the same identifier, so each overwrote the other's entry, and a lookup through the other cache then found an entry it could not decode. Entries are now scoped by item type, so caches over different item types can share a directory and a subfolder without reading, overwriting or clearing each other's entries. Consumers who did not register their own file system client were not affected, because 6.0.0 shipped no live file system value and wrote nothing to disk for them. (#39)

FileSystemCache writes to disk with no setup. In 6.0.0, unless you registered your own file system client, a FileSystemCache reported every stash as a success and wrote nothing. This package now supplies a live client backed by FileManager. Two consequences:

  • On a non-sandboxed macOS process, .documents is the user's real ~/Documents, and a cache nominating it now creates a folder there on first use.
  • The live value is supplied by a conformance, so it takes effect everywhere in a binary that links Cache, including code that never imports Cache. (#39)

Unchanged in tests and previews. In a test, or in an Xcode preview, FileSystemCache still does not reach the real file system. Setting an item succeeds, every lookup reports nil, no directory is created, and nothing warns you. Supply your own fileSystemResourceClient, or opt into the live context, as QuickStart's "In a test, and in a preview" section shows. (#39)

Subfolders stay inside the base directory. A FileSystemCache subfolder that contains a .. component, including one that stays inside the base directory such as "a/../b", or that is reached through a symbolic link pointing outside the base directory, is now refused. Every operation on such a cache throws CocoaError(.fileWriteInvalidFileName), and nothing is created or written below the base directory. Nested subfolders such as "a/b", and links that stay inside the base directory, are unaffected. (#54)

A link at an entry's filename is not followed. setItem(_:expiry:) now writes each entry atomically. A symbolic link at an entry's filename is replaced rather than followed, so a write can no longer land outside the cache's base directory. Reading an identifier whose filename holds a link reports nil and removes the link without reading its target. Entry bytes and filenames are unchanged. A custom fileSystemResourceClient receives .atomic in the write options and must honour it to get the same protection. (#58)

Expiry reads the injected time. Setting an item counts its Expiry from the time @Dependency(\.date) gives, and a lookup or a sweep judges the expiry against a fresh reading. Apps and tests set the time with withDependencies, and no initialiser gained a clock parameter. An entry is still served at the instant it expires, and not after it. (#56)

New API

  • removeExpired() on Cache, VolatileCache and FileSystemCache: @discardableResult func removeExpired() async throws -> Int. It removes every expired entry and returns how many it removed. Nothing calls it for you: no timer, no read, no write. FileSystemCache judges an entry by its stored expiry alone, so an expired entry whose item no longer decodes is removed too. The sweep does not reach another item type's entries, files you placed in the folder, or entries from an earlier layout. It is not transactional: if one delete fails, the error is thrown and the entries removed before it stay removed. (#42)

  • CacheDirectory, with the cases documents, caches, applicationSupport and temporary, and a FileSystemCache initialiser that takes one. CacheDirectory is declared in this package, so creating a cache needs only import Cache. Calls written with a leading dot, such as FileSystemCache<Item>(.caches), compile unchanged and keep writing to the same directory. (#57)

  • UUID identifiers. VolatileCache no longer requires Item.ID to be LosslessStringConvertible, only Sendable. FileSystemCache requires CustomStringConvertible in its place. UUID now works with both caches, and every identifier type 6.0.0 accepted is still accepted. FileSystemCache names an entry's file by a digest of the identifier's description, so that text must be the same on every launch and different for different identifiers. Nothing checks either condition. (#46)

  • A live value for FileSystemResourceClientKey, backed by FileManager. See "FileSystemCache writes to disk with no setup" above. (#39)

  • setItem(_:expiry:), item(for:), removeItem(for:) and removeAll() on Cache, VolatileCache and FileSystemCache, replacing the 6.0.0 names. setItem(_:expiry:) replaces any entry already held for the item's identifier, expiry included. (#61)

  • Expiry.after(_:), which takes a Duration counted from the moment the item is set, and Expiry.at(_:), which takes a Date. .short, .medium and .long are presets of after(_:): 60, 180 and 3600 seconds, as before. An entry is served up to and including the instant it expires, so .after(.zero) is served only by a lookup at the instant it was set. Expiry is now Hashable, and two expiries are equal when they state the same policy. (#61)

Deprecated

Each of these compiles with a deprecation warning that names its replacement, and is removed in 8.0.0, under the deprecate-then-remove policy in CONTRIBUTING.md.

Deprecated Use instead
Resource and CodableResource Pass the item to setItem(_:expiry:) directly, or declare your own type to pair an item with an expiry date. (#53)
The default removeExpired() in an extension of Cache, which removes nothing and throws CocoaError(.featureUnsupported) Implement removeExpired() in your conformance. (#53)
FileSystemCache.init(_:subfolder:) taking a FileSystemDirectory from Files The initialiser taking a CacheDirectory, with the case of the same name. (#57)

| stash(_:duration:), resource(for:), removeResource(for:) and reset() | setItem(_:expiry:), item(for:), removeItem(for:) and removeAll(). Each deprecation carries a fix-it. (#61) |
| A conformance to Cache that implements the 6.0.0 names only, which compiles through defaults of the new names that call the old ones | Implement the new names in your conformance. (#61) |
| Expiry.custom(_:), now a static function | Expiry.at(_:). (#61) |

API breaks against 6.0.0

swift package diagnose-api-breaking-changes 6.0.0 reports 24 breaking changes. Grouped by cause:

Cache.Item and Item.ID are bound to Sendable (1 change, #45). A conformance outside this package whose Item or Item.ID is not Sendable no longer compiles. Both caches in this package already required both.

  • protocol Cache: generic signature gains Self.Item : Sendable and Self.Item.ID : Sendable.

FileSystemCache relaxes Item.ID from LosslessStringConvertible to CustomStringConvertible (3 changes, #46). Generic signature change on:

  • struct FileSystemCache
  • init(_:subfolder:), the initialiser taking a FileSystemDirectory, now deprecated
  • typealias FileSystemCache.Item

VolatileCache drops the LosslessStringConvertible requirement on Item.ID (3 changes, #46). Generic signature change on:

  • struct VolatileCache
  • init()
  • typealias VolatileCache.Item

The 6 changes from #46 widen what compiles. Code that calls either cache with an identifier type 6.0.0 accepted compiles unchanged.

Expiry's cases change (9 changes, #61). short, medium and long change from cases to static properties, each reported twice. after and at are added as cases, and custom is removed as a case. A case .custom(let date) pattern and an exhaustive switch over Expiry stop compiling; see Upgrading.

The 6.0.0 operation names move from the caches to Cache (8 changes, #61). The tool reports stash(_:duration:), resource(for:), removeResource(for:) and reset() as removed from VolatileCache and from FileSystemCache. Calls of them on either cache still compile, reaching the deprecated defaults on Cache, so these are changes to the symbols, not to source.

Not in the list:

  • The removal of Resource and CodableResource, and the new removeExpired() requirement on Cache. Deprecated shims keep 6.0.0 code compiling. (#53)
  • The new requirements setItem(_:expiry:), item(for:), removeItem(for:) and removeAll() on Cache, which have defaults. A conformance that already has a method with one of those signatures is affected; see Upgrading. (#61)
  • Three changes the tool cannot see, all listed under Upgrading or Behaviour changes: the duplicate FileSystemResourceClientKey conformance (#39), the \.date test failure (#56), and the on-disk layout (#30, #39).

Fixes

  • reset() deleted the directory the cache lived in, not only its entries. With the default subfolder: nil that was the base directory, so calling reset() on a cache created as FileSystemCache<Cheese>(.documents) removed the app's entire Documents directory. removeAll(), as reset() is now named, deletes only entries this cache wrote, and nothing in the package deletes a directory. As with the collision above, this reached disk in 6.0.0 only for consumers who registered their own file system client. (#30)
  • A miss reports nil. FileSystemCache.resource(for:) threw for an identifier it held nothing for, and removeResource(for:) threw when there was nothing to remove, both with an error type internal to Files. A miss is now nil from a lookup and no error from a removal, as VolatileCache and the Cache documentation already had it. (#38)
  • An entry that no longer decodes is cleared. An entry written before an app update changed the item's Codable shape could not be read, and could not be removed except by reset(). A lookup now reports nil and deletes it, and removeItem(for:) deletes by identifier without reading the entry first. A failure to read an entry that is present, such as a permissions error, still throws and leaves the entry in place. (#38)
  • Errors are the file system's own. A failed write or delete was wrapped in an error type internal to Files, which a caller could not name. It now surfaces Foundation's error, such as CocoaError. (#39)
  • A cache directory that cannot be searched throws. Lookups and removals reported it as a miss for every identifier, while reset() threw in the same state. (#39)
  • Documentation
    • VolatileCache no longer claims a record limit or eviction, both removed in 4.0.0. Nothing bounds it. An expired entry is removed when its identifier is next looked up, or by removeExpired(). Lookup and removal by identifier now take constant time. (#44)
    • Expiry.short, .medium and .long now state their exact durations: 60, 180 and 3600 seconds from the moment the item is set. (#43)
    • The type documentation no longer says a cache may be shared between components. Two caches over the same folder do not serialise their access to it. (#43)
    • QuickStart was rewritten against what the package does, and the test suite compiles and runs its snippets. The previous version's code did not compile as written. (#39)