@effect/platform-browser 4.0.0-beta.9 → 4.0.0-beta.91

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
@@ -1,29 +1,177 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Browser-backed `KeyValueStore` layers for client-side Effect programs.
3
+ *
4
+ * This module provides browser implementations of the unstable persistence
5
+ * `KeyValueStore` service. Use {@link layerLocalStorage} for small
6
+ * origin-scoped values that should survive reloads and browser restarts, use
7
+ * {@link layerSessionStorage} for tab / page-session state, and use
8
+ * {@link layerIndexedDb} when the store should be asynchronous and backed by
9
+ * IndexedDB. The IndexedDB layer requires the browser `IndexedDb` service and
10
+ * accepts an optional database name.
11
+ *
12
+ * @since 4.0.0
3
13
  */
4
- import type * as Layer from "effect/Layer"
14
+ import * as Effect from "effect/Effect"
15
+ import * as Layer from "effect/Layer"
5
16
  import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
17
+ import { IndexedDb } from "./IndexedDb.ts"
6
18
 
7
19
  /**
8
- * Creates a `KeyValueStore` layer that uses the browser's `localStorage` api.
9
- *
10
- * Values are stored between sessions.
20
+ * Creates a `KeyValueStore` layer that uses the browser's `localStorage` API and stores values between browser sessions.
11
21
  *
12
- * @since 1.0.0
13
- * @category Layers
22
+ * @category layers
23
+ * @since 4.0.0
14
24
  */
15
25
  export const layerLocalStorage: Layer.Layer<KeyValueStore.KeyValueStore> = KeyValueStore.layerStorage(() =>
16
26
  globalThis.localStorage
17
27
  )
18
28
 
19
29
  /**
20
- * Creates a `KeyValueStore` layer that uses the browser's `sessionStorage` api.
30
+ * Creates a `KeyValueStore` layer that uses the browser's `sessionStorage` API and stores values only for the current session.
21
31
  *
22
- * Values are stored only for the current session.
23
- *
24
- * @since 1.0.0
25
- * @category Layers
32
+ * @category layers
33
+ * @since 4.0.0
26
34
  */
27
35
  export const layerSessionStorage: Layer.Layer<KeyValueStore.KeyValueStore> = KeyValueStore.layerStorage(() =>
28
36
  globalThis.sessionStorage
29
37
  )
38
+
39
+ /**
40
+ * Creates a `KeyValueStore` layer backed by IndexedDB.
41
+ *
42
+ * **When to use**
43
+ *
44
+ * Use when you need persistent asynchronous IndexedDB storage for a browser
45
+ * `KeyValueStore` instead of the synchronous Web Storage APIs.
46
+ *
47
+ * **Details**
48
+ *
49
+ * The database name defaults to `"effect_key_value_store"`. The layer requires
50
+ * the `IndexedDb` service and stores string and `Uint8Array` values in the same
51
+ * backing object store.
52
+ *
53
+ * **Gotchas**
54
+ *
55
+ * IndexedDB may be unavailable or blocked by browser settings, private browsing,
56
+ * quota limits, or restricted contexts. The string and `Uint8Array` accessors do
57
+ * not coerce values stored with the other representation.
58
+ *
59
+ * @see {@link layerLocalStorage} for synchronous persistent Web Storage
60
+ * @see {@link layerSessionStorage} for synchronous tab-session Web Storage
61
+ *
62
+ * @category layers
63
+ * @since 4.0.0
64
+ */
65
+ export const layerIndexedDb = (options?: {
66
+ readonly database?: string | undefined
67
+ }): Layer.Layer<KeyValueStore.KeyValueStore, never, IndexedDb> =>
68
+ Layer.effect(KeyValueStore.KeyValueStore)(
69
+ Effect.gen(function*() {
70
+ const db = yield* Effect.acquireRelease(
71
+ openDatabase(options?.database ?? "effect_key_value_store"),
72
+ (db) => Effect.sync(() => db.close())
73
+ ).pipe(Effect.orDie)
74
+
75
+ return KeyValueStore.make({
76
+ clear: Effect.suspend(() => {
77
+ const store = getKvsEntriesStore(db, "readwrite")
78
+ return idbRequest({ method: "clear", message: "Failed to clear backing store" }, () => store.clear())
79
+ }),
80
+ get: (key: string) =>
81
+ Effect.map(
82
+ Effect.suspend(() => {
83
+ const store = getKvsEntriesStore(db, "readonly")
84
+ return idbRequest<{ key: string; value: string } | undefined>({
85
+ method: "get",
86
+ message: "Failed to get value from backing store",
87
+ key
88
+ }, () => store.get(key))
89
+ }),
90
+ (found) => typeof found?.value === "string" ? found.value : undefined
91
+ ),
92
+ getUint8Array: (key: string) =>
93
+ Effect.map(
94
+ Effect.suspend(() => {
95
+ const store = getKvsEntriesStore(db, "readonly")
96
+ return idbRequest<{ key: string; value: Uint8Array } | undefined>({
97
+ method: "getUint8Array",
98
+ message: "Failed to get value from backing store",
99
+ key
100
+ }, () => store.get(key))
101
+ }),
102
+ (found) => found?.value && found.value instanceof Uint8Array ? found.value : undefined
103
+ ),
104
+ set: (key: string, value: string | Uint8Array) =>
105
+ Effect.asVoid(Effect.suspend(() => {
106
+ const store = getKvsEntriesStore(db, "readwrite")
107
+ return idbRequest(
108
+ { method: "set", message: "Failed to set value in backing store", key },
109
+ () => store.put({ key, value })
110
+ )
111
+ })),
112
+ size: Effect.suspend(() => {
113
+ const store = getKvsEntriesStore(db, "readonly")
114
+ return idbRequest<number>(
115
+ { method: "size", message: "Failed to get backing store size" },
116
+ () => store.count()
117
+ )
118
+ }),
119
+ remove: (key: string) =>
120
+ Effect.asVoid(Effect.suspend(() => {
121
+ const store = getKvsEntriesStore(db, "readwrite")
122
+ return idbRequest(
123
+ { method: "remove", message: "Failed to remove value from backing store", key },
124
+ () => store.delete(key)
125
+ )
126
+ }))
127
+ })
128
+ })
129
+ )
130
+
131
+ const databaseVersion = 1
132
+ const entriesStoreName = "entries"
133
+ const openDatabase = Effect.fnUntraced(function*(database: string) {
134
+ const idb = (yield* IndexedDb).indexedDB
135
+ const openRequest = yield* Effect.try({
136
+ try: () => idb.open(database, databaseVersion),
137
+ catch: (cause) =>
138
+ new KeyValueStore.KeyValueStoreError({
139
+ method: "open",
140
+ message: "Failed to open backing store database",
141
+ cause
142
+ })
143
+ })
144
+ openRequest.onupgradeneeded = () => {
145
+ const db = openRequest.result
146
+ if (!db.objectStoreNames.contains(entriesStoreName)) {
147
+ db.createObjectStore(entriesStoreName, { keyPath: "key" })
148
+ }
149
+ }
150
+ return yield* idbRequest({ method: "open", message: "Failed to open backing store database" }, () => openRequest)
151
+ })
152
+
153
+ const idbRequest = <A>(
154
+ failArgs: { method: string; message: string; key?: string },
155
+ evaluate: () => IDBRequest<A>
156
+ ): Effect.Effect<A, KeyValueStore.KeyValueStoreError> =>
157
+ Effect.callback<A, KeyValueStore.KeyValueStoreError>((resume) => {
158
+ const request = evaluate()
159
+ if (request.readyState === "done") {
160
+ return resume(Effect.succeed(request.result))
161
+ }
162
+ request.onsuccess = () => {
163
+ resume(Effect.succeed(request.result))
164
+ }
165
+ request.onerror = () =>
166
+ resume(Effect.fail(
167
+ new KeyValueStore.KeyValueStoreError({
168
+ ...failArgs,
169
+ cause: request.error
170
+ })
171
+ ))
172
+ })
173
+
174
+ const getKvsEntriesStore = (db: IDBDatabase, mode: IDBTransactionMode) => {
175
+ const transaction = db.transaction(entriesStoreName, mode)
176
+ return transaction.objectStore(entriesStoreName)
177
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * IndexedDB-backed persistence layers for browser Effect programs.
3
+ *
4
+ * This module provides a low-level `BackingPersistence` layer and a higher-level
5
+ * `Persistence` layer that store object values in IndexedDB. Values are stored
6
+ * by persistence store id and key, and reads check TTL expiration before
7
+ * returning stored data. The database name can be customized and defaults to
8
+ * `"effect_persistence"`.
9
+ *
10
+ * @since 4.0.0
11
+ */
12
+ import type * as Arr from "effect/Array"
13
+ import * as Clock from "effect/Clock"
14
+ import type * as Duration from "effect/Duration"
15
+ import * as Effect from "effect/Effect"
16
+ import * as Layer from "effect/Layer"
17
+ import * as Persistence from "effect/unstable/persistence/Persistence"
18
+
19
+ /**
20
+ * Creates a `BackingPersistence` layer backed by IndexedDB, optionally using the provided database name.
21
+ *
22
+ * **When to use**
23
+ *
24
+ * Use when composing persistence manually and the lower-level
25
+ * `BackingPersistence` service should be backed by browser IndexedDB.
26
+ *
27
+ * **Details**
28
+ *
29
+ * The database name defaults to `"effect_persistence"`. Entries are stored by
30
+ * persistence store id and key in a shared object store, and TTL expiration is
31
+ * checked when values are read.
32
+ *
33
+ * **Gotchas**
34
+ *
35
+ * Opening the database is defected during layer acquisition if IndexedDB is
36
+ * unavailable or cannot be opened. Store operations report `PersistenceError`
37
+ * for IndexedDB request, transaction, quota, and structured-clone failures.
38
+ *
39
+ * @see {@link layerIndexedDb} for providing the higher-level `Persistence` service
40
+ *
41
+ * @category layers
42
+ * @since 4.0.0
43
+ */
44
+ export const layerBackingIndexedDb = (options?: {
45
+ readonly database?: string | undefined
46
+ }): Layer.Layer<Persistence.BackingPersistence> =>
47
+ Layer.effect(Persistence.BackingPersistence)(Effect.gen(function*() {
48
+ const db = yield* Effect.acquireRelease(
49
+ openDatabase(options?.database ?? defaultDatabase),
50
+ (db) => Effect.sync(() => db.close())
51
+ ).pipe(Effect.orDie)
52
+
53
+ return Persistence.BackingPersistence.of({
54
+ make: Effect.fnUntraced(function*(storeId) {
55
+ const clock = yield* Clock.Clock
56
+ return {
57
+ get: (key) => get(db, clock, storeId, key),
58
+ getMany: (keys) => getMany(db, clock, storeId, keys),
59
+ set: (key, value, ttl) => set(db, clock, storeId, key, value, ttl),
60
+ setMany: (entries) => setMany(db, clock, storeId, entries),
61
+ remove: (key) => remove(db, storeId, key),
62
+ clear: clear(db, storeId)
63
+ }
64
+ })
65
+ })
66
+ }))
67
+
68
+ const defaultDatabase = "effect_persistence"
69
+ const databaseVersion = 1
70
+ const entriesStoreName = "entries"
71
+ const storeIdIndexName = "storeId"
72
+
73
+ /**
74
+ * Creates a `Persistence` layer backed by IndexedDB, optionally using the provided database name.
75
+ *
76
+ * @category layers
77
+ * @since 4.0.0
78
+ */
79
+ export const layerIndexedDb = (options?: {
80
+ readonly database?: string | undefined
81
+ }): Layer.Layer<Persistence.Persistence> =>
82
+ Persistence.layer.pipe(
83
+ Layer.provide(layerBackingIndexedDb(options))
84
+ )
85
+
86
+ const openDatabase = (database: string): Effect.Effect<IDBDatabase, Persistence.PersistenceError> =>
87
+ Effect.gen(function*() {
88
+ const openRequest = yield* Effect.try({
89
+ try: () => globalThis.indexedDB.open(database, databaseVersion),
90
+ catch: (cause) =>
91
+ new Persistence.PersistenceError({
92
+ message: "Failed to open backing store database",
93
+ cause
94
+ })
95
+ })
96
+
97
+ openRequest.onupgradeneeded = () => {
98
+ const db = openRequest.result
99
+ const entries = db.objectStoreNames.contains(entriesStoreName)
100
+ ? openRequest.transaction?.objectStore(entriesStoreName)
101
+ : db.createObjectStore(entriesStoreName, { keyPath: ["storeId", "id"] })
102
+ if (entries && !entries.indexNames.contains(storeIdIndexName)) {
103
+ entries.createIndex(storeIdIndexName, storeIdIndexName, { unique: false })
104
+ }
105
+ }
106
+
107
+ return yield* idbRequest("Failed to open backing store database", () => openRequest)
108
+ })
109
+
110
+ interface EntryRow {
111
+ readonly storeId: string
112
+ readonly id: string
113
+ readonly value: object
114
+ readonly expires: number | null
115
+ }
116
+
117
+ const isExpired = (row: EntryRow, now: number): boolean => row.expires !== null && row.expires <= now
118
+
119
+ const get = (
120
+ db: IDBDatabase,
121
+ clock: Clock.Clock,
122
+ storeId: string,
123
+ key: string
124
+ ): Effect.Effect<object | undefined, Persistence.PersistenceError> =>
125
+ withEntriesTransaction<object | undefined>(
126
+ db,
127
+ "readwrite",
128
+ `Failed to get key ${key} from backing store`,
129
+ (
130
+ entries,
131
+ setResult,
132
+ fail
133
+ ) => {
134
+ const now = clock.currentTimeMillisUnsafe()
135
+ const id: [string, string] = [storeId, key]
136
+ const request = entries.get(id)
137
+ request.onerror = () => fail(request.error)
138
+ request.onsuccess = () => {
139
+ const row = request.result as EntryRow | undefined
140
+ if (!row || !isExpired(row, now)) {
141
+ setResult(row?.value)
142
+ return
143
+ }
144
+
145
+ const deleteRequest = entries.delete(id)
146
+ deleteRequest.onerror = () => fail(deleteRequest.error)
147
+ deleteRequest.onsuccess = () => setResult(undefined)
148
+ }
149
+ }
150
+ )
151
+
152
+ const getMany = (
153
+ db: IDBDatabase,
154
+ clock: Clock.Clock,
155
+ storeId: string,
156
+ keys: Arr.NonEmptyArray<string>
157
+ ): Effect.Effect<Arr.NonEmptyArray<object | undefined>, Persistence.PersistenceError> =>
158
+ withEntriesTransaction(
159
+ db,
160
+ "readwrite",
161
+ "Failed to getMany from backing store",
162
+ (entries, setResult, fail) => {
163
+ const now = clock.currentTimeMillisUnsafe()
164
+ const results = new Array<object | undefined>(keys.length)
165
+ setResult(results as any)
166
+
167
+ for (let i = 0; i < keys.length; i++) {
168
+ const key = keys[i]
169
+ const keyPath = [storeId, key]
170
+ const request = entries.get(keyPath)
171
+ request.onerror = () => fail(request.error)
172
+ request.onsuccess = () => {
173
+ const row = request.result as EntryRow | undefined
174
+ if (!row) return
175
+ else if (!isExpired(row, now)) {
176
+ results[i] = row.value
177
+ return
178
+ }
179
+ const deleteRequest = entries.delete(keyPath)
180
+ deleteRequest.onerror = () => fail(deleteRequest.error)
181
+ }
182
+ }
183
+ }
184
+ )
185
+
186
+ const set = (
187
+ db: IDBDatabase,
188
+ clock: Clock.Clock,
189
+ storeId: string,
190
+ key: string,
191
+ value: object,
192
+ ttl: Duration.Duration | undefined
193
+ ): Effect.Effect<void, Persistence.PersistenceError> =>
194
+ withEntriesTransaction(
195
+ db,
196
+ "readwrite",
197
+ `Failed to set key ${key} in backing store`,
198
+ (entries, setResult, fail) => {
199
+ const request = entries.put(
200
+ {
201
+ storeId,
202
+ id: key,
203
+ value,
204
+ expires: Persistence.unsafeTtlToExpires(clock, ttl)
205
+ } satisfies EntryRow
206
+ )
207
+ request.onerror = () => fail(request.error)
208
+ request.onsuccess = () => setResult(undefined)
209
+ }
210
+ )
211
+
212
+ const setMany = (
213
+ db: IDBDatabase,
214
+ clock: Clock.Clock,
215
+ storeId: string,
216
+ entries: Arr.NonEmptyArray<readonly [key: string, value: object, ttl: Duration.Duration | undefined]>
217
+ ): Effect.Effect<void, Persistence.PersistenceError> =>
218
+ withEntriesTransaction(
219
+ db,
220
+ "readwrite",
221
+ "Failed to setMany in backing store",
222
+ (store, setResult, fail) => {
223
+ for (const [key, value, ttl] of entries) {
224
+ const request = store.put(
225
+ {
226
+ storeId,
227
+ id: key,
228
+ value,
229
+ expires: Persistence.unsafeTtlToExpires(clock, ttl)
230
+ } satisfies EntryRow
231
+ )
232
+ request.onerror = () => fail(request.error)
233
+ request.onsuccess = () => setResult(undefined)
234
+ }
235
+ }
236
+ )
237
+
238
+ const remove = (
239
+ db: IDBDatabase,
240
+ storeId: string,
241
+ key: string
242
+ ): Effect.Effect<void, Persistence.PersistenceError> =>
243
+ withEntriesTransaction(
244
+ db,
245
+ "readwrite",
246
+ `Failed to remove key ${key} from backing store`,
247
+ (entries, setResult, fail) => {
248
+ const request = entries.delete([storeId, key])
249
+ request.onerror = () => fail(request.error)
250
+ request.onsuccess = () => setResult(undefined)
251
+ }
252
+ )
253
+
254
+ const clear = (db: IDBDatabase, storeId: string): Effect.Effect<void, Persistence.PersistenceError> =>
255
+ withEntriesTransaction(db, "readwrite", "Failed to clear backing store", (entries, setResult, fail) => {
256
+ const index = entries.index(storeIdIndexName)
257
+ const cursorRequest = index.openCursor(storeId)
258
+ cursorRequest.onerror = () => fail(cursorRequest.error)
259
+ cursorRequest.onsuccess = () => {
260
+ const cursor = cursorRequest.result
261
+ if (!cursor) {
262
+ setResult(undefined)
263
+ return
264
+ }
265
+ const deleteRequest = cursor.delete()
266
+ deleteRequest.onerror = () => fail(deleteRequest.error)
267
+ deleteRequest.onsuccess = () => cursor.continue()
268
+ }
269
+ })
270
+
271
+ const withEntriesTransaction = <A>(
272
+ db: IDBDatabase,
273
+ mode: IDBTransactionMode,
274
+ message: string,
275
+ run: (
276
+ entries: IDBObjectStore,
277
+ onResult: (result: A) => void,
278
+ fail: (cause: unknown) => void
279
+ ) => void
280
+ ): Effect.Effect<A, Persistence.PersistenceError> =>
281
+ Effect.callback<A, Persistence.PersistenceError>((resume) => {
282
+ const tx = db.transaction(entriesStoreName, mode)
283
+ const entries = tx.objectStore(entriesStoreName)
284
+
285
+ let result: A | undefined
286
+ let setResult = false
287
+ let done = false
288
+
289
+ const fail = (cause: unknown) => {
290
+ done = true
291
+ resume(Effect.fail(new Persistence.PersistenceError({ message, cause })))
292
+ }
293
+
294
+ tx.oncomplete = () => {
295
+ done = true
296
+ if (setResult) resume(Effect.succeed(result!))
297
+ }
298
+ tx.onerror = () => {
299
+ done = true
300
+ fail(tx.error)
301
+ }
302
+ tx.onabort = () => {
303
+ done = true
304
+ fail(tx.error)
305
+ }
306
+
307
+ run(entries, (next) => {
308
+ if (done) return resume(Effect.succeed(next))
309
+ setResult = true
310
+ result = next
311
+ }, fail)
312
+
313
+ return Effect.sync(() => {
314
+ tx.abort()
315
+ })
316
+ })
317
+
318
+ const idbRequest = <A>(
319
+ message: string,
320
+ evaluate: () => IDBRequest<A>
321
+ ): Effect.Effect<A, Persistence.PersistenceError> =>
322
+ Effect.callback<A, Persistence.PersistenceError>((resume) => {
323
+ const request = evaluate()
324
+ const fail = (cause: unknown) => {
325
+ resume(Effect.fail(new Persistence.PersistenceError({ message, cause })))
326
+ }
327
+ if (request.readyState === "done") {
328
+ resume(Effect.succeed(request.result))
329
+ }
330
+ request.onsuccess = () => {
331
+ resume(Effect.succeed(request.result))
332
+ }
333
+ request.onerror = () => fail(request.error)
334
+ })
@@ -1,17 +1,59 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Browser helper for running a root Effect program.
3
+ *
4
+ * This module exports `runMain`, a browser version of the main Effect runner.
5
+ * It forwards runner options to the core runtime and adds a `beforeunload`
6
+ * listener that interrupts the main fiber when the page is unloading.
7
+ *
8
+ * @since 4.0.0
3
9
  */
4
10
  import type * as Effect from "effect/Effect"
5
11
  import { makeRunMain, type Teardown } from "effect/Runtime"
6
12
 
7
13
  /**
8
- * @since 1.0.0
14
+ * Runs an effect as the browser main program and interrupts its fiber when the page receives a `beforeunload` event.
15
+ *
16
+ * **When to use**
17
+ *
18
+ * Use to launch a browser page, single-page application, demo, or browser test
19
+ * harness as a root Effect program.
20
+ *
21
+ * **Details**
22
+ *
23
+ * Supports both direct and curried call forms. Options are forwarded to
24
+ * `makeRunMain`, including `disableErrorReporting` and custom `teardown`
25
+ * behavior.
26
+ *
27
+ * **Gotchas**
28
+ *
29
+ * The `beforeunload` interruption is best-effort. Browser teardown may prevent
30
+ * asynchronous finalizers, network work, timers, or prompts from completing.
31
+ *
9
32
  * @category Runtime
33
+ * @since 4.0.0
10
34
  */
11
35
  export const runMain: {
12
36
  /**
13
- * @since 1.0.0
37
+ * Runs an effect as the browser main program and interrupts its fiber when the page receives a `beforeunload` event.
38
+ *
39
+ * **When to use**
40
+ *
41
+ * Use to launch a browser page, single-page application, demo, or browser test
42
+ * harness as a root Effect program.
43
+ *
44
+ * **Details**
45
+ *
46
+ * Supports both direct and curried call forms. Options are forwarded to
47
+ * `makeRunMain`, including `disableErrorReporting` and custom `teardown`
48
+ * behavior.
49
+ *
50
+ * **Gotchas**
51
+ *
52
+ * The `beforeunload` interruption is best-effort. Browser teardown may prevent
53
+ * asynchronous finalizers, network work, timers, or prompts from completing.
54
+ *
14
55
  * @category Runtime
56
+ * @since 4.0.0
15
57
  */
16
58
  (
17
59
  options?: {
@@ -20,8 +62,26 @@ export const runMain: {
20
62
  }
21
63
  ): <E, A>(effect: Effect.Effect<A, E>) => void
22
64
  /**
23
- * @since 1.0.0
65
+ * Runs an effect as the browser main program and interrupts its fiber when the page receives a `beforeunload` event.
66
+ *
67
+ * **When to use**
68
+ *
69
+ * Use to launch a browser page, single-page application, demo, or browser test
70
+ * harness as a root Effect program.
71
+ *
72
+ * **Details**
73
+ *
74
+ * Supports both direct and curried call forms. Options are forwarded to
75
+ * `makeRunMain`, including `disableErrorReporting` and custom `teardown`
76
+ * behavior.
77
+ *
78
+ * **Gotchas**
79
+ *
80
+ * The `beforeunload` interruption is best-effort. Browser teardown may prevent
81
+ * asynchronous finalizers, network work, timers, or prompts from completing.
82
+ *
24
83
  * @category Runtime
84
+ * @since 4.0.0
25
85
  */
26
86
  <E, A>(
27
87
  effect: Effect.Effect<A, E>,
@@ -1,12 +1,39 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Browser WebSocket layers for Effect sockets.
3
+ *
4
+ * `layerWebSocket` creates a `Socket.Socket` connected to a WebSocket URL using
5
+ * the browser `WebSocket` constructor. `layerWebSocketConstructor` provides
6
+ * only the browser-backed constructor service for lower-level socket code.
7
+ *
8
+ * @since 4.0.0
3
9
  */
4
10
  import * as Layer from "effect/Layer"
5
11
  import * as Socket from "effect/unstable/socket/Socket"
6
12
 
7
13
  /**
8
- * @since 1.0.0
9
- * @category Layers
14
+ * Creates a `Socket` layer connected to the given URL using the browser `WebSocket` constructor.
15
+ *
16
+ * **When to use**
17
+ *
18
+ * Use when you need browser code to satisfy the platform socket service from a
19
+ * URL without wiring the browser constructor service separately.
20
+ *
21
+ * **Details**
22
+ *
23
+ * Delegates socket construction to `Socket.makeWebSocket` and provides the
24
+ * browser-backed `WebSocketConstructor` service.
25
+ *
26
+ * **Gotchas**
27
+ *
28
+ * Browser WebSocket rules still control URL schemes, mixed-content blocking,
29
+ * cookies, authentication, origin checks, subprotocols, and extensions. Close
30
+ * events are errors unless `closeCodeIsError` classifies the close code as
31
+ * clean.
32
+ *
33
+ * @see {@link layerWebSocketConstructor} for providing only the browser constructor service
34
+ *
35
+ * @category layers
36
+ * @since 4.0.0
10
37
  */
11
38
  export const layerWebSocket = (url: string, options?: {
12
39
  readonly closeCodeIsError?: (code: number) => boolean
@@ -16,10 +43,10 @@ export const layerWebSocket = (url: string, options?: {
16
43
  )
17
44
 
18
45
  /**
19
- * A WebSocket constructor that uses `globalThis.WebSocket`.
46
+ * Layer that provides a `WebSocketConstructor` service backed by `globalThis.WebSocket`.
20
47
  *
21
- * @since 1.0.0
22
- * @category Layers
48
+ * @category layers
49
+ * @since 4.0.0
23
50
  */
24
51
  export const layerWebSocketConstructor: Layer.Layer<Socket.WebSocketConstructor> =
25
52
  Socket.layerWebSocketConstructorGlobal