| /** | |
| * Stores encoded results for `Persistable` requests. | |
| * | |
| * The `Persistence` service creates scoped stores keyed by each request's | |
| * `PrimaryKey`. Stores read and write schema-encoded `Exit` values with | |
| * optional TTLs, letting request workflows reuse expensive or idempotent results | |
| * across fibers, process restarts, or workers that share a backing store. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "../../Array.js"; | |
| import * as Clock from "../../Clock.js"; | |
| import * as Context from "../../Context.js"; | |
| import * as Duration from "../../Duration.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Exit from "../../Exit.js"; | |
| import { identity } from "../../Function.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as PrimaryKey from "../../PrimaryKey.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as SqlClient from "../sql/SqlClient.js"; | |
| import * as KeyValueStore from "./KeyValueStore.js"; | |
| import * as Persistable from "./Persistable.js"; | |
| import * as Redis from "./Redis.js"; | |
| const ErrorTypeId = "~effect/persistence/Persistence/PersistenceError"; | |
| /** | |
| * Error raised by persistence and backing-store operations. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class PersistenceError extends /*#__PURE__*/Schema.ErrorClass(ErrorTypeId)({ | |
| _tag: /*#__PURE__*/Schema.tag("PersistenceError"), | |
| message: Schema.String, | |
| cause: /*#__PURE__*/Schema.optional(/*#__PURE__*/Schema.Defect()) | |
| }) { | |
| /** | |
| * Marks this value as a persistence error for runtime guards. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| [ErrorTypeId] = ErrorTypeId; | |
| } | |
| /** | |
| * Service for creating scoped stores of persisted `Persistable` request | |
| * results. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export class Persistence extends /*#__PURE__*/Context.Service()("effect/persistence/Persistence") {} | |
| /** | |
| * Service for creating raw backing stores for persistence store ids. | |
| * | |
| * @category BackingPersistence | |
| * @since 4.0.0 | |
| */ | |
| export class BackingPersistence extends /*#__PURE__*/Context.Service()("effect/persistence/BackingPersistence") {} | |
| /** | |
| * Provides `Persistence` from `BackingPersistence`. | |
| * | |
| * **Details** | |
| * | |
| * The layer serializes and deserializes `Persistable` exits, applies | |
| * per-entry TTLs, and skips writes whose TTL is zero or negative. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layer = /*#__PURE__*/Layer.effect(Persistence)(/*#__PURE__*/Effect.gen(function* () { | |
| const backing = yield* BackingPersistence; | |
| const scope = yield* Effect.scope; | |
| return Persistence.of({ | |
| make: Effect.fnUntraced(function* (options) { | |
| const storage = yield* backing.make(options.storeId); | |
| const timeToLive = options.timeToLive ?? (() => Duration.infinity); | |
| return identity({ | |
| get: key => Effect.flatMap(storage.get(PrimaryKey.value(key)), result => result ? Persistable.deserializeExit(key, result) : Effect.undefined), | |
| getMany: Effect.fnUntraced(function* (keys) { | |
| const primaryKeys = Arr.empty(); | |
| const persistables = Arr.empty(); | |
| for (const key of keys) { | |
| primaryKeys.push(PrimaryKey.value(key)); | |
| persistables.push(key); | |
| } | |
| if (!Arr.isArrayNonEmpty(primaryKeys)) return []; | |
| const results = yield* storage.getMany(primaryKeys); | |
| if (results.length !== primaryKeys.length) { | |
| return yield* new PersistenceError({ | |
| message: `Expected ${primaryKeys.length} results but got ${results.length} from backing store` | |
| }); | |
| } | |
| const out = new Array(primaryKeys.length); | |
| let toRemove; | |
| for (let i = 0; i < results.length; i++) { | |
| const key = persistables[i]; | |
| const result = results[i]; | |
| if (result === undefined) { | |
| out[i] = undefined; | |
| continue; | |
| } | |
| const eff = Persistable.deserializeExit(key, result); | |
| const exit = Exit.isExit(eff) ? eff : yield* Effect.exit(eff); | |
| if (Exit.isFailure(exit)) { | |
| toRemove ??= []; | |
| toRemove.push(PrimaryKey.value(key)); | |
| out[i] = undefined; | |
| continue; | |
| } | |
| out[i] = exit.value; | |
| } | |
| if (toRemove) { | |
| for (let i = 0; i < toRemove.length; i++) { | |
| yield* Effect.forkIn(storage.remove(toRemove[i]), scope); | |
| } | |
| } | |
| return out; | |
| }), | |
| set(key, value) { | |
| const ttl = Duration.fromInputUnsafe(timeToLive(value, key)); | |
| if (Duration.isZero(ttl) || Duration.isNegative(ttl)) return Effect.void; | |
| return Persistable.serializeExit(key, value).pipe(Effect.flatMap(encoded => storage.set(PrimaryKey.value(key), encoded, Duration.isFinite(ttl) ? ttl : undefined))); | |
| }, | |
| setMany: Effect.fnUntraced(function* (entries) { | |
| const encodedEntries = Arr.empty(); | |
| for (const [key, value] of entries) { | |
| const ttl = Duration.fromInputUnsafe(timeToLive(value, key)); | |
| if (Duration.isZero(ttl) || Duration.isNegative(ttl)) continue; | |
| const encoded = Persistable.serializeExit(key, value); | |
| const exit = Exit.isExit(encoded) ? encoded : yield* Effect.exit(encoded); | |
| if (Exit.isFailure(exit)) { | |
| return yield* exit; | |
| } | |
| encodedEntries.push([PrimaryKey.value(key), exit.value, Duration.isFinite(ttl) ? ttl : undefined]); | |
| } | |
| if (!Arr.isArrayNonEmpty(encodedEntries)) return; | |
| return yield* storage.setMany(encodedEntries); | |
| }), | |
| remove: key => storage.remove(PrimaryKey.value(key)), | |
| clear: storage.clear | |
| }); | |
| }) | |
| }); | |
| })); | |
| /** | |
| * Provides an in-memory `BackingPersistence` grouped by store id. | |
| * | |
| * **Details** | |
| * | |
| * Entries are process-local and expire according to their stored TTL. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerBackingMemory = /*#__PURE__*/Layer.sync(BackingPersistence)(() => { | |
| const stores = new Map(); | |
| const getStore = storeId => { | |
| let store = stores.get(storeId); | |
| if (store === undefined) { | |
| store = new Map(); | |
| stores.set(storeId, store); | |
| } | |
| return store; | |
| }; | |
| return BackingPersistence.of({ | |
| make: storeId => Effect.clockWith(clock => { | |
| const map = getStore(storeId); | |
| const unsafeGet = key => { | |
| const value = map.get(key); | |
| if (value === undefined) { | |
| return undefined; | |
| } else if (value[1] !== null && value[1] <= clock.currentTimeMillisUnsafe()) { | |
| map.delete(key); | |
| return undefined; | |
| } | |
| return value[0]; | |
| }; | |
| return Effect.succeed({ | |
| get: key => Effect.sync(() => unsafeGet(key)), | |
| getMany: keys => Effect.sync(() => Arr.map(keys, unsafeGet)), | |
| set: (key, value, ttl) => Effect.sync(() => map.set(key, [value, unsafeTtlToExpires(clock, ttl)])), | |
| setMany: entries => Effect.sync(() => { | |
| for (const [key, value, ttl] of entries) { | |
| map.set(key, [value, unsafeTtlToExpires(clock, ttl)]); | |
| } | |
| }), | |
| remove: key => Effect.sync(() => map.delete(key)), | |
| clear: Effect.sync(() => map.clear()) | |
| }); | |
| }) | |
| }); | |
| }); | |
| /** | |
| * Provides SQL-backed persistence using one table per store id. | |
| * | |
| * **Details** | |
| * | |
| * Each table is created if needed and stores JSON-encoded values with optional | |
| * expiration timestamps. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerBackingSqlMultiTable = /*#__PURE__*/Layer.effect(BackingPersistence)(/*#__PURE__*/Effect.gen(function* () { | |
| const sql = (yield* SqlClient.SqlClient).withoutTransforms(); | |
| return BackingPersistence.of({ | |
| make: Effect.fnUntraced(function* (storeId) { | |
| const clock = yield* Clock.Clock; | |
| const table = sql(`effect_persistence_${storeId}`); | |
| yield* sql.onDialectOrElse({ | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| id VARCHAR(191) PRIMARY KEY, | |
| value TEXT NOT NULL, | |
| expires BIGINT | |
| ) | |
| `, | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| id TEXT PRIMARY KEY, | |
| value TEXT NOT NULL, | |
| expires BIGINT | |
| ) | |
| `, | |
| mssql: () => sql` | |
| IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${table} AND xtype='U') | |
| CREATE TABLE ${table} ( | |
| id NVARCHAR(450) PRIMARY KEY, | |
| value NVARCHAR(MAX) NOT NULL, | |
| expires BIGINT | |
| ) | |
| `, | |
| // sqlite | |
| orElse: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| id TEXT PRIMARY KEY, | |
| value TEXT NOT NULL, | |
| expires INTEGER | |
| ) | |
| ` | |
| }).pipe(Effect.orDie); | |
| // Cleanup expired entries on startup | |
| yield* Effect.ignore(sql`DELETE FROM ${table} WHERE expires IS NOT NULL AND expires <= ${clock.currentTimeMillisUnsafe()}`); | |
| const upsert = sql.onDialectOrElse({ | |
| pg: () => entries => sql` | |
| INSERT INTO ${table} ${sql.insert(entries)} | |
| ON CONFLICT (id) DO UPDATE SET value=EXCLUDED.value, expires=EXCLUDED.expires | |
| `.unprepared, | |
| mysql: () => entries => sql` | |
| INSERT INTO ${table} ${sql.insert(entries)} | |
| ON DUPLICATE KEY UPDATE value=VALUES(value), expires=VALUES(expires) | |
| `.unprepared, | |
| // sqlite | |
| orElse: () => entries => sql` | |
| INSERT INTO ${table} ${sql.insert(entries)} | |
| ON CONFLICT(id) DO UPDATE SET value=excluded.value, expires=excluded.expires | |
| `.unprepared | |
| }); | |
| const wrapString = sql.onDialectOrElse({ | |
| mssql: () => s => `N'${s}'`, | |
| orElse: () => s => `'${s}'` | |
| }); | |
| return identity({ | |
| get: key => sql`SELECT value FROM ${table} WHERE id = ${key} AND (expires IS NULL OR expires > ${clock.currentTimeMillisUnsafe()})`.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to get key ${key} from backing store`, | |
| cause | |
| })), Effect.flatMap(rows => { | |
| if (rows.length === 0) { | |
| return Effect.undefined; | |
| } | |
| try { | |
| return Effect.succeed(JSON.parse(rows[0].value)); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to parse value for key ${key} from backing store`, | |
| cause | |
| })); | |
| } | |
| })), | |
| getMany: keys => sql`SELECT id, value FROM ${table} WHERE id IN (${sql.literal(keys.map(wrapString).join(", "))}) AND (expires IS NULL OR expires > ${clock.currentTimeMillisUnsafe()})`.unprepared.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to getMany from backing store`, | |
| cause | |
| })), Effect.flatMap(rows => { | |
| const out = new Array(keys.length); | |
| for (let i = 0; i < rows.length; i++) { | |
| const row = rows[i]; | |
| const index = keys.indexOf(row.id); | |
| if (index === -1) continue; | |
| try { | |
| out[index] = JSON.parse(row.value); | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| return Effect.succeed(out); | |
| })), | |
| set: (key, value, ttl) => Effect.suspend(() => { | |
| try { | |
| return upsert([{ | |
| id: key, | |
| value: JSON.stringify(value), | |
| expires: unsafeTtlToExpires(clock, ttl) | |
| }]).pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to set key ${key} in backing store`, | |
| cause | |
| })), Effect.asVoid); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to serialize value for key ${key} to backing store`, | |
| cause | |
| })); | |
| } | |
| }), | |
| setMany: entries => Effect.suspend(() => { | |
| try { | |
| const encoded = entries.map(([key, value, ttl]) => ({ | |
| id: key, | |
| value: JSON.stringify(value), | |
| expires: unsafeTtlToExpires(clock, ttl) | |
| })); | |
| return upsert(encoded).pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to setMany in backing store`, | |
| cause | |
| })), Effect.asVoid); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to serialize values into backing store`, | |
| cause | |
| })); | |
| } | |
| }), | |
| remove: key => sql`DELETE FROM ${table} WHERE id = ${key}`.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to remove key ${key} from backing store`, | |
| cause | |
| })), Effect.asVoid), | |
| clear: sql`DELETE FROM ${table}`.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to clear backing store`, | |
| cause | |
| })), Effect.asVoid) | |
| }); | |
| }) | |
| }); | |
| })); | |
| /** | |
| * Provides SQL-backed persistence using a shared `effect_persistence` table. | |
| * | |
| * **Details** | |
| * | |
| * Rows are partitioned by `store_id` and store JSON-encoded values with | |
| * optional expiration timestamps. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerBackingSql = /*#__PURE__*/Layer.effect(BackingPersistence)(/*#__PURE__*/Effect.gen(function* () { | |
| const sql = (yield* SqlClient.SqlClient).withoutTransforms(); | |
| const table = sql("effect_persistence"); | |
| yield* sql.onDialectOrElse({ | |
| mysql: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| store_id VARCHAR(191) NOT NULL, | |
| id VARCHAR(191) NOT NULL, | |
| value TEXT NOT NULL, | |
| expires BIGINT, | |
| PRIMARY KEY (store_id, id) | |
| ) | |
| `, | |
| pg: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| store_id TEXT NOT NULL, | |
| id TEXT NOT NULL, | |
| value TEXT NOT NULL, | |
| expires BIGINT, | |
| PRIMARY KEY (store_id, id) | |
| ) | |
| `, | |
| mssql: () => sql` | |
| IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${table} AND xtype='U') | |
| CREATE TABLE ${table} ( | |
| store_id NVARCHAR(191) NOT NULL, | |
| id NVARCHAR(191) NOT NULL, | |
| value NVARCHAR(MAX) NOT NULL, | |
| expires BIGINT, | |
| PRIMARY KEY (store_id, id) | |
| ) | |
| `, | |
| // sqlite | |
| orElse: () => sql` | |
| CREATE TABLE IF NOT EXISTS ${table} ( | |
| store_id TEXT NOT NULL, | |
| id TEXT NOT NULL, | |
| value TEXT NOT NULL, | |
| expires INTEGER, | |
| PRIMARY KEY (store_id, id) | |
| ) | |
| ` | |
| }).pipe(Effect.orDie); | |
| const upsert = sql.onDialectOrElse({ | |
| pg: () => entries => sql` | |
| INSERT INTO ${table} ${sql.insert(entries)} | |
| ON CONFLICT (store_id, id) DO UPDATE SET value=EXCLUDED.value, expires=EXCLUDED.expires | |
| `.unprepared, | |
| mysql: () => entries => sql` | |
| INSERT INTO ${table} ${sql.insert(entries)} | |
| ON DUPLICATE KEY UPDATE value=VALUES(value), expires=VALUES(expires) | |
| `.unprepared, | |
| mssql: () => entries => Effect.forEach(entries, entry => sql` | |
| MERGE ${table} AS target | |
| USING (SELECT ${entry.store_id} AS store_id, ${entry.id} AS id, ${entry.value} AS value, ${entry.expires} AS expires) AS source | |
| ON target.store_id = source.store_id AND target.id = source.id | |
| WHEN MATCHED THEN UPDATE SET value = source.value, expires = source.expires | |
| WHEN NOT MATCHED THEN INSERT (store_id, id, value, expires) | |
| VALUES (source.store_id, source.id, source.value, source.expires); | |
| `, { | |
| discard: true | |
| }), | |
| // sqlite | |
| orElse: () => entries => sql` | |
| INSERT INTO ${table} ${sql.insert(entries)} | |
| ON CONFLICT(store_id, id) DO UPDATE SET value=excluded.value, expires=excluded.expires | |
| `.unprepared | |
| }); | |
| const wrapString = sql.onDialectOrElse({ | |
| mssql: () => s => `N'${s}'`, | |
| orElse: () => s => `'${s}'` | |
| }); | |
| return BackingPersistence.of({ | |
| make: Effect.fnUntraced(function* (storeId) { | |
| const clock = yield* Clock.Clock; | |
| // Cleanup expired entries on startup | |
| yield* Effect.ignore(sql`DELETE FROM ${table} WHERE store_id = ${storeId} AND expires IS NOT NULL AND expires <= ${clock.currentTimeMillisUnsafe()}`); | |
| return identity({ | |
| get: key => sql`SELECT value FROM ${table} WHERE store_id = ${storeId} AND id = ${key} AND (expires IS NULL OR expires > ${clock.currentTimeMillisUnsafe()})`.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to get key ${key} from backing store`, | |
| cause | |
| })), Effect.flatMap(rows => { | |
| if (rows.length === 0) { | |
| return Effect.undefined; | |
| } | |
| try { | |
| return Effect.succeed(JSON.parse(rows[0].value)); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to parse value for key ${key} from backing store`, | |
| cause | |
| })); | |
| } | |
| })), | |
| getMany: keys => sql`SELECT id, value FROM ${table} WHERE store_id = ${storeId} AND id IN (${sql.literal(keys.map(wrapString).join(", "))}) AND (expires IS NULL OR expires > ${clock.currentTimeMillisUnsafe()})`.unprepared.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to getMany from backing store`, | |
| cause | |
| })), Effect.flatMap(rows => { | |
| const out = new Array(keys.length); | |
| for (let i = 0; i < rows.length; i++) { | |
| const row = rows[i]; | |
| const index = keys.indexOf(row.id); | |
| if (index === -1) continue; | |
| try { | |
| out[index] = JSON.parse(row.value); | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| return Effect.succeed(out); | |
| })), | |
| set: (key, value, ttl) => Effect.suspend(() => { | |
| try { | |
| return upsert([{ | |
| store_id: storeId, | |
| id: key, | |
| value: JSON.stringify(value), | |
| expires: unsafeTtlToExpires(clock, ttl) | |
| }]).pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to set key ${key} in backing store`, | |
| cause | |
| })), Effect.asVoid); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to serialize value for key ${key} to backing store`, | |
| cause | |
| })); | |
| } | |
| }), | |
| setMany: entries => Effect.suspend(() => { | |
| try { | |
| const encoded = entries.map(([key, value, ttl]) => ({ | |
| store_id: storeId, | |
| id: key, | |
| value: JSON.stringify(value), | |
| expires: unsafeTtlToExpires(clock, ttl) | |
| })); | |
| return upsert(encoded).pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to setMany in backing store`, | |
| cause | |
| })), Effect.asVoid); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to serialize values into backing store`, | |
| cause | |
| })); | |
| } | |
| }), | |
| remove: key => sql`DELETE FROM ${table} WHERE store_id = ${storeId} AND id = ${key}`.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to remove key ${key} from backing store`, | |
| cause | |
| })), Effect.asVoid), | |
| clear: sql`DELETE FROM ${table} WHERE store_id = ${storeId}`.pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to clear backing store`, | |
| cause | |
| })), Effect.asVoid) | |
| }); | |
| }) | |
| }); | |
| })); | |
| /** | |
| * Provides Redis-backed persistence. | |
| * | |
| * **Details** | |
| * | |
| * Each store id is used as a key prefix, values are JSON-encoded, and finite | |
| * TTLs are stored with Redis expiration. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerBackingRedis = /*#__PURE__*/Layer.effect(BackingPersistence)(/*#__PURE__*/Effect.gen(function* () { | |
| const redis = yield* Redis.Redis; | |
| const setMany = redis.eval(setManyRedis); | |
| return BackingPersistence.of({ | |
| make: prefix => Effect.sync(() => { | |
| const prefixed = key => `${prefix}:${key}`; | |
| const parse = str => { | |
| if (str === null) { | |
| return Effect.undefined; | |
| } | |
| try { | |
| return Effect.succeed(JSON.parse(str)); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to parse value from Redis`, | |
| cause | |
| })); | |
| } | |
| }; | |
| return identity({ | |
| get: key => Effect.flatMap(Effect.mapError(redis.send("GET", prefixed(key)), ({ | |
| cause | |
| }) => new PersistenceError({ | |
| message: `Failed to get key ${key} from Redis`, | |
| cause | |
| })), parse), | |
| getMany: keys => Effect.flatMap(Effect.mapError(redis.send("mget", ...keys.map(prefixed)), ({ | |
| cause | |
| }) => new PersistenceError({ | |
| message: `Failed to getMany from Redis`, | |
| cause | |
| })), values => { | |
| const out = new Array(keys.length); | |
| for (let i = 0; i < keys.length; i++) { | |
| const value = values[i]; | |
| try { | |
| out[i] = value === null ? undefined : JSON.parse(value); | |
| } catch { | |
| // TODO: remove bad entries? | |
| out[i] = undefined; | |
| } | |
| } | |
| return Effect.succeed(out); | |
| }), | |
| set: (key, value, ttl) => Effect.mapError(ttl === undefined ? redis.send("SET", prefixed(key), JSON.stringify(value)) : redis.send("SET", prefixed(key), JSON.stringify(value), "PX", String(Duration.toMillis(ttl))), ({ | |
| cause | |
| }) => new PersistenceError({ | |
| message: `Failed to set key ${key} in Redis`, | |
| cause | |
| })), | |
| setMany: entries => Effect.suspend(() => { | |
| const sets = new Map(); | |
| const expires = new Map(); | |
| for (const [key, value, ttl] of entries) { | |
| const pkey = prefixed(key); | |
| sets.set(pkey, JSON.stringify(value)); | |
| if (ttl) { | |
| expires.set(pkey, Duration.toMillis(ttl)); | |
| } | |
| } | |
| return Effect.mapError(setMany({ | |
| sets, | |
| expires | |
| }), ({ | |
| cause | |
| }) => new PersistenceError({ | |
| message: `Failed to setMany in Redis`, | |
| cause | |
| })); | |
| }), | |
| remove: key => Effect.mapError(redis.send("DEL", prefixed(key)), ({ | |
| cause | |
| }) => new PersistenceError({ | |
| message: `Failed to remove key ${key} from Redis`, | |
| cause | |
| })), | |
| clear: redis.send("KEYS", `${prefix}:*`).pipe(Effect.flatMap(keys => redis.send("DEL", ...keys)), Effect.mapError(({ | |
| cause | |
| }) => new PersistenceError({ | |
| message: `Failed to clear keys from Redis`, | |
| cause | |
| }))) | |
| }); | |
| }) | |
| }); | |
| })); | |
| const setManyRedis = /*#__PURE__*/Redis.script(options => [...options.sets.keys(), ...options.expires.keys(), options.sets.size, options.expires.size, ...options.sets.values(), ...options.expires.values()], { | |
| numberOfKeys: options => options.sets.size + options.expires.size, | |
| lua: ` | |
| local num_sets = tonumber(ARGV[1]) | |
| local num_expires = tonumber(ARGV[2]) | |
| local index = 3 | |
| for i = 1, num_sets do | |
| local key = KEYS[i] | |
| local value = ARGV[index] | |
| redis.call("SET", key, value) | |
| index = index + 1 | |
| end | |
| for i = 1, num_expires do | |
| local key = KEYS[num_sets + i] | |
| local expire = tonumber(ARGV[index]) | |
| redis.call("PEXPIRE", key, expire) | |
| index = index + 1 | |
| end | |
| ` | |
| }); | |
| /** | |
| * Provides `BackingPersistence` using a `KeyValueStore`. | |
| * | |
| * **Details** | |
| * | |
| * Each store id becomes a key prefix, and values are stored as JSON with | |
| * optional expiration timestamps. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerBackingKvs = /*#__PURE__*/Layer.effect(BackingPersistence)(/*#__PURE__*/Effect.gen(function* () { | |
| const backing = yield* KeyValueStore.KeyValueStore; | |
| const clock = yield* Clock.Clock; | |
| return BackingPersistence.of({ | |
| make: storeId => Effect.sync(() => { | |
| const store = KeyValueStore.prefix(backing, storeId); | |
| const get = key => Effect.flatMap(Effect.mapError(store.get(key), error => new PersistenceError({ | |
| message: `Failed to get key ${key} from backing store`, | |
| cause: error | |
| })), str => { | |
| if (str === undefined) { | |
| return Effect.undefined; | |
| } | |
| try { | |
| const parsed = JSON.parse(str); | |
| if (!Array.isArray(parsed)) return Effect.undefined; | |
| const [value, expires] = parsed; | |
| if (expires !== null && expires <= clock.currentTimeMillisUnsafe()) { | |
| return Effect.as(Effect.ignore(store.remove(key)), undefined); | |
| } | |
| return Effect.succeed(value); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to parse value for key ${key} from backing store`, | |
| cause | |
| })); | |
| } | |
| }); | |
| return identity({ | |
| get, | |
| getMany: keys => Effect.forEach(keys, get, { | |
| concurrency: "unbounded" | |
| }), | |
| set: (key, value, ttl) => Effect.suspend(() => { | |
| try { | |
| return Effect.mapError(store.set(key, JSON.stringify([value, unsafeTtlToExpires(clock, ttl)])), cause => new PersistenceError({ | |
| message: `Failed to set key ${key} in backing store`, | |
| cause | |
| })); | |
| } catch (cause) { | |
| return Effect.fail(new PersistenceError({ | |
| message: `Failed to serialize value for key ${key} to backing store`, | |
| cause | |
| })); | |
| } | |
| }), | |
| setMany: entries => Effect.forEach(entries, ([key, value, ttl]) => { | |
| const expires = unsafeTtlToExpires(clock, ttl); | |
| if (expires === null) return Effect.void; | |
| const encoded = JSON.stringify([value, expires]); | |
| return store.set(key, encoded); | |
| }, { | |
| concurrency: "unbounded", | |
| discard: true | |
| }).pipe(Effect.mapError(cause => new PersistenceError({ | |
| message: `Failed to setMany in backing store`, | |
| cause | |
| }))), | |
| remove: key => Effect.mapError(store.remove(key), cause => new PersistenceError({ | |
| message: `Failed to remove key ${key} from backing store`, | |
| cause | |
| })), | |
| clear: Effect.mapError(store.clear, cause => new PersistenceError({ | |
| message: `Failed to clear backing store`, | |
| cause | |
| })) | |
| }); | |
| }) | |
| }); | |
| })); | |
| /** | |
| * Provides `Persistence` backed by the current `KeyValueStore`. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerKvs = /*#__PURE__*/layer.pipe(/*#__PURE__*/Layer.provide(layerBackingKvs)); | |
| /** | |
| * Provides `Persistence` backed by process-local in-memory storage. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerMemory = /*#__PURE__*/layer.pipe(/*#__PURE__*/Layer.provide(layerBackingMemory)); | |
| /** | |
| * Provides `Persistence` backed by the current `Redis` service. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerRedis = /*#__PURE__*/layer.pipe(/*#__PURE__*/Layer.provide(layerBackingRedis)); | |
| /** | |
| * Provides `Persistence` backed by SQL with one table per store id. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerSqlMultiTable = /*#__PURE__*/layer.pipe(/*#__PURE__*/Layer.provide(layerBackingSqlMultiTable)); | |
| /** | |
| * Provides `Persistence` backed by SQL using a shared persistence table. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerSql = /*#__PURE__*/layer.pipe(/*#__PURE__*/Layer.provide(layerBackingSql)); | |
| /** | |
| * Converts a TTL to an absolute expiration timestamp in milliseconds. | |
| * | |
| * **Details** | |
| * | |
| * Returns `null` for no TTL and uses `clock.currentTimeMillisUnsafe`, so it is | |
| * intended for backing-store internals. | |
| * | |
| * @category converting | |
| * @since 4.0.0 | |
| */ | |
| export const unsafeTtlToExpires = (clock, ttl) => ttl ? clock.currentTimeMillisUnsafe() + Duration.toMillis(ttl) : null; | |
| //# sourceMappingURL=Persistence.js.map |
Xet Storage Details
- Size:
- 28.3 kB
- Xet hash:
- 09363725e2e3fa2fedf0878aa80609f76d17441940d5efc5b87ca3cb106384cb
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.