@effect/platform-browser 4.0.0-beta.7 → 4.0.0-beta.71

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