| /** | |
| * Provides effectful key/value storage for persistence backends. | |
| * | |
| * `KeyValueStore` is a service for storing string or binary values by key. It | |
| * is useful for lightweight durable state, browser storage, local files, SQL | |
| * tables, tests, and as a storage building block for higher-level persistence | |
| * APIs. This module includes store operations, prefixed views, schema-aware JSON | |
| * storage, error values, and layers for memory, filesystem, Web Storage, and | |
| * SQL-backed stores. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Context from "../../Context.js"; | |
| import * as Data from "../../Data.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Encoding from "../../Encoding.js"; | |
| import * as FileSystem from "../../FileSystem.js"; | |
| import { dual, identity } from "../../Function.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import * as Path from "../../Path.js"; | |
| import * as Predicate from "../../Predicate.js"; | |
| import * as Result from "../../Result.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as UndefinedOr from "../../UndefinedOr.js"; | |
| import * as SqlClient from "../sql/SqlClient.js"; | |
| const TypeId = "~effect/persistence/KeyValueStore"; | |
| const ErrorTypeId = "~effect/persistence/KeyValueStore/KeyValueStoreError"; | |
| /** | |
| * Error raised by key/value store operations, including the failed method, | |
| * optional key, message, and cause. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class KeyValueStoreError extends /*#__PURE__*/Data.TaggedError("KeyValueStoreError") { | |
| /** | |
| * Marks this value as a key-value store error for runtime guards. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| [ErrorTypeId] = ErrorTypeId; | |
| } | |
| /** | |
| * Service tag for string and binary key/value storage. | |
| * | |
| * **When to use** | |
| * | |
| * Use to access or provide the persistence store used for lightweight durable | |
| * state. | |
| * | |
| * @category services | |
| * @since 4.0.0 | |
| */ | |
| export const KeyValueStore = /*#__PURE__*/Context.Service("effect/persistence/KeyValueStore"); | |
| /** | |
| * Constructs a `KeyValueStore` from primitive store operations. | |
| * | |
| * **Details** | |
| * | |
| * Default implementations are derived for `has`, `isEmpty`, `modify`, and | |
| * `modifyUint8Array` unless they are provided in the options. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const make = options => KeyValueStore.of({ | |
| [TypeId]: TypeId, | |
| has: key => Effect.map(options.get(key), Predicate.isNotUndefined), | |
| isEmpty: Effect.map(options.size, size => size === 0), | |
| modify: (key, f) => Effect.flatMap(options.get(key), o => { | |
| if (o === undefined) { | |
| return Effect.undefined; | |
| } | |
| const newValue = f(o); | |
| return Effect.as(options.set(key, newValue), newValue); | |
| }), | |
| modifyUint8Array: (key, f) => Effect.flatMap(options.getUint8Array(key), o => { | |
| if (o === undefined) { | |
| return Effect.undefined; | |
| } | |
| const newValue = f(o); | |
| return Effect.as(options.set(key, newValue), newValue); | |
| }), | |
| ...options | |
| }); | |
| /** | |
| * Adapts a string-only backing store into a `KeyValueStore`. | |
| * | |
| * **Details** | |
| * | |
| * `Uint8Array` values are stored as base64 strings. `getUint8Array` decodes | |
| * base64 values and falls back to UTF-8 encoding for non-base64 strings. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeStringOnly = options => { | |
| const encoder = new TextEncoder(); | |
| return make({ | |
| ...options, | |
| getUint8Array: key => options.get(key).pipe(Effect.map(UndefinedOr.map(value => Result.match(Encoding.decodeBase64(value), { | |
| onFailure: () => encoder.encode(value), | |
| onSuccess: identity | |
| })))), | |
| set: (key, value) => typeof value === "string" ? options.set(key, value) : Effect.suspend(() => options.set(key, Encoding.encodeBase64(value))) | |
| }); | |
| }; | |
| /** | |
| * Returns a view of a `KeyValueStore` that prepends the given prefix to every | |
| * key. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const prefix = /*#__PURE__*/dual(2, (self, prefix) => ({ | |
| ...self, | |
| get: key => self.get(`${prefix}${key}`), | |
| getUint8Array: key => self.getUint8Array(`${prefix}${key}`), | |
| set: (key, value) => self.set(`${prefix}${key}`, value), | |
| remove: key => self.remove(`${prefix}${key}`), | |
| has: key => self.has(`${prefix}${key}`), | |
| modify: (key, f) => self.modify(`${prefix}${key}`, f), | |
| modifyUint8Array: (key, f) => self.modifyUint8Array(`${prefix}${key}`, f) | |
| })); | |
| /** | |
| * Provides a process-local in-memory `KeyValueStore` backed by a `Map`. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerMemory = /*#__PURE__*/Layer.sync(KeyValueStore)(() => { | |
| const store = new Map(); | |
| const encoder = new TextEncoder(); | |
| return make({ | |
| get: key => Effect.sync(() => { | |
| const value = store.get(key); | |
| return value === undefined ? undefined : typeof value === "string" ? value : Encoding.encodeBase64(value); | |
| }), | |
| getUint8Array: key => Effect.sync(() => { | |
| const value = store.get(key); | |
| return value === undefined ? undefined : typeof value === "string" ? encoder.encode(value) : value; | |
| }), | |
| set: (key, value) => Effect.sync(() => store.set(key, value)), | |
| remove: key => Effect.sync(() => store.delete(key)), | |
| clear: Effect.sync(() => store.clear()), | |
| size: Effect.sync(() => store.size) | |
| }); | |
| }); | |
| /** | |
| * Provides a `KeyValueStore` backed by files in the specified directory. | |
| * | |
| * **Details** | |
| * | |
| * The directory is created if needed, and each key is encoded as a file name. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerFileSystem = directory => Layer.effect(KeyValueStore)(Effect.gen(function* () { | |
| const fs = yield* FileSystem.FileSystem; | |
| const path = yield* Path.Path; | |
| const keyPath = key => path.join(directory, encodeURIComponent(key)); | |
| if (!(yield* fs.exists(directory))) { | |
| yield* fs.makeDirectory(directory, { | |
| recursive: true | |
| }); | |
| } | |
| return make({ | |
| get: key => Effect.catchTag(fs.readFileString(keyPath(key)), "PlatformError", cause => cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail(new KeyValueStoreError({ | |
| method: "get", | |
| key, | |
| message: `Unable to get item with key ${key}`, | |
| cause | |
| }))), | |
| getUint8Array: key => Effect.catchTag(fs.readFile(keyPath(key)), "PlatformError", cause => cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail(new KeyValueStoreError({ | |
| method: "getUint8Array", | |
| key, | |
| message: `Unable to get item with key ${key}`, | |
| cause | |
| }))), | |
| set: (key, value) => Effect.mapError(typeof value === "string" ? fs.writeFileString(keyPath(key), value) : fs.writeFile(keyPath(key), value), cause => new KeyValueStoreError({ | |
| method: "set", | |
| key, | |
| message: `Unable to set item with key ${key}`, | |
| cause | |
| })), | |
| remove: key => Effect.mapError(fs.remove(keyPath(key)), cause => new KeyValueStoreError({ | |
| method: "remove", | |
| key, | |
| message: `Unable to remove item with key ${key}`, | |
| cause | |
| })), | |
| has: key => Effect.mapError(fs.exists(keyPath(key)), cause => new KeyValueStoreError({ | |
| method: "has", | |
| key, | |
| message: `Unable to check existence of item with key ${key}`, | |
| cause | |
| })), | |
| clear: Effect.mapError(Effect.andThen(fs.remove(directory, { | |
| recursive: true | |
| }), fs.makeDirectory(directory, { | |
| recursive: true | |
| })), cause => new KeyValueStoreError({ | |
| method: "clear", | |
| message: `Unable to clear storage`, | |
| cause | |
| })), | |
| size: Effect.matchEffect(fs.readDirectory(directory), { | |
| onSuccess: files => Effect.succeed(files.length), | |
| onFailure: cause => Effect.fail(new KeyValueStoreError({ | |
| method: "size", | |
| message: `Unable to get size`, | |
| cause | |
| })) | |
| }) | |
| }); | |
| })); | |
| /** | |
| * Provides a SQL-backed `KeyValueStore`. | |
| * | |
| * **Details** | |
| * | |
| * The layer creates the configured table if it does not exist and stores both | |
| * string and binary values through the current `SqlClient`. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerSql = (options = {}) => Layer.effect(KeyValueStore)(Effect.gen(function* () { | |
| const sql = (yield* SqlClient.SqlClient).withoutTransforms(); | |
| const table = sql(options.table ?? "effect_key_value_store"); | |
| yield* sql.onDialectOrElse({ | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| id VARCHAR(191) PRIMARY KEY, | |
| value BLOB NOT NULL, | |
| value_type SMALLINT NOT NULL | |
| ) | |
| `, | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| id TEXT PRIMARY KEY, | |
| value BYTEA NOT NULL, | |
| value_type SMALLINT NOT NULL | |
| ) | |
| `, | |
| mssql: () => sql` | |
| IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${table} AND xtype='U') | |
| CREATE TABLE ${table} ( | |
| id NVARCHAR(450) PRIMARY KEY, | |
| value VARBINARY(MAX) NOT NULL, | |
| value_type SMALLINT NOT NULL | |
| ) | |
| `, | |
| // sqlite | |
| orElse: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| id TEXT PRIMARY KEY, | |
| value BLOB NOT NULL, | |
| value_type INTEGER NOT NULL | |
| ) | |
| ` | |
| }).pipe(Effect.orDie); | |
| const upsert = sql.onDialectOrElse({ | |
| pg: () => entry => sql` | |
| INSERT INTO ${table} (id, value, value_type) VALUES (${entry.id}, ${entry.value}, ${entry.value_type}) | |
| ON CONFLICT (id) DO UPDATE SET value=EXCLUDED.value, value_type=EXCLUDED.value_type | |
| `.unprepared, | |
| mysql: () => entry => sql` | |
| INSERT INTO ${table} (id, value, value_type) VALUES (${entry.id}, ${entry.value}, ${entry.value_type}) | |
| ON DUPLICATE KEY UPDATE value=VALUES(value), value_type=VALUES(value_type) | |
| `, | |
| mssql: () => entry => sql` | |
| MERGE ${table} AS target | |
| USING (SELECT ${entry.id} AS id, ${entry.value} AS value, ${entry.value_type} AS value_type) AS source | |
| ON target.id = source.id | |
| WHEN MATCHED THEN UPDATE SET value = source.value, value_type = source.value_type | |
| WHEN NOT MATCHED THEN INSERT (id, value, value_type) | |
| VALUES (source.id, source.value, source.value_type); | |
| `, | |
| // sqlite | |
| orElse: () => entry => sql` | |
| INSERT INTO ${table} (id, value, value_type) VALUES (${entry.id}, ${entry.value}, ${entry.value_type}) | |
| ON CONFLICT(id) DO UPDATE SET value=excluded.value, value_type=excluded.value_type | |
| `.unprepared | |
| }); | |
| const encoder = new TextEncoder(); | |
| const decoder = new TextDecoder(); | |
| const ValueTypeString = 0; | |
| const ValueTypeUint8Array = 1; | |
| return make({ | |
| get: key => sql`SELECT value, value_type FROM ${table} WHERE id = ${key}`.pipe(Effect.mapError(cause => new KeyValueStoreError({ | |
| method: "get", | |
| key, | |
| message: `Unable to get item with key ${key}`, | |
| cause | |
| })), Effect.flatMap(rows => { | |
| if (rows.length === 0) { | |
| return Effect.undefined; | |
| } | |
| const row = rows[0]; | |
| switch (row.value_type) { | |
| case ValueTypeString: | |
| return Effect.succeed(decoder.decode(row.value)); | |
| case ValueTypeUint8Array: | |
| return Effect.succeed(Encoding.encodeBase64(row.value)); | |
| default: | |
| return Effect.fail(new KeyValueStoreError({ | |
| method: "get", | |
| key, | |
| message: `Invalid stored value type for key ${key}: ${row.value_type}` | |
| })); | |
| } | |
| })), | |
| getUint8Array: key => sql`SELECT value, value_type FROM ${table} WHERE id = ${key}`.pipe(Effect.mapError(cause => new KeyValueStoreError({ | |
| method: "getUint8Array", | |
| key, | |
| message: `Unable to get item with key ${key}`, | |
| cause | |
| })), Effect.flatMap(rows => { | |
| if (rows.length === 0) { | |
| return Effect.undefined; | |
| } | |
| const row = rows[0]; | |
| switch (row.value_type) { | |
| case ValueTypeString: | |
| return Effect.succeed(row.value); | |
| case ValueTypeUint8Array: | |
| return Effect.succeed(row.value); | |
| default: | |
| return Effect.fail(new KeyValueStoreError({ | |
| method: "getUint8Array", | |
| key, | |
| message: `Invalid stored value type for key ${key}: ${row.value_type}` | |
| })); | |
| } | |
| })), | |
| set: (key, value) => upsert({ | |
| id: key, | |
| value: typeof value === "string" ? encoder.encode(value) : value, | |
| value_type: typeof value === "string" ? ValueTypeString : ValueTypeUint8Array | |
| }).pipe(Effect.mapError(cause => new KeyValueStoreError({ | |
| method: "set", | |
| key, | |
| message: `Unable to set item with key ${key}`, | |
| cause | |
| })), Effect.asVoid), | |
| remove: key => sql`DELETE FROM ${table} WHERE id = ${key}`.pipe(Effect.mapError(cause => new KeyValueStoreError({ | |
| method: "remove", | |
| key, | |
| message: `Unable to remove item with key ${key}`, | |
| cause | |
| })), Effect.asVoid), | |
| clear: sql`DELETE FROM ${table}`.pipe(Effect.mapError(cause => new KeyValueStoreError({ | |
| method: "clear", | |
| message: `Unable to clear storage`, | |
| cause | |
| })), Effect.asVoid), | |
| size: sql`SELECT COUNT(*) as count FROM ${table}`.pipe(Effect.mapError(cause => new KeyValueStoreError({ | |
| method: "size", | |
| message: `Unable to get size`, | |
| cause | |
| })), Effect.map(rows => rows.length === 0 ? 0 : Number(rows[0].count))) | |
| }); | |
| })); | |
| const SchemaStoreTypeId = "~effect/persistence/KeyValueStore/SchemaStore"; | |
| /** | |
| * Adapts a `KeyValueStore` into a `SchemaStore` using the schema's JSON codec. | |
| * | |
| * @category SchemaStore | |
| * @since 4.0.0 | |
| */ | |
| export const toSchemaStore = (self, schema) => { | |
| const serializer = Schema.toCodecJson(schema); | |
| const jsonSchema = Schema.fromJsonString(serializer); | |
| const decode = Schema.decodeEffect(jsonSchema); | |
| const encode = Schema.encodeEffect(jsonSchema); | |
| const get = key => Effect.flatMap(self.get(key), UndefinedOr.match({ | |
| onUndefined: () => Effect.succeedNone, | |
| onDefined: value => Effect.asSome(decode(value)) | |
| })); | |
| const set = (key, value) => Effect.flatMap(encode(value), json => self.set(key, json)); | |
| const modify = (key, f) => Effect.flatMap(get(key), o => { | |
| if (Option.isNone(o)) { | |
| return Effect.succeedNone; | |
| } | |
| const newValue = f(o.value); | |
| return Effect.as(set(key, newValue), Option.some(newValue)); | |
| }); | |
| return { | |
| [SchemaStoreTypeId]: SchemaStoreTypeId, | |
| get, | |
| set, | |
| modify, | |
| remove: self.remove, | |
| clear: self.clear, | |
| size: self.size, | |
| has: self.has, | |
| isEmpty: self.isEmpty | |
| }; | |
| }; | |
| /** | |
| * Provides a `KeyValueStore` backed by a Web `Storage` instance such as | |
| * `localStorage` or `sessionStorage`. | |
| * | |
| * **Details** | |
| * | |
| * This layer uses the Web Storage API: | |
| * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerStorage = evaluate => Layer.sync(KeyValueStore)(() => { | |
| const storage = evaluate(); | |
| return makeStringOnly({ | |
| get: key => Effect.try({ | |
| try: () => storage.getItem(key) ?? undefined, | |
| catch: cause => new KeyValueStoreError({ | |
| key, | |
| method: "get", | |
| message: `Unable to get item with key ${key}`, | |
| cause | |
| }) | |
| }), | |
| set: (key, value) => Effect.try({ | |
| try: () => storage.setItem(key, value), | |
| catch: cause => new KeyValueStoreError({ | |
| key, | |
| method: "set", | |
| message: `Unable to set item with key ${key}`, | |
| cause | |
| }) | |
| }), | |
| remove: key => Effect.try({ | |
| try: () => storage.removeItem(key), | |
| catch: cause => new KeyValueStoreError({ | |
| key, | |
| method: "remove", | |
| message: `Unable to remove item with key ${key}`, | |
| cause | |
| }) | |
| }), | |
| clear: Effect.try({ | |
| try: () => storage.clear(), | |
| catch: cause => new KeyValueStoreError({ | |
| method: "clear", | |
| message: `Unable to clear storage`, | |
| cause | |
| }) | |
| }), | |
| size: Effect.try({ | |
| try: () => storage.length, | |
| catch: cause => new KeyValueStoreError({ | |
| method: "size", | |
| message: `Unable to get size`, | |
| cause | |
| }) | |
| }) | |
| }); | |
| }); | |
| //# sourceMappingURL=KeyValueStore.js.map |
Xet Storage Details
- Size:
- 16 kB
- Xet hash:
- 163b2108b47161a4c7f25c67a3dc0af8d2d98c0aef93a0421f74d1d0ef371a3c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.