@effect/platform-browser 4.0.0-beta.7 → 4.0.0-beta.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BrowserCrypto.d.ts +81 -0
- package/dist/BrowserCrypto.d.ts.map +1 -0
- package/dist/BrowserCrypto.js +119 -0
- package/dist/BrowserCrypto.js.map +1 -0
- package/dist/BrowserHttpClient.d.ts +40 -16
- package/dist/BrowserHttpClient.d.ts.map +1 -1
- package/dist/BrowserHttpClient.js +74 -18
- package/dist/BrowserHttpClient.js.map +1 -1
- package/dist/BrowserKeyValueStore.d.ts +37 -14
- package/dist/BrowserKeyValueStore.d.ts.map +1 -1
- package/dist/BrowserKeyValueStore.js +185 -10
- package/dist/BrowserKeyValueStore.js.map +1 -1
- package/dist/BrowserPersistence.d.ts +40 -0
- package/dist/BrowserPersistence.d.ts.map +1 -0
- package/dist/BrowserPersistence.js +207 -0
- package/dist/BrowserPersistence.js.map +1 -0
- package/dist/BrowserRuntime.d.ts +79 -4
- package/dist/BrowserRuntime.d.ts.map +1 -1
- package/dist/BrowserRuntime.js +19 -1
- package/dist/BrowserRuntime.js.map +1 -1
- package/dist/BrowserSocket.d.ts +64 -6
- package/dist/BrowserSocket.d.ts.map +1 -1
- package/dist/BrowserSocket.js +64 -6
- package/dist/BrowserSocket.js.map +1 -1
- package/dist/BrowserStream.d.ts +48 -5
- package/dist/BrowserStream.d.ts.map +1 -1
- package/dist/BrowserStream.js +48 -5
- package/dist/BrowserStream.js.map +1 -1
- package/dist/BrowserWorker.d.ts +25 -4
- package/dist/BrowserWorker.d.ts.map +1 -1
- package/dist/BrowserWorker.js +62 -6
- package/dist/BrowserWorker.js.map +1 -1
- package/dist/BrowserWorkerRunner.d.ts +31 -6
- package/dist/BrowserWorkerRunner.d.ts.map +1 -1
- package/dist/BrowserWorkerRunner.js +65 -10
- package/dist/BrowserWorkerRunner.js.map +1 -1
- package/dist/Clipboard.d.ts +77 -13
- package/dist/Clipboard.d.ts.map +1 -1
- package/dist/Clipboard.js +57 -11
- package/dist/Clipboard.js.map +1 -1
- package/dist/Geolocation.d.ts +101 -24
- package/dist/Geolocation.d.ts.map +1 -1
- package/dist/Geolocation.js +69 -16
- package/dist/Geolocation.js.map +1 -1
- package/dist/IndexedDb.d.ts +89 -0
- package/dist/IndexedDb.d.ts.map +1 -0
- package/dist/IndexedDb.js +99 -0
- package/dist/IndexedDb.js.map +1 -0
- package/dist/IndexedDbDatabase.d.ts +169 -0
- package/dist/IndexedDbDatabase.d.ts.map +1 -0
- package/dist/IndexedDbDatabase.js +343 -0
- package/dist/IndexedDbDatabase.js.map +1 -0
- package/dist/IndexedDbQueryBuilder.d.ts +438 -0
- package/dist/IndexedDbQueryBuilder.d.ts.map +1 -0
- package/dist/IndexedDbQueryBuilder.js +935 -0
- package/dist/IndexedDbQueryBuilder.js.map +1 -0
- package/dist/IndexedDbTable.d.ts +191 -0
- package/dist/IndexedDbTable.d.ts.map +1 -0
- package/dist/IndexedDbTable.js +96 -0
- package/dist/IndexedDbTable.js.map +1 -0
- package/dist/IndexedDbVersion.d.ts +94 -0
- package/dist/IndexedDbVersion.d.ts.map +1 -0
- package/dist/IndexedDbVersion.js +39 -0
- package/dist/IndexedDbVersion.js.map +1 -0
- package/dist/Permissions.d.ts +76 -16
- package/dist/Permissions.d.ts.map +1 -1
- package/dist/Permissions.js +66 -11
- package/dist/Permissions.js.map +1 -1
- package/dist/index.d.ts +38 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/BrowserCrypto.ts +129 -0
- package/src/BrowserHttpClient.ts +84 -24
- package/src/BrowserKeyValueStore.ts +201 -12
- package/src/BrowserPersistence.ts +376 -0
- package/src/BrowserRuntime.ts +79 -4
- package/src/BrowserSocket.ts +64 -6
- package/src/BrowserStream.ts +48 -5
- package/src/BrowserWorker.ts +62 -6
- package/src/BrowserWorkerRunner.ts +65 -10
- package/src/Clipboard.ts +74 -13
- package/src/Geolocation.ts +97 -20
- package/src/IndexedDb.ts +127 -0
- package/src/IndexedDbDatabase.ts +664 -0
- package/src/IndexedDbQueryBuilder.ts +2048 -0
- package/src/IndexedDbTable.ts +285 -0
- package/src/IndexedDbVersion.ts +133 -0
- package/src/Permissions.ts +71 -14
- package/src/index.ts +45 -10
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB-backed persistence layers for browser Effect programs.
|
|
3
|
+
*
|
|
4
|
+
* `BrowserPersistence` connects the unstable persistence services to the
|
|
5
|
+
* browser's IndexedDB storage. Use it for client-side caches that should
|
|
6
|
+
* survive page reloads, offline-capable workflows, remembered query results,
|
|
7
|
+
* and other application-managed values that can be recomputed or invalidated
|
|
8
|
+
* when storage is unavailable.
|
|
9
|
+
*
|
|
10
|
+
* **Mental model**
|
|
11
|
+
*
|
|
12
|
+
* - {@link layerIndexedDb} provides the higher-level `Persistence` service by
|
|
13
|
+
* composing the IndexedDB backing layer with the core persistence layer.
|
|
14
|
+
* - {@link layerBackingIndexedDb} provides only `BackingPersistence` for code
|
|
15
|
+
* that composes persistence services manually.
|
|
16
|
+
* - Entries live in one IndexedDB database, defaulting to
|
|
17
|
+
* `"effect_persistence"`, and one object store keyed by `[storeId, key]`.
|
|
18
|
+
* - The `storeId` namespaces persisted request results; the key identifies one
|
|
19
|
+
* entry inside that store.
|
|
20
|
+
* - Finite TTLs are stored as expiration timestamps and expired entries are
|
|
21
|
+
* removed lazily when they are read.
|
|
22
|
+
*
|
|
23
|
+
* **Common tasks**
|
|
24
|
+
*
|
|
25
|
+
* - Provide browser persistence to `PersistedCache` or other persistence
|
|
26
|
+
* workflows with {@link layerIndexedDb}.
|
|
27
|
+
* - Isolate tests, apps, or migrations by passing a custom `database` option.
|
|
28
|
+
* - Use {@link layerBackingIndexedDb} when another layer should decide how to
|
|
29
|
+
* construct the higher-level `Persistence` service.
|
|
30
|
+
*
|
|
31
|
+
* **Gotchas**
|
|
32
|
+
*
|
|
33
|
+
* - This module requires browser IndexedDB and stores data per origin.
|
|
34
|
+
* - Opening IndexedDB is defected during layer acquisition if the database
|
|
35
|
+
* cannot be opened; later store operations report `PersistenceError`.
|
|
36
|
+
* - Values must be structured-cloneable objects, and writes can fail because of
|
|
37
|
+
* quota limits, browser settings, private browsing modes, or user-cleared
|
|
38
|
+
* storage.
|
|
39
|
+
* - Lazy TTL cleanup makes this suitable for caches, not for authoritative or
|
|
40
|
+
* security-sensitive state.
|
|
41
|
+
*
|
|
42
|
+
* **Example** (Providing browser persistence)
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { BrowserPersistence } from "@effect/platform-browser"
|
|
46
|
+
*
|
|
47
|
+
* export const PersistenceLive = BrowserPersistence.layerIndexedDb({
|
|
48
|
+
* database: "my-app-cache"
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @since 4.0.0
|
|
53
|
+
*/
|
|
54
|
+
import type * as Arr from "effect/Array"
|
|
55
|
+
import * as Clock from "effect/Clock"
|
|
56
|
+
import type * as Duration from "effect/Duration"
|
|
57
|
+
import * as Effect from "effect/Effect"
|
|
58
|
+
import * as Layer from "effect/Layer"
|
|
59
|
+
import * as Persistence from "effect/unstable/persistence/Persistence"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates a `BackingPersistence` layer backed by IndexedDB, optionally using the provided database name.
|
|
63
|
+
*
|
|
64
|
+
* **When to use**
|
|
65
|
+
*
|
|
66
|
+
* Use when composing persistence manually and the lower-level
|
|
67
|
+
* `BackingPersistence` service should be backed by browser IndexedDB.
|
|
68
|
+
*
|
|
69
|
+
* **Details**
|
|
70
|
+
*
|
|
71
|
+
* The database name defaults to `"effect_persistence"`. Entries are stored by
|
|
72
|
+
* persistence store id and key in a shared object store, and TTL expiration is
|
|
73
|
+
* checked when values are read.
|
|
74
|
+
*
|
|
75
|
+
* **Gotchas**
|
|
76
|
+
*
|
|
77
|
+
* Opening the database is defected during layer acquisition if IndexedDB is
|
|
78
|
+
* unavailable or cannot be opened. Store operations report `PersistenceError`
|
|
79
|
+
* for IndexedDB request, transaction, quota, and structured-clone failures.
|
|
80
|
+
*
|
|
81
|
+
* @see {@link layerIndexedDb} for providing the higher-level `Persistence` service
|
|
82
|
+
*
|
|
83
|
+
* @category layers
|
|
84
|
+
* @since 4.0.0
|
|
85
|
+
*/
|
|
86
|
+
export const layerBackingIndexedDb = (options?: {
|
|
87
|
+
readonly database?: string | undefined
|
|
88
|
+
}): Layer.Layer<Persistence.BackingPersistence> =>
|
|
89
|
+
Layer.effect(Persistence.BackingPersistence)(Effect.gen(function*() {
|
|
90
|
+
const db = yield* Effect.acquireRelease(
|
|
91
|
+
openDatabase(options?.database ?? defaultDatabase),
|
|
92
|
+
(db) => Effect.sync(() => db.close())
|
|
93
|
+
).pipe(Effect.orDie)
|
|
94
|
+
|
|
95
|
+
return Persistence.BackingPersistence.of({
|
|
96
|
+
make: Effect.fnUntraced(function*(storeId) {
|
|
97
|
+
const clock = yield* Clock.Clock
|
|
98
|
+
return {
|
|
99
|
+
get: (key) => get(db, clock, storeId, key),
|
|
100
|
+
getMany: (keys) => getMany(db, clock, storeId, keys),
|
|
101
|
+
set: (key, value, ttl) => set(db, clock, storeId, key, value, ttl),
|
|
102
|
+
setMany: (entries) => setMany(db, clock, storeId, entries),
|
|
103
|
+
remove: (key) => remove(db, storeId, key),
|
|
104
|
+
clear: clear(db, storeId)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
const defaultDatabase = "effect_persistence"
|
|
111
|
+
const databaseVersion = 1
|
|
112
|
+
const entriesStoreName = "entries"
|
|
113
|
+
const storeIdIndexName = "storeId"
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a `Persistence` layer backed by IndexedDB, optionally using the provided database name.
|
|
117
|
+
*
|
|
118
|
+
* @category layers
|
|
119
|
+
* @since 4.0.0
|
|
120
|
+
*/
|
|
121
|
+
export const layerIndexedDb = (options?: {
|
|
122
|
+
readonly database?: string | undefined
|
|
123
|
+
}): Layer.Layer<Persistence.Persistence> =>
|
|
124
|
+
Persistence.layer.pipe(
|
|
125
|
+
Layer.provide(layerBackingIndexedDb(options))
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const openDatabase = (database: string): Effect.Effect<IDBDatabase, Persistence.PersistenceError> =>
|
|
129
|
+
Effect.gen(function*() {
|
|
130
|
+
const openRequest = yield* Effect.try({
|
|
131
|
+
try: () => globalThis.indexedDB.open(database, databaseVersion),
|
|
132
|
+
catch: (cause) =>
|
|
133
|
+
new Persistence.PersistenceError({
|
|
134
|
+
message: "Failed to open backing store database",
|
|
135
|
+
cause
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
openRequest.onupgradeneeded = () => {
|
|
140
|
+
const db = openRequest.result
|
|
141
|
+
const entries = db.objectStoreNames.contains(entriesStoreName)
|
|
142
|
+
? openRequest.transaction?.objectStore(entriesStoreName)
|
|
143
|
+
: db.createObjectStore(entriesStoreName, { keyPath: ["storeId", "id"] })
|
|
144
|
+
if (entries && !entries.indexNames.contains(storeIdIndexName)) {
|
|
145
|
+
entries.createIndex(storeIdIndexName, storeIdIndexName, { unique: false })
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return yield* idbRequest("Failed to open backing store database", () => openRequest)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
interface EntryRow {
|
|
153
|
+
readonly storeId: string
|
|
154
|
+
readonly id: string
|
|
155
|
+
readonly value: object
|
|
156
|
+
readonly expires: number | null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isExpired = (row: EntryRow, now: number): boolean => row.expires !== null && row.expires <= now
|
|
160
|
+
|
|
161
|
+
const get = (
|
|
162
|
+
db: IDBDatabase,
|
|
163
|
+
clock: Clock.Clock,
|
|
164
|
+
storeId: string,
|
|
165
|
+
key: string
|
|
166
|
+
): Effect.Effect<object | undefined, Persistence.PersistenceError> =>
|
|
167
|
+
withEntriesTransaction<object | undefined>(
|
|
168
|
+
db,
|
|
169
|
+
"readwrite",
|
|
170
|
+
`Failed to get key ${key} from backing store`,
|
|
171
|
+
(
|
|
172
|
+
entries,
|
|
173
|
+
setResult,
|
|
174
|
+
fail
|
|
175
|
+
) => {
|
|
176
|
+
const now = clock.currentTimeMillisUnsafe()
|
|
177
|
+
const id: [string, string] = [storeId, key]
|
|
178
|
+
const request = entries.get(id)
|
|
179
|
+
request.onerror = () => fail(request.error)
|
|
180
|
+
request.onsuccess = () => {
|
|
181
|
+
const row = request.result as EntryRow | undefined
|
|
182
|
+
if (!row || !isExpired(row, now)) {
|
|
183
|
+
setResult(row?.value)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const deleteRequest = entries.delete(id)
|
|
188
|
+
deleteRequest.onerror = () => fail(deleteRequest.error)
|
|
189
|
+
deleteRequest.onsuccess = () => setResult(undefined)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const getMany = (
|
|
195
|
+
db: IDBDatabase,
|
|
196
|
+
clock: Clock.Clock,
|
|
197
|
+
storeId: string,
|
|
198
|
+
keys: Arr.NonEmptyArray<string>
|
|
199
|
+
): Effect.Effect<Arr.NonEmptyArray<object | undefined>, Persistence.PersistenceError> =>
|
|
200
|
+
withEntriesTransaction(
|
|
201
|
+
db,
|
|
202
|
+
"readwrite",
|
|
203
|
+
"Failed to getMany from backing store",
|
|
204
|
+
(entries, setResult, fail) => {
|
|
205
|
+
const now = clock.currentTimeMillisUnsafe()
|
|
206
|
+
const results = new Array<object | undefined>(keys.length)
|
|
207
|
+
setResult(results as any)
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < keys.length; i++) {
|
|
210
|
+
const key = keys[i]
|
|
211
|
+
const keyPath = [storeId, key]
|
|
212
|
+
const request = entries.get(keyPath)
|
|
213
|
+
request.onerror = () => fail(request.error)
|
|
214
|
+
request.onsuccess = () => {
|
|
215
|
+
const row = request.result as EntryRow | undefined
|
|
216
|
+
if (!row) return
|
|
217
|
+
else if (!isExpired(row, now)) {
|
|
218
|
+
results[i] = row.value
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
const deleteRequest = entries.delete(keyPath)
|
|
222
|
+
deleteRequest.onerror = () => fail(deleteRequest.error)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const set = (
|
|
229
|
+
db: IDBDatabase,
|
|
230
|
+
clock: Clock.Clock,
|
|
231
|
+
storeId: string,
|
|
232
|
+
key: string,
|
|
233
|
+
value: object,
|
|
234
|
+
ttl: Duration.Duration | undefined
|
|
235
|
+
): Effect.Effect<void, Persistence.PersistenceError> =>
|
|
236
|
+
withEntriesTransaction(
|
|
237
|
+
db,
|
|
238
|
+
"readwrite",
|
|
239
|
+
`Failed to set key ${key} in backing store`,
|
|
240
|
+
(entries, setResult, fail) => {
|
|
241
|
+
const request = entries.put(
|
|
242
|
+
{
|
|
243
|
+
storeId,
|
|
244
|
+
id: key,
|
|
245
|
+
value,
|
|
246
|
+
expires: Persistence.unsafeTtlToExpires(clock, ttl)
|
|
247
|
+
} satisfies EntryRow
|
|
248
|
+
)
|
|
249
|
+
request.onerror = () => fail(request.error)
|
|
250
|
+
request.onsuccess = () => setResult(undefined)
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
const setMany = (
|
|
255
|
+
db: IDBDatabase,
|
|
256
|
+
clock: Clock.Clock,
|
|
257
|
+
storeId: string,
|
|
258
|
+
entries: Arr.NonEmptyArray<readonly [key: string, value: object, ttl: Duration.Duration | undefined]>
|
|
259
|
+
): Effect.Effect<void, Persistence.PersistenceError> =>
|
|
260
|
+
withEntriesTransaction(
|
|
261
|
+
db,
|
|
262
|
+
"readwrite",
|
|
263
|
+
"Failed to setMany in backing store",
|
|
264
|
+
(store, setResult, fail) => {
|
|
265
|
+
for (const [key, value, ttl] of entries) {
|
|
266
|
+
const request = store.put(
|
|
267
|
+
{
|
|
268
|
+
storeId,
|
|
269
|
+
id: key,
|
|
270
|
+
value,
|
|
271
|
+
expires: Persistence.unsafeTtlToExpires(clock, ttl)
|
|
272
|
+
} satisfies EntryRow
|
|
273
|
+
)
|
|
274
|
+
request.onerror = () => fail(request.error)
|
|
275
|
+
request.onsuccess = () => setResult(undefined)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const remove = (
|
|
281
|
+
db: IDBDatabase,
|
|
282
|
+
storeId: string,
|
|
283
|
+
key: string
|
|
284
|
+
): Effect.Effect<void, Persistence.PersistenceError> =>
|
|
285
|
+
withEntriesTransaction(
|
|
286
|
+
db,
|
|
287
|
+
"readwrite",
|
|
288
|
+
`Failed to remove key ${key} from backing store`,
|
|
289
|
+
(entries, setResult, fail) => {
|
|
290
|
+
const request = entries.delete([storeId, key])
|
|
291
|
+
request.onerror = () => fail(request.error)
|
|
292
|
+
request.onsuccess = () => setResult(undefined)
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const clear = (db: IDBDatabase, storeId: string): Effect.Effect<void, Persistence.PersistenceError> =>
|
|
297
|
+
withEntriesTransaction(db, "readwrite", "Failed to clear backing store", (entries, setResult, fail) => {
|
|
298
|
+
const index = entries.index(storeIdIndexName)
|
|
299
|
+
const cursorRequest = index.openCursor(storeId)
|
|
300
|
+
cursorRequest.onerror = () => fail(cursorRequest.error)
|
|
301
|
+
cursorRequest.onsuccess = () => {
|
|
302
|
+
const cursor = cursorRequest.result
|
|
303
|
+
if (!cursor) {
|
|
304
|
+
setResult(undefined)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
const deleteRequest = cursor.delete()
|
|
308
|
+
deleteRequest.onerror = () => fail(deleteRequest.error)
|
|
309
|
+
deleteRequest.onsuccess = () => cursor.continue()
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
const withEntriesTransaction = <A>(
|
|
314
|
+
db: IDBDatabase,
|
|
315
|
+
mode: IDBTransactionMode,
|
|
316
|
+
message: string,
|
|
317
|
+
run: (
|
|
318
|
+
entries: IDBObjectStore,
|
|
319
|
+
onResult: (result: A) => void,
|
|
320
|
+
fail: (cause: unknown) => void
|
|
321
|
+
) => void
|
|
322
|
+
): Effect.Effect<A, Persistence.PersistenceError> =>
|
|
323
|
+
Effect.callback<A, Persistence.PersistenceError>((resume) => {
|
|
324
|
+
const tx = db.transaction(entriesStoreName, mode)
|
|
325
|
+
const entries = tx.objectStore(entriesStoreName)
|
|
326
|
+
|
|
327
|
+
let result: A | undefined
|
|
328
|
+
let setResult = false
|
|
329
|
+
let done = false
|
|
330
|
+
|
|
331
|
+
const fail = (cause: unknown) => {
|
|
332
|
+
done = true
|
|
333
|
+
resume(Effect.fail(new Persistence.PersistenceError({ message, cause })))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
tx.oncomplete = () => {
|
|
337
|
+
done = true
|
|
338
|
+
if (setResult) resume(Effect.succeed(result!))
|
|
339
|
+
}
|
|
340
|
+
tx.onerror = () => {
|
|
341
|
+
done = true
|
|
342
|
+
fail(tx.error)
|
|
343
|
+
}
|
|
344
|
+
tx.onabort = () => {
|
|
345
|
+
done = true
|
|
346
|
+
fail(tx.error)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
run(entries, (next) => {
|
|
350
|
+
if (done) return resume(Effect.succeed(next))
|
|
351
|
+
setResult = true
|
|
352
|
+
result = next
|
|
353
|
+
}, fail)
|
|
354
|
+
|
|
355
|
+
return Effect.sync(() => {
|
|
356
|
+
tx.abort()
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const idbRequest = <A>(
|
|
361
|
+
message: string,
|
|
362
|
+
evaluate: () => IDBRequest<A>
|
|
363
|
+
): Effect.Effect<A, Persistence.PersistenceError> =>
|
|
364
|
+
Effect.callback<A, Persistence.PersistenceError>((resume) => {
|
|
365
|
+
const request = evaluate()
|
|
366
|
+
const fail = (cause: unknown) => {
|
|
367
|
+
resume(Effect.fail(new Persistence.PersistenceError({ message, cause })))
|
|
368
|
+
}
|
|
369
|
+
if (request.readyState === "done") {
|
|
370
|
+
resume(Effect.succeed(request.result))
|
|
371
|
+
}
|
|
372
|
+
request.onsuccess = () => {
|
|
373
|
+
resume(Effect.succeed(request.result))
|
|
374
|
+
}
|
|
375
|
+
request.onerror = () => fail(request.error)
|
|
376
|
+
})
|
package/src/BrowserRuntime.ts
CHANGED
|
@@ -1,17 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Browser entry-point helpers for running Effect programs.
|
|
3
|
+
*
|
|
4
|
+
* This module exposes `runMain`, a browser-oriented main runner for launching
|
|
5
|
+
* an Effect as the root program of a page, single-page application, demo, or
|
|
6
|
+
* browser test harness. It delegates execution to the core Effect runtime while
|
|
7
|
+
* adding the browser lifecycle hook needed to interrupt the main fiber when the
|
|
8
|
+
* page receives `beforeunload`.
|
|
9
|
+
*
|
|
10
|
+
* `BrowserRuntime` does not provide application services by itself. Provide
|
|
11
|
+
* any required layers, such as browser HTTP, storage, worker, geolocation, or
|
|
12
|
+
* permission services, before passing the effect to `runMain`. Keep long-lived
|
|
13
|
+
* browser resources scoped so interruption can run their finalizers while the
|
|
14
|
+
* page is still active.
|
|
15
|
+
*
|
|
16
|
+
* Browser unload is more constrained than a process signal. Finalizers that
|
|
17
|
+
* need the network, timers, prompts, or long asynchronous work may not complete
|
|
18
|
+
* once navigation or tab close has started, and browsers do not expose a
|
|
19
|
+
* process exit status. Use `runMain` to connect the page lifecycle to Effect
|
|
20
|
+
* interruption, and use browser-specific persistence or delivery APIs for work
|
|
21
|
+
* that must survive page teardown.
|
|
22
|
+
*
|
|
23
|
+
* @since 4.0.0
|
|
3
24
|
*/
|
|
4
25
|
import type * as Effect from "effect/Effect"
|
|
5
26
|
import { makeRunMain, type Teardown } from "effect/Runtime"
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
|
-
*
|
|
29
|
+
* Runs an effect as the browser main program and interrupts its fiber when the page receives a `beforeunload` event.
|
|
30
|
+
*
|
|
31
|
+
* **When to use**
|
|
32
|
+
*
|
|
33
|
+
* Use to launch a browser page, single-page application, demo, or browser test
|
|
34
|
+
* harness as a root Effect program.
|
|
35
|
+
*
|
|
36
|
+
* **Details**
|
|
37
|
+
*
|
|
38
|
+
* Supports both direct and curried call forms. Options are forwarded to
|
|
39
|
+
* `makeRunMain`, including `disableErrorReporting` and custom `teardown`
|
|
40
|
+
* behavior.
|
|
41
|
+
*
|
|
42
|
+
* **Gotchas**
|
|
43
|
+
*
|
|
44
|
+
* The `beforeunload` interruption is best-effort. Browser teardown may prevent
|
|
45
|
+
* asynchronous finalizers, network work, timers, or prompts from completing.
|
|
46
|
+
*
|
|
9
47
|
* @category Runtime
|
|
48
|
+
* @since 4.0.0
|
|
10
49
|
*/
|
|
11
50
|
export const runMain: {
|
|
12
51
|
/**
|
|
13
|
-
*
|
|
52
|
+
* Runs an effect as the browser main program and interrupts its fiber when the page receives a `beforeunload` event.
|
|
53
|
+
*
|
|
54
|
+
* **When to use**
|
|
55
|
+
*
|
|
56
|
+
* Use to launch a browser page, single-page application, demo, or browser test
|
|
57
|
+
* harness as a root Effect program.
|
|
58
|
+
*
|
|
59
|
+
* **Details**
|
|
60
|
+
*
|
|
61
|
+
* Supports both direct and curried call forms. Options are forwarded to
|
|
62
|
+
* `makeRunMain`, including `disableErrorReporting` and custom `teardown`
|
|
63
|
+
* behavior.
|
|
64
|
+
*
|
|
65
|
+
* **Gotchas**
|
|
66
|
+
*
|
|
67
|
+
* The `beforeunload` interruption is best-effort. Browser teardown may prevent
|
|
68
|
+
* asynchronous finalizers, network work, timers, or prompts from completing.
|
|
69
|
+
*
|
|
14
70
|
* @category Runtime
|
|
71
|
+
* @since 4.0.0
|
|
15
72
|
*/
|
|
16
73
|
(
|
|
17
74
|
options?: {
|
|
@@ -20,8 +77,26 @@ export const runMain: {
|
|
|
20
77
|
}
|
|
21
78
|
): <E, A>(effect: Effect.Effect<A, E>) => void
|
|
22
79
|
/**
|
|
23
|
-
*
|
|
80
|
+
* Runs an effect as the browser main program and interrupts its fiber when the page receives a `beforeunload` event.
|
|
81
|
+
*
|
|
82
|
+
* **When to use**
|
|
83
|
+
*
|
|
84
|
+
* Use to launch a browser page, single-page application, demo, or browser test
|
|
85
|
+
* harness as a root Effect program.
|
|
86
|
+
*
|
|
87
|
+
* **Details**
|
|
88
|
+
*
|
|
89
|
+
* Supports both direct and curried call forms. Options are forwarded to
|
|
90
|
+
* `makeRunMain`, including `disableErrorReporting` and custom `teardown`
|
|
91
|
+
* behavior.
|
|
92
|
+
*
|
|
93
|
+
* **Gotchas**
|
|
94
|
+
*
|
|
95
|
+
* The `beforeunload` interruption is best-effort. Browser teardown may prevent
|
|
96
|
+
* asynchronous finalizers, network work, timers, or prompts from completing.
|
|
97
|
+
*
|
|
24
98
|
* @category Runtime
|
|
99
|
+
* @since 4.0.0
|
|
25
100
|
*/
|
|
26
101
|
<E, A>(
|
|
27
102
|
effect: Effect.Effect<A, E>,
|
package/src/BrowserSocket.ts
CHANGED
|
@@ -1,12 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Provide Effect sockets backed by the browser `WebSocket` implementation.
|
|
3
|
+
*
|
|
4
|
+
* This module is the browser entry point for Effect's socket abstraction. Use
|
|
5
|
+
* {@link layerWebSocket} when a client-side Effect program needs a complete
|
|
6
|
+
* `Socket.Socket` connected to a WebSocket URL. Use
|
|
7
|
+
* {@link layerWebSocketConstructor} when lower-level socket code only needs the
|
|
8
|
+
* browser-backed constructor service.
|
|
9
|
+
*
|
|
10
|
+
* ## Mental model
|
|
11
|
+
*
|
|
12
|
+
* `layerWebSocket` delegates socket behavior to Effect's WebSocket support and
|
|
13
|
+
* supplies `globalThis.WebSocket` as the constructor. Incoming browser messages
|
|
14
|
+
* are normalized to strings or binary `Uint8Array` values. Browser `Blob`
|
|
15
|
+
* messages are read into bytes before they reach the socket consumer.
|
|
16
|
+
*
|
|
17
|
+
* Outgoing data should already be serialized to a string or bytes. To close the
|
|
18
|
+
* underlying browser socket with a specific code and reason, send a
|
|
19
|
+
* `CloseEvent` value so the close metadata is preserved.
|
|
20
|
+
*
|
|
21
|
+
* ## Common tasks
|
|
22
|
+
*
|
|
23
|
+
* - Connect RPC transports, browser tests, or realtime UI features to a
|
|
24
|
+
* WebSocket URL with {@link layerWebSocket}.
|
|
25
|
+
* - Provide only the browser constructor service with
|
|
26
|
+
* {@link layerWebSocketConstructor} when another socket layer builds the
|
|
27
|
+
* connection.
|
|
28
|
+
* - Customize `closeCodeIsError` for protocols that treat specific close codes
|
|
29
|
+
* as normal completion instead of socket failure.
|
|
30
|
+
*
|
|
31
|
+
* ## Gotchas
|
|
32
|
+
*
|
|
33
|
+
* Browser WebSocket rules still apply. URL schemes, subprotocol negotiation,
|
|
34
|
+
* mixed-content blocking, cookies, authentication, server origin checks, and
|
|
35
|
+
* extension negotiation are controlled by the browser and server rather than by
|
|
36
|
+
* Effect. Close events become socket errors unless `closeCodeIsError`
|
|
37
|
+
* classifies the close code as clean.
|
|
38
|
+
*
|
|
39
|
+
* @since 4.0.0
|
|
3
40
|
*/
|
|
4
41
|
import * as Layer from "effect/Layer"
|
|
5
42
|
import * as Socket from "effect/unstable/socket/Socket"
|
|
6
43
|
|
|
7
44
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
45
|
+
* Creates a `Socket` layer connected to the given URL using the browser `WebSocket` constructor.
|
|
46
|
+
*
|
|
47
|
+
* **When to use**
|
|
48
|
+
*
|
|
49
|
+
* Use when browser or client-side code needs a complete `Socket` layer
|
|
50
|
+
* connected to a WebSocket URL.
|
|
51
|
+
*
|
|
52
|
+
* **Details**
|
|
53
|
+
*
|
|
54
|
+
* Delegates socket construction to `Socket.makeWebSocket` and provides the
|
|
55
|
+
* browser-backed `WebSocketConstructor` service.
|
|
56
|
+
*
|
|
57
|
+
* **Gotchas**
|
|
58
|
+
*
|
|
59
|
+
* Browser WebSocket rules still control URL schemes, mixed-content blocking,
|
|
60
|
+
* cookies, authentication, origin checks, subprotocols, and extensions. Close
|
|
61
|
+
* events are errors unless `closeCodeIsError` classifies the close code as
|
|
62
|
+
* clean.
|
|
63
|
+
*
|
|
64
|
+
* @see {@link layerWebSocketConstructor} for providing only the browser constructor service
|
|
65
|
+
*
|
|
66
|
+
* @category layers
|
|
67
|
+
* @since 4.0.0
|
|
10
68
|
*/
|
|
11
69
|
export const layerWebSocket = (url: string, options?: {
|
|
12
70
|
readonly closeCodeIsError?: (code: number) => boolean
|
|
@@ -16,10 +74,10 @@ export const layerWebSocket = (url: string, options?: {
|
|
|
16
74
|
)
|
|
17
75
|
|
|
18
76
|
/**
|
|
19
|
-
*
|
|
77
|
+
* Layer that provides a `WebSocketConstructor` service backed by `globalThis.WebSocket`.
|
|
20
78
|
*
|
|
21
|
-
* @
|
|
22
|
-
* @
|
|
79
|
+
* @category layers
|
|
80
|
+
* @since 4.0.0
|
|
23
81
|
*/
|
|
24
82
|
export const layerWebSocketConstructor: Layer.Layer<Socket.WebSocketConstructor> =
|
|
25
83
|
Socket.layerWebSocketConstructorGlobal
|
package/src/BrowserStream.ts
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Convert browser DOM events into Effect streams.
|
|
3
|
+
*
|
|
4
|
+
* This module provides typed constructors for listening to `window` and
|
|
5
|
+
* `document` events from Effect programs. Use {@link fromEventListenerWindow}
|
|
6
|
+
* for viewport, network, focus, pointer, keyboard, and other `Window` events,
|
|
7
|
+
* and use {@link fromEventListenerDocument} for document lifecycle,
|
|
8
|
+
* visibility, selection, fullscreen, and other `Document` events.
|
|
9
|
+
*
|
|
10
|
+
* ## Mental model
|
|
11
|
+
*
|
|
12
|
+
* Each constructor registers a DOM `addEventListener` callback when the stream
|
|
13
|
+
* is consumed and removes it when the stream is finalized. Browser events are
|
|
14
|
+
* push-based `EventTarget` notifications, so the browser does not slow down the
|
|
15
|
+
* event source when downstream stream processing is busy. Events are buffered
|
|
16
|
+
* inside the stream until a consumer pulls them.
|
|
17
|
+
*
|
|
18
|
+
* ## Common tasks
|
|
19
|
+
*
|
|
20
|
+
* - Track browser state such as resize, online / offline, focus, visibility, or
|
|
21
|
+
* pointer activity with Effect stream operators.
|
|
22
|
+
* - Scope DOM listeners to a fiber so they are removed when the consuming
|
|
23
|
+
* effect is interrupted or completes.
|
|
24
|
+
* - Set `bufferSize` for bursty event sources before applying sampling,
|
|
25
|
+
* throttling, debouncing, or dropping logic downstream.
|
|
26
|
+
*
|
|
27
|
+
* ## Gotchas
|
|
28
|
+
*
|
|
29
|
+
* The default buffer is unbounded. High-frequency sources such as `scroll`,
|
|
30
|
+
* `pointermove`, or `mousemove` should usually specify `bufferSize` and reduce
|
|
31
|
+
* the event rate with stream operators.
|
|
32
|
+
*
|
|
33
|
+
* These helpers are for DOM events, not for `ReadableStream` request or
|
|
34
|
+
* response bodies. Fetch bodies follow Web Streams rules such as
|
|
35
|
+
* single-consumer locking and disturbed bodies after reads. When using the DOM
|
|
36
|
+
* `once` option, also use `Stream.take(1)` if the consuming code needs a finite
|
|
37
|
+
* stream.
|
|
38
|
+
*
|
|
39
|
+
* @since 4.0.0
|
|
3
40
|
*/
|
|
4
41
|
|
|
5
42
|
import * as Stream from "effect/Stream"
|
|
@@ -7,11 +44,14 @@ import * as Stream from "effect/Stream"
|
|
|
7
44
|
/**
|
|
8
45
|
* Creates a `Stream` from `window.addEventListener`.
|
|
9
46
|
*
|
|
47
|
+
* **Details**
|
|
48
|
+
*
|
|
10
49
|
* By default, the underlying buffer is unbounded in size. You can customize the
|
|
11
|
-
* buffer size an object as the second argument with the `bufferSize`
|
|
50
|
+
* buffer size by passing an object as the second argument with the `bufferSize`
|
|
51
|
+
* field.
|
|
12
52
|
*
|
|
13
|
-
* @since 1.0.0
|
|
14
53
|
* @category Streams
|
|
54
|
+
* @since 4.0.0
|
|
15
55
|
*/
|
|
16
56
|
export const fromEventListenerWindow = <K extends keyof WindowEventMap>(
|
|
17
57
|
type: K,
|
|
@@ -26,11 +66,14 @@ export const fromEventListenerWindow = <K extends keyof WindowEventMap>(
|
|
|
26
66
|
/**
|
|
27
67
|
* Creates a `Stream` from `document.addEventListener`.
|
|
28
68
|
*
|
|
69
|
+
* **Details**
|
|
70
|
+
*
|
|
29
71
|
* By default, the underlying buffer is unbounded in size. You can customize the
|
|
30
|
-
* buffer size an object as the second argument with the `bufferSize`
|
|
72
|
+
* buffer size by passing an object as the second argument with the `bufferSize`
|
|
73
|
+
* field.
|
|
31
74
|
*
|
|
32
|
-
* @since 1.0.0
|
|
33
75
|
* @category Streams
|
|
76
|
+
* @since 4.0.0
|
|
34
77
|
*/
|
|
35
78
|
export const fromEventListenerDocument = <K extends keyof DocumentEventMap>(
|
|
36
79
|
type: K,
|