| 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 | */ |
| 14 | import type { NonEmptyReadonlyArray } from "effect/Array" |
| 15 | import * as Cause from "effect/Cause" |
| 16 | import * as Context from "effect/Context" |
| 17 | import * as Data from "effect/Data" |
| 18 | import * as Effect from "effect/Effect" |
| 19 | import * as Effectable from "effect/Effectable" |
| 20 | import * as Fiber from "effect/Fiber" |
| 21 | import type { Inspectable } from "effect/Inspectable" |
| 22 | import { BaseProto } from "effect/Inspectable" |
| 23 | import type * as MutableRef from "effect/MutableRef" |
| 24 | import * as Option from "effect/Option" |
| 25 | import * as Pipeable from "effect/Pipeable" |
| 26 | import type * as Queue from "effect/Queue" |
| 27 | import type * as Reactivity from "effect/reactivity/Reactivity" |
| 28 | import type * as Record from "effect/Record" |
| 29 | import * as References from "effect/References" |
| 30 | import * as Schema from "effect/Schema" |
| 31 | import * as SchemaIssue from "effect/SchemaIssue" |
| 32 | import * as SchemaParser from "effect/SchemaParser" |
| 33 | import type * as Scope from "effect/Scope" |
| 34 | import * as Stream from "effect/Stream" |
| 35 | import * as Utils from "effect/Utils" |
| 36 | import type * as IndexedDb from "./IndexedDb.ts" |
| 37 | import type * as IndexedDbDatabase from "./IndexedDbDatabase.ts" |
| 38 | import type * as IndexedDbTable from "./IndexedDbTable.ts" |
| 39 | import type * as IndexedDbVersion from "./IndexedDbVersion.ts" |
| 40 | |
| 41 | const ErrorTypeId = "~@effect/platform-browser/IndexedDbQueryBuilder/IndexedDbQueryError" |
| 42 | |
| 43 | const 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 | */ |
| 62 | export 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 | */ |
| 82 | export 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 | */ |
| 104 | export 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 | */ |
| 152 | export 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 | */ |
| 162 | export 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 | */ |
| 171 | export 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 | */ |
| 730 | export class IndexedDbTransaction extends Context.Service<IndexedDbTransaction, globalThis.IDBTransaction>()( |
| 731 | "@effect/platform-browser/IndexedDbQueryBuilder/IndexedDbTransaction" |
| 732 | ) {} |
| 733 | |
| 734 | // ----------------------------------------------------------------------------- |
| 735 | // internal |
| 736 | // ----------------------------------------------------------------------------- |
| 737 | |
| 738 | type 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 | |
| 744 | type 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 | |
| 752 | const 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 | |
| 857 | const 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 | |
| 900 | const 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 | |
| 987 | const 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 | |
| 1062 | const 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 | |
| 1127 | const 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 | |
| 1244 | const 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 | |
| 1273 | const 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 | |
| 1313 | const 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 | |
| 1335 | const 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 | |
| 1410 | const 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 | |
| 1429 | const 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 | |
| 1491 | const 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 | |
| 1504 | const 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 | |
| 1546 | const 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 | |
| 1575 | const 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 | |
| 1648 | const 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 | |
| 1675 | const 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 | |
| 1812 | const 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 | |
| 1847 | const 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 | |
| 1878 | const 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 | |
| 1889 | const 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 | |
| 1914 | const 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 | |
| 1926 | const 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 | |
| 1948 | const 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 | |
| 1962 | const 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 | |
| 2019 | const abortTransaction = (transaction: globalThis.IDBTransaction) => |
| 2020 | Effect.try({ |
| 2021 | try: () => transaction.abort(), |
| 2022 | catch: () => undefined |
| 2023 | }).pipe(Effect.ignore) |
| 2024 | |
| 2025 | const 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 | */ |
| 2049 | export 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 | |
| 2069 | const 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 | } |