@effect/platform-browser 4.0.0-beta.6 → 4.0.0-beta.60

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 (59) hide show
  1. package/dist/BrowserHttpClient.d.ts +3 -3
  2. package/dist/BrowserHttpClient.d.ts.map +1 -1
  3. package/dist/BrowserHttpClient.js +11 -6
  4. package/dist/BrowserHttpClient.js.map +1 -1
  5. package/dist/BrowserPersistence.d.ts +17 -0
  6. package/dist/BrowserPersistence.d.ts.map +1 -0
  7. package/dist/BrowserPersistence.js +184 -0
  8. package/dist/BrowserPersistence.js.map +1 -0
  9. package/dist/BrowserWorkerRunner.js +1 -1
  10. package/dist/BrowserWorkerRunner.js.map +1 -1
  11. package/dist/Clipboard.d.ts +6 -3
  12. package/dist/Clipboard.d.ts.map +1 -1
  13. package/dist/Clipboard.js +2 -2
  14. package/dist/Clipboard.js.map +1 -1
  15. package/dist/Geolocation.d.ts +6 -6
  16. package/dist/Geolocation.d.ts.map +1 -1
  17. package/dist/Geolocation.js +2 -2
  18. package/dist/Geolocation.js.map +1 -1
  19. package/dist/IndexedDb.d.ts +50 -0
  20. package/dist/IndexedDb.d.ts.map +1 -0
  21. package/dist/IndexedDb.js +65 -0
  22. package/dist/IndexedDb.js.map +1 -0
  23. package/dist/IndexedDbDatabase.d.ts +103 -0
  24. package/dist/IndexedDbDatabase.d.ts.map +1 -0
  25. package/dist/IndexedDbDatabase.js +291 -0
  26. package/dist/IndexedDbDatabase.js.map +1 -0
  27. package/dist/IndexedDbQueryBuilder.d.ts +342 -0
  28. package/dist/IndexedDbQueryBuilder.d.ts.map +1 -0
  29. package/dist/IndexedDbQueryBuilder.js +888 -0
  30. package/dist/IndexedDbQueryBuilder.js.map +1 -0
  31. package/dist/IndexedDbTable.d.ts +109 -0
  32. package/dist/IndexedDbTable.d.ts.map +1 -0
  33. package/dist/IndexedDbTable.js +38 -0
  34. package/dist/IndexedDbTable.js.map +1 -0
  35. package/dist/IndexedDbVersion.d.ts +50 -0
  36. package/dist/IndexedDbVersion.d.ts.map +1 -0
  37. package/dist/IndexedDbVersion.js +23 -0
  38. package/dist/IndexedDbVersion.js.map +1 -0
  39. package/dist/Permissions.d.ts +8 -5
  40. package/dist/Permissions.d.ts.map +1 -1
  41. package/dist/Permissions.js +2 -2
  42. package/dist/Permissions.js.map +1 -1
  43. package/dist/index.d.ts +24 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +24 -0
  46. package/dist/index.js.map +1 -1
  47. package/package.json +4 -3
  48. package/src/BrowserHttpClient.ts +17 -10
  49. package/src/BrowserPersistence.ts +303 -0
  50. package/src/BrowserWorkerRunner.ts +1 -1
  51. package/src/Clipboard.ts +2 -2
  52. package/src/Geolocation.ts +2 -2
  53. package/src/IndexedDb.ts +97 -0
  54. package/src/IndexedDbDatabase.ts +598 -0
  55. package/src/IndexedDbQueryBuilder.ts +1904 -0
  56. package/src/IndexedDbTable.ts +203 -0
  57. package/src/IndexedDbVersion.ts +89 -0
  58. package/src/Permissions.ts +2 -2
  59. package/src/index.ts +30 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import type * as Arr from "effect/Array"
5
+ import * as Clock from "effect/Clock"
6
+ import type * as Duration from "effect/Duration"
7
+ import * as Effect from "effect/Effect"
8
+ import * as Layer from "effect/Layer"
9
+ import * as Persistence from "effect/unstable/persistence/Persistence"
10
+
11
+ /**
12
+ * @since 1.0.0
13
+ * @category layers
14
+ */
15
+ export const layerBackingIndexedDb = (options?: {
16
+ readonly database?: string | undefined
17
+ }): Layer.Layer<Persistence.BackingPersistence> =>
18
+ Layer.effect(Persistence.BackingPersistence)(Effect.gen(function*() {
19
+ const db = yield* Effect.acquireRelease(
20
+ openDatabase(options?.database ?? defaultDatabase),
21
+ (db) => Effect.sync(() => db.close())
22
+ ).pipe(Effect.orDie)
23
+
24
+ return Persistence.BackingPersistence.of({
25
+ make: Effect.fnUntraced(function*(storeId) {
26
+ const clock = yield* Clock.Clock
27
+ return {
28
+ get: (key) => get(db, clock, storeId, key),
29
+ getMany: (keys) => getMany(db, clock, storeId, keys),
30
+ set: (key, value, ttl) => set(db, clock, storeId, key, value, ttl),
31
+ setMany: (entries) => setMany(db, clock, storeId, entries),
32
+ remove: (key) => remove(db, storeId, key),
33
+ clear: clear(db, storeId)
34
+ }
35
+ })
36
+ })
37
+ }))
38
+
39
+ const defaultDatabase = "effect_persistence"
40
+ const databaseVersion = 1
41
+ const entriesStoreName = "entries"
42
+ const storeIdIndexName = "storeId"
43
+
44
+ /**
45
+ * @since 1.0.0
46
+ * @category layers
47
+ */
48
+ export const layerIndexedDb = (options?: {
49
+ readonly database?: string | undefined
50
+ }): Layer.Layer<Persistence.Persistence> =>
51
+ Persistence.layer.pipe(
52
+ Layer.provide(layerBackingIndexedDb(options))
53
+ )
54
+
55
+ const openDatabase = (database: string): Effect.Effect<IDBDatabase, Persistence.PersistenceError> =>
56
+ Effect.gen(function*() {
57
+ const openRequest = yield* Effect.try({
58
+ try: () => globalThis.indexedDB.open(database, databaseVersion),
59
+ catch: (cause) =>
60
+ new Persistence.PersistenceError({
61
+ message: "Failed to open backing store database",
62
+ cause
63
+ })
64
+ })
65
+
66
+ openRequest.onupgradeneeded = () => {
67
+ const db = openRequest.result
68
+ const entries = db.objectStoreNames.contains(entriesStoreName)
69
+ ? openRequest.transaction?.objectStore(entriesStoreName)
70
+ : db.createObjectStore(entriesStoreName, { keyPath: ["storeId", "id"] })
71
+ if (entries && !entries.indexNames.contains(storeIdIndexName)) {
72
+ entries.createIndex(storeIdIndexName, storeIdIndexName, { unique: false })
73
+ }
74
+ }
75
+
76
+ return yield* idbRequest("Failed to open backing store database", () => openRequest)
77
+ })
78
+
79
+ interface EntryRow {
80
+ readonly storeId: string
81
+ readonly id: string
82
+ readonly value: object
83
+ readonly expires: number | null
84
+ }
85
+
86
+ const isExpired = (row: EntryRow, now: number): boolean => row.expires !== null && row.expires <= now
87
+
88
+ const get = (
89
+ db: IDBDatabase,
90
+ clock: Clock.Clock,
91
+ storeId: string,
92
+ key: string
93
+ ): Effect.Effect<object | undefined, Persistence.PersistenceError> =>
94
+ withEntriesTransaction<object | undefined>(
95
+ db,
96
+ "readwrite",
97
+ `Failed to get key ${key} from backing store`,
98
+ (
99
+ entries,
100
+ setResult,
101
+ fail
102
+ ) => {
103
+ const now = clock.currentTimeMillisUnsafe()
104
+ const id: [string, string] = [storeId, key]
105
+ const request = entries.get(id)
106
+ request.onerror = () => fail(request.error)
107
+ request.onsuccess = () => {
108
+ const row = request.result as EntryRow | undefined
109
+ if (!row || !isExpired(row, now)) {
110
+ setResult(row?.value)
111
+ return
112
+ }
113
+
114
+ const deleteRequest = entries.delete(id)
115
+ deleteRequest.onerror = () => fail(deleteRequest.error)
116
+ deleteRequest.onsuccess = () => setResult(undefined)
117
+ }
118
+ }
119
+ )
120
+
121
+ const getMany = (
122
+ db: IDBDatabase,
123
+ clock: Clock.Clock,
124
+ storeId: string,
125
+ keys: Arr.NonEmptyArray<string>
126
+ ): Effect.Effect<Arr.NonEmptyArray<object | undefined>, Persistence.PersistenceError> =>
127
+ withEntriesTransaction(
128
+ db,
129
+ "readwrite",
130
+ "Failed to getMany from backing store",
131
+ (entries, setResult, fail) => {
132
+ const now = clock.currentTimeMillisUnsafe()
133
+ const results = new Array<object | undefined>(keys.length)
134
+ setResult(results as any)
135
+
136
+ for (let i = 0; i < keys.length; i++) {
137
+ const key = keys[i]
138
+ const keyPath = [storeId, key]
139
+ const request = entries.get(keyPath)
140
+ request.onerror = () => fail(request.error)
141
+ request.onsuccess = () => {
142
+ const row = request.result as EntryRow | undefined
143
+ if (!row) return
144
+ else if (!isExpired(row, now)) {
145
+ results[i] = row.value
146
+ return
147
+ }
148
+ const deleteRequest = entries.delete(keyPath)
149
+ deleteRequest.onerror = () => fail(deleteRequest.error)
150
+ }
151
+ }
152
+ }
153
+ )
154
+
155
+ const set = (
156
+ db: IDBDatabase,
157
+ clock: Clock.Clock,
158
+ storeId: string,
159
+ key: string,
160
+ value: object,
161
+ ttl: Duration.Duration | undefined
162
+ ): Effect.Effect<void, Persistence.PersistenceError> =>
163
+ withEntriesTransaction(
164
+ db,
165
+ "readwrite",
166
+ `Failed to set key ${key} in backing store`,
167
+ (entries, setResult, fail) => {
168
+ const request = entries.put(
169
+ {
170
+ storeId,
171
+ id: key,
172
+ value,
173
+ expires: Persistence.unsafeTtlToExpires(clock, ttl)
174
+ } satisfies EntryRow
175
+ )
176
+ request.onerror = () => fail(request.error)
177
+ request.onsuccess = () => setResult(undefined)
178
+ }
179
+ )
180
+
181
+ const setMany = (
182
+ db: IDBDatabase,
183
+ clock: Clock.Clock,
184
+ storeId: string,
185
+ entries: Arr.NonEmptyArray<readonly [key: string, value: object, ttl: Duration.Duration | undefined]>
186
+ ): Effect.Effect<void, Persistence.PersistenceError> =>
187
+ withEntriesTransaction(
188
+ db,
189
+ "readwrite",
190
+ "Failed to setMany in backing store",
191
+ (store, setResult, fail) => {
192
+ for (const [key, value, ttl] of entries) {
193
+ const request = store.put(
194
+ {
195
+ storeId,
196
+ id: key,
197
+ value,
198
+ expires: Persistence.unsafeTtlToExpires(clock, ttl)
199
+ } satisfies EntryRow
200
+ )
201
+ request.onerror = () => fail(request.error)
202
+ request.onsuccess = () => setResult(undefined)
203
+ }
204
+ }
205
+ )
206
+
207
+ const remove = (
208
+ db: IDBDatabase,
209
+ storeId: string,
210
+ key: string
211
+ ): Effect.Effect<void, Persistence.PersistenceError> =>
212
+ withEntriesTransaction(
213
+ db,
214
+ "readwrite",
215
+ `Failed to remove key ${key} from backing store`,
216
+ (entries, setResult, fail) => {
217
+ const request = entries.delete([storeId, key])
218
+ request.onerror = () => fail(request.error)
219
+ request.onsuccess = () => setResult(undefined)
220
+ }
221
+ )
222
+
223
+ const clear = (db: IDBDatabase, storeId: string): Effect.Effect<void, Persistence.PersistenceError> =>
224
+ withEntriesTransaction(db, "readwrite", "Failed to clear backing store", (entries, setResult, fail) => {
225
+ const index = entries.index(storeIdIndexName)
226
+ const cursorRequest = index.openCursor(storeId)
227
+ cursorRequest.onerror = () => fail(cursorRequest.error)
228
+ cursorRequest.onsuccess = () => {
229
+ const cursor = cursorRequest.result
230
+ if (!cursor) {
231
+ setResult(undefined)
232
+ return
233
+ }
234
+ const deleteRequest = cursor.delete()
235
+ deleteRequest.onerror = () => fail(deleteRequest.error)
236
+ deleteRequest.onsuccess = () => cursor.continue()
237
+ }
238
+ })
239
+
240
+ const withEntriesTransaction = <A>(
241
+ db: IDBDatabase,
242
+ mode: IDBTransactionMode,
243
+ message: string,
244
+ run: (
245
+ entries: IDBObjectStore,
246
+ onResult: (result: A) => void,
247
+ fail: (cause: unknown) => void
248
+ ) => void
249
+ ): Effect.Effect<A, Persistence.PersistenceError> =>
250
+ Effect.callback<A, Persistence.PersistenceError>((resume) => {
251
+ const tx = db.transaction(entriesStoreName, mode)
252
+ const entries = tx.objectStore(entriesStoreName)
253
+
254
+ let result: A | undefined
255
+ let setResult = false
256
+ let done = false
257
+
258
+ const fail = (cause: unknown) => {
259
+ done = true
260
+ resume(Effect.fail(new Persistence.PersistenceError({ message, cause })))
261
+ }
262
+
263
+ tx.oncomplete = () => {
264
+ done = true
265
+ if (setResult) resume(Effect.succeed(result!))
266
+ }
267
+ tx.onerror = () => {
268
+ done = true
269
+ fail(tx.error)
270
+ }
271
+ tx.onabort = () => {
272
+ done = true
273
+ fail(tx.error)
274
+ }
275
+
276
+ run(entries, (next) => {
277
+ if (done) return resume(Effect.succeed(next))
278
+ setResult = true
279
+ result = next
280
+ }, fail)
281
+
282
+ return Effect.sync(() => {
283
+ tx.abort()
284
+ })
285
+ })
286
+
287
+ const idbRequest = <A>(
288
+ message: string,
289
+ evaluate: () => IDBRequest<A>
290
+ ): Effect.Effect<A, Persistence.PersistenceError> =>
291
+ Effect.callback<A, Persistence.PersistenceError>((resume) => {
292
+ const request = evaluate()
293
+ const fail = (cause: unknown) => {
294
+ resume(Effect.fail(new Persistence.PersistenceError({ message, cause })))
295
+ }
296
+ if (request.readyState === "done") {
297
+ resume(Effect.succeed(request.result))
298
+ }
299
+ request.onsuccess = () => {
300
+ resume(Effect.succeed(request.result))
301
+ }
302
+ request.onerror = () => fail(request.error)
303
+ })
@@ -44,7 +44,7 @@ export const make = (self: MessagePort | Window): WorkerRunner.WorkerRunnerPlatf
44
44
  Effect.scopedWith(Effect.fnUntraced(function*(scope) {
45
45
  const closeLatch = Deferred.makeUnsafe<void, WorkerError>()
46
46
  const trackFiber = Fiber.runIn(scope)
47
- const services = yield* Effect.services<R>()
47
+ const services = yield* Effect.context<R>()
48
48
  const runFork = Effect.runForkWith(services)
49
49
  const onExit = (exit: Exit.Exit<any, E>) => {
50
50
  if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) {
package/src/Clipboard.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import * as Context from "effect/Context"
4
5
  import * as Data from "effect/Data"
5
6
  import * as Effect from "effect/Effect"
6
7
  import * as Layer from "effect/Layer"
7
- import * as ServiceMap from "effect/ServiceMap"
8
8
 
9
9
  const TypeId = "~@effect/platform-browser/Clipboard"
10
10
  const ErrorTypeId = "~@effect/platform-browser/Clipboard/ClipboardError"
@@ -38,7 +38,7 @@ export class ClipboardError extends Data.TaggedError("ClipboardError")<{
38
38
  * @since 1.0.0
39
39
  * @category Service
40
40
  */
41
- export const Clipboard: ServiceMap.Service<Clipboard, Clipboard> = ServiceMap.Service<Clipboard>(TypeId)
41
+ export const Clipboard: Context.Service<Clipboard, Clipboard> = Context.Service<Clipboard>(TypeId)
42
42
 
43
43
  /**
44
44
  * @since 1.0.0
@@ -2,11 +2,11 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
  import * as Cause from "effect/Cause"
5
+ import * as Context from "effect/Context"
5
6
  import * as Data from "effect/Data"
6
7
  import * as Effect from "effect/Effect"
7
8
  import * as Layer from "effect/Layer"
8
9
  import * as Queue from "effect/Queue"
9
- import * as ServiceMap from "effect/ServiceMap"
10
10
  import * as Stream from "effect/Stream"
11
11
 
12
12
  const TypeId = "~@effect/platform-browser/Geolocation"
@@ -34,7 +34,7 @@ export interface Geolocation {
34
34
  * @since 1.0.0
35
35
  * @category Service
36
36
  */
37
- export const Geolocation: ServiceMap.Service<Geolocation, Geolocation> = ServiceMap.Service<Geolocation>(TypeId)
37
+ export const Geolocation: Context.Service<Geolocation, Geolocation> = Context.Service<Geolocation>(TypeId)
38
38
 
39
39
  /**
40
40
  * @since 1.0.0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @since 4.0.0
3
+ */
4
+ import * as Config from "effect/Config"
5
+ import * as Context from "effect/Context"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Layer from "effect/Layer"
8
+ import * as Schema from "effect/Schema"
9
+ import * as SchemaIssue from "effect/SchemaIssue"
10
+
11
+ const TypeId = "~@effect/platform-browser/IndexedDb"
12
+
13
+ /**
14
+ * @since 4.0.0
15
+ * @category models
16
+ */
17
+ export interface IndexedDb {
18
+ readonly [TypeId]: typeof TypeId
19
+ readonly indexedDB: globalThis.IDBFactory
20
+ readonly IDBKeyRange: typeof globalThis.IDBKeyRange
21
+ }
22
+
23
+ /**
24
+ * @since 4.0.0
25
+ * @category tag
26
+ */
27
+ export const IndexedDb: Context.Service<IndexedDb, IndexedDb> = Context.Service<IndexedDb, IndexedDb>(TypeId)
28
+
29
+ /** @internal */
30
+ const IDBFlatKey = Schema.Union([
31
+ Schema.String,
32
+ Schema.Number.check(Schema.makeFilter((input) => !Number.isNaN(input))),
33
+ Schema.DateValid,
34
+ Schema.declare(
35
+ (input): input is BufferSource =>
36
+ input instanceof ArrayBuffer ||
37
+ (ArrayBuffer.isView(input) && input.buffer instanceof ArrayBuffer)
38
+ )
39
+ ])
40
+
41
+ /**
42
+ * Schema for `IDBValidKey` (`number | string | Date | BufferSource | IDBValidKey[]`).
43
+ *
44
+ * @since 4.0.0
45
+ * @category schemas
46
+ */
47
+ export const IDBValidKey = Schema.Union([IDBFlatKey, Schema.Array(IDBFlatKey)])
48
+
49
+ /**
50
+ * Schema for `autoIncrement` key path (`number`).
51
+ *
52
+ * @since 4.0.0
53
+ * @category schemas
54
+ */
55
+ export const AutoIncrement = Schema.Int.check(
56
+ Schema.isBetween({ minimum: 1, maximum: 2 ** 53 })
57
+ ).annotate({
58
+ identifier: "AutoIncrement",
59
+ title: "autoIncrement",
60
+ description: "Defines a valid autoIncrement key path for the IndexedDb table"
61
+ })
62
+
63
+ /**
64
+ * @since 4.0.0
65
+ * @category constructor
66
+ */
67
+ export const make = (impl: Omit<IndexedDb, typeof TypeId>): IndexedDb => IndexedDb.of({ [TypeId]: TypeId, ...impl })
68
+
69
+ /**
70
+ * Instance of IndexedDb from the `window` object.
71
+ *
72
+ * @since 4.0.0
73
+ * @category constructors
74
+ */
75
+ export const layerWindow: Layer.Layer<IndexedDb, Config.ConfigError> = Layer.effect(
76
+ IndexedDb,
77
+ Effect.suspend(() => {
78
+ if (window.indexedDB && window.IDBKeyRange) {
79
+ return Effect.succeed(
80
+ make({
81
+ indexedDB: window.indexedDB,
82
+ IDBKeyRange: window.IDBKeyRange
83
+ })
84
+ )
85
+ } else {
86
+ return Effect.fail(
87
+ new Config.ConfigError(
88
+ new Schema.SchemaError(
89
+ new SchemaIssue.MissingKey({
90
+ messageMissingKey: "window.indexedDB is not available"
91
+ })
92
+ )
93
+ )
94
+ )
95
+ }
96
+ })
97
+ )