diff --git a/.changeset/eight-poems-buy.md b/.changeset/eight-poems-buy.md new file mode 100644 index 0000000000..1d0e8f6079 --- /dev/null +++ b/.changeset/eight-poems-buy.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": patch +--- + +add binary support to KeyValueStore diff --git a/packages/platform/src/KeyValueStore.ts b/packages/platform/src/KeyValueStore.ts index e766a8913c..113bbcdec0 100644 --- a/packages/platform/src/KeyValueStore.ts +++ b/packages/platform/src/KeyValueStore.ts @@ -36,10 +36,15 @@ export interface KeyValueStore { */ readonly get: (key: string) => Effect.Effect, PlatformError.PlatformError> + /** + * Returns the value of the specified key if it exists. + */ + readonly getUint8Array: (key: string) => Effect.Effect, PlatformError.PlatformError> + /** * Sets the value of the specified key. */ - readonly set: (key: string, value: string) => Effect.Effect + readonly set: (key: string, value: string | Uint8Array) => Effect.Effect /** * Removes the specified key. @@ -64,6 +69,14 @@ export interface KeyValueStore { f: (value: string) => string ) => Effect.Effect, PlatformError.PlatformError> + /** + * Updates the value of the specified key if it exists. + */ + readonly modifyUint8Array: ( + key: string, + f: (value: Uint8Array) => Uint8Array + ) => Effect.Effect, PlatformError.PlatformError> + /** * Returns true if the KeyValueStore contains the specified key. */ @@ -104,6 +117,16 @@ export const make: ( impl: Omit & Partial ) => KeyValueStore = internal.make +/** + * @since 1.0.0 + * @category constructors + */ +export const makeStringOnly: ( + impl: Pick & Partial> & { + readonly set: (key: string, value: string) => Effect.Effect + } +) => KeyValueStore = internal.makeStringOnly + /** * @since 1.0.0 * @category combinators diff --git a/packages/platform/src/internal/keyValueStore.ts b/packages/platform/src/internal/keyValueStore.ts index 2112fe992d..244e43fc79 100644 --- a/packages/platform/src/internal/keyValueStore.ts +++ b/packages/platform/src/internal/keyValueStore.ts @@ -1,8 +1,10 @@ import * as Schema from "@effect/schema/Schema" import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import * as Either from "effect/Either" +import * as Encoding from "effect/Encoding" import type { LazyArg } from "effect/Function" -import { dual, pipe } from "effect/Function" +import { dual, identity, pipe } from "effect/Function" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as PlatformError from "../Error.js" @@ -21,7 +23,10 @@ export const keyValueStoreTag = Context.GenericTag( /** @internal */ export const make: ( impl: - & Omit + & Omit< + KeyValueStore.KeyValueStore, + KeyValueStore.TypeId | "has" | "modify" | "modifyUint8Array" | "isEmpty" | "forSchema" + > & Partial ) => KeyValueStore.KeyValueStore = (impl) => keyValueStoreTag.of({ @@ -42,12 +47,55 @@ export const make: ( ) } ), + modifyUint8Array: (key, f) => + Effect.flatMap( + impl.getUint8Array(key), + (o) => { + if (Option.isNone(o)) { + return Effect.succeedNone + } + const newValue = f(o.value) + return Effect.as( + impl.set(key, newValue), + Option.some(newValue) + ) + } + ), forSchema(schema) { return makeSchemaStore(this, schema) }, ...impl }) +/** @internal */ +export const makeStringOnly: ( + impl: + & Pick< + KeyValueStore.KeyValueStore, + "get" | "remove" | "clear" | "size" + > + & Partial> + & { readonly set: (key: string, value: string) => Effect.Effect } +) => KeyValueStore.KeyValueStore = (impl) => { + const encoder = new TextEncoder() + return make({ + ...impl, + getUint8Array: (key) => + impl.get(key).pipe( + Effect.map(Option.map((value) => + Either.match(Encoding.decodeBase64(value), { + onLeft: () => encoder.encode(value), + onRight: identity + }) + )) + ), + set: (key, value) => + typeof value === "string" + ? impl.set(key, value) + : Effect.suspend(() => impl.set(key, Encoding.encodeBase64(value))) + }) +} + /** @internal */ export const prefix = dual< (prefix: string) => (self: S) => S, @@ -119,11 +167,23 @@ const makeSchemaStore = ( /** @internal */ export const layerMemory = Layer.sync(keyValueStoreTag, () => { - const store = new Map() + const store = new Map() + const encoder = new TextEncoder() return make({ - get: (key: string) => Effect.sync(() => Option.fromNullable(store.get(key))), - set: (key: string, value: string) => Effect.sync(() => store.set(key, value)), + get: (key: string) => + Effect.sync(() => + Option.fromNullable(store.get(key)).pipe( + Option.map((value) => typeof value === "string" ? value : Encoding.encodeBase64(value)) + ) + ), + getUint8Array: (key: string) => + Effect.sync(() => + Option.fromNullable(store.get(key)).pipe( + Option.map((value) => typeof value === "string" ? encoder.encode(value) : value) + ) + ), + set: (key: string, value: string | Uint8Array) => Effect.sync(() => store.set(key, value)), remove: (key: string) => Effect.sync(() => store.delete(key)), clear: Effect.sync(() => store.clear()), size: Effect.sync(() => store.size) @@ -152,7 +212,16 @@ export const layerFileSystem = (directory: string) => (sysError) => sysError.reason === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(sysError) ) ), - set: (key: string, value: string) => fs.writeFileString(keyPath(key), value), + getUint8Array: (key: string) => + pipe( + Effect.map(fs.readFile(keyPath(key)), Option.some), + Effect.catchTag( + "SystemError", + (sysError) => sysError.reason === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(sysError) + ) + ), + set: (key: string, value: string | Uint8Array) => + typeof value === "string" ? fs.writeFileString(keyPath(key), value) : fs.writeFile(keyPath(key), value), remove: (key: string) => fs.remove(keyPath(key)), has: (key: string) => fs.exists(keyPath(key)), clear: Effect.zipRight( @@ -189,7 +258,7 @@ const storageError = (props: Omit[0 export const layerStorage = (evaluate: LazyArg) => Layer.sync(keyValueStoreTag, () => { const storage = evaluate() - return make({ + return makeStringOnly({ get: (key: string) => Effect.try({ try: () => Option.fromNullable(storage.getItem(key)),