@effect/platform-browser 4.0.0-beta.8 → 4.0.0-beta.80

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