1/**
2 * Provides effectful key/value storage for persistence backends.
3 *
4 * `KeyValueStore` is a service for storing string or binary values by key. It
5 * is useful for lightweight durable state, browser storage, local files, SQL
6 * tables, tests, and as a storage building block for higher-level persistence
7 * APIs. This module includes store operations, prefixed views, schema-aware JSON
8 * storage, error values, and layers for memory, filesystem, Web Storage, and
9 * SQL-backed stores.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Context from "../Context.ts"
15import * as Data from "../Data.ts"
16import * as Effect from "../Effect.ts"
17import * as Base64 from "../encoding/Base64.ts"
18import * as FileSystem from "../FileSystem.ts"
19import { dual, identity, type LazyArg } from "../Function.ts"
20import * as Layer from "../Layer.ts"
21import * as Option from "../Option.ts"
22import * as Path from "../Path.ts"
23import type { PlatformError } from "../PlatformError.ts"
24import * as Predicate from "../Predicate.ts"
25import * as Result from "../Result.ts"
26import * as Schema from "../Schema.ts"
27import * as SqlClient from "../sql/SqlClient.ts"
28import type { SqlError } from "../sql/SqlError.ts"
29import * as UndefinedOr from "../UndefinedOr.ts"
30
31const TypeId = "~effect/persistence/KeyValueStore" as const
32
33/**
34 * Effectful key/value store service for string and binary values.
35 *
36 * @stability unstable
37 * @category models
38 * @since 4.0.0
39 */
40export interface KeyValueStore {
41 readonly [TypeId]: typeof TypeId
42 /**
43 * Returns the value of the specified key if it exists.
44 */
45 readonly get: (key: string) => Effect.Effect<string | undefined, KeyValueStoreError>
46
47 /**
48 * Returns the value of the specified key if it exists.
49 */
50 readonly getUint8Array: (key: string) => Effect.Effect<Uint8Array | undefined, KeyValueStoreError>
51
52 /**
53 * Sets the value of the specified key.
54 */
55 readonly set: (key: string, value: string | Uint8Array) => Effect.Effect<void, KeyValueStoreError>
56
57 /**
58 * Removes the specified key.
59 */
60 readonly remove: (key: string) => Effect.Effect<void, KeyValueStoreError>
61
62 /**
63 * Removes all entries.
64 */
65 readonly clear: Effect.Effect<void, KeyValueStoreError>
66
67 /**
68 * Returns the number of entries.
69 */
70 readonly size: Effect.Effect<number, KeyValueStoreError>
71
72 /**
73 * Updates the value of the specified key if it exists.
74 */
75 readonly modify: (
76 key: string,
77 f: (value: string) => string
78 ) => Effect.Effect<string | undefined, KeyValueStoreError>
79
80 /**
81 * Updates the value of the specified key if it exists.
82 */
83 readonly modifyUint8Array: (
84 key: string,
85 f: (value: Uint8Array) => Uint8Array
86 ) => Effect.Effect<Uint8Array | undefined, KeyValueStoreError>
87
88 /**
89 * Returns true if the KeyValueStore contains the specified key.
90 */
91 readonly has: (key: string) => Effect.Effect<boolean, KeyValueStoreError>
92
93 /**
94 * Checks whether the KeyValueStore contains any entries.
95 */
96 readonly isEmpty: Effect.Effect<boolean, KeyValueStoreError>
97}
98
99/**
100 * Implementation callbacks used by `make` to construct a `KeyValueStore`.
101 *
102 * **Details**
103 *
104 * Primitive operations are required, while helpers such as `has`, `isEmpty`,
105 * and `modify` can be supplied to override the defaults.
106 *
107 * @stability unstable
108 * @category options
109 * @since 4.0.0
110 */
111export type MakeOptions = Partial<KeyValueStore> & {
112 /**
113 * Returns the value of the specified key if it exists.
114 */
115 readonly get: (key: string) => Effect.Effect<string | undefined, KeyValueStoreError>
116
117 /**
118 * Returns the value of the specified key if it exists.
119 */
120 readonly getUint8Array: (key: string) => Effect.Effect<Uint8Array | undefined, KeyValueStoreError>
121
122 /**
123 * Sets the value of the specified key.
124 */
125 readonly set: (key: string, value: string | Uint8Array) => Effect.Effect<void, KeyValueStoreError>
126
127 /**
128 * Removes the specified key.
129 */
130 readonly remove: (key: string) => Effect.Effect<void, KeyValueStoreError>
131
132 /**
133 * Removes all entries.
134 */
135 readonly clear: Effect.Effect<void, KeyValueStoreError>
136
137 /**
138 * Returns the number of entries.
139 */
140 readonly size: Effect.Effect<number, KeyValueStoreError>
141}
142
143/**
144 * Implementation callbacks for adapting a string-only backing store into a
145 * `KeyValueStore`.
146 *
147 * @stability unstable
148 * @category options
149 * @since 4.0.0
150 */
151export type MakeStringOptions = Partial<Omit<KeyValueStore, "set">> & {
152 /**
153 * Returns the value of the specified key if it exists.
154 */
155 readonly get: (key: string) => Effect.Effect<string | undefined, KeyValueStoreError>
156
157 /**
158 * Sets the value of the specified key.
159 */
160 readonly set: (key: string, value: string) => Effect.Effect<void, KeyValueStoreError>
161
162 /**
163 * Removes the specified key.
164 */
165 readonly remove: (key: string) => Effect.Effect<void, KeyValueStoreError>
166
167 /**
168 * Removes all entries.
169 */
170 readonly clear: Effect.Effect<void, KeyValueStoreError>
171
172 /**
173 * Returns the number of entries.
174 */
175 readonly size: Effect.Effect<number, KeyValueStoreError>
176}
177
178const ErrorTypeId = "~effect/persistence/KeyValueStore/KeyValueStoreError" as const
179
180/**
181 * Error raised by key/value store operations, including the failed method,
182 * optional key, message, and cause.
183 *
184 * @stability unstable
185 * @category errors
186 * @since 4.0.0
187 */
188export class KeyValueStoreError extends Data.TaggedError("KeyValueStoreError")<{
189 message: string
190 method: string
191 key?: string
192 cause?: unknown
193}> {
194 /**
195 * Marks this value as a key-value store error for runtime guards.
196 *
197 * @stability unstable
198 * @since 4.0.0
199 */
200 readonly [ErrorTypeId]: typeof ErrorTypeId = ErrorTypeId
201}
202
203/**
204 * Service tag for string and binary key/value storage.
205 *
206 * **When to use**
207 *
208 * Use to access or provide the persistence store used for lightweight durable
209 * state.
210 *
211 * @stability unstable
212 * @category services
213 * @since 4.0.0
214 */
215export const KeyValueStore: Context.Service<
216 KeyValueStore,
217 KeyValueStore
218> = Context.Service("effect/persistence/KeyValueStore")
219
220/**
221 * Constructs a `KeyValueStore` from primitive store operations.
222 *
223 * **Details**
224 *
225 * Default implementations are derived for `has`, `isEmpty`, `modify`, and
226 * `modifyUint8Array` unless they are provided in the options.
227 *
228 * @stability unstable
229 * @category constructors
230 * @since 4.0.0
231 */
232export const make = (options: MakeOptions): KeyValueStore =>
233 KeyValueStore.of({
234 [TypeId]: TypeId,
235 has: (key) => Effect.map(options.get(key), Predicate.isNotUndefined),
236 isEmpty: Effect.map(options.size, (size) => size === 0),
237 modify: (key, f) =>
238 Effect.flatMap(
239 options.get(key),
240 (o) => {
241 if (o === undefined) {
242 return Effect.undefined
243 }
244 const newValue = f(o)
245 return Effect.as(
246 options.set(key, newValue),
247 newValue
248 )
249 }
250 ),
251 modifyUint8Array: (key, f) =>
252 Effect.flatMap(
253 options.getUint8Array(key),
254 (o) => {
255 if (o === undefined) {
256 return Effect.undefined
257 }
258 const newValue = f(o)
259 return Effect.as(options.set(key, newValue), newValue)
260 }
261 ),
262 ...options
263 })
264
265/**
266 * Adapts a string-only backing store into a `KeyValueStore`.
267 *
268 * **Details**
269 *
270 * `Uint8Array` values are stored as base64 strings. `getUint8Array` decodes
271 * base64 values and falls back to UTF-8 encoding for non-base64 strings.
272 *
273 * @stability unstable
274 * @category constructors
275 * @since 4.0.0
276 */
277export const makeStringOnly = (
278 options: MakeStringOptions
279): KeyValueStore => {
280 const encoder = new TextEncoder()
281 return make({
282 ...options,
283 getUint8Array: (key) =>
284 options.get(key).pipe(
285 Effect.map(UndefinedOr.map((value) =>
286 Result.match(Base64.decode(value), {
287 onFailure: () => encoder.encode(value),
288 onSuccess: identity
289 })
290 ))
291 ),
292 set: (key, value) =>
293 typeof value === "string"
294 ? options.set(key, value)
295 : Effect.suspend(() => options.set(key, Base64.encode(value)))
296 })
297}
298
299/**
300 * Returns a view of a `KeyValueStore` that prepends the given prefix to every
301 * key.
302 *
303 * @stability unstable
304 * @category combinators
305 * @since 4.0.0
306 */
307export const prefix: {
308 (prefix: string): (self: KeyValueStore) => KeyValueStore
309 (self: KeyValueStore, prefix: string): KeyValueStore
310} = dual(2, (self: KeyValueStore, prefix: string): KeyValueStore => ({
311 ...self,
312 get: (key) => self.get(`${prefix}${key}`),
313 getUint8Array: (key) => self.getUint8Array(`${prefix}${key}`),
314 set: (key, value) => self.set(`${prefix}${key}`, value),
315 remove: (key) => self.remove(`${prefix}${key}`),
316 has: (key) => self.has(`${prefix}${key}`),
317 modify: (key, f) => self.modify(`${prefix}${key}`, f),
318 modifyUint8Array: (key, f) => self.modifyUint8Array(`${prefix}${key}`, f)
319}))
320
321/**
322 * Provides a process-local in-memory `KeyValueStore` backed by a `Map`.
323 *
324 * @stability unstable
325 * @category layers
326 * @since 4.0.0
327 */
328export const layerMemory: Layer.Layer<KeyValueStore> = Layer.sync(KeyValueStore)(() => {
329 const store = new Map<string, string | Uint8Array>()
330 const encoder = new TextEncoder()
331
332 return make({
333 get: (key: string) =>
334 Effect.sync(() => {
335 const value = store.get(key)
336 return value === undefined ? undefined : typeof value === "string" ? value : Base64.encode(value)
337 }),
338 getUint8Array: (key: string) =>
339 Effect.sync(() => {
340 const value = store.get(key)
341 return value === undefined ? undefined : typeof value === "string" ? encoder.encode(value) : value
342 }),
343 set: (key: string, value: string | Uint8Array) => Effect.sync(() => store.set(key, value)),
344 remove: (key: string) => Effect.sync(() => store.delete(key)),
345 clear: Effect.sync(() => store.clear()),
346 size: Effect.sync(() => store.size)
347 })
348})
349
350/**
351 * Provides a `KeyValueStore` backed by files in the specified directory.
352 *
353 * **Details**
354 *
355 * The directory is created if needed, and each key is percent-encoded as a
356 * single file name. Empty keys, `.` and `..` are rejected. Keys are only
357 * guaranteed to be distinct on case-sensitive file systems.
358 *
359 * `clear` removes the directory recursively, so it must not be shared with
360 * unrelated data.
361 *
362 * @stability unstable
363 * @category layers
364 * @since 4.0.0
365 */
366export const layerFileSystem = (
367 directory: string
368): Layer.Layer<KeyValueStore, PlatformError, FileSystem.FileSystem | Path.Path> =>
369 Layer.effect(KeyValueStore)(Effect.gen(function*() {
370 const fs = yield* FileSystem.FileSystem
371 const path = yield* Path.Path
372 const withKeyPath = <A>(
373 method: string,
374 key: string,
375 f: (path: string) => Effect.Effect<A, KeyValueStoreError>
376 ): Effect.Effect<A, KeyValueStoreError> =>
377 key.length === 0 || key === "." || key === ".."
378 ? Effect.fail(
379 new KeyValueStoreError({
380 method,
381 key,
382 message: `Invalid key ${key}`
383 })
384 )
385 : f(path.join(directory, encodeURIComponent(key)))
386
387 if (!(yield* fs.exists(directory))) {
388 yield* fs.makeDirectory(directory, { recursive: true })
389 }
390
391 return make({
392 get: (key: string) =>
393 withKeyPath("get", key, (path) =>
394 Effect.catchTag(
395 fs.readFileString(path),
396 "PlatformError",
397 (cause) =>
398 cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail(
399 new KeyValueStoreError({
400 method: "get",
401 key,
402 message: `Unable to get item with key ${key}`,
403 cause
404 })
405 )
406 )),
407 getUint8Array: (key: string) =>
408 withKeyPath("getUint8Array", key, (path) =>
409 Effect.catchTag(
410 fs.readFile(path),
411 "PlatformError",
412 (cause) =>
413 cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail(
414 new KeyValueStoreError({
415 method: "getUint8Array",
416 key,
417 message: `Unable to get item with key ${key}`,
418 cause
419 })
420 )
421 )),
422 set: (key: string, value: string | Uint8Array) =>
423 withKeyPath("set", key, (path) =>
424 Effect.mapError(
425 typeof value === "string" ? fs.writeFileString(path, value) : fs.writeFile(path, value),
426 (cause) =>
427 new KeyValueStoreError({
428 method: "set",
429 key,
430 message: `Unable to set item with key ${key}`,
431 cause
432 })
433 )),
434 remove: (key: string) =>
435 withKeyPath("remove", key, (path) =>
436 Effect.mapError(fs.remove(path), (cause) =>
437 new KeyValueStoreError({
438 method: "remove",
439 key,
440 message: `Unable to remove item with key ${key}`,
441 cause
442 }))),
443 has: (key: string) =>
444 withKeyPath("has", key, (path) =>
445 Effect.mapError(fs.exists(path), (cause) =>
446 new KeyValueStoreError({
447 method: "has",
448 key,
449 message: `Unable to check existence of item with key ${key}`,
450 cause
451 }))),
452 clear: Effect.mapError(
453 Effect.andThen(
454 fs.remove(directory, { recursive: true }),
455 fs.makeDirectory(directory, { recursive: true })
456 ),
457 (cause) =>
458 new KeyValueStoreError({
459 method: "clear",
460 message: `Unable to clear storage`,
461 cause
462 })
463 ),
464 size: Effect.matchEffect(
465 fs.readDirectory(directory),
466 {
467 onSuccess: (files) => Effect.succeed(files.length),
468 onFailure: (cause) =>
469 Effect.fail(
470 new KeyValueStoreError({
471 method: "size",
472 message: `Unable to get size`,
473 cause
474 })
475 )
476 }
477 )
478 })
479 }))
480
481/**
482 * Options for configuring the SQL-backed `KeyValueStore` layer.
483 *
484 * @stability unstable
485 * @category layers
486 * @since 4.0.0
487 */
488export interface LayerSqlOptions {
489 /**
490 * The SQL table name used to store values.
491 *
492 * @default "effect_key_value_store"
493 */
494 readonly table?: string
495}
496
497/**
498 * Provides a SQL-backed `KeyValueStore`.
499 *
500 * **Details**
501 *
502 * The layer creates the configured table if it does not exist and stores both
503 * string and binary values through the current `SqlClient`.
504 *
505 * @stability unstable
506 * @category layers
507 * @since 4.0.0
508 */
509export const layerSql = (
510 options: LayerSqlOptions = {}
511): Layer.Layer<KeyValueStore, never, SqlClient.SqlClient> =>
512 Layer.effect(KeyValueStore)(
513 Effect.gen(function*() {
514 const sql = (yield* SqlClient.SqlClient).withoutTransforms()
515 const table = sql(options.table ?? "effect_key_value_store")
516
517 yield* sql.onDialectOrElse({
518 mysql: () =>
519 sql`
520 CREATE TABLE IF NOT EXISTS ${table} (
521 id VARCHAR(191) PRIMARY KEY,
522 value BLOB NOT NULL,
523 value_type SMALLINT NOT NULL
524 )
525 `,
526 pg: () =>
527 sql`
528 CREATE TABLE IF NOT EXISTS ${table} (
529 id TEXT PRIMARY KEY,
530 value BYTEA NOT NULL,
531 value_type SMALLINT NOT NULL
532 )
533 `,
534 mssql: () =>
535 sql`
536 IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${table} AND xtype='U')
537 CREATE TABLE ${table} (
538 id NVARCHAR(450) PRIMARY KEY,
539 value VARBINARY(MAX) NOT NULL,
540 value_type SMALLINT NOT NULL
541 )
542 `,
543 // sqlite
544 orElse: () =>
545 sql`
546 CREATE TABLE IF NOT EXISTS ${table} (
547 id TEXT PRIMARY KEY,
548 value BLOB NOT NULL,
549 value_type INTEGER NOT NULL
550 )
551 `
552 }).pipe(Effect.orDie)
553
554 type UpsertFn = (entry: {
555 id: string
556 value: Uint8Array
557 value_type: number
558 }) => Effect.Effect<unknown, SqlError>
559
560 const upsert = sql.onDialectOrElse({
561 pg: (): UpsertFn => (entry) =>
562 sql`
563 INSERT INTO ${table} (id, value, value_type) VALUES (${entry.id}, ${entry.value}, ${entry.value_type})
564 ON CONFLICT (id) DO UPDATE SET value=EXCLUDED.value, value_type=EXCLUDED.value_type
565 `.unprepared,
566 mysql: (): UpsertFn => (entry) =>
567 sql`
568 INSERT INTO ${table} (id, value, value_type) VALUES (${entry.id}, ${entry.value}, ${entry.value_type})
569 ON DUPLICATE KEY UPDATE value=VALUES(value), value_type=VALUES(value_type)
570 `,
571 mssql: (): UpsertFn => (entry) =>
572 sql`
573 MERGE ${table} AS target
574 USING (SELECT ${entry.id} AS id, ${entry.value} AS value, ${entry.value_type} AS value_type) AS source
575 ON target.id = source.id
576 WHEN MATCHED THEN UPDATE SET value = source.value, value_type = source.value_type
577 WHEN NOT MATCHED THEN INSERT (id, value, value_type)
578 VALUES (source.id, source.value, source.value_type);
579 `,
580 // sqlite
581 orElse: (): UpsertFn => (entry) =>
582 sql`
583 INSERT INTO ${table} (id, value, value_type) VALUES (${entry.id}, ${entry.value}, ${entry.value_type})
584 ON CONFLICT(id) DO UPDATE SET value=excluded.value, value_type=excluded.value_type
585 `.unprepared
586 })
587
588 const encoder = new TextEncoder()
589 const decoder = new TextDecoder()
590 const ValueTypeString = 0
591 const ValueTypeUint8Array = 1
592
593 type Row = {
594 value: Uint8Array
595 value_type: number
596 }
597
598 return make({
599 get: (key: string) =>
600 sql<Row>`SELECT value, value_type FROM ${table} WHERE id = ${key}`.pipe(
601 Effect.mapError((cause) =>
602 new KeyValueStoreError({
603 method: "get",
604 key,
605 message: `Unable to get item with key ${key}`,
606 cause
607 })
608 ),
609 Effect.flatMap((rows) => {
610 if (rows.length === 0) {
611 return Effect.undefined
612 }
613 const row = rows[0]
614 switch (row.value_type) {
615 case ValueTypeString:
616 return Effect.succeed(decoder.decode(row.value))
617 case ValueTypeUint8Array:
618 return Effect.succeed(Base64.encode(row.value))
619 default:
620 return Effect.fail(
621 new KeyValueStoreError({
622 method: "get",
623 key,
624 message: `Invalid stored value type for key ${key}: ${row.value_type}`
625 })
626 )
627 }
628 })
629 ),
630 getUint8Array: (key: string) =>
631 sql<Row>`SELECT value, value_type FROM ${table} WHERE id = ${key}`.pipe(
632 Effect.mapError((cause) =>
633 new KeyValueStoreError({
634 method: "getUint8Array",
635 key,
636 message: `Unable to get item with key ${key}`,
637 cause
638 })
639 ),
640 Effect.flatMap((rows) => {
641 if (rows.length === 0) {
642 return Effect.undefined
643 }
644 const row = rows[0]
645 switch (row.value_type) {
646 case ValueTypeString:
647 return Effect.succeed(row.value)
648 case ValueTypeUint8Array:
649 return Effect.succeed(row.value)
650 default:
651 return Effect.fail(
652 new KeyValueStoreError({
653 method: "getUint8Array",
654 key,
655 message: `Invalid stored value type for key ${key}: ${row.value_type}`
656 })
657 )
658 }
659 })
660 ),
661 set: (key: string, value: string | Uint8Array) =>
662 upsert({
663 id: key,
664 value: typeof value === "string" ? encoder.encode(value) : value,
665 value_type: typeof value === "string" ? ValueTypeString : ValueTypeUint8Array
666 }).pipe(
667 Effect.mapError((cause) =>
668 new KeyValueStoreError({
669 method: "set",
670 key,
671 message: `Unable to set item with key ${key}`,
672 cause
673 })
674 ),
675 Effect.asVoid
676 ),
677 remove: (key: string) =>
678 sql`DELETE FROM ${table} WHERE id = ${key}`.pipe(
679 Effect.mapError((cause) =>
680 new KeyValueStoreError({
681 method: "remove",
682 key,
683 message: `Unable to remove item with key ${key}`,
684 cause
685 })
686 ),
687 Effect.asVoid
688 ),
689 clear: sql`DELETE FROM ${table}`.pipe(
690 Effect.mapError((cause) =>
691 new KeyValueStoreError({
692 method: "clear",
693 message: `Unable to clear storage`,
694 cause
695 })
696 ),
697 Effect.asVoid
698 ),
699 size: sql<{ count: number }>`SELECT COUNT(*) as count FROM ${table}`.pipe(
700 Effect.mapError((cause) =>
701 new KeyValueStoreError({
702 method: "size",
703 message: `Unable to get size`,
704 cause
705 })
706 ),
707 Effect.map((rows) => rows.length === 0 ? 0 : Number(rows[0].count))
708 )
709 })
710 })
711 )
712
713const SchemaStoreTypeId = "~effect/persistence/KeyValueStore/SchemaStore" as const
714
715/**
716 * Schema-aware view of a `KeyValueStore` that stores values as encoded JSON.
717 *
718 * @stability unstable
719 * @category models
720 * @since 4.0.0
721 */
722export interface SchemaStore<S extends Schema.Constraint> {
723 readonly [SchemaStoreTypeId]: typeof SchemaStoreTypeId
724 /**
725 * Returns the value of the specified key if it exists.
726 */
727 readonly get: (
728 key: string
729 ) => Effect.Effect<Option.Option<S["Type"]>, KeyValueStoreError | Schema.SchemaError, S["DecodingServices"]>
730
731 /**
732 * Sets the value of the specified key.
733 */
734 readonly set: (
735 key: string,
736 value: S["Type"]
737 ) => Effect.Effect<void, KeyValueStoreError | Schema.SchemaError, S["EncodingServices"]>
738
739 /**
740 * Removes the specified key.
741 */
742 readonly remove: (key: string) => Effect.Effect<void, KeyValueStoreError>
743
744 /**
745 * Removes all entries.
746 */
747 readonly clear: Effect.Effect<void, KeyValueStoreError>
748
749 /**
750 * Returns the number of entries.
751 */
752 readonly size: Effect.Effect<number, KeyValueStoreError>
753
754 /**
755 * Updates the value of the specified key if it exists.
756 */
757 readonly modify: (
758 key: string,
759 f: (value: S["Type"]) => S["Type"]
760 ) => Effect.Effect<
761 Option.Option<S["Type"]>,
762 KeyValueStoreError | Schema.SchemaError,
763 S["DecodingServices"] | S["EncodingServices"]
764 >
765
766 /**
767 * Returns true if the KeyValueStore contains the specified key.
768 */
769 readonly has: (key: string) => Effect.Effect<boolean, KeyValueStoreError>
770
771 /**
772 * Checks whether the KeyValueStore contains any entries.
773 */
774 readonly isEmpty: Effect.Effect<boolean, KeyValueStoreError>
775}
776
777/**
778 * Adapts a `KeyValueStore` into a `SchemaStore` using the schema's JSON codec.
779 *
780 * @stability unstable
781 * @category converting
782 * @since 4.0.0
783 */
784export const toSchemaStore = <S extends Schema.Constraint>(self: KeyValueStore, schema: S): SchemaStore<S> => {
785 const serializer = Schema.toCodecJson(schema)
786 const jsonSchema = Schema.fromJsonString(serializer)
787 const decode = Schema.decodeEffect(jsonSchema)
788 const encode = Schema.encodeEffect(jsonSchema)
789
790 const get = (key: string) =>
791 Effect.flatMap(
792 self.get(key),
793 UndefinedOr.match({
794 onUndefined: () => Effect.succeedNone,
795 onDefined: (value) => Effect.asSome(decode(value))
796 })
797 )
798
799 const set = (key: string, value: S["Type"]) => Effect.flatMap(encode(value), (json) => self.set(key, json))
800
801 const modify = (key: string, f: (value: S["Type"]) => S["Type"]) =>
802 Effect.flatMap(
803 get(key),
804 (o) => {
805 if (Option.isNone(o)) {
806 return Effect.succeedNone
807 }
808 const newValue = f(o.value)
809 return Effect.as(
810 set(key, newValue),
811 Option.some(newValue)
812 )
813 }
814 )
815
816 return {
817 [SchemaStoreTypeId]: SchemaStoreTypeId,
818 get,
819 set,
820 modify,
821 remove: self.remove,
822 clear: self.clear,
823 size: self.size,
824 has: self.has,
825 isEmpty: self.isEmpty
826 }
827}
828
829/**
830 * Provides a `KeyValueStore` backed by a Web `Storage` instance such as
831 * `localStorage` or `sessionStorage`.
832 *
833 * **Details**
834 *
835 * This layer uses the Web Storage API:
836 * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
837 *
838 * @stability unstable
839 * @category layers
840 * @since 4.0.0
841 */
842export const layerStorage = (
843 evaluate: LazyArg<Storage>
844): Layer.Layer<KeyValueStore> =>
845 Layer.sync(KeyValueStore)(() => {
846 const storage = evaluate()
847 return makeStringOnly({
848 get: (key: string) =>
849 Effect.try({
850 try: () => storage.getItem(key) ?? undefined,
851 catch: (cause) =>
852 new KeyValueStoreError({
853 key,
854 method: "get",
855 message: `Unable to get item with key ${key}`,
856 cause
857 })
858 }),
859
860 set: (key: string, value: string) =>
861 Effect.try({
862 try: () => storage.setItem(key, value),
863 catch: (cause) =>
864 new KeyValueStoreError({
865 key,
866 method: "set",
867 message: `Unable to set item with key ${key}`,
868 cause
869 })
870 }),
871
872 remove: (key: string) =>
873 Effect.try({
874 try: () => storage.removeItem(key),
875 catch: (cause) =>
876 new KeyValueStoreError({
877 key,
878 method: "remove",
879 message: `Unable to remove item with key ${key}`,
880 cause
881 })
882 }),
883
884 clear: Effect.try({
885 try: () => storage.clear(),
886 catch: (cause) =>
887 new KeyValueStoreError({
888 method: "clear",
889 message: `Unable to clear storage`,
890 cause
891 })
892 }),
893
894 size: Effect.try({
895 try: () => storage.length,
896 catch: (cause) =>
897 new KeyValueStoreError({
898 method: "size",
899 message: `Unable to get size`,
900 cause
901 })
902 })
903 })
904 })