1/**
2 * Builds effectful, schema-aware queries for typed browser IndexedDB versions.
3 *
4 * An `IndexedDbQueryBuilder` is created from an open database and a version's
5 * table descriptors, then exposes `from(tableName)` as the entry point for
6 * table operations. Query objects can select, count, delete, insert, upsert,
7 * clear tables, stream paged reads, react to invalidations, and run multiple
8 * effects in a shared `IDBTransaction` with `withTransaction`. Reads decode
9 * stored rows with the table schema, and writes encode input values before
10 * sending them to IndexedDB.
11 *
12 * @since 4.0.0
13 */
14import type { NonEmptyReadonlyArray } from "effect/Array"
15import * as Cause from "effect/Cause"
16import * as Context from "effect/Context"
17import * as Data from "effect/Data"
18import * as Effect from "effect/Effect"
19import * as Effectable from "effect/Effectable"
20import * as Fiber from "effect/Fiber"
21import type { Inspectable } from "effect/Inspectable"
22import { BaseProto } from "effect/Inspectable"
23import type * as MutableRef from "effect/MutableRef"
24import * as Option from "effect/Option"
25import * as Pipeable from "effect/Pipeable"
26import type * as Queue from "effect/Queue"
27import type * as Reactivity from "effect/reactivity/Reactivity"
28import type * as Record from "effect/Record"
29import * as References from "effect/References"
30import * as Schema from "effect/Schema"
31import * as SchemaIssue from "effect/SchemaIssue"
32import * as SchemaParser from "effect/SchemaParser"
33import type * as Scope from "effect/Scope"
34import * as Stream from "effect/Stream"
35import * as Utils from "effect/Utils"
36import type * as IndexedDb from "./IndexedDb.ts"
37import type * as IndexedDbDatabase from "./IndexedDbDatabase.ts"
38import type * as IndexedDbTable from "./IndexedDbTable.ts"
39import type * as IndexedDbVersion from "./IndexedDbVersion.ts"
40
41const ErrorTypeId = "~@effect/platform-browser/IndexedDbQueryBuilder/IndexedDbQueryError"
42
43const CommonProto = {
44 [Symbol.iterator]() {
45 return new Utils.SingleShotGen(this) as any
46 },
47 ...Pipeable.Prototype,
48 ...BaseProto,
49 toJSON(this: any) {
50 return {
51 _id: "IndexedDbQueryBuilder"
52 }
53 }
54}
55
56/**
57 * String union describing IndexedDB query failure categories such as decoding, encoding, and transaction errors.
58 *
59 * @category errors
60 * @since 4.0.0
61 */
62export type ErrorReason =
63 | "UnknownError"
64 | "DecodeError"
65 | "EncodeError"
66 | "TransactionError"
67
68/**
69 * Tagged error for IndexedDB query operations, carrying a query error reason and the original cause.
70 *
71 * **Details**
72 *
73 * `reason` is the query failure category, `cause` preserves the underlying
74 * schema, IndexedDB request, transaction, or user callback failure, and
75 * `message` is set to the reason.
76 *
77 * @see {@link ErrorReason} for the supported failure categories
78 *
79 * @category errors
80 * @since 4.0.0
81 */
82export class IndexedDbQueryError extends Data.TaggedError(
83 "IndexedDbQueryError"
84)<{
85 reason: ErrorReason
86 cause: unknown
87}> {
88 /**
89 * Marks this value as an IndexedDB query builder error for runtime guards.
90 *
91 * @since 4.0.0
92 */
93 readonly [ErrorTypeId]: typeof ErrorTypeId = ErrorTypeId
94
95 override readonly message = this.reason
96}
97
98/**
99 * Typed query builder for an IndexedDB version, with helpers for table queries, database access, clearing data, and running effects in a shared transaction.
100 *
101 * @category models
102 * @since 4.0.0
103 */
104export interface IndexedDbQueryBuilder<
105 Source extends IndexedDbVersion.AnyWithProps
106> extends Pipeable.Pipeable, Inspectable {
107 readonly tables: ReadonlyMap<string, IndexedDbVersion.Tables<Source>>
108 readonly database: MutableRef.MutableRef<globalThis.IDBDatabase>
109 readonly reactivity: Reactivity.Reactivity
110 readonly IDBKeyRange: typeof globalThis.IDBKeyRange
111 readonly IDBTransaction: globalThis.IDBTransaction | undefined
112
113 readonly use: <A = unknown>(
114 f: (database: globalThis.IDBDatabase) => A
115 ) => Effect.Effect<A, IndexedDbQueryError>
116
117 readonly from: <
118 const Name extends IndexedDbTable.TableName<
119 IndexedDbVersion.Tables<Source>
120 >
121 >(
122 table: Name
123 ) => IndexedDbQuery.From<IndexedDbVersion.TableWithName<Source, Name>>
124
125 /** @internal */
126 readonly fromCache: Map<string, IndexedDbQuery.From<IndexedDbVersion.TableWithName<Source, any>>>
127
128 readonly clearAll: Effect.Effect<void, IndexedDbQueryError>
129
130 readonly withTransaction: <
131 Tables extends NonEmptyReadonlyArray<
132 IndexedDbTable.TableName<IndexedDbVersion.Tables<Source>>
133 >,
134 Mode extends "readonly" | "readwrite"
135 >(options: {
136 readonly tables: Tables
137 readonly mode: Mode
138 readonly durability?: IDBTransactionDurability
139 }) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<
140 A,
141 E | IndexedDbQueryError,
142 Exclude<R, IndexedDbTransaction>
143 >
144}
145
146/**
147 * Valid key-path type for a table schema, using encoded fields whose values are IndexedDB-valid keys.
148 *
149 * @category utility types
150 * @since 4.0.0
151 */
152export type KeyPath<TableSchema extends IndexedDbTable.AnySchemaStruct> =
153 | IndexedDbValidKeys<TableSchema>
154 | NonEmptyReadonlyArray<IndexedDbValidKeys<TableSchema>>
155
156/**
157 * Valid numeric key-path type for a table schema, used for auto-increment key paths.
158 *
159 * @category utility types
160 * @since 4.0.0
161 */
162export type KeyPathNumber<TableSchema extends IndexedDbTable.AnySchemaStruct> =
163 | IndexedDbValidNumberKeys<TableSchema>
164 | NonEmptyReadonlyArray<IndexedDbValidNumberKeys<TableSchema>>
165
166/**
167 * Namespace containing the typed IndexedDB query model interfaces and helper types.
168 *
169 * @since 4.0.0
170 */
171export declare namespace IndexedDbQuery {
172 /**
173 * Decoded row type returned by select queries, adding a `key` field when the table does not define a key path.
174 *
175 * @category utility types
176 * @since 4.0.0
177 */
178 export type SelectType<
179 Table extends IndexedDbTable.AnyWithProps
180 > = [IndexedDbTable.KeyPath<Table>] extends [undefined] ? IndexedDbTable.TableSchema<Table>["Type"] & {
181 readonly key: (typeof IndexedDb.IDBValidKey)["Type"]
182 } :
183 IndexedDbTable.TableSchema<Table>["Type"]
184
185 /**
186 * Input type for insert and upsert operations, adjusted for auto-increment keys and out-of-line keys.
187 *
188 * @category utility types
189 * @since 4.0.0
190 */
191 export type ModifyType<
192 Table extends IndexedDbTable.AnyWithProps
193 > =
194 & (IndexedDbTable.AutoIncrement<Table> extends true ?
195 & {
196 [
197 key in keyof Schema.Struct.MakeIn<
198 Omit<
199 IndexedDbTable.TableSchema<Table>["fields"],
200 IndexedDbTable.KeyPath<Table>
201 >
202 >
203 ]: key extends keyof Schema.Struct.MakeIn<
204 IndexedDbTable.TableSchema<Table>["fields"]
205 > ? Schema.Struct.MakeIn<
206 IndexedDbTable.TableSchema<Table>["fields"]
207 >[key]
208 : never
209 }
210 & {
211 [key in IndexedDbTable.KeyPath<Table>]?: number | undefined
212 }
213 : Schema.Struct.MakeIn<IndexedDbTable.TableSchema<Table>["fields"]>)
214 & ([IndexedDbTable.KeyPath<Table>] extends [undefined] ? {
215 key: IDBValidKey
216 }
217 : {})
218
219 /**
220 * Value type accepted by `equals` comparisons for a table key path or index.
221 *
222 * @category utility types
223 * @since 4.0.0
224 */
225 export type EqualsType<
226 Table extends IndexedDbTable.AnyWithProps,
227 Index extends keyof Table["indexes"],
228 KeyPath = [Index] extends [never] ? Table["keyPath"] : Table["indexes"][Index],
229 Type = Table["tableSchema"]["Encoded"]
230 > = [KeyPath] extends [undefined] ? IDBValidKey
231 : KeyPath extends keyof Type ? Type[KeyPath]
232 : { [I in keyof KeyPath]: KeyPath[I] extends keyof Type ? Type[KeyPath[I]] | [] : never }
233
234 /**
235 * Value type accepted by range comparisons for a table key path or index, including partial tuples for compound indexes.
236 *
237 * @category utility types
238 * @since 4.0.0
239 */
240 export type ExtractIndexType<
241 Table extends IndexedDbTable.AnyWithProps,
242 Index extends keyof Table["indexes"],
243 KeyPath = [Index] extends [never] ? Table["keyPath"] : Table["indexes"][Index],
244 Type = Table["tableSchema"]["Encoded"]
245 > = KeyPath extends keyof Type ? Type[KeyPath]
246 : KeyPath extends readonly [infer K, ...infer Rest] ? K extends keyof Type ? [
247 Type[K],
248 ...{ [P in keyof Rest]?: Rest[P] extends keyof Type ? Type[Rest[P]] | [] : never }
249 ] :
250 never :
251 never
252
253 /**
254 * Mutation input type for insert and upsert operations, including any required key fields.
255 *
256 * @category utility types
257 * @since 4.0.0
258 */
259 export type ModifyWithKey<Table extends IndexedDbTable.AnyWithProps> = ModifyType<Table>
260
261 /**
262 * Query entry point for a table, exposing clear, select, count, delete, insert, and upsert operations.
263 *
264 * @category models
265 * @since 4.0.0
266 */
267 export interface From<Table extends IndexedDbTable.AnyWithProps> {
268 readonly table: Table
269 readonly database: MutableRef.MutableRef<globalThis.IDBDatabase>
270 readonly IDBKeyRange: typeof globalThis.IDBKeyRange
271 readonly reactivity: Reactivity.Reactivity
272
273 readonly clear: Effect.Effect<void, IndexedDbQueryError>
274
275 readonly select: {
276 <Index extends IndexedDbDatabase.IndexFromTable<Table>>(
277 index: Index
278 ): Select<Table, Index>
279 (): Select<Table, never>
280 }
281
282 /** @internal */
283 readonly selectCache: Map<
284 string | undefined,
285 IndexedDbQuery.Select<any, never>
286 >
287
288 readonly count: {
289 <Index extends IndexedDbDatabase.IndexFromTable<Table>>(
290 index: Index
291 ): Count<Table, Index>
292 (): Count<Table, never>
293 }
294
295 /** @internal */
296 readonly countCache: Map<
297 string | undefined,
298 IndexedDbQuery.Count<any, never>
299 >
300
301 readonly delete: {
302 <Index extends IndexedDbDatabase.IndexFromTable<Table>>(
303 index: Index
304 ): DeletePartial<Table, Index>
305 (): DeletePartial<Table, never>
306 }
307
308 /** @internal */
309 readonly deleteCache: Map<
310 string | undefined,
311 IndexedDbQuery.DeletePartial<any, never>
312 >
313
314 readonly insert: (value: ModifyWithKey<Table>) => Modify<Table>
315 readonly insertAll: (
316 values: Array<ModifyWithKey<Table>>
317 ) => ModifyAll<Table>
318 readonly upsert: (value: ModifyWithKey<Table>) => Modify<Table>
319 readonly upsertAll: (
320 values: Array<ModifyWithKey<Table>>
321 ) => ModifyAll<Table>
322 }
323
324 /**
325 * Effect model for clearing all rows from a table.
326 *
327 * @category models
328 * @since 4.0.0
329 */
330 export interface Clear<
331 Table extends IndexedDbTable.AnyWithProps
332 > extends Effect.Effect<void, IndexedDbQueryError> {
333 readonly from: From<Table>
334 }
335
336 type ComparisonKeys = "equals" | "gte" | "lte" | "gt" | "lt" | "between"
337
338 /**
339 * Effect model for counting table rows, optionally constrained by an index and key-range comparisons.
340 *
341 * @category models
342 * @since 4.0.0
343 */
344 export interface Count<
345 Table extends IndexedDbTable.AnyWithProps,
346 Index extends IndexedDbDatabase.IndexFromTable<Table>
347 > extends Effect.Effect<number, IndexedDbQueryError> {
348 readonly from: From<Table>
349 readonly index?: Index
350 readonly only?: ExtractIndexType<Table, Index>
351 readonly lowerBound?: ExtractIndexType<Table, Index>
352 readonly upperBound?: ExtractIndexType<Table, Index>
353 readonly excludeLowerBound?: boolean
354 readonly excludeUpperBound?: boolean
355
356 readonly equals: (
357 value: EqualsType<Table, Index>
358 ) => Omit<Count<Table, Index>, ComparisonKeys>
359
360 readonly gte: (
361 value: ExtractIndexType<Table, Index>
362 ) => Omit<Count<Table, Index>, ComparisonKeys>
363
364 readonly lte: (
365 value: ExtractIndexType<Table, Index>
366 ) => Omit<Count<Table, Index>, ComparisonKeys>
367
368 readonly gt: (
369 value: ExtractIndexType<Table, Index>
370 ) => Omit<Count<Table, Index>, ComparisonKeys>
371
372 readonly lt: (
373 value: ExtractIndexType<Table, Index>
374 ) => Omit<Count<Table, Index>, ComparisonKeys>
375
376 readonly between: (
377 lowerBound: ExtractIndexType<Table, Index>,
378 upperBound: ExtractIndexType<Table, Index>,
379 options?: { excludeLowerBound?: boolean; excludeUpperBound?: boolean }
380 ) => Omit<Count<Table, Index>, ComparisonKeys>
381 }
382
383 /**
384 * Intermediate delete builder used to choose a key range or limit before producing an executable delete query.
385 *
386 * @category models
387 * @since 4.0.0
388 */
389 export interface DeletePartial<
390 Table extends IndexedDbTable.AnyWithProps,
391 Index extends IndexedDbDatabase.IndexFromTable<Table>
392 > {
393 readonly from: From<Table>
394 readonly index?: Index
395
396 readonly equals: (
397 value: EqualsType<Table, Index>
398 ) => Delete<Table, Index>
399
400 readonly gte: (
401 value: ExtractIndexType<Table, Index>
402 ) => Delete<Table, Index>
403
404 readonly lte: (
405 value: ExtractIndexType<Table, Index>
406 ) => Delete<Table, Index>
407
408 readonly gt: (
409 value: ExtractIndexType<Table, Index>
410 ) => Delete<Table, Index>
411
412 readonly lt: (
413 value: ExtractIndexType<Table, Index>
414 ) => Delete<Table, Index>
415
416 readonly between: (
417 lowerBound: ExtractIndexType<Table, Index>,
418 upperBound: ExtractIndexType<Table, Index>,
419 options?: { excludeLowerBound?: boolean; excludeUpperBound?: boolean }
420 ) => Delete<Table, Index>
421
422 readonly limit: (
423 limit: number
424 ) => DeleteWithout<Table, Index, "limit">
425 }
426
427 type DeleteWithout<
428 Table extends IndexedDbTable.AnyWithProps,
429 Index extends IndexedDbDatabase.IndexFromTable<Table>,
430 ExcludedKeys extends string
431 > = Omit<Delete<Table, Index, ExcludedKeys>, ExcludedKeys>
432
433 /**
434 * Effect model for deleting rows from a table, with optional key-range, limit, filter, and reactivity invalidation helpers.
435 *
436 * @category models
437 * @since 4.0.0
438 */
439 export interface Delete<
440 Table extends IndexedDbTable.AnyWithProps,
441 Index extends IndexedDbDatabase.IndexFromTable<Table>,
442 ExcludedKeys extends string = never
443 > extends Effect.Effect<void, IndexedDbQueryError> {
444 readonly delete: DeletePartial<Table, Index>
445 readonly index?: Index
446 readonly limitValue?: number
447 readonly only?: ExtractIndexType<Table, Index>
448 readonly lowerBound?: ExtractIndexType<Table, Index>
449 readonly upperBound?: ExtractIndexType<Table, Index>
450 readonly excludeLowerBound?: boolean
451 readonly excludeUpperBound?: boolean
452 readonly predicate?: (item: IndexedDbTable.Encoded<Table>) => boolean
453
454 readonly limit: (
455 limit: number
456 ) => DeleteWithout<Table, Index, ExcludedKeys | "limit">
457
458 readonly filter: (
459 f: (value: IndexedDbTable.Encoded<Table>) => boolean
460 ) => DeleteWithout<Table, Index, ExcludedKeys>
461
462 /**
463 * Invalidate any queries using Reactivity service with the provided keys.
464 *
465 * **Details**
466 *
467 * If no keys are provided, the table name is used as the reactivity key.
468 */
469 readonly invalidate: (
470 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
471 ) => Effect.Effect<void, IndexedDbQueryError, IndexedDbTable.Context<Table>>
472 }
473
474 type SelectWithout<
475 Table extends IndexedDbTable.AnyWithProps,
476 Index extends IndexedDbDatabase.IndexFromTable<Table>,
477 ExcludedKeys extends string
478 > = Omit<Select<Table, Index, ExcludedKeys>, ExcludedKeys>
479
480 /**
481 * Effect model for selecting rows from a table, with chainable range, paging, filtering, streaming, and reactive query helpers.
482 *
483 * @category models
484 * @since 4.0.0
485 */
486 export interface Select<
487 Table extends IndexedDbTable.AnyWithProps,
488 Index extends IndexedDbDatabase.IndexFromTable<Table>,
489 ExcludedKeys extends string = never
490 > extends
491 Effect.Effect<
492 Array<SelectType<Table>>,
493 IndexedDbQueryError,
494 IndexedDbTable.Context<Table>
495 >
496 {
497 readonly from: From<Table>
498 readonly index?: Index
499 readonly limitValue?: number
500 readonly offsetValue?: number
501 readonly reverseValue?: boolean
502 readonly only?: ExtractIndexType<Table, Index>
503 readonly lowerBound?: ExtractIndexType<Table, Index>
504 readonly upperBound?: ExtractIndexType<Table, Index>
505 readonly excludeLowerBound?: boolean
506 readonly excludeUpperBound?: boolean
507 readonly predicate?: (item: IndexedDbTable.Encoded<Table>) => boolean
508
509 readonly equals: (
510 value: EqualsType<Table, Index>
511 ) => SelectWithout<Table, Index, ExcludedKeys | ComparisonKeys>
512
513 readonly gte: (
514 value: ExtractIndexType<Table, Index>
515 ) => SelectWithout<Table, Index, ExcludedKeys | ComparisonKeys>
516
517 readonly lte: (
518 value: ExtractIndexType<Table, Index>
519 ) => SelectWithout<Table, Index, ExcludedKeys | ComparisonKeys>
520
521 readonly gt: (
522 value: ExtractIndexType<Table, Index>
523 ) => SelectWithout<Table, Index, ExcludedKeys | ComparisonKeys>
524
525 readonly lt: (
526 value: ExtractIndexType<Table, Index>
527 ) => SelectWithout<Table, Index, ExcludedKeys | ComparisonKeys>
528
529 readonly between: (
530 lowerBound: ExtractIndexType<Table, Index>,
531 upperBound: ExtractIndexType<Table, Index>,
532 options?: { excludeLowerBound?: boolean; excludeUpperBound?: boolean }
533 ) => SelectWithout<Table, Index, ExcludedKeys | ComparisonKeys>
534
535 readonly limit: (
536 limit: number
537 ) => SelectWithout<Table, Index, ExcludedKeys | "limit" | "first">
538
539 readonly offset: (
540 offset: number
541 ) => SelectWithout<Table, Index, ExcludedKeys | "offset" | "first">
542
543 readonly reverse: () => SelectWithout<Table, Index, ExcludedKeys | "reverse" | "first">
544
545 readonly filter: (
546 f: (value: IndexedDbTable.Encoded<Table>) => boolean
547 ) => SelectWithout<Table, Index, ExcludedKeys | "first">
548
549 readonly first: () => First<Table, Index>
550
551 /**
552 * Stream the selected data.
553 *
554 * **Details**
555 *
556 * The default chunk size is 100.
557 */
558 readonly stream: (options?: {
559 readonly chunkSize?: number | undefined
560 }) => Stream.Stream<
561 SelectType<Table>,
562 IndexedDbQueryError,
563 IndexedDbTable.Context<Table>
564 >
565
566 /**
567 * Use the Reactivity service to react to changes to the selected data.
568 *
569 * **Details**
570 *
571 * By default, the table name is used as the reactivity key.
572 */
573 readonly reactive: (
574 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
575 ) => Stream.Stream<
576 Array<SelectType<Table>>,
577 IndexedDbQueryError,
578 IndexedDbTable.Context<Table>
579 >
580 readonly reactiveQueue: (
581 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
582 ) => Effect.Effect<
583 Queue.Dequeue<Array<SelectType<Table>>, IndexedDbQueryError>,
584 never,
585 Scope.Scope | IndexedDbTable.Context<Table>
586 >
587 }
588
589 /**
590 * Effect model for selecting the first matching row, failing with `NoSuchElementError` when no row is found.
591 *
592 * @category models
593 * @since 4.0.0
594 */
595 export interface First<
596 Table extends IndexedDbTable.AnyWithProps,
597 Index extends IndexedDbDatabase.IndexFromTable<Table>
598 > extends
599 Effect.Effect<
600 SelectType<Table>,
601 IndexedDbQueryError | Cause.NoSuchElementError,
602 IndexedDbTable.Context<Table>
603 >
604 {
605 readonly select: Select<Table, Index>
606
607 /**
608 * Use the Reactivity service to react to changes to the selected data.
609 *
610 * **Details**
611 *
612 * By default, the table name is used as the reactivity key.
613 */
614 readonly reactive: (
615 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
616 ) => Stream.Stream<
617 SelectType<Table>,
618 IndexedDbQueryError | Cause.NoSuchElementError,
619 IndexedDbTable.Context<Table>
620 >
621
622 /**
623 * Use the Reactivity service to react to changes to the selected data.
624 *
625 * **Details**
626 *
627 * By default, the table name is used as the reactivity key.
628 */
629 readonly reactiveQueue: (
630 keys: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>>
631 ) => Effect.Effect<
632 Queue.Dequeue<SelectType<Table>, IndexedDbQueryError | Cause.NoSuchElementError>,
633 never,
634 Scope.Scope | IndexedDbTable.Context<Table>
635 >
636 }
637
638 /**
639 * Effect model for a select query filtered by one or more predicates over encoded table rows.
640 *
641 * @category models
642 * @since 4.0.0
643 */
644 export interface Filter<
645 Table extends IndexedDbTable.AnyWithProps,
646 Index extends IndexedDbDatabase.IndexFromTable<Table>
647 > extends
648 Effect.Effect<
649 Array<SelectType<Table>>,
650 IndexedDbQueryError,
651 IndexedDbTable.Context<Table>
652 >
653 {
654 readonly select: Select<Table, Index>
655 readonly predicate: (item: IndexedDbTable.Encoded<Table>) => boolean
656 readonly filter: (
657 f: (value: IndexedDbTable.Encoded<Table>) => boolean
658 ) => Filter<Table, Index>
659 }
660
661 /**
662 * Effect model for inserting or upserting one row, returning the resulting IndexedDB key and supporting reactivity invalidation.
663 *
664 * @category models
665 * @since 4.0.0
666 */
667 export interface Modify<
668 Table extends IndexedDbTable.AnyWithProps
669 > extends
670 Effect.Effect<
671 globalThis.IDBValidKey,
672 IndexedDbQueryError,
673 IndexedDbTable.Context<Table>
674 >
675 {
676 readonly operation: "add" | "put"
677 readonly from: From<Table>
678 readonly value: ModifyWithKey<Table>
679
680 /**
681 * Invalidate any queries using Reactivity service with the provided keys.
682 *
683 * **Details**
684 *
685 * If no keys are provided, the table name is used as the reactivity key.
686 */
687 readonly invalidate: (
688 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
689 ) => Effect.Effect<globalThis.IDBValidKey, IndexedDbQueryError, IndexedDbTable.Context<Table>>
690 }
691
692 /**
693 * Effect model for inserting or upserting multiple rows, returning the resulting IndexedDB keys and supporting reactivity invalidation.
694 *
695 * @category models
696 * @since 4.0.0
697 */
698 export interface ModifyAll<
699 Table extends IndexedDbTable.AnyWithProps
700 > extends
701 Effect.Effect<
702 Array<globalThis.IDBValidKey>,
703 IndexedDbQueryError,
704 IndexedDbTable.Context<Table>
705 >
706 {
707 readonly operation: "add" | "put"
708 readonly from: From<Table>
709 readonly values: Array<ModifyWithKey<Table>>
710
711 /**
712 * Invalidate any queries using Reactivity service with the provided keys.
713 *
714 * **Details**
715 *
716 * If no keys are provided, the table name is used as the reactivity key.
717 */
718 readonly invalidate: (
719 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
720 ) => Effect.Effect<globalThis.IDBValidKey, IndexedDbQueryError, IndexedDbTable.Context<Table>>
721 }
722}
723
724/**
725 * Service tag for the active `IDBTransaction` used to share a transaction across IndexedDB query effects.
726 *
727 * @category services
728 * @since 4.0.0
729 */
730export class IndexedDbTransaction extends Context.Service<IndexedDbTransaction, globalThis.IDBTransaction>()(
731 "@effect/platform-browser/IndexedDbQueryBuilder/IndexedDbTransaction"
732) {}
733
734// -----------------------------------------------------------------------------
735// internal
736// -----------------------------------------------------------------------------
737
738type IndexedDbValidKeys<TableSchema extends IndexedDbTable.AnySchemaStruct> = keyof TableSchema["Encoded"] extends
739 infer K ? K extends keyof TableSchema["Encoded"] ? TableSchema["Encoded"][K] extends Readonly<IDBValidKey> ? K
740 : never
741 : never
742 : never
743
744type IndexedDbValidNumberKeys<
745 TableSchema extends IndexedDbTable.AnySchemaStruct
746> = keyof TableSchema["Encoded"] extends infer K
747 ? K extends keyof TableSchema["Encoded"] ? [TableSchema["Encoded"][K]] extends [number | undefined] ? K
748 : never
749 : never
750 : never
751
752const applyDelete = (query: IndexedDbQuery.Delete<any, never>) =>
753 Effect.callback<any, IndexedDbQueryError>((resume) => {
754 const database = query.delete.from.database
755 const IDBKeyRange = query.delete.from.IDBKeyRange
756 const transaction = getOrCreateTransaction(database.current, [query.delete.from.table.tableName], "readwrite", {
757 durability: query.delete.from.table.durability
758 })
759 const objectStore = transaction.objectStore(query.delete.from.table.tableName)
760 const store = query.delete.index === undefined ? objectStore : objectStore.index(query.delete.index)
761 const predicate = query.predicate
762
763 let keyRange: globalThis.IDBKeyRange | undefined = undefined
764
765 if (query.only !== undefined) {
766 keyRange = IDBKeyRange.only(query.only)
767 } else if (
768 query.lowerBound !== undefined &&
769 query.upperBound !== undefined
770 ) {
771 keyRange = IDBKeyRange.bound(
772 query.lowerBound,
773 query.upperBound,
774 query.excludeLowerBound,
775 query.excludeUpperBound
776 )
777 } else if (query.lowerBound !== undefined) {
778 keyRange = IDBKeyRange.lowerBound(
779 query.lowerBound,
780 query.excludeLowerBound
781 )
782 } else if (query.upperBound !== undefined) {
783 keyRange = IDBKeyRange.upperBound(
784 query.upperBound,
785 query.excludeUpperBound
786 )
787 }
788
789 let request: globalThis.IDBRequest
790
791 if (query.delete.index !== undefined || query.limitValue !== undefined || predicate) {
792 const cursorRequest = store.openCursor(keyRange)
793 let count = 0
794
795 cursorRequest.onerror = () => {
796 resume(
797 Effect.fail(
798 new IndexedDbQueryError({
799 reason: "TransactionError",
800 cause: cursorRequest.error
801 })
802 )
803 )
804 }
805
806 cursorRequest.onsuccess = () => {
807 const cursor = cursorRequest.result
808 if (cursor === null) {
809 return resume(Effect.void)
810 }
811
812 if (predicate === undefined || predicate(cursor.value)) {
813 const deleteRequest = cursor.delete()
814 deleteRequest.onerror = () => {
815 resume(
816 Effect.fail(
817 new IndexedDbQueryError({
818 reason: "TransactionError",
819 cause: deleteRequest.error
820 })
821 )
822 )
823 }
824 count += 1
825 }
826
827 if (query.limitValue === undefined || count < query.limitValue) {
828 return cursor.continue()
829 }
830
831 resume(Effect.void)
832 }
833 } else if (keyRange !== undefined) {
834 request = objectStore.delete(keyRange)
835
836 request.onerror = (event) => {
837 resume(
838 Effect.fail(
839 new IndexedDbQueryError({
840 reason: "TransactionError",
841 cause: event
842 })
843 )
844 )
845 }
846
847 request.onsuccess = () => {
848 resume(Effect.succeed(request.result))
849 }
850 } else {
851 resume(
852 Effect.die(new Error("No key range provided for delete operation"))
853 )
854 }
855 })
856
857const getReadonlyObjectStore = (
858 query: IndexedDbQuery.Select<any, never> | IndexedDbQuery.Count<any, never>
859) => {
860 const database = query.from.database
861 const IDBKeyRange = query.from.IDBKeyRange
862 const transaction = getOrCreateTransaction(database.current, [query.from.table.tableName], "readonly", {
863 durability: query.from.table.durability
864 })
865 const objectStore = transaction.objectStore(query.from.table.tableName)
866
867 let keyRange: globalThis.IDBKeyRange | undefined = undefined
868 let store: globalThis.IDBObjectStore | globalThis.IDBIndex
869
870 if (query.only !== undefined) {
871 keyRange = IDBKeyRange.only(query.only)
872 } else if (query.lowerBound !== undefined && query.upperBound !== undefined) {
873 keyRange = IDBKeyRange.bound(
874 query.lowerBound,
875 query.upperBound,
876 query.excludeLowerBound,
877 query.excludeUpperBound
878 )
879 } else if (query.lowerBound !== undefined) {
880 keyRange = IDBKeyRange.lowerBound(
881 query.lowerBound,
882 query.excludeLowerBound
883 )
884 } else if (query.upperBound !== undefined) {
885 keyRange = IDBKeyRange.upperBound(
886 query.upperBound,
887 query.excludeUpperBound
888 )
889 }
890
891 if (query.index !== undefined) {
892 store = objectStore.index(query.index)
893 } else {
894 store = objectStore
895 }
896
897 return { store, keyRange }
898}
899
900const applySelect = Effect.fnUntraced(function*(
901 query: IndexedDbQuery.Select<any, never, any>
902): Effect.fn.Return<Array<any>, IndexedDbQueryError, unknown> {
903 const keyPath = query.from.table.keyPath
904 const predicate = query.predicate
905
906 const data = predicate || keyPath === undefined || query.offsetValue !== undefined || query.reverseValue ?
907 yield* Effect.callback<Array<any>, IndexedDbQueryError>((resume) => {
908 const { keyRange, store } = getReadonlyObjectStore(query)
909
910 const cursorRequest = store.openCursor(keyRange, query.reverseValue ? "prev" : "next")
911 const results: Array<any> = []
912 let count = 0
913 let offsetApplied = false
914
915 cursorRequest.onerror = () => {
916 resume(
917 Effect.fail(
918 new IndexedDbQueryError({
919 reason: "TransactionError",
920 cause: cursorRequest.error
921 })
922 )
923 )
924 }
925
926 cursorRequest.onsuccess = () => {
927 const cursor = cursorRequest.result
928 if (cursor === null) {
929 return resume(Effect.succeed(results))
930 }
931
932 if (query.offsetValue && !offsetApplied) {
933 offsetApplied = true
934 return cursor.advance(query.offsetValue)
935 }
936
937 if (predicate === undefined || predicate(cursor.value)) {
938 results.push(
939 keyPath === undefined
940 ? { ...cursor.value, key: cursor.primaryKey }
941 : cursor.value
942 )
943 count += 1
944 }
945
946 if (query.limitValue === undefined || count < query.limitValue) {
947 return cursor.continue()
948 }
949
950 resume(Effect.succeed(results))
951 }
952 }) :
953 yield* Effect.callback<Array<any>, IndexedDbQueryError>((resume) => {
954 const { keyRange, store } = getReadonlyObjectStore(query)
955 const request = store.getAll(keyRange, query.limitValue)
956 request.onerror = (event) => {
957 resume(
958 Effect.fail(
959 new IndexedDbQueryError({
960 reason: "TransactionError",
961 cause: event
962 })
963 )
964 )
965 }
966 request.onsuccess = () => {
967 if (query.reverseValue) {
968 request.result.reverse()
969 }
970 resume(Effect.succeed(request.result))
971 }
972 })
973
974 const tableSchema = (query.from.table as IndexedDbTable.AnyWithProps).arraySchema
975
976 return yield* Schema.decodeUnknownEffect(tableSchema)(data).pipe(
977 Effect.mapError(
978 (error) =>
979 new IndexedDbQueryError({
980 reason: "DecodeError",
981 cause: error
982 })
983 )
984 ) as Effect.Effect<Array<any>, IndexedDbQueryError, unknown>
985})
986
987const applyFirst = Effect.fnUntraced(function*(
988 query: IndexedDbQuery.First<any, never>
989) {
990 const keyPath = query.select.from.table.keyPath
991
992 const data = yield* Effect.callback<any, IndexedDbQueryError | Cause.NoSuchElementError>((resume) => {
993 const { keyRange, store } = getReadonlyObjectStore(query.select)
994
995 if (keyRange !== undefined && keyPath !== undefined) {
996 const request = store.get(keyRange)
997
998 request.onerror = (event) => {
999 resume(
1000 Effect.fail(
1001 new IndexedDbQueryError({
1002 reason: "TransactionError",
1003 cause: event
1004 })
1005 )
1006 )
1007 }
1008
1009 request.onsuccess = () => {
1010 if (request.result === undefined) {
1011 resume(
1012 Effect.fail(new Cause.NoSuchElementError(`No such element in table ${query.select.from.table.tableName}`))
1013 )
1014 } else {
1015 resume(Effect.succeed(request.result))
1016 }
1017 }
1018 } else {
1019 const request = store.openCursor(keyRange)
1020
1021 request.onerror = (event) => {
1022 resume(
1023 Effect.fail(
1024 new IndexedDbQueryError({
1025 reason: "TransactionError",
1026 cause: event
1027 })
1028 )
1029 )
1030 }
1031
1032 request.onsuccess = () => {
1033 const value = request.result?.value
1034 const key = request.result?.primaryKey
1035
1036 if (value === undefined) {
1037 resume(
1038 Effect.fail(new Cause.NoSuchElementError(`No such element in table ${query.select.from.table.tableName}`))
1039 )
1040 } else {
1041 resume(
1042 Effect.succeed(keyPath === undefined ? { ...value, key } : value)
1043 )
1044 }
1045 }
1046 }
1047 })
1048
1049 return yield* Schema.decodeUnknownEffect(query.select.from.table.readSchema)(
1050 data
1051 ).pipe(
1052 Effect.mapError(
1053 (error) =>
1054 new IndexedDbQueryError({
1055 reason: "DecodeError",
1056 cause: error
1057 })
1058 )
1059 )
1060})
1061
1062const applyModify = Effect.fnUntraced(function*({
1063 query,
1064 value
1065}: {
1066 query: IndexedDbQuery.Modify<any>
1067 value: any
1068}) {
1069 const autoIncrement = query.from.table.autoIncrement as boolean
1070 const keyPath = query.from.table.keyPath
1071 const table = query.from.table
1072 const schema: Schema.Top = autoIncrement && value[keyPath] === undefined
1073 ? table.autoincrementSchema
1074 : table.tableSchema
1075
1076 const encodedValue = yield* schema.makeEffect(value).pipe(
1077 Effect.flatMap(Schema.encodeUnknownEffect(schema)),
1078 Effect.mapError(
1079 (error) =>
1080 new IndexedDbQueryError({
1081 reason: "EncodeError",
1082 cause: error
1083 })
1084 )
1085 )
1086
1087 return yield* Effect.callback<any, IndexedDbQueryError>((resume) => {
1088 const database = query.from.database
1089 const transaction = getOrCreateTransaction(database.current, [query.from.table.tableName], "readwrite", {
1090 durability: query.from.table.durability
1091 })
1092 const objectStore = transaction.objectStore(query.from.table.tableName)
1093
1094 let request: globalThis.IDBRequest<IDBValidKey>
1095
1096 if (query.operation === "add") {
1097 request = objectStore.add(
1098 encodedValue,
1099 keyPath === undefined ? value["key"] : undefined
1100 )
1101 } else if (query.operation === "put") {
1102 request = objectStore.put(
1103 encodedValue,
1104 keyPath === undefined ? value["key"] : undefined
1105 )
1106 } else {
1107 return resume(Effect.die(new Error("Invalid modify operation")))
1108 }
1109
1110 request.onerror = (event) => {
1111 resume(
1112 Effect.fail(
1113 new IndexedDbQueryError({
1114 reason: "TransactionError",
1115 cause: event
1116 })
1117 )
1118 )
1119 }
1120
1121 request.onsuccess = () => {
1122 resume(Effect.succeed(request.result))
1123 }
1124 })
1125})
1126
1127const applyModifyAll = Effect.fnUntraced(
1128 function*({
1129 query,
1130 values
1131 }: {
1132 query: IndexedDbQuery.ModifyAll<any>
1133 values: Array<any>
1134 }) {
1135 const autoIncrement = query.from.table.autoIncrement as boolean
1136 const keyPath = query.from.table.keyPath
1137 const schema = query.from.table.tableSchema
1138 const encodedValues = new Array(values.length)
1139 const makeValue = SchemaParser.makeEffect(schema)
1140 const encodeValue = SchemaParser.encodeUnknownEffect(schema)
1141 const makeValueAutoincrement = SchemaParser.makeEffect(query.from.table.autoincrementSchema)
1142 const encodeValueAutoincrement = SchemaParser.encodeUnknownEffect(query.from.table.autoincrementSchema)
1143
1144 for (let i = 0; i < values.length; i++) {
1145 const value = values[i]
1146 if (autoIncrement && value[keyPath] === undefined) {
1147 encodedValues[i] = yield* encodeValueAutoincrement(yield* makeValueAutoincrement(value))
1148 } else {
1149 encodedValues[i] = yield* encodeValue(yield* makeValue(value))
1150 }
1151 }
1152
1153 return yield* Effect.callback<
1154 Array<globalThis.IDBValidKey>,
1155 IndexedDbQueryError
1156 >((resume) => {
1157 if (encodedValues.length === 0) {
1158 return resume(Effect.succeed([]))
1159 }
1160
1161 const database = query.from.database
1162 const transaction = getOrCreateTransaction(database.current, [query.from.table.tableName], "readwrite", {
1163 durability: query.from.table.durability
1164 })
1165 const objectStore = transaction.objectStore(query.from.table.tableName)
1166
1167 const results: Array<globalThis.IDBValidKey> = new Array(encodedValues.length)
1168 let remaining = encodedValues.length
1169
1170 if (query.operation === "add") {
1171 for (let i = 0; i < encodedValues.length; i++) {
1172 const request = objectStore.add(
1173 encodedValues[i],
1174 keyPath === undefined ? values[i]["key"] : undefined
1175 )
1176
1177 request.onerror = () => {
1178 resume(
1179 Effect.fail(
1180 new IndexedDbQueryError({
1181 reason: "TransactionError",
1182 cause: request.error
1183 })
1184 )
1185 )
1186 }
1187
1188 request.onsuccess = () => {
1189 results[i] = request.result
1190 remaining -= 1
1191 if (remaining === 0) {
1192 resume(Effect.succeed(results))
1193 }
1194 }
1195 }
1196 } else if (query.operation === "put") {
1197 for (let i = 0; i < encodedValues.length; i++) {
1198 const request = objectStore.put(
1199 encodedValues[i],
1200 keyPath === undefined ? values[i]["key"] : undefined
1201 )
1202
1203 request.onerror = () => {
1204 resume(
1205 Effect.fail(
1206 new IndexedDbQueryError({
1207 reason: "TransactionError",
1208 cause: request.error
1209 })
1210 )
1211 )
1212 }
1213
1214 request.onsuccess = () => {
1215 results[i] = request.result
1216 remaining -= 1
1217 if (remaining === 0) {
1218 resume(Effect.succeed(results))
1219 }
1220 }
1221 }
1222 } else {
1223 return resume(Effect.die(new Error("Invalid modify all operation")))
1224 }
1225
1226 objectStore.transaction.onerror = () => {
1227 resume(
1228 Effect.fail(
1229 new IndexedDbQueryError({
1230 reason: "TransactionError",
1231 cause: objectStore.transaction.error
1232 })
1233 )
1234 )
1235 }
1236 })
1237 },
1238 Effect.catchIf(
1239 SchemaIssue.isIssue,
1240 (issue) => Effect.fail(new IndexedDbQueryError({ reason: "EncodeError", cause: new Schema.SchemaError(issue) }))
1241 )
1242)
1243
1244const applyClear = (options: {
1245 readonly database: globalThis.IDBDatabase
1246 readonly table: IndexedDbTable.AnyWithProps
1247}) =>
1248 Effect.callback<void, IndexedDbQueryError>((resume) => {
1249 const database = options.database
1250 const transaction = getOrCreateTransaction(database, [options.table.tableName], "readwrite", {
1251 durability: options.table.durability
1252 })
1253 const objectStore = transaction.objectStore(options.table.tableName)
1254
1255 const request = objectStore.clear()
1256
1257 request.onerror = (event) => {
1258 resume(
1259 Effect.fail(
1260 new IndexedDbQueryError({
1261 reason: "TransactionError",
1262 cause: event
1263 })
1264 )
1265 )
1266 }
1267
1268 request.onsuccess = () => {
1269 resume(Effect.void)
1270 }
1271 })
1272
1273const applyClearAll = (options: {
1274 readonly database: globalThis.IDBDatabase
1275}) =>
1276 Effect.callback<void, IndexedDbQueryError>((resume) => {
1277 const database = options.database
1278 const tables = database.objectStoreNames
1279 const transaction = getOrCreateTransaction(database, [...tables], "readwrite")
1280
1281 for (let t = 0; t < tables.length; t++) {
1282 const objectStore = transaction.objectStore(tables[t])
1283 const request = objectStore.clear()
1284
1285 request.onerror = () => {
1286 resume(
1287 Effect.fail(
1288 new IndexedDbQueryError({
1289 reason: "TransactionError",
1290 cause: request.error
1291 })
1292 )
1293 )
1294 }
1295 }
1296
1297 transaction.onerror = () => {
1298 resume(
1299 Effect.fail(
1300 new IndexedDbQueryError({
1301 reason: "TransactionError",
1302 cause: transaction.error
1303 })
1304 )
1305 )
1306 }
1307
1308 transaction.oncomplete = () => {
1309 resume(Effect.void)
1310 }
1311 })
1312
1313const getCount = (query: IndexedDbQuery.Count<any, never>) =>
1314 Effect.callback<number, IndexedDbQueryError>((resume) => {
1315 const { keyRange, store } = getReadonlyObjectStore(query)
1316
1317 const request = store.count(keyRange)
1318
1319 request.onerror = (event) => {
1320 resume(
1321 Effect.fail(
1322 new IndexedDbQueryError({
1323 reason: "TransactionError",
1324 cause: event
1325 })
1326 )
1327 )
1328 }
1329
1330 request.onsuccess = () => {
1331 resume(Effect.succeed(request.result))
1332 }
1333 })
1334
1335const FromProto: Omit<
1336 IndexedDbQuery.From<any>,
1337 | "table"
1338 | "database"
1339 | "IDBKeyRange"
1340 | "transaction"
1341 | "reactivity"
1342 | "selectCache"
1343 | "countCache"
1344 | "deleteCache"
1345> = {
1346 select<Index extends IndexedDbDatabase.IndexFromTable<any>>(
1347 this: IndexedDbQuery.From<any>,
1348 index?: Index
1349 ) {
1350 let select = this.selectCache.get(index)
1351 if (select === undefined) {
1352 select = makeSelect({
1353 from: this,
1354 index
1355 })
1356 this.selectCache.set(index, select)
1357 }
1358 return select
1359 },
1360 count<Index extends IndexedDbDatabase.IndexFromTable<any>>(
1361 this: IndexedDbQuery.From<any>,
1362 index?: Index
1363 ) {
1364 let count = this.countCache.get(index)
1365 if (count === undefined) {
1366 count = makeCount({
1367 from: this,
1368 index
1369 })
1370 this.countCache.set(index, count)
1371 }
1372 return count
1373 },
1374 delete<Index extends IndexedDbDatabase.IndexFromTable<any>>(
1375 this: IndexedDbQuery.From<any>,
1376 index?: Index
1377 ) {
1378 let cached = this.deleteCache.get(index)
1379 if (cached === undefined) {
1380 cached = makeDeletePartial({
1381 from: this,
1382 index
1383 })
1384 this.deleteCache.set(index, cached)
1385 }
1386 return cached
1387 },
1388 insert(this: IndexedDbQuery.From<any>, value: any) {
1389 return makeModify({ from: this, value, operation: "add" })
1390 },
1391 upsert(this: IndexedDbQuery.From<any>, value: any) {
1392 return makeModify({ from: this, value, operation: "put" })
1393 },
1394 insertAll(this: IndexedDbQuery.From<any>, values: Array<any>) {
1395 return makeModifyAll({ from: this, values, operation: "add" })
1396 },
1397 upsertAll(this: IndexedDbQuery.From<any>, values: Array<any>) {
1398 return makeModifyAll({ from: this, values, operation: "put" })
1399 },
1400 get clear() {
1401 const self = this as IndexedDbQuery.From<any>
1402 return applyClear({
1403 database: self.database.current,
1404 table: self.table
1405 })
1406 },
1407 ...CommonProto
1408}
1409
1410const makeFrom = <
1411 const Table extends IndexedDbTable.AnyWithProps
1412>(options: {
1413 readonly table: Table
1414 readonly database: MutableRef.MutableRef<globalThis.IDBDatabase>
1415 readonly IDBKeyRange: typeof globalThis.IDBKeyRange
1416 readonly reactivity: Reactivity.Reactivity
1417}): IndexedDbQuery.From<Table> => {
1418 const self = Object.create(FromProto)
1419 self.table = options.table
1420 self.database = options.database
1421 self.IDBKeyRange = options.IDBKeyRange
1422 self.reactivity = options.reactivity
1423 self.selectCache = new Map()
1424 self.countCache = new Map()
1425 self.deleteCache = new Map()
1426 return self
1427}
1428
1429const DeletePartialProto: Omit<
1430 IndexedDbQuery.DeletePartial<any, never>,
1431 | "from"
1432 | "index"
1433> = {
1434 ...CommonProto,
1435 limit(this: IndexedDbQuery.DeletePartial<any, never>, limit: number) {
1436 return makeDelete({
1437 delete: this as any,
1438 limitValue: limit
1439 })
1440 },
1441 equals(this: IndexedDbQuery.DeletePartial<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1442 return makeDelete({
1443 delete: this as any,
1444 only: value
1445 })
1446 },
1447 gte(this: IndexedDbQuery.DeletePartial<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1448 return makeDelete({
1449 delete: this as any,
1450 lowerBound: value,
1451 excludeLowerBound: false
1452 })
1453 },
1454 lte(this: IndexedDbQuery.DeletePartial<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1455 return makeDelete({
1456 delete: this as any,
1457 upperBound: value,
1458 excludeUpperBound: false
1459 })
1460 },
1461 gt(this: IndexedDbQuery.DeletePartial<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1462 return makeDelete({
1463 delete: this as any,
1464 lowerBound: value,
1465 excludeLowerBound: true
1466 })
1467 },
1468 lt(this: IndexedDbQuery.DeletePartial<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1469 return makeDelete({
1470 delete: this as any,
1471 upperBound: value,
1472 excludeUpperBound: true
1473 })
1474 },
1475 between(
1476 this: IndexedDbQuery.DeletePartial<any, never>,
1477 lowerBound: IndexedDbQuery.ExtractIndexType<any, never>,
1478 upperBound: IndexedDbQuery.ExtractIndexType<any, never>,
1479 queryOptions?: { excludeLowerBound?: boolean; excludeUpperBound?: boolean }
1480 ) {
1481 return makeDelete({
1482 delete: this as any,
1483 lowerBound,
1484 upperBound,
1485 excludeLowerBound: queryOptions?.excludeLowerBound ?? false,
1486 excludeUpperBound: queryOptions?.excludeUpperBound ?? false
1487 })
1488 }
1489}
1490
1491const makeDeletePartial = <
1492 Table extends IndexedDbTable.AnyWithProps,
1493 Index extends IndexedDbDatabase.IndexFromTable<Table>
1494>(options: {
1495 readonly from: IndexedDbQuery.From<Table>
1496 readonly index: Index | undefined
1497}): IndexedDbQuery.DeletePartial<Table, Index> => {
1498 const self = Object.create(DeletePartialProto)
1499 self.from = options.from
1500 self.index = options.index
1501 return self as any
1502}
1503
1504const DeleteProto: Omit<
1505 IndexedDbQuery.Delete<any, never>,
1506 | "delete"
1507 | "limitValue"
1508 | "only"
1509 | "lowerBound"
1510 | "upperBound"
1511 | "excludeLowerBound"
1512 | "excludeUpperBound"
1513 | "predicate"
1514> = {
1515 ...CommonProto,
1516 ...Effectable.Prototype<IndexedDbQuery.Delete<any, never>>({
1517 label: "IndexedDbQuery.Delete",
1518 evaluate() {
1519 return applyDelete(this)
1520 }
1521 }),
1522 limit(this: IndexedDbQuery.Delete<any, never>, limit: number) {
1523 return makeDelete({
1524 ...this,
1525 limitValue: limit
1526 })
1527 },
1528 filter(this: IndexedDbQuery.Delete<any, never>, filter: (value: IndexedDbTable.Encoded<any>) => boolean) {
1529 const prev = this.predicate
1530 return makeDelete({
1531 ...this,
1532 predicate: prev ? (item) => prev(item) && filter(item) : filter
1533 })
1534 },
1535 invalidate(
1536 this: IndexedDbQuery.Delete<any, never>,
1537 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
1538 ) {
1539 keys ??= this.only !== undefined
1540 ? { [this.delete.from.table.tableName]: [this.only] }
1541 : [this.delete.from.table.tableName]
1542 return this.delete.from.reactivity.mutation(keys, this)
1543 }
1544}
1545
1546const makeDelete = <
1547 Table extends IndexedDbTable.AnyWithProps,
1548 Index extends IndexedDbDatabase.IndexFromTable<Table>
1549>(options: {
1550 readonly delete: IndexedDbQuery.DeletePartial<Table, Index>
1551 readonly limitValue?: number | undefined
1552 readonly only?: IndexedDbQuery.ExtractIndexType<Table, Index> | undefined
1553 readonly lowerBound?:
1554 | IndexedDbQuery.ExtractIndexType<Table, Index>
1555 | undefined
1556 readonly upperBound?:
1557 | IndexedDbQuery.ExtractIndexType<Table, Index>
1558 | undefined
1559 readonly excludeLowerBound?: boolean | undefined
1560 readonly excludeUpperBound?: boolean | undefined
1561 readonly predicate?: ((item: IndexedDbTable.Encoded<Table>) => boolean) | undefined
1562}): IndexedDbQuery.Delete<Table, Index> => {
1563 const self = Object.create(DeleteProto)
1564 self.delete = options.delete
1565 self.limitValue = options.limitValue
1566 self.only = options.only
1567 self.lowerBound = options.lowerBound
1568 self.upperBound = options.upperBound
1569 self.excludeLowerBound = options.excludeLowerBound ?? false
1570 self.excludeUpperBound = options.excludeUpperBound ?? false
1571 self.predicate = options.predicate
1572 return self
1573}
1574
1575const CountProto: Omit<
1576 IndexedDbQuery.Count<any, never>,
1577 | "from"
1578 | "index"
1579 | "only"
1580 | "lowerBound"
1581 | "upperBound"
1582 | "excludeLowerBound"
1583 | "excludeUpperBound"
1584> = {
1585 ...CommonProto,
1586 ...Effectable.Prototype<IndexedDbQuery.Count<any, never>>({
1587 label: "IndexedDbQuery.Count",
1588 evaluate() {
1589 return getCount(this)
1590 }
1591 }),
1592 equals(this: IndexedDbQuery.Count<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1593 return makeCount({
1594 from: this.from,
1595 index: this.index,
1596 only: value
1597 })
1598 },
1599 gte(this: IndexedDbQuery.Count<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1600 return makeCount({
1601 from: this.from,
1602 index: this.index,
1603 lowerBound: value,
1604 excludeLowerBound: false
1605 })
1606 },
1607 lte(this: IndexedDbQuery.Count<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1608 return makeCount({
1609 from: this.from,
1610 index: this.index,
1611 upperBound: value,
1612 excludeUpperBound: false
1613 })
1614 },
1615 gt(this: IndexedDbQuery.Count<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1616 return makeCount({
1617 from: this.from,
1618 index: this.index,
1619 lowerBound: value,
1620 excludeLowerBound: true
1621 })
1622 },
1623 lt(this: IndexedDbQuery.Count<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1624 return makeCount({
1625 from: this.from,
1626 index: this.index,
1627 upperBound: value,
1628 excludeUpperBound: true
1629 })
1630 },
1631 between(
1632 this: IndexedDbQuery.Count<any, never>,
1633 lowerBound: IndexedDbQuery.ExtractIndexType<any, never>,
1634 upperBound: IndexedDbQuery.ExtractIndexType<any, never>,
1635 queryOptions?: { excludeLowerBound?: boolean; excludeUpperBound?: boolean }
1636 ) {
1637 return makeCount({
1638 from: this.from,
1639 index: this.index,
1640 lowerBound,
1641 upperBound,
1642 excludeLowerBound: queryOptions?.excludeLowerBound ?? false,
1643 excludeUpperBound: queryOptions?.excludeUpperBound ?? false
1644 })
1645 }
1646}
1647
1648const makeCount = <
1649 Table extends IndexedDbTable.AnyWithProps,
1650 Index extends IndexedDbDatabase.IndexFromTable<Table>
1651>(options: {
1652 readonly from: IndexedDbQuery.From<Table>
1653 readonly index: Index | undefined
1654 readonly only?: IndexedDbQuery.ExtractIndexType<Table, Index> | undefined
1655 readonly lowerBound?:
1656 | IndexedDbQuery.ExtractIndexType<Table, Index>
1657 | undefined
1658 readonly upperBound?:
1659 | IndexedDbQuery.ExtractIndexType<Table, Index>
1660 | undefined
1661 readonly excludeLowerBound?: boolean | undefined
1662 readonly excludeUpperBound?: boolean | undefined
1663}): IndexedDbQuery.Count<Table, Index> => {
1664 const self = Object.create(CountProto)
1665 self.from = options.from
1666 self.index = options.index
1667 self.only = options.only
1668 self.lowerBound = options.lowerBound
1669 self.upperBound = options.upperBound
1670 self.excludeLowerBound = options.excludeLowerBound
1671 self.excludeUpperBound = options.excludeUpperBound
1672 return self
1673}
1674
1675const SelectProto: Omit<
1676 IndexedDbQuery.Select<any, never>,
1677 | "from"
1678 | "index"
1679 | "limitValue"
1680 | "reverseValue"
1681 | "only"
1682 | "lowerBound"
1683 | "upperBound"
1684 | "excludeLowerBound"
1685 | "excludeUpperBound"
1686> = {
1687 ...CommonProto,
1688 ...Effectable.Prototype<IndexedDbQuery.Select<any, never>>({
1689 label: "IndexedDbQuery.Select",
1690 evaluate() {
1691 return applySelect(this)
1692 }
1693 }),
1694 limit(this: IndexedDbQuery.Select<any, never>, limit: number) {
1695 return makeSelect({
1696 ...this,
1697 limitValue: limit
1698 })
1699 },
1700 offset(this: IndexedDbQuery.Select<any, never>, offset: number) {
1701 return makeSelect({
1702 ...this,
1703 offsetValue: offset
1704 })
1705 },
1706 equals(this: IndexedDbQuery.Select<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1707 return makeSelect({
1708 ...this,
1709 only: value
1710 })
1711 },
1712 gte(this: IndexedDbQuery.Select<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1713 return makeSelect({
1714 ...this,
1715 lowerBound: value,
1716 excludeLowerBound: false
1717 })
1718 },
1719 lte(this: IndexedDbQuery.Select<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1720 return makeSelect({
1721 ...this,
1722 upperBound: value,
1723 excludeUpperBound: false
1724 })
1725 },
1726 gt(this: IndexedDbQuery.Select<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1727 return makeSelect({
1728 ...this,
1729 lowerBound: value,
1730 excludeLowerBound: true
1731 })
1732 },
1733 lt(this: IndexedDbQuery.Select<any, never>, value: IndexedDbQuery.ExtractIndexType<any, never>) {
1734 return makeSelect({
1735 ...this,
1736 upperBound: value,
1737 excludeUpperBound: true
1738 })
1739 },
1740 between(
1741 this: IndexedDbQuery.Select<any, never>,
1742 lowerBound: IndexedDbQuery.ExtractIndexType<any, never>,
1743 upperBound: IndexedDbQuery.ExtractIndexType<any, never>,
1744 queryOptions?: { excludeLowerBound?: boolean; excludeUpperBound?: boolean }
1745 ) {
1746 return makeSelect({
1747 ...this,
1748 lowerBound,
1749 upperBound,
1750 excludeLowerBound: queryOptions?.excludeLowerBound ?? false,
1751 excludeUpperBound: queryOptions?.excludeUpperBound ?? false
1752 })
1753 },
1754 reverse(this: IndexedDbQuery.Select<any, never>) {
1755 return makeSelect({
1756 ...this,
1757 reverseValue: true
1758 })
1759 },
1760 first(this: IndexedDbQuery.Select<any, never>) {
1761 return makeFirst({ select: this })
1762 },
1763 filter(this: IndexedDbQuery.Select<any, never>, filter: (value: IndexedDbTable.Encoded<any>) => boolean) {
1764 const prev = this.predicate
1765 return makeSelect({
1766 ...this,
1767 predicate: prev ? (item) => prev(item) && filter(item) : filter
1768 })
1769 },
1770 stream(this: IndexedDbQuery.Select<any, never>, options?: {
1771 readonly chunkSize?: number | undefined
1772 }) {
1773 const limit = this.limitValue
1774 const chunkSize = Math.min(options?.chunkSize ?? 100, limit ?? Number.MAX_SAFE_INTEGER)
1775 const initialOffset = this.offsetValue ?? 0
1776 return Stream.suspend(() => {
1777 let total = 0
1778 const initial = this.limit(chunkSize)
1779 return Stream.paginate(initial, (select) =>
1780 Effect.map(
1781 applySelect(select as any),
1782 (data) => {
1783 total += data.length
1784 const reachedLimit = limit && total >= limit
1785 const isPartial = data.length < chunkSize
1786 const next = makeSelect({
1787 ...select,
1788 limitValue: limit === undefined ? chunkSize : Math.min(chunkSize, limit - total),
1789 offsetValue: initialOffset + total
1790 })
1791 return [data, isPartial || reachedLimit ? Option.none() : Option.some(next)] as const
1792 }
1793 ))
1794 })
1795 },
1796 reactive(
1797 this: IndexedDbQuery.Select<any, never>,
1798 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
1799 ) {
1800 keys ??= [this.from.table.tableName]
1801 return this.from.reactivity.stream(keys, this)
1802 },
1803 reactiveQueue(
1804 this: IndexedDbQuery.Select<any, never>,
1805 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
1806 ) {
1807 keys ??= [this.from.table.tableName]
1808 return this.from.reactivity.query(keys, this)
1809 }
1810}
1811
1812const makeSelect = <
1813 Table extends IndexedDbTable.AnyWithProps,
1814 Index extends IndexedDbDatabase.IndexFromTable<Table>
1815>(options: {
1816 readonly from: IndexedDbQuery.From<Table>
1817 readonly index?: Index | undefined
1818 readonly limitValue?: number | undefined
1819 readonly offsetValue?: number | undefined
1820 readonly reverseValue?: boolean | undefined
1821 readonly only?: IndexedDbQuery.ExtractIndexType<Table, Index> | undefined
1822 readonly lowerBound?:
1823 | IndexedDbQuery.ExtractIndexType<Table, Index>
1824 | undefined
1825 readonly upperBound?:
1826 | IndexedDbQuery.ExtractIndexType<Table, Index>
1827 | undefined
1828 readonly excludeLowerBound?: boolean | undefined
1829 readonly excludeUpperBound?: boolean | undefined
1830 readonly predicate?: ((item: IndexedDbTable.Encoded<Table>) => boolean) | undefined
1831}): IndexedDbQuery.Select<Table, Index> => {
1832 const self = Object.create(SelectProto)
1833 self.from = options.from
1834 self.index = options.index
1835 self.only = options.only
1836 self.limitValue = options.limitValue
1837 self.offsetValue = options.offsetValue
1838 self.reverseValue = options.reverseValue
1839 self.lowerBound = options.lowerBound
1840 self.upperBound = options.upperBound
1841 self.excludeLowerBound = options.excludeLowerBound
1842 self.excludeUpperBound = options.excludeUpperBound
1843 self.predicate = options.predicate
1844 return self as any
1845}
1846
1847const FirstProto: Omit<
1848 IndexedDbQuery.First<any, never>,
1849 "select"
1850> = {
1851 ...CommonProto,
1852 ...Effectable.Prototype<IndexedDbQuery.First<any, never>>({
1853 label: "IndexedDbQuery.First",
1854 evaluate() {
1855 return applyFirst(this)
1856 }
1857 }),
1858 reactive(
1859 this: IndexedDbQuery.First<any, never>,
1860 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
1861 ) {
1862 keys ??= this.select.only !== undefined
1863 ? [`${this.select.from.table.tableName}:${String(this.select.only)}`]
1864 : [this.select.from.table.tableName]
1865 return this.select.from.reactivity.stream(keys, this)
1866 },
1867 reactiveQueue(
1868 this: IndexedDbQuery.First<any, never>,
1869 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
1870 ) {
1871 keys ??= this.select.only !== undefined
1872 ? [`${this.select.from.table.tableName}:${this.select.only}`]
1873 : [this.select.from.table.tableName]
1874 return this.select.from.reactivity.query(keys, this)
1875 }
1876}
1877
1878const makeFirst = <
1879 Table extends IndexedDbTable.AnyWithProps,
1880 Index extends IndexedDbDatabase.IndexFromTable<Table>
1881>(options: {
1882 readonly select: IndexedDbQuery.Select<Table, Index>
1883}): IndexedDbQuery.First<Table, Index> => {
1884 const self = Object.create(FirstProto)
1885 self.select = options.select
1886 return self as any
1887}
1888
1889const ModifyProto: Omit<
1890 IndexedDbQuery.Modify<any>,
1891 | "from"
1892 | "value"
1893 | "operation"
1894> = {
1895 ...CommonProto,
1896 ...Effectable.Prototype<IndexedDbQuery.Modify<any>>({
1897 label: "IndexedDbQuery.Modify",
1898 evaluate() {
1899 return applyModify({ query: this, value: this.value })
1900 }
1901 }),
1902 invalidate(
1903 this: IndexedDbQuery.Modify<any>,
1904 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
1905 ) {
1906 const keyPath = this.from.table.keyPath
1907 keys ??= typeof keyPath === "string" && this.value[keyPath] !== undefined
1908 ? { [this.from.table.tableName]: [this.value[keyPath]] }
1909 : [this.from.table.tableName]
1910 return this.from.reactivity.mutation(keys, this)
1911 }
1912}
1913
1914const makeModify = <Table extends IndexedDbTable.AnyWithProps>(options: {
1915 readonly from: IndexedDbQuery.From<Table>
1916 readonly value: IndexedDbTable.TableSchema<Table>["Type"]
1917 readonly operation: "add" | "put"
1918}): IndexedDbQuery.Modify<Table> => {
1919 const self = Object.create(ModifyProto)
1920 self.from = options.from
1921 self.value = options.value
1922 self.operation = options.operation
1923 return self as any
1924}
1925
1926const ModifyAllProto: Omit<
1927 IndexedDbQuery.ModifyAll<any>,
1928 | "from"
1929 | "values"
1930 | "operation"
1931> = {
1932 ...CommonProto,
1933 ...Effectable.Prototype<IndexedDbQuery.ModifyAll<any>>({
1934 label: "IndexedDbQuery.ModifyAll",
1935 evaluate() {
1936 return applyModifyAll({ query: this, values: this.values })
1937 }
1938 }),
1939 invalidate(
1940 this: IndexedDbQuery.ModifyAll<any>,
1941 keys?: ReadonlyArray<unknown> | Record.ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
1942 ) {
1943 keys ??= [this.from.table.tableName]
1944 return this.from.reactivity.mutation(keys, this)
1945 }
1946}
1947
1948const makeModifyAll = <
1949 Table extends IndexedDbTable.AnyWithProps
1950>(options: {
1951 readonly from: IndexedDbQuery.From<Table>
1952 readonly values: Array<IndexedDbTable.TableSchema<Table>["Type"]>
1953 readonly operation: "add" | "put"
1954}): IndexedDbQuery.ModifyAll<Table> => {
1955 const self = Object.create(ModifyAllProto)
1956 self.from = options.from
1957 self.values = options.values
1958 self.operation = options.operation
1959 return self as any
1960}
1961
1962const QueryBuilderProto: Omit<
1963 IndexedDbQueryBuilder<any>,
1964 | "fromCache"
1965 | "tables"
1966 | "database"
1967 | "IDBKeyRange"
1968 | "IDBTransaction"
1969 | "reactivity"
1970> = {
1971 use(this: IndexedDbQueryBuilder<any>, f: (database: globalThis.IDBDatabase) => any) {
1972 return Effect.try({
1973 try: () => f(this.database.current),
1974 catch: (error) =>
1975 new IndexedDbQueryError({
1976 reason: "UnknownError",
1977 cause: error
1978 })
1979 })
1980 },
1981 from(this: IndexedDbQueryBuilder<any>, table: any) {
1982 let cached = this.fromCache.get(table)
1983 if (cached === undefined) {
1984 cached = makeFrom({
1985 database: this.database,
1986 IDBKeyRange: this.IDBKeyRange,
1987 table: this.tables.get(table)!,
1988 reactivity: this.reactivity
1989 })
1990 this.fromCache.set(table, cached)
1991 }
1992 return cached as any
1993 },
1994 get clearAll() {
1995 const self = this as IndexedDbQueryBuilder<any>
1996 return applyClearAll({ database: self.database.current })
1997 },
1998 ...CommonProto,
1999 withTransaction(this: IndexedDbQueryBuilder<any>, options: {
2000 readonly tables: NonEmptyReadonlyArray<any>
2001 readonly mode: globalThis.IDBTransactionMode
2002 readonly durability?: IDBTransactionDurability
2003 }) {
2004 return (effect) =>
2005 Effect.suspend(() => {
2006 const transaction = this.database.current.transaction(options.tables, options.mode, options)
2007 return Effect.provideService(effect, IndexedDbTransaction, transaction).pipe(
2008 Effect.onExit((exit) =>
2009 exit._tag === "Success" ? awaitTransaction(transaction) : abortTransaction(transaction)
2010 )
2011 )
2012 }).pipe(
2013 // To prevent async gaps between transaction queries
2014 Effect.provideService(References.PreventSchedulerYield, true)
2015 )
2016 }
2017}
2018
2019const abortTransaction = (transaction: globalThis.IDBTransaction) =>
2020 Effect.try({
2021 try: () => transaction.abort(),
2022 catch: () => undefined
2023 }).pipe(Effect.ignore)
2024
2025const awaitTransaction = (transaction: globalThis.IDBTransaction) =>
2026 Effect.callback<void, IndexedDbQueryError>((resume) => {
2027 transaction.oncomplete = () => {
2028 resume(Effect.void)
2029 }
2030 transaction.onabort = () => {
2031 resume(
2032 Effect.fail(
2033 new IndexedDbQueryError({
2034 reason: "TransactionError",
2035 cause: transaction.error
2036 })
2037 )
2038 )
2039 }
2040 return abortTransaction(transaction)
2041 })
2042
2043/**
2044 * Creates an `IndexedDbQueryBuilder` from an open database reference, key-range constructor, table map, and reactivity service.
2045 *
2046 * @category constructors
2047 * @since 4.0.0
2048 */
2049export const make = <Source extends IndexedDbVersion.AnyWithProps>({
2050 IDBKeyRange,
2051 database,
2052 tables,
2053 reactivity
2054}: {
2055 readonly database: MutableRef.MutableRef<globalThis.IDBDatabase>
2056 readonly IDBKeyRange: typeof globalThis.IDBKeyRange
2057 readonly tables: ReadonlyMap<string, IndexedDbVersion.Tables<Source>>
2058 readonly reactivity: Reactivity.Reactivity
2059}): IndexedDbQueryBuilder<Source> => {
2060 const self = Object.create(QueryBuilderProto)
2061 self.tables = tables
2062 self.database = database
2063 self.reactivity = reactivity
2064 self.IDBKeyRange = IDBKeyRange
2065 self.fromCache = new Map()
2066 return self
2067}
2068
2069const getOrCreateTransaction = (
2070 database: globalThis.IDBDatabase,
2071 tables: ReadonlyArray<string>,
2072 mode: globalThis.IDBTransactionMode,
2073 options?: IDBTransactionOptions
2074) => {
2075 const fiber = Fiber.getCurrent()!
2076 return Context.getOrUndefined(fiber.context, IndexedDbTransaction) ?? database.transaction(tables, mode, options)
2077}