@effect/platform-browser 4.0.0-beta.41 → 4.0.0-beta.43

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.
@@ -0,0 +1,598 @@
1
+ /**
2
+ * @since 4.0.0
3
+ */
4
+ import * as Data from "effect/Data"
5
+ import * as Effect from "effect/Effect"
6
+ import * as Fiber from "effect/Fiber"
7
+ import * as Inspectable from "effect/Inspectable"
8
+ import * as Layer from "effect/Layer"
9
+ import * as Pipeable from "effect/Pipeable"
10
+ import * as ServiceMap from "effect/ServiceMap"
11
+ import * as Reactivity from "effect/unstable/reactivity/Reactivity"
12
+ import * as Utils from "effect/Utils"
13
+ import * as IndexedDb from "./IndexedDb.ts"
14
+ import * as IndexedDbQueryBuilder from "./IndexedDbQueryBuilder.ts"
15
+ import type * as IndexedDbTable from "./IndexedDbTable.ts"
16
+ import type * as IndexedDbVersion from "./IndexedDbVersion.ts"
17
+
18
+ const TypeId = "~@effect/platform-browser/IndexedDbDatabase"
19
+ const ErrorTypeId = "~@effect/platform-browser/IndexedDbDatabase/IndexedDbDatabaseError"
20
+
21
+ const YieldableProto = {
22
+ [Symbol.iterator]() {
23
+ return new Utils.SingleShotGen(this) as any
24
+ }
25
+ }
26
+
27
+ const PipeInspectableProto = {
28
+ ...Pipeable.Prototype,
29
+ ...Inspectable.BaseProto,
30
+ toJSON(this: any) {
31
+ return { _id: "IndexedDbDatabase" }
32
+ }
33
+ }
34
+
35
+ const CommonProto = {
36
+ [TypeId]: {
37
+ _A: (_: never) => _
38
+ },
39
+ ...PipeInspectableProto,
40
+ ...YieldableProto
41
+ }
42
+
43
+ /**
44
+ * @since 4.0.0
45
+ * @category errors
46
+ */
47
+ export type ErrorReason =
48
+ | "TransactionError"
49
+ | "MissingTable"
50
+ | "OpenError"
51
+ | "UpgradeError"
52
+ | "Aborted"
53
+ | "Blocked"
54
+ | "MissingIndex"
55
+
56
+ /**
57
+ * @since 4.0.0
58
+ * @category errors
59
+ */
60
+ export class IndexedDbDatabaseError extends Data.TaggedError(
61
+ "IndexedDbDatabaseError"
62
+ )<{
63
+ reason: ErrorReason
64
+ cause: unknown
65
+ }> {
66
+ /**
67
+ * @since 4.0.0
68
+ */
69
+ readonly [ErrorTypeId]: typeof ErrorTypeId = ErrorTypeId
70
+ override readonly message = this.reason
71
+ }
72
+
73
+ /**
74
+ * @since 4.0.0
75
+ * @category models
76
+ */
77
+ export class IndexedDbDatabase extends ServiceMap.Service<
78
+ IndexedDbDatabase,
79
+ {
80
+ readonly database: globalThis.IDBDatabase
81
+ readonly IDBKeyRange: typeof globalThis.IDBKeyRange
82
+ readonly reactivity: Reactivity.Reactivity["Service"]
83
+ }
84
+ >()(TypeId) {}
85
+
86
+ /**
87
+ * @since 4.0.0
88
+ * @category models
89
+ */
90
+ export interface IndexedDbSchema<
91
+ in out FromVersion extends IndexedDbVersion.AnyWithProps,
92
+ in out ToVersion extends IndexedDbVersion.AnyWithProps,
93
+ out Error = never
94
+ > extends
95
+ Pipeable.Pipeable,
96
+ Inspectable.Inspectable,
97
+ Effect.YieldableClass<
98
+ IndexedDbQueryBuilder.IndexedDbQueryBuilder<ToVersion>,
99
+ never,
100
+ IndexedDbDatabase
101
+ >
102
+ {
103
+ new(_: never): {}
104
+
105
+ readonly previous: [FromVersion] extends [never] ? undefined
106
+ : IndexedDbSchema<never, FromVersion, Error>
107
+ readonly fromVersion: FromVersion
108
+ readonly version: ToVersion
109
+
110
+ readonly migrate: [FromVersion] extends [never] ? (query: Transaction<ToVersion>) => Effect.Effect<void, Error>
111
+ : (
112
+ fromQuery: Transaction<FromVersion>,
113
+ toQuery: Transaction<ToVersion>
114
+ ) => Effect.Effect<void, Error>
115
+
116
+ readonly add: <Version extends IndexedDbVersion.AnyWithProps, MigrationError>(
117
+ version: Version,
118
+ migrate: (
119
+ fromQuery: Transaction<ToVersion>,
120
+ toQuery: Transaction<Version>
121
+ ) => Effect.Effect<void, MigrationError>
122
+ ) => IndexedDbSchema<ToVersion, Version, MigrationError | Error>
123
+
124
+ readonly getQueryBuilder: Effect.Effect<
125
+ IndexedDbQueryBuilder.IndexedDbQueryBuilder<ToVersion>,
126
+ never,
127
+ IndexedDbDatabase
128
+ >
129
+
130
+ readonly layer: (
131
+ databaseName: string
132
+ ) => Layer.Layer<
133
+ IndexedDbDatabase,
134
+ IndexedDbDatabaseError,
135
+ IndexedDb.IndexedDb
136
+ >
137
+ }
138
+
139
+ /**
140
+ * @since 4.0.0
141
+ * @category models
142
+ */
143
+ export interface Transaction<
144
+ Source extends IndexedDbVersion.AnyWithProps = never
145
+ > extends Pipeable.Pipeable, Omit<IndexedDbQueryBuilder.IndexedDbQueryBuilder<Source>, "transaction"> {
146
+ readonly transaction: globalThis.IDBTransaction
147
+
148
+ readonly createObjectStore: <
149
+ A extends IndexedDbTable.TableName<IndexedDbVersion.Tables<Source>>
150
+ >(
151
+ table: A
152
+ ) => Effect.Effect<globalThis.IDBObjectStore, IndexedDbDatabaseError>
153
+
154
+ readonly deleteObjectStore: <
155
+ A extends IndexedDbTable.TableName<IndexedDbVersion.Tables<Source>>
156
+ >(
157
+ table: A
158
+ ) => Effect.Effect<void, IndexedDbDatabaseError>
159
+
160
+ readonly createIndex: <
161
+ Name extends IndexedDbTable.TableName<IndexedDbVersion.Tables<Source>>
162
+ >(
163
+ table: Name,
164
+ indexName: IndexFromTableName<Source, Name>,
165
+ options?: IDBIndexParameters
166
+ ) => Effect.Effect<globalThis.IDBIndex, IndexedDbDatabaseError>
167
+
168
+ readonly deleteIndex: <
169
+ Name extends IndexedDbTable.TableName<IndexedDbVersion.Tables<Source>>
170
+ >(
171
+ table: Name,
172
+ indexName: IndexFromTableName<Source, Name>
173
+ ) => Effect.Effect<void, IndexedDbDatabaseError>
174
+ }
175
+
176
+ /**
177
+ * @since 4.0.0
178
+ * @category models
179
+ */
180
+ export type IndexFromTable<Table extends IndexedDbTable.AnyWithProps> = IsStringLiteral<
181
+ Extract<keyof IndexedDbTable.Indexes<Table>, string>
182
+ > extends true ? Extract<keyof IndexedDbTable.Indexes<Table>, string>
183
+ : never
184
+
185
+ /**
186
+ * @since 4.0.0
187
+ * @category models
188
+ */
189
+ export type IndexFromTableName<
190
+ Version extends IndexedDbVersion.AnyWithProps,
191
+ Table extends string
192
+ > = IndexFromTable<
193
+ IndexedDbTable.WithName<IndexedDbVersion.Tables<Version>, Table>
194
+ >
195
+
196
+ /**
197
+ * @since 4.0.0
198
+ * @category models
199
+ */
200
+ export interface Any {
201
+ readonly previous?: Any | undefined
202
+ readonly layer: (
203
+ databaseName: string
204
+ ) => Layer.Layer<
205
+ IndexedDbDatabase,
206
+ IndexedDbDatabaseError,
207
+ IndexedDb.IndexedDb
208
+ >
209
+ }
210
+
211
+ /**
212
+ * @since 4.0.0
213
+ * @category models
214
+ */
215
+ export type AnySchema = IndexedDbSchema<
216
+ IndexedDbVersion.AnyWithProps,
217
+ IndexedDbVersion.AnyWithProps,
218
+ any
219
+ >
220
+
221
+ /**
222
+ * @since 4.0.0
223
+ * @category constructors
224
+ */
225
+ export const make = <
226
+ InitialVersion extends IndexedDbVersion.AnyWithProps,
227
+ Error
228
+ >(
229
+ initialVersion: InitialVersion,
230
+ init: (toQuery: Transaction<InitialVersion>) => Effect.Effect<void, Error>
231
+ ): IndexedDbSchema<never, InitialVersion, Error> =>
232
+ (function() {
233
+ // oxlint-disable-next-line typescript/no-extraneous-class
234
+ class Initial {}
235
+ Object.assign(Initial, CommonProto)
236
+ ;(Initial as any).version = initialVersion
237
+ ;(Initial as any).migrate = init
238
+ ;(Initial as any)._tag = "Initial"
239
+ ;(Initial as any).add = <Version extends IndexedDbVersion.AnyWithProps>(
240
+ version: Version,
241
+ migrate: (
242
+ fromQuery: Transaction<InitialVersion>,
243
+ toQuery: Transaction<Version>
244
+ ) => Effect.Effect<void, Error>
245
+ ) =>
246
+ makeProto({
247
+ fromVersion: initialVersion,
248
+ version,
249
+ migrate,
250
+ previous: Initial as any
251
+ })
252
+ ;(Initial as any).getQueryBuilder = Effect.gen(function*() {
253
+ const { IDBKeyRange, database, reactivity } = yield* IndexedDbDatabase
254
+ return IndexedDbQueryBuilder.make({
255
+ database,
256
+ IDBKeyRange,
257
+ tables: initialVersion.tables,
258
+ transaction: undefined,
259
+ reactivity
260
+ })
261
+ })
262
+ ;(Initial as any).asEffect = function() {
263
+ return this.getQueryBuilder
264
+ }
265
+ ;(Initial as any).layer = <DatabaseName extends string>(
266
+ databaseName: DatabaseName
267
+ ) => layer(databaseName, Initial as any)
268
+
269
+ return Initial as any
270
+ })()
271
+
272
+ const makeProto = <
273
+ FromVersion extends IndexedDbVersion.AnyWithProps,
274
+ ToVersion extends IndexedDbVersion.AnyWithProps,
275
+ Error
276
+ >(options: {
277
+ readonly previous:
278
+ | IndexedDbSchema<FromVersion, ToVersion, Error>
279
+ | IndexedDbSchema<never, FromVersion, Error>
280
+ readonly fromVersion: FromVersion
281
+ readonly version: ToVersion
282
+ readonly migrate: (
283
+ fromQuery: Transaction<FromVersion>,
284
+ toQuery: Transaction<ToVersion>
285
+ ) => Effect.Effect<void, Error>
286
+ }): IndexedDbSchema<FromVersion, ToVersion, Error> =>
287
+ (function() {
288
+ // oxlint-disable-next-line typescript/no-extraneous-class
289
+ class Migration {}
290
+ Object.assign(Migration, CommonProto)
291
+ ;(Migration as any).previous = options.previous
292
+ ;(Migration as any).fromVersion = options.fromVersion
293
+ ;(Migration as any).version = options.version
294
+ ;(Migration as any).migrate = options.migrate
295
+ ;(Migration as any)._tag = "Migration"
296
+ ;(Migration as any).getQueryBuilder = Effect.gen(function*() {
297
+ const { IDBKeyRange, database, reactivity } = yield* IndexedDbDatabase
298
+ return IndexedDbQueryBuilder.make({
299
+ database,
300
+ IDBKeyRange,
301
+ tables: options.version.tables,
302
+ transaction: undefined,
303
+ reactivity
304
+ })
305
+ })
306
+ ;(Migration as any).asEffect = function() {
307
+ return this.getQueryBuilder
308
+ }
309
+ ;(Migration as any).layer = <DatabaseName extends string>(
310
+ databaseName: DatabaseName
311
+ ) => layer(databaseName, Migration as any)
312
+
313
+ return Migration as any
314
+ })()
315
+
316
+ const layer = <DatabaseName extends string>(
317
+ databaseName: DatabaseName,
318
+ migration: Any
319
+ ) =>
320
+ Layer.effect(
321
+ IndexedDbDatabase,
322
+ Effect.gen(function*() {
323
+ const { IDBKeyRange, indexedDB } = yield* IndexedDb.IndexedDb
324
+ const reactivity = yield* Reactivity.Reactivity
325
+ const serviceMap = yield* Effect.services()
326
+ const runForkWith = Effect.runForkWith(serviceMap)
327
+
328
+ let oldVersion = 0
329
+ const migrations: Array<Any> = []
330
+ let current = migration
331
+ while (current) {
332
+ migrations.unshift(current)
333
+ current = (current as unknown as AnySchema).previous as any
334
+ }
335
+
336
+ const version = migrations.length
337
+ const database = yield* Effect.acquireRelease(
338
+ Effect.callback<globalThis.IDBDatabase, IndexedDbDatabaseError>(
339
+ (resume) => {
340
+ const request = indexedDB.open(databaseName, version)
341
+
342
+ request.onblocked = (event) => {
343
+ resume(
344
+ Effect.fail(
345
+ new IndexedDbDatabaseError({
346
+ reason: "Blocked",
347
+ cause: event
348
+ })
349
+ )
350
+ )
351
+ }
352
+
353
+ request.onerror = (event) => {
354
+ const idbRequest = event.target as IDBRequest<IDBDatabase>
355
+
356
+ resume(
357
+ Effect.fail(
358
+ new IndexedDbDatabaseError({
359
+ reason: "OpenError",
360
+ cause: idbRequest.error
361
+ })
362
+ )
363
+ )
364
+ }
365
+
366
+ let fiber: Fiber.Fiber<void, IndexedDbDatabaseError> | undefined
367
+ request.onupgradeneeded = (event) => {
368
+ const idbRequest = event.target as IDBRequest<IDBDatabase>
369
+ const database = idbRequest.result
370
+ const transaction = idbRequest.transaction
371
+ oldVersion = event.oldVersion
372
+
373
+ if (transaction === null) {
374
+ return resume(
375
+ Effect.fail(
376
+ new IndexedDbDatabaseError({
377
+ reason: "TransactionError",
378
+ cause: null
379
+ })
380
+ )
381
+ )
382
+ }
383
+
384
+ transaction.onabort = (event) => {
385
+ resume(
386
+ Effect.fail(
387
+ new IndexedDbDatabaseError({
388
+ reason: "Aborted",
389
+ cause: event
390
+ })
391
+ )
392
+ )
393
+ }
394
+
395
+ transaction.onerror = (event) => {
396
+ resume(
397
+ Effect.fail(
398
+ new IndexedDbDatabaseError({
399
+ reason: "TransactionError",
400
+ cause: event
401
+ })
402
+ )
403
+ )
404
+ }
405
+
406
+ const effect = Effect.forEach(
407
+ migrations.slice(oldVersion),
408
+ (untypedMigration) => {
409
+ if (untypedMigration.previous === undefined) {
410
+ const migration = untypedMigration as any as AnySchema
411
+ const api = makeTransactionProto({
412
+ database,
413
+ IDBKeyRange,
414
+ tables: migration.version.tables,
415
+ transaction,
416
+ reactivity
417
+ })
418
+ return (migration as any).migrate(api) as Effect.Effect<
419
+ void,
420
+ IndexedDbDatabaseError
421
+ >
422
+ } else if (untypedMigration.previous) {
423
+ const migration = untypedMigration as any as AnySchema
424
+ const fromApi = makeTransactionProto({
425
+ database,
426
+ IDBKeyRange,
427
+ tables: migration.fromVersion.tables,
428
+ transaction,
429
+ reactivity
430
+ })
431
+ const toApi = makeTransactionProto({
432
+ database,
433
+ IDBKeyRange,
434
+ tables: migration.version.tables,
435
+ transaction,
436
+ reactivity
437
+ })
438
+ return migration.migrate(fromApi, toApi) as Effect.Effect<
439
+ void,
440
+ IndexedDbDatabaseError
441
+ >
442
+ }
443
+
444
+ return Effect.die(new Error("Invalid migration"))
445
+ },
446
+ { discard: true }
447
+ ).pipe(
448
+ Effect.mapError(
449
+ (cause) =>
450
+ new IndexedDbDatabaseError({
451
+ reason: "UpgradeError",
452
+ cause
453
+ })
454
+ )
455
+ )
456
+ fiber = runForkWith(effect)
457
+ fiber.currentDispatcher.flush()
458
+ }
459
+
460
+ request.onsuccess = (event) => {
461
+ const idbRequest = event.target as IDBRequest<IDBDatabase>
462
+ const database = idbRequest.result
463
+ if (fiber) {
464
+ // ensure migration errors are propagated
465
+ resume(Effect.as(Fiber.join(fiber), database))
466
+ } else {
467
+ resume(Effect.succeed(database))
468
+ }
469
+ }
470
+ }
471
+ ),
472
+ (database) => Effect.sync(() => database.close())
473
+ )
474
+
475
+ return IndexedDbDatabase.of({ database, IDBKeyRange, reactivity })
476
+ })
477
+ ).pipe(
478
+ Layer.provide(Reactivity.layer)
479
+ )
480
+
481
+ // -----------------------------------------------------------------------------
482
+ // Internal
483
+ // -----------------------------------------------------------------------------
484
+
485
+ type IsStringLiteral<T> = T extends string ? string extends T ? false
486
+ : true
487
+ : false
488
+
489
+ const makeTransactionProto = <Source extends IndexedDbVersion.AnyWithProps>({
490
+ IDBKeyRange,
491
+ database,
492
+ tables,
493
+ transaction,
494
+ reactivity
495
+ }: {
496
+ readonly database: globalThis.IDBDatabase
497
+ readonly IDBKeyRange: typeof globalThis.IDBKeyRange
498
+ readonly tables: ReadonlyMap<string, IndexedDbVersion.Tables<Source>>
499
+ readonly transaction: globalThis.IDBTransaction
500
+ readonly reactivity: Reactivity.Reactivity["Service"]
501
+ }): Transaction<Source> => {
502
+ const migration = IndexedDbQueryBuilder.make({
503
+ database,
504
+ IDBKeyRange,
505
+ tables,
506
+ transaction,
507
+ reactivity
508
+ }) as any
509
+
510
+ migration.transaction = transaction
511
+ migration.createObjectStore = Effect.fnUntraced(function*(table: string) {
512
+ const createTable = yield* Effect.fromNullishOr(tables.get(table)).pipe(
513
+ Effect.mapError(
514
+ (cause) =>
515
+ new IndexedDbDatabaseError({
516
+ reason: "MissingTable",
517
+ cause
518
+ })
519
+ )
520
+ )
521
+
522
+ return yield* Effect.try({
523
+ try: () =>
524
+ database.createObjectStore(createTable.tableName, {
525
+ keyPath: createTable.keyPath,
526
+ autoIncrement: createTable.autoIncrement
527
+ }),
528
+ catch: (cause) =>
529
+ new IndexedDbDatabaseError({
530
+ reason: "TransactionError",
531
+ cause
532
+ })
533
+ })
534
+ })
535
+
536
+ migration.deleteObjectStore = Effect.fnUntraced(function*(table: string) {
537
+ const createTable = yield* Effect.fromNullishOr(tables.get(table)).pipe(
538
+ Effect.mapError(
539
+ (cause) =>
540
+ new IndexedDbDatabaseError({
541
+ reason: "MissingTable",
542
+ cause
543
+ })
544
+ )
545
+ )
546
+
547
+ return yield* Effect.try({
548
+ try: () => database.deleteObjectStore(createTable.tableName),
549
+ catch: (cause) =>
550
+ new IndexedDbDatabaseError({
551
+ reason: "TransactionError",
552
+ cause
553
+ })
554
+ })
555
+ })
556
+
557
+ migration.createIndex = Effect.fnUntraced(function*(
558
+ table: string,
559
+ indexName: string,
560
+ options?: IDBIndexParameters
561
+ ) {
562
+ const store = transaction.objectStore(table)
563
+ const sourceTable = tables.get(table)!
564
+
565
+ const keyPath = yield* Effect.fromNullishOr(
566
+ sourceTable.indexes[indexName]
567
+ ).pipe(
568
+ Effect.mapError(
569
+ (cause) =>
570
+ new IndexedDbDatabaseError({
571
+ reason: "MissingIndex",
572
+ cause
573
+ })
574
+ )
575
+ )
576
+
577
+ return yield* Effect.try({
578
+ try: () => store.createIndex(indexName, keyPath, options),
579
+ catch: (cause) =>
580
+ new IndexedDbDatabaseError({
581
+ reason: "TransactionError",
582
+ cause
583
+ })
584
+ })
585
+ })
586
+
587
+ migration.deleteIndex = (table: string, indexName: string) =>
588
+ Effect.try({
589
+ try: () => transaction.objectStore(table).deleteIndex(indexName),
590
+ catch: (cause) =>
591
+ new IndexedDbDatabaseError({
592
+ reason: "TransactionError",
593
+ cause
594
+ })
595
+ })
596
+
597
+ return migration
598
+ }