Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import * as BrowserCrypto from "@effect/platform-browser/BrowserCrypto"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Layer } from "effect"
|
||||
import * as Crypto from "effect/Crypto"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
|
||||
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
||||
const uuidV7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
||||
|
||||
const getRandomValues = <T extends ArrayBufferView | null>(array: T): T => {
|
||||
if (array instanceof Uint8Array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = i & 0xff
|
||||
}
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
describe("BrowserCrypto", () => {
|
||||
it.effect("generates UUIDv4 values from getRandomValues", () =>
|
||||
Effect.gen(function*() {
|
||||
const crypto = yield* Crypto.Crypto
|
||||
const uuid = yield* crypto.randomUUIDv4
|
||||
assert.strictEqual(uuid, "00010203-0405-4607-8809-0a0b0c0d0e0f")
|
||||
assert.match(uuid, uuidV4Regex)
|
||||
}).pipe(Effect.provide(BrowserCrypto.layer.pipe(
|
||||
Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, {
|
||||
...crypto,
|
||||
getRandomValues(array) {
|
||||
return getRandomValues(array)
|
||||
}
|
||||
}))
|
||||
))))
|
||||
|
||||
it.effect("generates UUIDv7 values from getRandomValues and the Clock", () =>
|
||||
Effect.gen(function*() {
|
||||
yield* TestClock.setTime(0x0123456789ab)
|
||||
const crypto = yield* Crypto.Crypto
|
||||
const uuid = yield* crypto.randomUUIDv7
|
||||
assert.strictEqual(uuid, "01234567-89ab-7607-8809-0a0b0c0d0e0f")
|
||||
assert.match(uuid, uuidV7Regex)
|
||||
}).pipe(Effect.provide(BrowserCrypto.layer.pipe(
|
||||
Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, {
|
||||
...crypto,
|
||||
getRandomValues(array) {
|
||||
return getRandomValues(array)
|
||||
}
|
||||
}))
|
||||
))))
|
||||
|
||||
it.effect("computes digests with subtle crypto", () => {
|
||||
const buffer = new ArrayBuffer(3)
|
||||
new Uint8Array(buffer).set([1, 2, 3])
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const crypto = yield* Crypto.Crypto
|
||||
const digest = yield* crypto.digest("SHA-256", new Uint8Array(buffer))
|
||||
assert.deepStrictEqual(digest, new Uint8Array([1, 2, 3]))
|
||||
}).pipe(
|
||||
Effect.provide(BrowserCrypto.layer.pipe(
|
||||
Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, {
|
||||
...crypto,
|
||||
subtle: {
|
||||
...crypto.subtle,
|
||||
digest() {
|
||||
return Promise.resolve(buffer)
|
||||
}
|
||||
}
|
||||
}))
|
||||
))
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as BrowserHttpClient from "@effect/platform-browser/BrowserHttpClient"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Stream from "effect/Stream"
|
||||
import * as Cookies from "effect/unstable/http/Cookies"
|
||||
import * as HttpClient from "effect/unstable/http/HttpClient"
|
||||
import * as MXHR from "mock-xmlhttprequest"
|
||||
|
||||
const layer = (...args: Parameters<typeof MXHR.newServer>) =>
|
||||
Layer.unwrap(Effect.sync(() => {
|
||||
const server = MXHR.newServer(...args)
|
||||
return BrowserHttpClient.layerXMLHttpRequest.pipe(
|
||||
Layer.provide(Layer.succeed(BrowserHttpClient.XMLHttpRequest, server.xhrFactory))
|
||||
)
|
||||
}))
|
||||
|
||||
describe("BrowserHttpClient", () => {
|
||||
it.effect("json", () =>
|
||||
Effect.gen(function*() {
|
||||
const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe(
|
||||
Effect.flatMap((_) => _.json)
|
||||
)
|
||||
assert.deepStrictEqual(body, { message: "Success!" })
|
||||
}).pipe(Effect.provide(layer({
|
||||
get: ["http://localhost:8080/my/url", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{ \"message\": \"Success!\" }"
|
||||
}]
|
||||
}))))
|
||||
|
||||
it.effect("stream", () =>
|
||||
Effect.gen(function*() {
|
||||
const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe(
|
||||
Effect.flatMap((response) =>
|
||||
response.stream.pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.mkString
|
||||
)
|
||||
)
|
||||
)
|
||||
assert.deepStrictEqual(body, "{ \"message\": \"Success!\" }")
|
||||
}).pipe(Effect.provide(layer({
|
||||
get: ["http://localhost:8080/my/url", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{ \"message\": \"Success!\" }"
|
||||
}]
|
||||
}))))
|
||||
|
||||
it.effect("cookies", () =>
|
||||
Effect.gen(function*() {
|
||||
const cookies = yield* HttpClient.get("http://localhost:8080/my/url").pipe(
|
||||
Effect.map((response) => response.cookies)
|
||||
)
|
||||
assert.deepStrictEqual(Cookies.toRecord(cookies), {
|
||||
foo: "bar"
|
||||
})
|
||||
}).pipe(Effect.provide(layer({
|
||||
get: ["http://localhost:8080/my/url", {
|
||||
headers: { "Content-Type": "application/json", "Set-Cookie": "foo=bar; HttpOnly; Secure" },
|
||||
body: "{ \"message\": \"Success!\" }"
|
||||
}]
|
||||
}))))
|
||||
|
||||
it.effect("arrayBuffer", () =>
|
||||
Effect.gen(function*() {
|
||||
const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe(
|
||||
Effect.flatMap((response) => response.arrayBuffer),
|
||||
BrowserHttpClient.withXHRArrayBuffer
|
||||
)
|
||||
assert.strictEqual(new TextDecoder().decode(body), "{ \"message\": \"Success!\" }")
|
||||
}).pipe(Effect.provide(layer({
|
||||
get: ["http://localhost:8080/my/url", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{ \"message\": \"Success!\" }"
|
||||
}]
|
||||
}))))
|
||||
|
||||
it.effect("arrayBuffer without withXHRArrayBuffer", () =>
|
||||
Effect.gen(function*() {
|
||||
const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe(
|
||||
Effect.flatMap((response) => response.arrayBuffer)
|
||||
)
|
||||
assert.strictEqual(new TextDecoder().decode(body), "{ \"message\": \"Success!\" }")
|
||||
}).pipe(Effect.provide(layer({
|
||||
get: ["http://localhost:8080/my/url", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{ \"message\": \"Success!\" }"
|
||||
}]
|
||||
}))))
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore"
|
||||
import * as IndexedDb from "@effect/platform-browser/IndexedDb"
|
||||
import { describe } from "@effect/vitest"
|
||||
import { Layer } from "effect"
|
||||
import { testLayer } from "effect-test/unstable/persistence/KeyValueStore.test"
|
||||
import { IDBKeyRange, indexedDB } from "fake-indexeddb"
|
||||
|
||||
describe("KeyValueStore / layerLocalStorage", () => testLayer(BrowserKeyValueStore.layerLocalStorage))
|
||||
|
||||
describe("KeyValueStore / layerSessionStorage", () => testLayer(BrowserKeyValueStore.layerSessionStorage))
|
||||
|
||||
describe("KeyValueStore / layerIndexedDb", () => {
|
||||
const layerFakeIndexedDb = Layer.succeed(
|
||||
IndexedDb.IndexedDb,
|
||||
IndexedDb.make({ indexedDB, IDBKeyRange })
|
||||
)
|
||||
|
||||
testLayer(
|
||||
BrowserKeyValueStore.layerIndexedDb({ database: "kvs_test_db" }).pipe(
|
||||
Layer.provide(layerFakeIndexedDb)
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,209 @@
|
||||
import * as BrowserPersistence from "@effect/platform-browser/BrowserPersistence"
|
||||
import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest"
|
||||
import * as Duration from "effect/Duration"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import * as Persistence from "effect/unstable/persistence/Persistence"
|
||||
import { indexedDB as fakeIndexedDb } from "fake-indexeddb"
|
||||
|
||||
const defaultDatabase = "effect_persistence"
|
||||
const customDatabase = "effect_persistence_custom"
|
||||
|
||||
interface EntryRow {
|
||||
readonly storeId: string
|
||||
readonly id: string
|
||||
readonly value: object
|
||||
readonly expires: number | null
|
||||
}
|
||||
|
||||
let previousIndexedDb: unknown
|
||||
|
||||
beforeEach(() => {
|
||||
previousIndexedDb = Reflect.get(globalThis, "indexedDB")
|
||||
Reflect.set(globalThis, "indexedDB", fakeIndexedDb)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fakeIndexedDb.deleteDatabase(defaultDatabase)
|
||||
fakeIndexedDb.deleteDatabase(customDatabase)
|
||||
if (previousIndexedDb === undefined) {
|
||||
Reflect.deleteProperty(globalThis, "indexedDB")
|
||||
return
|
||||
}
|
||||
Reflect.set(globalThis, "indexedDB", previousIndexedDb)
|
||||
})
|
||||
|
||||
describe.sequential("BrowserPersistence", () => {
|
||||
it.effect("set + get", () =>
|
||||
Effect.gen(function*() {
|
||||
const backing = yield* Persistence.BackingPersistence
|
||||
const store = yield* backing.make("users")
|
||||
|
||||
yield* store.set("key1", { name: "Alice" }, undefined)
|
||||
|
||||
const value = yield* store.get("key1")
|
||||
assert.deepStrictEqual(value, { name: "Alice" })
|
||||
}).pipe(
|
||||
Effect.provide(BrowserPersistence.layerBackingIndexedDb())
|
||||
))
|
||||
|
||||
it.effect("setMany + getMany preserves order, missing keys, and duplicates", () =>
|
||||
Effect.gen(function*() {
|
||||
const backing = yield* Persistence.BackingPersistence
|
||||
const store = yield* backing.make("users")
|
||||
|
||||
yield* store.setMany([
|
||||
["key1", { name: "Alice" }, undefined],
|
||||
["key2", { name: "Bob" }, undefined],
|
||||
["key3", { name: "Charlie" }, undefined]
|
||||
])
|
||||
|
||||
const values = yield* store.getMany(["key3", "missing", "key1", "key1", "key2"])
|
||||
assert.deepStrictEqual(values, [
|
||||
{ name: "Charlie" },
|
||||
undefined,
|
||||
{ name: "Alice" },
|
||||
{ name: "Alice" },
|
||||
{ name: "Bob" }
|
||||
])
|
||||
}).pipe(
|
||||
Effect.provide(BrowserPersistence.layerBackingIndexedDb())
|
||||
))
|
||||
|
||||
it.effect("remove", () =>
|
||||
Effect.gen(function*() {
|
||||
const backing = yield* Persistence.BackingPersistence
|
||||
const store = yield* backing.make("users")
|
||||
|
||||
yield* store.setMany([
|
||||
["key1", { name: "Alice" }, undefined],
|
||||
["key2", { name: "Bob" }, undefined]
|
||||
])
|
||||
|
||||
yield* store.remove("key1")
|
||||
|
||||
const values = yield* store.getMany(["key1", "key2"])
|
||||
assert.deepStrictEqual(values, [undefined, { name: "Bob" }])
|
||||
}).pipe(
|
||||
Effect.provide(BrowserPersistence.layerBackingIndexedDb())
|
||||
))
|
||||
|
||||
it.effect("clear only affects the current storeId", () =>
|
||||
Effect.gen(function*() {
|
||||
const backing = yield* Persistence.BackingPersistence
|
||||
const storeA = yield* backing.make("store-a")
|
||||
const storeB = yield* backing.make("store-b")
|
||||
|
||||
yield* storeA.set("shared", { owner: "A" }, undefined)
|
||||
yield* storeA.set("only-a", { owner: "A" }, undefined)
|
||||
yield* storeB.set("shared", { owner: "B" }, undefined)
|
||||
|
||||
yield* storeA.clear
|
||||
|
||||
assert.deepStrictEqual(yield* storeA.get("shared"), undefined)
|
||||
assert.deepStrictEqual(yield* storeA.get("only-a"), undefined)
|
||||
assert.deepStrictEqual(yield* storeB.get("shared"), { owner: "B" })
|
||||
}).pipe(
|
||||
Effect.provide(BrowserPersistence.layerBackingIndexedDb())
|
||||
))
|
||||
|
||||
it.effect("TTL expiry performs lazy deletion", () =>
|
||||
Effect.gen(function*() {
|
||||
const backing = yield* Persistence.BackingPersistence
|
||||
const store = yield* backing.make("users")
|
||||
|
||||
yield* store.set("ttl-key", { name: "expiring" }, Duration.seconds(10))
|
||||
|
||||
const rowBefore = yield* getRawEntry(defaultDatabase, "users", "ttl-key").pipe(Effect.orDie)
|
||||
assert.isTrue(rowBefore !== undefined)
|
||||
|
||||
yield* TestClock.adjust(Duration.seconds(11))
|
||||
|
||||
const expired = yield* store.get("ttl-key")
|
||||
assert.deepStrictEqual(expired, undefined)
|
||||
|
||||
const rowAfter = yield* getRawEntry(defaultDatabase, "users", "ttl-key").pipe(Effect.orDie)
|
||||
assert.deepStrictEqual(rowAfter, undefined)
|
||||
}).pipe(
|
||||
Effect.provide(BrowserPersistence.layerBackingIndexedDb())
|
||||
))
|
||||
|
||||
it.effect("store isolation across storeIds", () =>
|
||||
Effect.gen(function*() {
|
||||
const backing = yield* Persistence.BackingPersistence
|
||||
const storeA = yield* backing.make("store-a")
|
||||
const storeB = yield* backing.make("store-b")
|
||||
|
||||
yield* storeA.set("same-key", { value: "A" }, undefined)
|
||||
yield* storeB.set("same-key", { value: "B" }, undefined)
|
||||
|
||||
yield* storeA.remove("same-key")
|
||||
|
||||
assert.deepStrictEqual(yield* storeA.get("same-key"), undefined)
|
||||
assert.deepStrictEqual(yield* storeB.get("same-key"), { value: "B" })
|
||||
}).pipe(
|
||||
Effect.provide(BrowserPersistence.layerBackingIndexedDb())
|
||||
))
|
||||
|
||||
it.effect("custom database option", () =>
|
||||
Effect.gen(function*() {
|
||||
yield* withStore("users", undefined, (store) => store.set("shared", { database: "default" }, undefined))
|
||||
|
||||
yield* withStore("users", { database: customDatabase }, (store) =>
|
||||
store.set("shared", { database: "custom" }, undefined))
|
||||
|
||||
const fromDefault = yield* withStore("users", undefined, (store) =>
|
||||
store.get("shared"))
|
||||
const fromCustom = yield* withStore("users", { database: customDatabase }, (store) =>
|
||||
store.get("shared"))
|
||||
|
||||
assert.deepStrictEqual(fromDefault, { database: "default" })
|
||||
assert.deepStrictEqual(fromCustom, { database: "custom" })
|
||||
}))
|
||||
})
|
||||
|
||||
const withStore = <A>(
|
||||
storeId: string,
|
||||
options: {
|
||||
readonly database?: string | undefined
|
||||
} | undefined,
|
||||
f: (store: Persistence.BackingPersistenceStore) => Effect.Effect<A, Persistence.PersistenceError>
|
||||
) =>
|
||||
Effect.gen(function*() {
|
||||
const backing = yield* Persistence.BackingPersistence
|
||||
const store = yield* backing.make(storeId)
|
||||
return yield* f(store)
|
||||
}).pipe(Effect.provide(BrowserPersistence.layerBackingIndexedDb(options)))
|
||||
|
||||
const withDatabase = <A>(
|
||||
database: string,
|
||||
f: (db: IDBDatabase) => Effect.Effect<A, unknown>
|
||||
): Effect.Effect<A, unknown> =>
|
||||
Effect.acquireUseRelease(
|
||||
idbRequest(() => globalThis.indexedDB.open(database, 1)),
|
||||
f,
|
||||
(db) => Effect.sync(() => db.close())
|
||||
)
|
||||
|
||||
const getRawEntry = (database: string, storeId: string, id: string): Effect.Effect<EntryRow | undefined, unknown> =>
|
||||
withDatabase(
|
||||
database,
|
||||
(db) => idbRequest(() => db.transaction("entries", "readonly").objectStore("entries").get([storeId, id]))
|
||||
)
|
||||
|
||||
const idbRequest = <A>(evaluate: () => IDBRequest<A>): Effect.Effect<A, unknown> =>
|
||||
Effect.flatMap(
|
||||
Effect.try({
|
||||
try: evaluate,
|
||||
catch: (cause) => cause
|
||||
}),
|
||||
(request) =>
|
||||
Effect.callback<A, unknown>((resume) => {
|
||||
if (request.readyState === "done") {
|
||||
resume(Effect.succeed(request.result))
|
||||
return
|
||||
}
|
||||
request.onsuccess = () => resume(Effect.succeed(request.result))
|
||||
request.onerror = () => resume(Effect.fail(request.error))
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as BrowserPersistence from "@effect/platform-browser/BrowserPersistence"
|
||||
import { afterEach, beforeEach, describe } from "@effect/vitest"
|
||||
import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest"
|
||||
import { indexedDB as fakeIndexedDb } from "fake-indexeddb"
|
||||
|
||||
const database = "effect_persistence_integration"
|
||||
|
||||
let previousIndexedDb: unknown
|
||||
|
||||
beforeEach(() => {
|
||||
previousIndexedDb = Reflect.get(globalThis, "indexedDB")
|
||||
Reflect.set(globalThis, "indexedDB", fakeIndexedDb)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fakeIndexedDb.deleteDatabase(database)
|
||||
if (previousIndexedDb === undefined) {
|
||||
Reflect.deleteProperty(globalThis, "indexedDB")
|
||||
return
|
||||
}
|
||||
Reflect.set(globalThis, "indexedDB", previousIndexedDb)
|
||||
})
|
||||
|
||||
describe.sequential("BrowserPersistence / PersistedCache", () => {
|
||||
PersistedCacheTest.suite(
|
||||
"browser-indexeddb",
|
||||
BrowserPersistence.layerIndexedDb({ database })
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,253 @@
|
||||
import { IndexedDb, IndexedDbDatabase, IndexedDbTable, IndexedDbVersion } from "@effect/platform-browser"
|
||||
import { afterEach, assert, describe, it } from "@effect/vitest"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { IDBKeyRange, indexedDB } from "fake-indexeddb"
|
||||
|
||||
const databaseName = "db"
|
||||
|
||||
const layerFakeIndexedDb = Layer.succeed(
|
||||
IndexedDb.IndexedDb,
|
||||
IndexedDb.make({ indexedDB, IDBKeyRange })
|
||||
)
|
||||
|
||||
const provideMigration = (database: IndexedDbDatabase.Any) =>
|
||||
Effect.provide(
|
||||
database.layer(databaseName).pipe(Layer.provide(layerFakeIndexedDb))
|
||||
)
|
||||
|
||||
afterEach(() => {
|
||||
indexedDB.deleteDatabase(databaseName)
|
||||
})
|
||||
|
||||
describe.sequential("IndexedDbDatabase", () => {
|
||||
it.effect("insert and read todos", () => {
|
||||
const Table = IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id",
|
||||
indexes: { titleIndex: "title" }
|
||||
})
|
||||
|
||||
const V1 = IndexedDbVersion.make(Table)
|
||||
|
||||
class Db extends IndexedDbDatabase.make(V1, (api) =>
|
||||
Effect.gen(function*() {
|
||||
yield* api.createObjectStore("todo")
|
||||
yield* api.createIndex("todo", "titleIndex")
|
||||
yield* api
|
||||
.from("todo")
|
||||
.insert({ id: 1, title: "test", completed: false })
|
||||
}))
|
||||
{}
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const db = yield* IndexedDbDatabase.IndexedDbDatabase
|
||||
const api = yield* Db.getQueryBuilder
|
||||
let todo = yield* api.from("todo").select()
|
||||
|
||||
const name = yield* api.use((database) => database.name)
|
||||
const version = yield* api.use((database) => database.version)
|
||||
const objectStoreNames = yield* api.use(
|
||||
(database) => database.objectStoreNames
|
||||
)
|
||||
const indexNames = yield* api.use(
|
||||
(database) => database.transaction("todo").objectStore("todo").indexNames
|
||||
)
|
||||
const index = yield* api.use((database) => database.transaction("todo").objectStore("todo").index("titleIndex"))
|
||||
|
||||
assert.equal(name, "db")
|
||||
assert.equal(version, 1)
|
||||
assert.deepStrictEqual(todo, [
|
||||
{ id: 1, title: "test", completed: false }
|
||||
])
|
||||
assert.deepStrictEqual(Array.from(objectStoreNames), ["todo"])
|
||||
assert.deepStrictEqual(Array.from(indexNames), ["titleIndex"])
|
||||
assert.deepStrictEqual(index.keyPath, "title")
|
||||
|
||||
yield* api.from("todo").insert({ id: 2, title: "test2", completed: false })
|
||||
|
||||
yield* db.rebuild
|
||||
todo = yield* api.from("todo").select()
|
||||
assert.deepStrictEqual(todo, [
|
||||
{ id: 1, title: "test", completed: false }
|
||||
])
|
||||
}).pipe(provideMigration(Db))
|
||||
})
|
||||
|
||||
it.effect("transaction", () => {
|
||||
const Table = IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id",
|
||||
indexes: { titleIndex: "title" }
|
||||
})
|
||||
|
||||
const Table2 = IndexedDbTable.make({
|
||||
name: "user",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
name: Schema.String
|
||||
}),
|
||||
keyPath: "id"
|
||||
})
|
||||
|
||||
const Db = IndexedDbVersion.make(Table, Table2)
|
||||
|
||||
class Migration extends IndexedDbDatabase.make(Db, (api) =>
|
||||
Effect.gen(function*() {
|
||||
yield* api.createObjectStore("todo")
|
||||
yield* api.createIndex("todo", "titleIndex")
|
||||
yield* api
|
||||
.from("todo")
|
||||
.insert({ id: 1, title: "test", completed: false })
|
||||
}))
|
||||
{}
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const api = yield* Migration.getQueryBuilder
|
||||
yield* api.withTransaction({ tables: ["todo"], mode: "readwrite" })(
|
||||
api.from("todo").insert({ id: 2, title: "test2", completed: false })
|
||||
)
|
||||
|
||||
const todo = yield* api.from("todo").select()
|
||||
assert.deepStrictEqual(todo, [
|
||||
{ id: 1, title: "test", completed: false },
|
||||
{
|
||||
id: 2,
|
||||
title: "test2",
|
||||
completed: false
|
||||
}
|
||||
])
|
||||
}).pipe(provideMigration(Migration))
|
||||
})
|
||||
|
||||
it.effect("migration sequence", () => {
|
||||
const Table1 = IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id",
|
||||
indexes: { titleIndex: "title" }
|
||||
})
|
||||
|
||||
const Table2 = IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
uuid: Schema.String.check(Schema.isUUID(4)),
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "uuid"
|
||||
})
|
||||
|
||||
const V1 = IndexedDbVersion.make(Table1)
|
||||
const V2 = IndexedDbVersion.make(Table2)
|
||||
const uuid = "9535a059-a61f-42e1-a2e0-35ec87203c24"
|
||||
|
||||
class Db extends IndexedDbDatabase.make(V1, (api) =>
|
||||
Effect.gen(function*() {
|
||||
yield* api.createObjectStore("todo")
|
||||
yield* api.createIndex("todo", "titleIndex")
|
||||
yield* api
|
||||
.from("todo")
|
||||
.insert({ id: 1, title: "test", completed: false })
|
||||
})).add(V2, (from, to) =>
|
||||
Effect.gen(function*() {
|
||||
const todo = yield* from.from("todo").select()
|
||||
yield* from.deleteIndex("todo", "titleIndex")
|
||||
yield* from.deleteObjectStore("todo")
|
||||
yield* to.createObjectStore("todo")
|
||||
yield* to.from("todo").insertAll(
|
||||
todo.map((t) => ({
|
||||
uuid,
|
||||
title: t.title,
|
||||
completed: t.completed
|
||||
}))
|
||||
)
|
||||
}))
|
||||
{}
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const api = yield* Db.getQueryBuilder
|
||||
const todo = yield* api.from("todo").select()
|
||||
const name = yield* api.use((database) => database.name)
|
||||
const version = yield* api.use((database) => database.version)
|
||||
const objectStoreNames = yield* api.use(
|
||||
(database) => database.objectStoreNames
|
||||
)
|
||||
const indexNames = yield* api.use(
|
||||
(database) => database.transaction("todo").objectStore("todo").indexNames
|
||||
)
|
||||
|
||||
assert.equal(name, "db")
|
||||
assert.equal(version, 2)
|
||||
assert.deepStrictEqual(todo, [{ uuid, title: "test", completed: false }])
|
||||
assert.deepStrictEqual(Array.from(objectStoreNames), ["todo"])
|
||||
assert.deepStrictEqual(Array.from(indexNames), [])
|
||||
}).pipe(provideMigration(Db))
|
||||
})
|
||||
|
||||
it.effect("delete object store migration", () => {
|
||||
const Table1 = IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id"
|
||||
})
|
||||
|
||||
const Table2 = IndexedDbTable.make({
|
||||
name: "user",
|
||||
schema: Schema.Struct({
|
||||
userId: Schema.Number,
|
||||
name: Schema.String,
|
||||
email: Schema.String
|
||||
}),
|
||||
keyPath: "userId"
|
||||
})
|
||||
|
||||
const V1 = IndexedDbVersion.make(Table1)
|
||||
const V2 = IndexedDbVersion.make(Table2, Table1)
|
||||
|
||||
class Migration extends IndexedDbDatabase.make(V1, (api) => api.createObjectStore("todo")).add(
|
||||
V2,
|
||||
Effect.fnUntraced(function*(from, to) {
|
||||
yield* from.deleteObjectStore("todo")
|
||||
yield* to.createObjectStore("user")
|
||||
yield* to.from("user").insert({
|
||||
userId: 1,
|
||||
name: "John Doe",
|
||||
email: "john.doe@example.com"
|
||||
})
|
||||
})
|
||||
) {}
|
||||
|
||||
return Effect.gen(function*() {
|
||||
const api = yield* Migration.getQueryBuilder
|
||||
const user = yield* api.from("user").select()
|
||||
|
||||
const name = yield* api.use((database) => database.name)
|
||||
const version = yield* api.use((database) => database.version)
|
||||
const objectStoreNames = yield* api.use((database) => database.objectStoreNames)
|
||||
assert.equal(name, "db")
|
||||
assert.equal(version, 2)
|
||||
assert.deepStrictEqual(user, [
|
||||
{ userId: 1, name: "John Doe", email: "john.doe@example.com" }
|
||||
])
|
||||
assert.deepStrictEqual(Array.from(objectStoreNames), ["user"])
|
||||
}).pipe(provideMigration(Migration))
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
import { IndexedDb, IndexedDbTable } from "@effect/platform-browser"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Schema } from "effect"
|
||||
|
||||
describe("IndexedDbTable", () => {
|
||||
it("make", () => {
|
||||
class Table extends IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id"
|
||||
}) {}
|
||||
|
||||
assert.equal(Table.tableName, "todo")
|
||||
assert.equal(Table.autoIncrement, false)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.id, Schema.Number)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.title, Schema.String)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.completed, Schema.Boolean)
|
||||
assert.equal(Table.keyPath, "id")
|
||||
})
|
||||
|
||||
it("autoIncrement", () => {
|
||||
class Table1 extends IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: IndexedDb.AutoIncrement,
|
||||
title: Schema.String
|
||||
}),
|
||||
keyPath: "id",
|
||||
autoIncrement: true
|
||||
}) {}
|
||||
|
||||
class Table2 extends IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: IndexedDb.AutoIncrement,
|
||||
title: Schema.String
|
||||
}),
|
||||
keyPath: "id",
|
||||
autoIncrement: false
|
||||
}) {}
|
||||
|
||||
assert.equal(Table1.tableName, "todo")
|
||||
assert.equal(Table1.autoIncrement, true)
|
||||
assert.deepStrictEqual(
|
||||
Table1.tableSchema.fields.id,
|
||||
IndexedDb.AutoIncrement
|
||||
)
|
||||
assert.deepStrictEqual(Table1.tableSchema.fields.title, Schema.String)
|
||||
assert.equal(Table1.keyPath, "id")
|
||||
|
||||
assert.equal(Table2.tableName, "todo")
|
||||
assert.equal(Table2.autoIncrement, false)
|
||||
assert.deepStrictEqual(
|
||||
Table2.tableSchema.fields.id,
|
||||
IndexedDb.AutoIncrement
|
||||
)
|
||||
assert.deepStrictEqual(Table2.tableSchema.fields.title, Schema.String)
|
||||
assert.equal(Table2.keyPath, "id")
|
||||
})
|
||||
|
||||
it("multiple keyPath", () => {
|
||||
const Table = IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: ["id", "title"]
|
||||
})
|
||||
|
||||
assert.equal(Table.tableName, "todo")
|
||||
assert.equal(Table.autoIncrement, false)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.id, Schema.Number)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.title, Schema.String)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.completed, Schema.Boolean)
|
||||
assert.deepStrictEqual(Table.keyPath, ["id", "title"])
|
||||
})
|
||||
|
||||
it("no keyPath", () => {
|
||||
const Table = IndexedDbTable.make({
|
||||
name: "todo",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
})
|
||||
})
|
||||
|
||||
assert.equal(Table.tableName, "todo")
|
||||
assert.equal(Table.autoIncrement, false)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.id, Schema.Number)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.title, Schema.String)
|
||||
assert.deepStrictEqual(Table.tableSchema.fields.completed, Schema.Boolean)
|
||||
assert.deepStrictEqual(Table.keyPath, undefined)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import { IndexedDbTable, IndexedDbVersion } from "@effect/platform-browser"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Schema } from "effect"
|
||||
|
||||
describe("IndexedDbVersion", () => {
|
||||
it("make single table", () => {
|
||||
class Table1 extends IndexedDbTable.make({
|
||||
name: "todo1",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id"
|
||||
}) {}
|
||||
|
||||
class Db extends IndexedDbVersion.make(Table1) {}
|
||||
|
||||
assert.equal(Db.tables.size, 1)
|
||||
assert.equal(Db.tables.get("todo1"), Table1)
|
||||
})
|
||||
|
||||
it("make multiple tables", () => {
|
||||
class Table1 extends IndexedDbTable.make({
|
||||
name: "todo1",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id"
|
||||
}) {}
|
||||
|
||||
class Table2 extends IndexedDbTable.make({
|
||||
name: "todo2",
|
||||
schema: Schema.Struct({
|
||||
id: Schema.Number,
|
||||
title: Schema.String,
|
||||
completed: Schema.Boolean
|
||||
}),
|
||||
keyPath: "id"
|
||||
}) {}
|
||||
|
||||
class Db extends IndexedDbVersion.make(Table1, Table2) {}
|
||||
|
||||
assert.equal(Db.tables.size, 2)
|
||||
assert.equal(Db.tables.get("todo1"), Table1)
|
||||
assert.equal(Db.tables.get("todo2"), Table2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as Permissions from "@effect/platform-browser/Permissions"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import * as Effect from "effect/Effect"
|
||||
|
||||
describe("Permissions", () => {
|
||||
it.effect("should be able to query permissions", () =>
|
||||
Effect.gen(function*() {
|
||||
const service = yield* Permissions.Permissions
|
||||
const permissions = yield* service.query("geolocation")
|
||||
assert.strictEqual(permissions.state, "granted")
|
||||
}).pipe(Effect.provide(Permissions.layer)))
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as BrowserWorker from "@effect/platform-browser/BrowserWorker"
|
||||
import { describe } from "@effect/vitest"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as RpcClient from "effect/unstable/rpc/RpcClient"
|
||||
import * as RpcServer from "effect/unstable/rpc/RpcServer"
|
||||
import { e2eSuite, UsersClient } from "./fixtures/rpc-e2e.ts"
|
||||
|
||||
describe("RpcWorker", () => {
|
||||
const WorkerClient = UsersClient.layer.pipe(
|
||||
Layer.provide(RpcClient.layerProtocolWorker({ size: 1 })),
|
||||
Layer.provide(BrowserWorker.layer(() => new Worker(new URL("./fixtures/rpc-worker.ts", import.meta.url)))),
|
||||
Layer.merge(
|
||||
Layer.succeed(RpcServer.Protocol)({
|
||||
supportsAck: true
|
||||
} as any)
|
||||
)
|
||||
)
|
||||
e2eSuite("e2e worker", WorkerClient, false)
|
||||
})
|
||||
141
repos/effect/packages/platform-browser/test/fixtures/rpc-e2e.ts
vendored
Normal file
141
repos/effect/packages/platform-browser/test/fixtures/rpc-e2e.ts
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import { Cause, Context, Effect, Fiber, Option, Stream } from "effect"
|
||||
// @ts-ignore
|
||||
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { NodeInspectSymbol } from "effect/Inspectable"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as RpcClient from "effect/unstable/rpc/RpcClient"
|
||||
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"
|
||||
import type * as RpcGroup from "effect/unstable/rpc/RpcGroup"
|
||||
import * as RpcServer from "effect/unstable/rpc/RpcServer"
|
||||
import * as RpcTest from "effect/unstable/rpc/RpcTest"
|
||||
import { AuthClient, AuthLive, TimingLive, User, UserRpcs, UsersLive } from "./rpc-schemas.ts"
|
||||
|
||||
export class UsersClient extends Context.Service<
|
||||
UsersClient,
|
||||
RpcClient.RpcClient<RpcGroup.Rpcs<typeof UserRpcs>, RpcClientError>
|
||||
>()("UsersClient") {
|
||||
static readonly layer = Layer.effect(UsersClient)(RpcClient.make(UserRpcs)).pipe(
|
||||
Layer.provide(AuthClient)
|
||||
)
|
||||
static layerTest = Layer.effect(UsersClient)(RpcTest.makeClient(UserRpcs)).pipe(
|
||||
Layer.provide([UsersLive, AuthLive, TimingLive, AuthClient])
|
||||
)
|
||||
}
|
||||
|
||||
export const e2eSuite = <E>(
|
||||
name: string,
|
||||
layer: Layer.Layer<UsersClient | RpcServer.Protocol, E>,
|
||||
concurrent = true
|
||||
) => {
|
||||
describe(name, { concurrent, timeout: 30_000 }, () => {
|
||||
it.effect("should get user", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
const user = yield* client.GetUser({ id: "1" })
|
||||
assert.instanceOf(user, User)
|
||||
assert.deepStrictEqual(user, new User({ id: "1", name: "Logged in user" }))
|
||||
}).pipe(Effect.provide(layer)))
|
||||
|
||||
it.effect("nested method", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
yield* client["nested.test"]()
|
||||
}).pipe(Effect.provide(layer)))
|
||||
|
||||
it.effect("should not flatten Option", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
const user = yield* client.GetUserOption({ id: "1" })
|
||||
assert.deepStrictEqual(user, Option.some(new User({ id: "1", name: "John" })))
|
||||
}).pipe(Effect.provide(layer)))
|
||||
|
||||
it.effect("headers", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
const user = yield* client.GetUser({ id: "1" })
|
||||
assert.instanceOf(user, User)
|
||||
assert.deepStrictEqual(user, new User({ id: "123", name: "Logged in user" }))
|
||||
}).pipe(
|
||||
RpcClient.withHeaders({ userId: "123" }),
|
||||
Effect.provide(layer)
|
||||
))
|
||||
|
||||
it.live("Stream", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
const users: Array<User> = []
|
||||
yield* client.StreamUsers({ id: "1" }).pipe(
|
||||
Stream.take(5),
|
||||
Stream.runForEach((user) =>
|
||||
Effect.sync(() => {
|
||||
users.push(user)
|
||||
})
|
||||
),
|
||||
Effect.forkChild
|
||||
)
|
||||
|
||||
yield* Effect.sleep(2000)
|
||||
assert.lengthOf(users, 5)
|
||||
|
||||
// test interrupts
|
||||
const interrupts = yield* client.GetInterrupts()
|
||||
assert.equal(interrupts, 1)
|
||||
|
||||
const { supportsAck } = yield* RpcServer.Protocol
|
||||
|
||||
// test backpressure
|
||||
if (supportsAck) {
|
||||
const emits = yield* client.GetEmits()
|
||||
assert.equal(emits, 5)
|
||||
}
|
||||
}).pipe(Effect.provide(layer)), { timeout: 20000 })
|
||||
|
||||
it.effect("defect", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
const cause = yield* client.ProduceDefect().pipe(
|
||||
Effect.sandbox,
|
||||
Effect.flip
|
||||
)
|
||||
assert.deepStrictEqual(cause, Cause.die("boom"))
|
||||
}).pipe(
|
||||
RpcClient.withHeaders({ userId: "123" }),
|
||||
Effect.provide(layer)
|
||||
))
|
||||
|
||||
it.live("never", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
const fiber = yield* client.Never().pipe(
|
||||
Effect.forkChild
|
||||
)
|
||||
yield* Effect.sleep(500)
|
||||
assert.isUndefined(fiber.pollUnsafe())
|
||||
|
||||
yield* Fiber.interrupt(fiber)
|
||||
yield* Effect.sleep(100)
|
||||
|
||||
const { supportsAck } = yield* RpcServer.Protocol
|
||||
if (supportsAck) {
|
||||
const interrupts = yield* client.GetInterrupts()
|
||||
assert.equal(interrupts, 1)
|
||||
}
|
||||
}).pipe(
|
||||
RpcClient.withHeaders({ userId: "123" }),
|
||||
Effect.provide(layer)
|
||||
))
|
||||
|
||||
it.effect("timing middleware", () =>
|
||||
Effect.gen(function*() {
|
||||
const client = yield* UsersClient
|
||||
const result = yield* client.TimedMethod({ shouldFail: false })
|
||||
assert.equal(result, 1)
|
||||
yield* client.TimedMethod({ shouldFail: true }).pipe(Effect.exit)
|
||||
const { count, defect, success } = yield* client.GetTimingMiddlewareMetrics()
|
||||
assert.notEqual(count, 0)
|
||||
assert.notEqual(defect, 0)
|
||||
assert.notEqual(success, 0)
|
||||
}).pipe(Effect.provide(layer)))
|
||||
})
|
||||
}
|
||||
157
repos/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts
vendored
Normal file
157
repos/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
import { Context, Effect, Layer, Metric, Option, Queue, Schema } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import * as Rpc from "effect/unstable/rpc/Rpc"
|
||||
import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
|
||||
import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware"
|
||||
import * as RpcServer from "effect/unstable/rpc/RpcServer"
|
||||
|
||||
export class User extends Schema.Class<User>("User")({
|
||||
id: Schema.String,
|
||||
name: Schema.String
|
||||
}) {}
|
||||
|
||||
class StreamUsers extends Rpc.make("StreamUsers", {
|
||||
success: User,
|
||||
payload: {
|
||||
id: Schema.String
|
||||
},
|
||||
stream: true
|
||||
}) {}
|
||||
|
||||
class CurrentUser extends Context.Service<CurrentUser, User>()("CurrentUser") {}
|
||||
|
||||
class Unauthorized extends Schema.ErrorClass<Unauthorized>("Unauthorized")({
|
||||
_tag: Schema.tag("Unauthorized")
|
||||
}) {}
|
||||
|
||||
class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, {
|
||||
provides: CurrentUser
|
||||
}>()("AuthMiddleware", {
|
||||
error: Unauthorized,
|
||||
requiredForClient: true
|
||||
}) {}
|
||||
|
||||
class TimingMiddleware extends RpcMiddleware.Service<TimingMiddleware>()("TimingMiddleware") {}
|
||||
|
||||
class GetUser extends Rpc.make("GetUser", {
|
||||
success: User,
|
||||
payload: { id: Schema.String }
|
||||
}) {}
|
||||
|
||||
export const UserRpcs = RpcGroup.make(
|
||||
GetUser,
|
||||
Rpc.make("GetUserOption", {
|
||||
success: Schema.Option(User),
|
||||
payload: { id: Schema.String }
|
||||
}),
|
||||
StreamUsers,
|
||||
Rpc.make("GetInterrupts", {
|
||||
success: Schema.Number
|
||||
}),
|
||||
Rpc.make("GetEmits", {
|
||||
success: Schema.Number
|
||||
}),
|
||||
Rpc.make("ProduceDefect"),
|
||||
Rpc.make("Never"),
|
||||
Rpc.make("nested.test"),
|
||||
Rpc.make("TimedMethod", {
|
||||
payload: {
|
||||
shouldFail: Schema.Boolean
|
||||
},
|
||||
success: Schema.Number
|
||||
}).middleware(TimingMiddleware),
|
||||
Rpc.make("GetTimingMiddlewareMetrics", {
|
||||
success: Schema.Struct({
|
||||
success: Schema.Number,
|
||||
defect: Schema.Number,
|
||||
count: Schema.Number
|
||||
})
|
||||
})
|
||||
).middleware(AuthMiddleware)
|
||||
|
||||
export const AuthLive = Layer.succeed(AuthMiddleware)(
|
||||
AuthMiddleware.of((effect, options) =>
|
||||
Effect.provideService(
|
||||
effect,
|
||||
CurrentUser,
|
||||
new User({ id: options.headers.userid ?? "1", name: options.headers.name ?? "Fallback name" })
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const rpcSuccesses = Metric.counter("rpc_middleware_success")
|
||||
const rpcDefects = Metric.counter("rpc_middleware_defects")
|
||||
const rpcCount = Metric.counter("rpc_middleware_count")
|
||||
export const TimingLive = Layer.succeed(TimingMiddleware)(
|
||||
TimingMiddleware.of((effect) =>
|
||||
effect.pipe(
|
||||
Effect.tap(Metric.update(rpcSuccesses, 1)),
|
||||
Effect.tapDefect(() => Metric.update(rpcDefects, 1)),
|
||||
Effect.ensuring(Metric.update(rpcCount, 1))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export const UsersLive = UserRpcs.toLayer(Effect.gen(function*() {
|
||||
let interrupts = 0
|
||||
let emits = 0
|
||||
return UserRpcs.of({
|
||||
GetUser: (_) =>
|
||||
CurrentUser.pipe(
|
||||
Rpc.fork
|
||||
),
|
||||
GetUserOption: Effect.fnUntraced(function*(req) {
|
||||
return Option.some(new User({ id: req.id, name: "John" }))
|
||||
}),
|
||||
StreamUsers: Effect.fnUntraced(function*(req, _) {
|
||||
const mailbox = yield* Queue.bounded<User>(0)
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
interrupts++
|
||||
})
|
||||
)
|
||||
|
||||
yield* Queue.offer(mailbox, new User({ id: req.id, name: "John" })).pipe(
|
||||
Effect.tap(() =>
|
||||
Effect.sync(() => {
|
||||
emits++
|
||||
})
|
||||
),
|
||||
Effect.delay(100),
|
||||
Effect.forever,
|
||||
Effect.forkScoped
|
||||
)
|
||||
|
||||
return mailbox
|
||||
}),
|
||||
GetInterrupts: () => Effect.sync(() => interrupts),
|
||||
GetEmits: () => Effect.sync(() => emits),
|
||||
ProduceDefect: () => Effect.die("boom"),
|
||||
Never: () => Effect.never.pipe(Effect.onInterrupt(() => Effect.sync(() => interrupts++))),
|
||||
"nested.test": () => Effect.void,
|
||||
TimedMethod: (_) => _.shouldFail ? Effect.die("boom") : Effect.succeed(1),
|
||||
GetTimingMiddlewareMetrics: () =>
|
||||
Effect.all({
|
||||
defect: Metric.value(rpcDefects).pipe(Effect.map((_) => _.count)),
|
||||
success: Metric.value(rpcSuccesses).pipe(Effect.map((_) => _.count)),
|
||||
count: Metric.value(rpcCount).pipe(Effect.map((_) => _.count))
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
export const RpcLive = RpcServer.layer(UserRpcs, {
|
||||
disableFatalDefects: true
|
||||
}).pipe(
|
||||
Layer.provide([
|
||||
UsersLive,
|
||||
AuthLive,
|
||||
TimingLive
|
||||
])
|
||||
)
|
||||
|
||||
export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) =>
|
||||
next({
|
||||
...request,
|
||||
headers: Headers.set(request.headers, "name", "Logged in user")
|
||||
}))
|
||||
12
repos/effect/packages/platform-browser/test/fixtures/rpc-worker.ts
vendored
Normal file
12
repos/effect/packages/platform-browser/test/fixtures/rpc-worker.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as BrowserWorkerRunner from "@effect/platform-browser/BrowserWorkerRunner"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as RpcServer from "effect/unstable/rpc/RpcServer"
|
||||
import { RpcLive } from "./rpc-schemas.ts"
|
||||
|
||||
const MainLive = RpcLive.pipe(
|
||||
Layer.provide(RpcServer.layerProtocolWorkerRunner),
|
||||
Layer.provide(BrowserWorkerRunner.layer)
|
||||
)
|
||||
|
||||
Effect.runFork(Layer.launch(MainLive))
|
||||
Reference in New Issue
Block a user