Bodega

A simple store for all your basic needs, and a foundational data layer primitive for iOS and Mac apps. 🐱

MIT License

Stars
308
Committers
10

Bot releases are hidden (Show)

Bodega - Ambiguity Shambiguity Latest Release

Published by mergesort 4 months ago

This release adds #33, to resolve an ambiguous reference to Expression which was added to Foundation in iOS 18/macOS 15. Thank you @samalone for the help!

Bodega - Stay Safe Out There

Published by mergesort 9 months ago

In version 2.1.1 I had accidentally committed [.unsafeFlags(["-strict-concurrency=complete"])] into Bodegal's Package.swift. That prevented the library from compiling on production versions of Xcode, it is now removed, so finally we're all safe again.

Bodega - The Right Key To Unlock Your Door

Published by mergesort 12 months ago

This release improves Bodega's API with fixes (#27) and improvements (#25) courtesy of @dannynorth, thank you!

The changes are:

  • A new keysExist function that more efficiently searches for and filters existing keys in a StorageEngine.
  • Fixes the readDataAndKeys function to always return the correct data and keys. Previously if the readDataAndKeys function was provided an invalid key in the keys array incorrect values would be returned after the invalid key index.
Bodega - Hard To Pin Down

Published by mergesort about 1 year ago

This release fixes incompatibilities with other libraries (#16) by removing the explicit dependency on Version 0.13.3 of SQLite.swift. Bodega is now pinned to any version of SQLite.swift greater than 0.13.2, that way conflicts are less likely to emerge.

Bodega - Never Feel Empty Using Bodega Again

Published by mergesort about 2 years ago

This release includes a small fix to the SQLiteStorageEngine.

Previously this code would throw an error.

let noItems = [] // You can imagine some computation that ends up with an empty array rather than setting [] directly
storageEngine.write(noItems)

Now it will not throw an error, instead the write function will return early and act as a no-op, more accurately matching a user's expectations.

Bodega - Dependency Fix Release

Published by mergesort about 2 years ago

Ugh you know how sometimes you accidentally mess up a tag and it's not easily fixable? This version fixes a dependency issue for Boutique, nothing more, nothing less.

Bodega - Version 2.0: Bring Your Own Database

Published by mergesort about 2 years ago

Welcome to the future!

Bodega v2 allows you to extend Bodega far and wide by giving you the power to bring your own database to Bodega in one easy step. Out of the box Bodega v2 is 5-10x faster because the default database is now powered by the blazing fast SQLiteStorageEngine. But if SQLite or the DiskStorageEngine aren't your jam, you can build your own StorageEngine by conforming to a simple protocol. Using Bodega is still as simple as it ever was, requiring very little code, no special objects, with simple defaults out the box.

As a bonus if you do that it will automatically with any Boutique-powered app, providing you the same single source of truth, realtime updates, and a fully offline-capable app you've come to know and love, in only a couple of lines of code.


Warning
This version contains breaking changes


In the Version 1.x series of Bodega the DiskStorage type was responsible for persisting data to disk. ObjectStorage was a Codable layer built on top of DiskStorage, letting you work with Swift types rather than Data. Like the name implies DiskStorage was backed by the file system, but what if you don't want to save Data to disk? Saving data to disk is a simple and effective starting point, but can get slow when working with large data sets. One of Bodega's goals is to work with every app without causing developers to make tradeoffs, so version 2.0 is focused on eliminating those tradeoffs without ruining the streamlined simplicity Bodega brings.

In the spirit of not making tradeoffs here's how Bodega works with any database you want, say hello to the new StorageEngine protocol.

public protocol StorageEngine: Actor {
    func write(_ data: Data, key: CacheKey) async throws
    func write(_ dataAndKeys: [(key: CacheKey, data: Data)]) async throws

    func read(key: CacheKey) async -> Data?
    func read(keys: [CacheKey]) async -> [Data]
    func readDataAndKeys(keys: [CacheKey]) async -> [(key: CacheKey, data: Data)]
    func readAllData() async -> [Data]
    func readAllDataAndKeys() async -> [(key: CacheKey, data: Data)]

    func remove(key: CacheKey) async throws
    func remove(keys: [CacheKey]) async throws
    func removeAllData() async throws

    func keyExists(_ key: CacheKey) async -> Bool
    func keyCount() async -> Int
    func allKeys() async -> [CacheKey]

    func createdAt(key: CacheKey) async -> Date?
    func updatedAt(key: CacheKey) async -> Date?
}

By providing your own write, read, remove, key, and timestamp related functions, you can make any persistence layer compatible with ObjectStorage. Whether your app is backed by Realm, Core Data, or even CloudKit, when you create a new StorageEngine it automatically becomes usable by ObjectStorage, with one drop dead simple API.

The first StorageEngine to be implemented is an SQLiteStorageEngine, bundled with Bodega. I'll explain all the possibilities below, but first let's take a second to see how much faster your apps using Bodega and Boutique will be.

StorageEngine Read Performance
StorageEngine Write Performance


If it's not obvious, a SQLite foundation for Bodega is incredibly faster than using the file system. The DiskStorageStorageEngine is still available, but if you use the SQLiteStorageEngine loading 10,000 objects into memory will be more than 400% faster, and writing 5,000 objects is more than 500% faster. With this release I feel confident that you should be able to use Bodega and Boutique in the largest of apps, while counterintuitively becoming a more flexible framework.

Breaking

There's only one change to your code, ObjectStorage now has a type constraint. This isn't required for ObjectStorage but was an oversight in the 1.x versions, and a big version bump felt like the right time to fix this mistake.

Before you would initialize a new ObjectStorage like this.

let storage = ObjectStorage(
    directory: .documents(appendingPath: "Animals")
)

And now it looks like this, a small tweak!

// Change #1: Instead of a directory we now provide a StorageEngine
// Change #2: Add a type constraint of <Animal> to ObjectStorage
let storage = ObjectStorage<Animal>(
    storage: SQLiteStorageEngine(directory: .documents(appendingPath: "Animals"))!
)

And of course the same thing is true if you use DiskStorage or any StorageEngine you may build.


You can find Bodega's documentation here, including a lot of updates for the v2 release.

P.S. If you build something useful to others, by all means file a pull request so I can add it to Bodega!

Bodega - Version 2.0 RC 2: Bring Your Own Database

Published by mergesort about 2 years ago

If you'd like to see all of the v2 changes please consult the v2 RC 1 release notes.

RC 2 Changes

  • Bodega is now fully documented, available at build.ms/bodega/docs

  • StorageEngine functions have now been marked async. Method signatures of conforming types will have to be updated to add async, but there is be no change to functionality. The StorageEngine protocol has an actor conformance so the functions would already run async, this change just enforces that in the function signature.

  • SQLiteStorageEngine provides two new ways of creating an SQLiteStorageEngine, either by calling SQLiteStorageEngine.default or SQLiteStorageEngine.default(appendingPath: "Red Panda Store").

  • Adding retry logic to SQLiteStorage. This wouldn't be an issue in an app, but would crop up as an issue when running tests.

Bodega - Version 2.0 RC 1: Bring Your Own Database

Published by mergesort about 2 years ago

Welcome to the future! Bodega v2 allows you to extend Bodega far and wide by giving you the power to bring your own database to Bodega in one easy step. Out of the box Bodega v2 is 5-10x faster because the default database is now powered by the blazing fast SQLiteStorageEngine. But if SQLite or the DiskStorageEngine aren't your jam, you can build your own StorageEngine by conforming to a simple protocol. Using Bodega is still as simple as it ever was, taking very little code, and no special objects, with simple defaults.

As a bonus if you do that it will automatically with any Boutique-powered app, providing you the same single source of truth, realtime updates, and a fully offline-capable app you've come to know and love, in only a couple of lines of code.


Warning
This version contains breaking changes


In the Version 1.x series of Bodega the DiskStorage type was responsible for persisting data to disk. ObjectStorage was a Codable layer built on top of DiskStorage, letting you work with Swift types rather than Data. Like the name implies DiskStorage was backed by the file system, but what if you don't want to save Data to disk? Saving data to disk is a simple and effective starting point, but can get slow when working with large data sets. One of Bodega's goals is to work with every app without causing developers to make tradeoffs, so version 2.0 is focused on eliminating those tradeoffs without ruining the streamlined simplicity Bodega brings.

In the spirit of not making tradeoffs here's how Bodega works with any database you want, say hello to the new StorageEngine protocol.

public protocol StorageEngine: Actor {
    func write(_ data: Data, key: CacheKey) async throws
    func write(_ dataAndKeys: [(key: CacheKey, data: Data)]) async throws

    func read(key: CacheKey) async -> Data?
    func read(keys: [CacheKey]) async -> [Data]
    func readDataAndKeys(keys: [CacheKey]) async -> [(key: CacheKey, data: Data)]
    func readAllData() async -> [Data]
    func readAllDataAndKeys() async -> [(key: CacheKey, data: Data)]

    func remove(key: CacheKey) async throws
    func remove(keys: [CacheKey]) async throws
    func removeAllData() async throws

    func keyExists(_ key: CacheKey) async -> Bool
    func keyCount() async -> Int
    func allKeys() async -> [CacheKey]

    func createdAt(key: CacheKey) async -> Date?
    func updatedAt(key: CacheKey) async -> Date?
}

By providing your own write, read, remove, key, and timestamp related functions, you can make any persistence layer compatible with ObjectStorage. Whether your app is backed by Realm, Core Data, or even CloudKit, when you create a new StorageEngine it automatically becomes usable by ObjectStorage, with one drop dead simple API.

The first StorageEngine to be implemented is an SQLiteStorageEngine, bundled with Bodega. I'll explain all the possibilities below, but first let's take a second to see how much faster your apps using Bodega and Boutique will be.

StorageEngine Read Performance
StorageEngine Write Performance


If it's not obvious, a SQLite foundation for Bodega is incredibly faster than using the file system. The DiskStorageStorageEngine is still available, but if you use the SQLiteStorageEngine loading 10,000 objects into memory will be more than 400% faster, and writing 5,000 objects is more than 500% faster. With this release I feel confident that you should be able to use Bodega and Boutique in the largest of apps, while counterintuitively becoming a more flexible framework.

Breaking

There's only one change to your code, ObjectStorage now has a type constraint. This isn't required for ObjectStorage but was an oversight in the 1.x versions, and a big version bump felt like the right time to fix this mistake.

Before you would initialize a new ObjectStorage like this.

let storage = ObjectStorage(
    storage: SQLiteStorageEngine(directory: .documents(appendingPath: "Animals"))!
)

And now it looks like this, one small tweak!

let storage = ObjectStorage<Animal>(
    storage: SQLiteStorageEngine(directory: .documents(appendingPath: "Animals"))!
)

And of course the same thing is true if you use DiskStorage or any StorageEngine you may build.


P.S. If you build something useful to others, by all means file a pull request so I can add it to Bodega!

Bodega - A Direct(ory) Improvement

Published by mergesort about 2 years ago

Adding a public FileManager.Directory initializer

In 1.1.0 I added a fancy new type, FileManager.Directory.

public extension FileManager {

    struct Directory {
        public let url: URL
    }

}

But I forgot to add a public initializer, so that’s what this release is for… whoops. 😅

Bodega - Improvements & Changes, Some Breaking

Published by mergesort over 2 years ago

This release has many changes, and some of them are breaking. This is work that's important for Bodega (and Boutique), improving upon and fixing some assumptions I'd made earlier in the development lifecycle of Bodega, before there even was a Boutique.

Aside from improvements this version will lay the foundation for a version 2.0 of Bodega and Boutique, one that will offer some very significant and much-needed performance improvements. Having gone through the exercise of optimizing most anything that can provide a reasonable boost in performance, now 85% of the time Bodega spends on reading or writing is filesystem-based operations, many of which are ones I can't tune. To remedy this Bodega 2.0 will offer a database-powered variant of the underlying Storage (currently DiskStorage), while leaving ObjectStorage unchanged. This will mean that your code doesn't have to change, but may require data that you can't repopulate to be manually migrated.

You will have the ability to stay on 1.x versions of the library if you don't want to make any changes, but Bodega 2.0 will provide a new DatabaseStorage option that uses SQLite under the hood and provides 400% faster performance. This is especially useful for apps that have large data sets (10,000+ objects), as many production apps do, and will be the default storage option for Boutique 2.0.


Now that we know why these changes are being made, here are the changes in this pull request.

  • Removing the concept of subdirectories, and all of subdirectory parameters. The subdirectory is complicated, error-prone, and in practice doesn't have much use. When I first started working on Bodega I was using subdirectories to shard objects, but now you can easily create a new ObjectStorage or DiskStorage pointing to a subdirectory to replicate the functionality the subdirectory parameter offers. The benefit is a much simpler and clearer API, and removes much surface area for bugs such as this code.
let keys = store.allKeys(inSubdirectory: "subdirectory")
let objects = store.objects(forKeys: keys, inSubdirectory: "subdirectory")
// Returns 0 objects because you're actually querying folder/subdirectory/subdirectory, not folder/subdirectory as you may expect.
  • Removing .lastAccessed() from ObjectStorage. When ObjectStorage was guaranteed to have a DiskStorage under the hood we could call the underlying DiskStorage's version of this method to figure out when the object was last accessed. But going forward ObjectStorage is no longer guaranteed to use DiskStorage, for example as we use will use DatabaseStorage in the future. The method will still remain available on DiskStorage, with no changes to lastModified() or creationDate().

  • Adding applicationSupportDirectory() on the Mac. If you have suggestions for other useful directories please let me know.

  • Adding a new type, FileManager.Directory, to provide a type-safe replacement for the folders in DiskStorage+Directories. The initializer for ObjectStorage or DiskStorage now looks like init(directory: Directory) rather than init(storagePath: URL), which allows for shorter, type-safe, and file-system safe initializers such as DiskStorage(directory: .documents(appendingPath: "Notes")).

Bodega - New Functions For All!

Published by mergesort over 2 years ago

  • Adding three new functions to DiskStorage and ObjectStorage, createdAt(forKey: CacheKey), lastAccessed(forKey: CacheKey), and lastModified(forKey: CacheKey).

These are useful methods for many use cases such as sorting an array of objects from oldest to newest, by most recently updated, and separately can be useful to derive information for your own purposes such as the last time a certain file was accessed. Thank you to @samalone for coming up with the idea and contributing the majority of the implementation.

  • CacheKey now conforms to Codable and Equatable, allowing it to be used in other Codable and Equatable types.
Bodega - Let's go shopping! 🛍️

Published by mergesort over 2 years ago

Bodega - CacheKey rules everything around me

Published by mergesort over 2 years ago

CacheKey Changes

  • Adding a CacheKey.init(verbatim:) initializer to make initializing an unsafe cache key explicit.
  • Similar to CacheKey.init(url:), CacheKey.init(_:) now hashes the String that's passed into the initializer.
  • Removing the ability to initialize a CacheKey from a String literal due to a compiler issue.

DiskStorage Changes

  • Removing a non-existent item from disk is now a no-op instead of throwing an error
Bodega - Getting Real Close…

Published by mergesort over 2 years ago

Coming soon, grab a snack in the mean time. 🍿