Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'

This commit is contained in:
-Puter
2026-07-19 03:25:10 +05:30
parent dd1071cc10
commit 2daf979036
2214 changed files with 673090 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SqliteClient } from "@effect/sql-sqlite-node"
import { assert, describe, it } from "@effect/vitest"
import { Effect, FileSystem } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
const makeClient = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
return yield* SqliteClient.make({
filename: dir + "/test.db"
})
}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))
describe("Client", () => {
it.effect("should work", () =>
Effect.gen(function*() {
const sql = yield* makeClient
let response
response = yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
assert.deepStrictEqual(response, [])
response = yield* sql`INSERT INTO test (name) VALUES ('hello')`
assert.deepStrictEqual(response, [])
response = yield* sql`SELECT * FROM test`
assert.deepStrictEqual(response, [{ id: 1, name: "hello" }])
response = yield* sql`SELECT * FROM test`.valuesUnprepared
assert.deepStrictEqual(response, [[1, "hello"]])
response = yield* sql`INSERT INTO test (name) VALUES ('world')`.pipe(sql.withTransaction)
assert.deepStrictEqual(response, [])
response = yield* sql`SELECT * FROM test`
assert.deepStrictEqual(response, [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
])
}))
it.effect("should work with raw", () =>
Effect.gen(function*() {
const sql = yield* makeClient
let response
response = yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`.raw
assert.deepStrictEqual(response, { changes: 0, lastInsertRowid: 0 })
response = yield* sql`INSERT INTO test (name) VALUES ('hello')`.raw
assert.deepStrictEqual(response, { changes: 1, lastInsertRowid: 1 })
response = yield* sql`SELECT * FROM test`.raw
assert.deepStrictEqual(response, [{ id: 1, name: "hello" }])
response = yield* sql`INSERT INTO test (name) VALUES ('world')`.raw.pipe(sql.withTransaction)
assert.deepStrictEqual(response, { changes: 1, lastInsertRowid: 2 })
response = yield* sql`SELECT * FROM test`
assert.deepStrictEqual(response, [
{ id: 1, name: "hello" },
{ id: 2, name: "world" }
])
}))
it.effect("withTransaction", () =>
Effect.gen(function*() {
const sql = yield* makeClient
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`)
const rows = yield* sql`SELECT * FROM test`
assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }])
}))
it.effect("withTransaction rollback", () =>
Effect.gen(function*() {
const sql = yield* makeClient
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe(
Effect.andThen(Effect.fail("boom")),
sql.withTransaction,
Effect.ignore
)
const rows = yield* sql`SELECT * FROM test`
assert.deepStrictEqual(rows, [])
}))
it.effect("supports backup and export", () =>
Effect.gen(function*() {
const sql = yield* makeClient
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
yield* sql`INSERT INTO test (name) VALUES ('hello')`
const metadata = yield* sql.backup(sql.config.filename + ".backup")
assert(metadata.totalPages > 0)
assert.strictEqual(metadata.remainingPages, 0)
}))
})

View File

@@ -0,0 +1,29 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SqliteClient } from "@effect/sql-sqlite-node"
import { Effect, FileSystem, Layer } from "effect"
import * as KeyValueStoreTest from "effect-test/unstable/persistence/KeyValueStoreTest"
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
import { Reactivity } from "effect/unstable/reactivity"
const ClientLayer = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
return SqliteClient.layer({
filename: dir + "/test.db"
})
}).pipe(
Layer.unwrap,
Layer.provide([NodeFileSystem.layer, Reactivity.layer])
)
KeyValueStoreTest.suite(
"sql-sqlite",
KeyValueStore.layerSql().pipe(Layer.provide(ClientLayer))
)
KeyValueStoreTest.suite(
"sql-sqlite-custom-table",
KeyValueStore.layerSql({ table: "effect_key_value_store_custom" }).pipe(
Layer.provide(ClientLayer)
)
)

View File

@@ -0,0 +1,109 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SqliteClient } from "@effect/sql-sqlite-node"
import { expect, it } from "@effect/vitest"
import { Duration, Effect, FileSystem, Layer } from "effect"
import { TestClock } from "effect/testing"
import { Persistence } from "effect/unstable/persistence"
import { Reactivity } from "effect/unstable/reactivity"
import type * as SqlClient from "effect/unstable/sql/SqlClient"
const ClientLayer = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
return SqliteClient.layer({
filename: dir + "/test.db"
})
}).pipe(
Layer.unwrap,
Layer.provide([NodeFileSystem.layer, Reactivity.layer])
)
const testLayer = (layer: Layer.Layer<Persistence.BackingPersistence, never, SqlClient.SqlClient>) =>
layer.pipe(Layer.provideMerge(ClientLayer))
const suite = (name: string, layer: Layer.Layer<Persistence.BackingPersistence, never, SqlClient.SqlClient>) =>
it.layer(testLayer(layer))(`Persistence (${name})`, (it) => {
it.effect("set + get", () =>
Effect.gen(function*() {
const persistence = yield* Persistence.BackingPersistence
const store = yield* persistence.make("test_store")
yield* store.set("key1", { name: "Alice" }, undefined)
const value = yield* store.get("key1")
expect(value).toEqual({ name: "Alice" })
// test upsert
yield* store.set("key1", { name: "Alice" }, undefined)
}))
it.effect("setMany + getMany", () =>
Effect.gen(function*() {
const persistence = yield* Persistence.BackingPersistence
const store = yield* persistence.make("test_store_2")
yield* store.setMany([
["key1", { name: "Alice" }, undefined],
["key2", { name: "Bob" }, undefined],
["key3", { name: "Charlie" }, undefined]
])
const values = yield* store.getMany(["key1", "key2", "key3", "key4"])
expect(values).toEqual([
{ name: "Alice" },
{ name: "Bob" },
{ name: "Charlie" },
undefined
])
}))
it.effect("remove", () =>
Effect.gen(function*() {
const persistence = yield* Persistence.BackingPersistence
const store = yield* persistence.make("test_store_2")
yield* store.setMany([
["key1", { name: "Alice" }, undefined],
["key2", { name: "Bob" }, undefined],
["key3", { name: "Charlie" }, undefined]
])
yield* store.remove("key2")
const valuesAfter = yield* store.getMany(["key1", "key2", "key3"])
expect(valuesAfter).toEqual([{ name: "Alice" }, undefined, { name: "Charlie" }])
}))
it.effect("expires", () =>
Effect.gen(function*() {
const persistence = yield* Persistence.BackingPersistence
const store = yield* persistence.make("test_store_3")
yield* store.setMany([
["key1", { name: "Alice" }, undefined],
["key2", { name: "Bob" }, undefined],
["key3", { name: "Charlie" }, Duration.seconds(10)]
])
let values = yield* store.getMany(["key1", "key2", "key3"])
expect(values).toEqual([{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }])
yield* TestClock.adjust(Duration.seconds(5))
values = yield* store.getMany(["key1", "key2", "key3"])
expect(values).toEqual([{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }])
yield* TestClock.adjust(Duration.seconds(5))
values = yield* store.getMany(["key1", "key2", "key3"])
expect(values).toEqual([{ name: "Alice" }, { name: "Bob" }, undefined])
}))
it.effect("isolation between stores", () =>
Effect.gen(function*() {
const persistence = yield* Persistence.BackingPersistence
const storeA = yield* persistence.make("test_store_a")
const storeB = yield* persistence.make("test_store_b")
yield* storeA.set("shared-key", { name: "Alice" }, undefined)
yield* storeB.set("shared-key", { name: "Bob" }, undefined)
expect(yield* storeA.get("shared-key")).toEqual({ name: "Alice" })
expect(yield* storeB.get("shared-key")).toEqual({ name: "Bob" })
yield* storeA.clear
expect(yield* storeA.get("shared-key")).toEqual(undefined)
expect(yield* storeB.get("shared-key")).toEqual({ name: "Bob" })
}))
})
suite("table-per-store", Persistence.layerBackingSqlMultiTable)
suite("single-table", Persistence.layerBackingSql)

View File

@@ -0,0 +1,183 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SqliteClient } from "@effect/sql-sqlite-node"
import { assert, describe, it } from "@effect/vitest"
import { Cause, Effect, FileSystem, Iterable, Schema } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlError, SqlResolver } from "effect/unstable/sql"
const makeClient = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
return yield* SqliteClient.make({
filename: dir + "/test.db"
})
}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))
const seededClient = Effect.gen(function*() {
const sql = yield* makeClient
yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`
for (const id of Iterable.range(1, 100)) {
yield* sql`INSERT INTO test ${sql.insert({ id, name: `name${id}` })}`
}
return sql
})
describe("Resolver", () => {
describe("ordered", () => {
it.effect("insert", () =>
Effect.gen(function*() {
const batches: Array<Array<string>> = []
const sql = yield* seededClient
const Insert = SqlResolver.ordered({
Request: Schema.String,
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
execute: (names) => {
batches.push(names)
return sql`INSERT INTO test ${sql.insert(names.map((name) => ({ name })))} RETURNING *`
}
})
const execute = SqlResolver.request(Insert)
assert.deepStrictEqual(
yield* Effect.all({
one: execute("one"),
two: execute("two")
}, { concurrency: "unbounded" }),
{
one: { id: 101, name: "one" },
two: { id: 102, name: "two" }
}
)
assert.deepStrictEqual(batches, [["one", "two"]])
}))
it.effect("result length mismatch", () =>
Effect.gen(function*() {
const batches: Array<Array<number>> = []
const sql = yield* seededClient
const Select = SqlResolver.ordered({
Request: Schema.Number,
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
execute: (ids) => {
batches.push(ids)
return sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
}
})
const execute = SqlResolver.request(Select)
const error = yield* Effect.all([
execute(1),
execute(2),
execute(3),
execute(101)
], { concurrency: "unbounded" })
.pipe(Effect.flip)
assert(error instanceof SqlError.ResultLengthMismatch)
assert.strictEqual(error.actual, 3)
assert.strictEqual(error.expected, 4)
assert.deepStrictEqual(batches, [[1, 2, 3, 101]])
}))
})
describe("grouped", () => {
it.effect("find by name", () =>
Effect.gen(function*() {
const sql = yield* seededClient
const FindByName = SqlResolver.grouped({
Request: Schema.String,
RequestGroupKey: (name) => name,
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
ResultGroupKey: (result) => result.name,
execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}`
})
yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}`
const execute = SqlResolver.request(FindByName)
assert.deepStrictEqual(
yield* Effect.all({
one: execute("name1"),
two: execute("name2"),
three: Effect.flip(execute("name0"))
}, { concurrency: "unbounded" }),
{
one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }],
two: [{ id: 2, name: "name2" }],
three: new Cause.NoSuchElementError()
}
)
}))
it.effect("using raw rows", () =>
Effect.gen(function*() {
const sql = yield* seededClient
const FindByName = SqlResolver.grouped({
Request: Schema.String,
RequestGroupKey: (name) => name,
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
ResultGroupKey: (_, result: any) => result.name,
execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}`
})
yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}`
const execute = SqlResolver.request(FindByName)
assert.deepStrictEqual(
yield* Effect.all({
one: execute("name1"),
two: execute("name2"),
three: Effect.flip(execute("name0"))
}, { concurrency: "unbounded" }),
{
one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }],
two: [{ id: 2, name: "name2" }],
three: new Cause.NoSuchElementError()
}
)
}))
})
describe("findById", () => {
it.effect("find by id", () =>
Effect.gen(function*() {
const sql = yield* seededClient
const FindById = SqlResolver.findById({
Id: Schema.Number,
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
ResultId: (result) => result.id,
execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
})
const execute = SqlResolver.request(FindById)
assert.deepStrictEqual(
yield* Effect.all({
one: execute(1),
two: execute(2),
three: Effect.flip(execute(101))
}, { concurrency: "unbounded" }),
{
one: { id: 1, name: "name1" },
two: { id: 2, name: "name2" },
three: new Cause.NoSuchElementError()
}
)
}))
it.effect("using raw rows", () =>
Effect.gen(function*() {
const sql = yield* seededClient
const FindById = SqlResolver.findById({
Id: Schema.Number,
Result: Schema.Struct({ id: Schema.Number, name: Schema.String }),
ResultId: (_, result: any) => result.id,
execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`
})
const execute = SqlResolver.request(FindById)
assert.deepStrictEqual(
yield* Effect.all({
one: execute(1),
two: execute(2),
three: Effect.flip(execute(101))
}, { concurrency: "unbounded" }),
{
one: { id: 1, name: "name1" },
two: { id: 2, name: "name2" },
three: new Cause.NoSuchElementError()
}
)
}))
})
})

View File

@@ -0,0 +1,84 @@
import { SqliteClient } from "@effect/sql-sqlite-node"
import { assert, describe, it } from "@effect/vitest"
import { Effect } from "effect"
import * as EventJournal from "effect/unstable/eventlog/EventJournal"
import * as SqlEventJournal from "effect/unstable/eventlog/SqlEventJournal"
import { Reactivity } from "effect/unstable/reactivity"
import * as SqlClient from "effect/unstable/sql/SqlClient"
const makeJournal = Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
return yield* SqlEventJournal.make().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
}).pipe(Effect.provide(Reactivity.layer))
describe("SqlEventJournal", () => {
it.effect("writes and reads entries", () =>
Effect.gen(function*() {
const journal = yield* makeJournal
let createdAt = 0
yield* journal.write({
event: "UserCreated",
primaryKey: "user-1",
payload: new Uint8Array([1]),
effect: (entry) =>
Effect.sync(() => {
createdAt = entry.createdAtMillis
})
})
const entries = yield* journal.entries
assert.strictEqual(entries.length, 1)
assert.strictEqual(entries[0].event, "UserCreated")
assert.strictEqual(entries[0].createdAtMillis, createdAt)
}))
it.effect("writes remote entries and sequences", () =>
Effect.gen(function*() {
const journal = yield* makeJournal
const remoteId = EventJournal.makeRemoteIdUnsafe()
const initial = yield* journal.nextRemoteSequence(remoteId)
assert.strictEqual(initial, 0)
const entryA = new EventJournal.Entry({
id: EventJournal.makeEntryIdUnsafe(),
event: "UserCreated",
primaryKey: "user-2",
payload: new Uint8Array([2])
}, { disableChecks: true })
const entryB = new EventJournal.Entry({
id: EventJournal.makeEntryIdUnsafe(),
event: "UserCreated",
primaryKey: "user-3",
payload: new Uint8Array([3])
}, { disableChecks: true })
const remoteEntries = [
new EventJournal.RemoteEntry({ remoteSequence: 0, entry: entryA }),
new EventJournal.RemoteEntry({ remoteSequence: 1, entry: entryB })
]
const seenConflicts: Array<ReadonlyArray<EventJournal.Entry>> = []
yield* journal.writeFromRemote({
remoteId,
entries: remoteEntries,
effect: ({ conflicts }) =>
Effect.sync(() => {
seenConflicts.push(conflicts)
})
})
const next = yield* journal.nextRemoteSequence(remoteId)
assert.strictEqual(next, 2)
yield* journal.write({
event: "LocalCreated",
primaryKey: "local-1",
payload: new Uint8Array([4]),
effect: () => Effect.void
})
const uncommitted = yield* journal.withRemoteUncommited(remoteId, (entries) => Effect.succeed(entries))
assert.strictEqual(uncommitted.length, 1)
assert.strictEqual(uncommitted[0].event, "LocalCreated")
assert.strictEqual(seenConflicts.length, 2)
assert.strictEqual(seenConflicts[0][0]?.idString, entryA.idString)
assert.strictEqual(seenConflicts[1][0]?.idString, entryB.idString)
}))
})

View File

@@ -0,0 +1,249 @@
import { SqliteClient } from "@effect/sql-sqlite-node"
import { assert, describe, it } from "@effect/vitest"
import { Effect, Layer, Queue, Redacted, Stream } from "effect"
import * as EventJournal from "effect/unstable/eventlog/EventJournal"
import * as EventLog from "effect/unstable/eventlog/EventLog"
import * as EventLogEncryption from "effect/unstable/eventlog/EventLogEncryption"
import * as EventLogMessage from "effect/unstable/eventlog/EventLogMessage"
import type { StoreId } from "effect/unstable/eventlog/EventLogMessage"
import * as EventLogServer from "effect/unstable/eventlog/EventLogServerEncrypted"
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth"
import { makeGetIdentityRootSecretMaterial } from "effect/unstable/eventlog/internal/identityRootSecretDerivation"
import * as SqlEventLogServer from "effect/unstable/eventlog/SqlEventLogServerEncrypted"
import { Reactivity } from "effect/unstable/reactivity"
import * as RpcTest from "effect/unstable/rpc/RpcTest"
import * as SqlClient from "effect/unstable/sql/SqlClient"
const storeIdA = "store-a" as StoreId
const storeIdB = "store-b" as StoreId
const makeEntry = (value: number) =>
new EventJournal.Entry({
id: EventJournal.makeEntryIdUnsafe(),
event: "UserCreated",
primaryKey: `user-${value}`,
payload: new Uint8Array([value])
}, { disableChecks: true })
const persistEntries = (
encryption: EventLogEncryption.EventLogEncryption["Service"],
identity: EventLog.Identity["Service"],
entries: ReadonlyArray<EventJournal.Entry>
) =>
Effect.gen(function*() {
const encrypted = yield* encryption.encrypt(identity, entries)
return encrypted.encryptedEntries.map((encryptedEntry, index) =>
new EventLogServer.PersistedEntry({
entryId: entries[index].id,
iv: encrypted.iv,
encryptedEntry
})
)
})
const makePersistedEntry = (index: number, entryId = EventJournal.makeEntryIdUnsafe()) =>
new EventLogServer.PersistedEntry({
entryId,
iv: new Uint8Array(12),
encryptedEntry: Uint8Array.of(index)
})
const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto)
const makeAuthenticateRequest = Effect.fnUntraced(function*(options: {
readonly identity: EventLog.Identity["Service"]
readonly challenge: Uint8Array
readonly remoteId: EventJournal.RemoteId
}) {
const rootSecretMaterial = yield* getIdentityRootSecretMaterial(options.identity)
const signature = yield* EventLogSessionAuth.signSessionAuthPayload({
remoteId: options.remoteId,
challenge: options.challenge,
publicKey: options.identity.publicKey,
signingPublicKey: rootSecretMaterial.signingPublicKey,
signingPrivateKey: Redacted.value(rootSecretMaterial.signingPrivateKey)
})
return new EventLogMessage.Authenticate({
publicKey: options.identity.publicKey,
signingPublicKey: rootSecretMaterial.signingPublicKey,
signature,
algorithm: "Ed25519"
})
})
describe("SqlEventLogServer", () => {
it.effect("persists remote id across storage instances", () =>
Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
const storageA = yield* SqlEventLogServer.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
const storageB = yield* SqlEventLogServer.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
const idA = yield* storageA.getId
const idB = yield* storageB.getId
assert.deepStrictEqual(idA, idB)
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
it.effect("rejects session-auth rebinding for an existing publicKey", () =>
Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
const storage = yield* SqlEventLogServer.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe(
Effect.provide(
EventLogServer.layerRpcHandlers.pipe(
Layer.provide(Layer.succeed(EventLogServer.Storage, storage))
)
)
)
const firstIdentity = yield* EventLog.makeIdentity
const secondIdentitySeed = yield* EventLog.makeIdentity
const secondIdentity: EventLog.Identity["Service"] = {
publicKey: firstIdentity.publicKey,
privateKey: secondIdentitySeed.privateKey
}
const firstMaterial = yield* getIdentityRootSecretMaterial(firstIdentity)
const secondMaterial = yield* getIdentityRootSecretMaterial(secondIdentity)
const sameSigningPublicKey =
firstMaterial.signingPublicKey.byteLength === secondMaterial.signingPublicKey.byteLength &&
firstMaterial.signingPublicKey.every((byte, index) => byte === secondMaterial.signingPublicKey[index])
assert.strictEqual(sameSigningPublicKey, false)
const firstHello = yield* rpcClient["EventLog.Hello"]()
yield* rpcClient["EventLog.Authenticate"](
yield* makeAuthenticateRequest({
identity: firstIdentity,
challenge: firstHello.challenge,
remoteId: firstHello.remoteId
})
)
const secondHello = yield* rpcClient["EventLog.Hello"]()
const error = yield* rpcClient["EventLog.Authenticate"](
yield* makeAuthenticateRequest({
identity: secondIdentity,
challenge: secondHello.challenge,
remoteId: secondHello.remoteId
})
).pipe(Effect.flip)
assert.instanceOf(error, EventLogMessage.EventLogProtocolError)
assert.strictEqual(error.code, "Forbidden")
assert.strictEqual(error.message, "Session auth signature verification failed")
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
it.effect("writes entries and streams changes", () =>
Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
const storage = yield* SqlEventLogServer.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
const encryption = yield* EventLogEncryption.EventLogEncryption
const identity = yield* encryption.generateIdentity
const entries = [makeEntry(1), makeEntry(2)]
const persisted = yield* persistEntries(encryption, identity, entries)
const written = yield* storage.write(identity.publicKey, storeIdA, persisted)
assert.deepStrictEqual(written.map((entry) => entry.sequence), [1, 2])
const changes = yield* storage.changes(identity.publicKey, storeIdA, 0).pipe(
Stream.toQueue({ capacity: "unbounded" })
)
const taken = yield* Queue.takeAll(changes)
assert.deepStrictEqual(taken.map((entry) => entry.sequence), [1, 2])
const nextEntry = makeEntry(3)
const nextPersisted = yield* persistEntries(encryption, identity, [nextEntry])
const updated = yield* storage.write(identity.publicKey, storeIdA, nextPersisted)
assert.deepStrictEqual(updated.map((entry) => entry.sequence), [3])
const next = yield* Queue.take(changes)
assert.strictEqual(next.sequence, 3)
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
it.effect("isolates same publicKey across storeIds", () =>
Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
const storage = yield* SqlEventLogServer.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
yield* storage.write("client-1", storeIdA, [makePersistedEntry(1)])
yield* storage.write("client-1", storeIdB, [makePersistedEntry(2)])
const storeAEntries = yield* storage.changes("client-1", storeIdA, 0).pipe(
Stream.take(1),
Stream.runCollect
)
const storeBEntries = yield* storage.changes("client-1", storeIdB, 0).pipe(
Stream.take(1),
Stream.runCollect
)
assert.deepStrictEqual(storeAEntries.map((entry) => entry.sequence), [1])
assert.deepStrictEqual(storeBEntries.map((entry) => entry.sequence), [1])
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
it.effect("isolates same storeId across publicKeys", () =>
Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
const storage = yield* SqlEventLogServer.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
yield* storage.write("client-1", storeIdA, [makePersistedEntry(1)])
yield* storage.write("client-2", storeIdA, [makePersistedEntry(2)])
const clientOneEntries = yield* storage.changes("client-1", storeIdA, 0).pipe(
Stream.take(1),
Stream.runCollect
)
const clientTwoEntries = yield* storage.changes("client-2", storeIdA, 0).pipe(
Stream.take(1),
Stream.runCollect
)
assert.deepStrictEqual(clientOneEntries.map((entry) => entry.sequence), [1])
assert.deepStrictEqual(clientTwoEntries.map((entry) => entry.sequence), [1])
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
it.effect("keeps deduplication isolated per encrypted scope", () =>
Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
const storage = yield* SqlEventLogServer.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql)
)
const sharedEntryId = EventJournal.makeEntryIdUnsafe()
yield* storage.write("client-1", storeIdA, [makePersistedEntry(1, sharedEntryId)])
yield* storage.write("client-1", storeIdA, [makePersistedEntry(2, sharedEntryId)])
yield* storage.write("client-1", storeIdB, [makePersistedEntry(3, sharedEntryId)])
yield* storage.write("client-2", storeIdA, [makePersistedEntry(4, sharedEntryId)])
assert.deepStrictEqual(
(yield* storage.changes("client-1", storeIdA, 0).pipe(
Stream.take(1),
Stream.runCollect
)).map((entry) => entry.sequence),
[1]
)
assert.deepStrictEqual(
(yield* storage.changes("client-1", storeIdB, 0).pipe(
Stream.take(1),
Stream.runCollect
)).map((entry) => entry.sequence),
[1]
)
assert.deepStrictEqual(
(yield* storage.changes("client-2", storeIdA, 0).pipe(
Stream.take(1),
Stream.runCollect
)).map((entry) => entry.sequence),
[1]
)
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
})

View File

@@ -0,0 +1,110 @@
import { SqliteClient } from "@effect/sql-sqlite-node"
import { assert, describe, it } from "@effect/vitest"
import { Effect, Layer, Redacted } from "effect"
import * as SqlEventLogServerUnencryptedStorageTest from "effect-test/unstable/eventlog/SqlEventLogServerUnencryptedStorageTest"
import type * as EventJournal from "effect/unstable/eventlog/EventJournal"
import * as EventLog from "effect/unstable/eventlog/EventLog"
import * as EventLogEncryption from "effect/unstable/eventlog/EventLogEncryption"
import * as EventLogMessage from "effect/unstable/eventlog/EventLogMessage"
import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted"
import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth"
import { makeGetIdentityRootSecretMaterial } from "effect/unstable/eventlog/internal/identityRootSecretDerivation"
import * as SqlEventLogServerUnencrypted from "effect/unstable/eventlog/SqlEventLogServerUnencrypted"
import { Reactivity } from "effect/unstable/reactivity"
import * as RpcTest from "effect/unstable/rpc/RpcTest"
import * as SqlClient from "effect/unstable/sql/SqlClient"
SqlEventLogServerUnencryptedStorageTest.suite(
"sql-sqlite-node",
SqliteClient.layer({
filename: ":memory:"
})
)
const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto)
const makeAuthenticateRequest = Effect.fnUntraced(function*(options: {
readonly identity: EventLog.Identity["Service"]
readonly challenge: Uint8Array
readonly remoteId: EventJournal.RemoteId
}) {
const rootSecretMaterial = yield* getIdentityRootSecretMaterial(options.identity)
const signature = yield* EventLogSessionAuth.signSessionAuthPayload({
remoteId: options.remoteId,
challenge: options.challenge,
publicKey: options.identity.publicKey,
signingPublicKey: rootSecretMaterial.signingPublicKey,
signingPrivateKey: Redacted.value(rootSecretMaterial.signingPrivateKey)
})
return new EventLogMessage.Authenticate({
publicKey: options.identity.publicKey,
signingPublicKey: rootSecretMaterial.signingPublicKey,
signature,
algorithm: "Ed25519"
})
})
describe("SqlEventLogServerUnencrypted (sql-sqlite-node)", () => {
it.effect("rejects session-auth rebinding for an existing publicKey", () =>
Effect.gen(function*() {
const sql = yield* SqliteClient.make({ filename: ":memory:" })
const storage = yield* SqlEventLogServerUnencrypted.makeStorage().pipe(
Effect.provideService(SqlClient.SqlClient, sql),
Effect.orDie
)
const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe(
Effect.provide(
EventLogServerUnencrypted.layerRpcHandlers.pipe(
Layer.provideMerge(EventLog.layerRegistry),
Layer.provide(Layer.succeed(EventLogServerUnencrypted.Storage, storage)),
Layer.provide(Layer.succeed(EventLogServerUnencrypted.StoreMapping, {
resolve: ({ storeId }) => Effect.succeed(storeId),
hasStore: () => Effect.succeed(true)
})),
Layer.provide(Layer.succeed(EventLogServerUnencrypted.EventLogServerAuthorization, {
authorizeWrite: () => Effect.void,
authorizeRead: () => Effect.void,
authorizeIdentity: () => Effect.void
}))
)
)
)
const firstIdentity = yield* EventLog.makeIdentity
const secondIdentitySeed = yield* EventLog.makeIdentity
const secondIdentity: EventLog.Identity["Service"] = {
publicKey: firstIdentity.publicKey,
privateKey: secondIdentitySeed.privateKey
}
const firstMaterial = yield* getIdentityRootSecretMaterial(firstIdentity)
const secondMaterial = yield* getIdentityRootSecretMaterial(secondIdentity)
const sameSigningPublicKey =
firstMaterial.signingPublicKey.byteLength === secondMaterial.signingPublicKey.byteLength &&
firstMaterial.signingPublicKey.every((byte, index) => byte === secondMaterial.signingPublicKey[index])
assert.strictEqual(sameSigningPublicKey, false)
const firstHello = yield* rpcClient["EventLog.Hello"]()
yield* rpcClient["EventLog.Authenticate"](
yield* makeAuthenticateRequest({
identity: firstIdentity,
challenge: firstHello.challenge,
remoteId: firstHello.remoteId
})
)
const secondHello = yield* rpcClient["EventLog.Hello"]()
const error = yield* rpcClient["EventLog.Authenticate"](
yield* makeAuthenticateRequest({
identity: secondIdentity,
challenge: secondHello.challenge,
remoteId: secondHello.remoteId
})
).pipe(Effect.flip)
assert.instanceOf(error, EventLogMessage.EventLogProtocolError)
assert.strictEqual(error.code, "Forbidden")
assert.strictEqual(error.message, "Session auth signature verification failed")
}).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle])))
})

View File

@@ -0,0 +1,48 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node"
import { assert, describe, it } from "@effect/vitest"
import { Effect, FileSystem } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
import * as SqlClient from "effect/unstable/sql/SqlClient"
import * as SqlError from "effect/unstable/sql/SqlError"
const makeClients = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
const filename = dir + "/test.db"
return {
lockClient: yield* SqliteClient.make({ filename }),
migratorClient: yield* SqliteClient.make({ filename })
}
}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))
describe("SqliteMigrator", () => {
it.effect("fails on lock errors", () =>
Effect.gen(function*() {
const { lockClient, migratorClient } = yield* makeClients
yield* migratorClient`PRAGMA busy_timeout = 1`
yield* SqliteMigrator.run({
loader: SqliteMigrator.fromRecord({})
}).pipe(Effect.provideService(SqlClient.SqlClient, migratorClient))
yield* Effect.acquireRelease(
lockClient`BEGIN IMMEDIATE`,
() => lockClient`ROLLBACK`.pipe(Effect.ignore)
)
const error = yield* Effect.flip(
SqliteMigrator.run({
loader: SqliteMigrator.fromRecord({
"1_test": Effect.void
})
}).pipe(Effect.provideService(SqlClient.SqlClient, migratorClient))
)
assert.strictEqual(error._tag, "SqlError")
assert(SqlError.isSqlError(error))
assert.strictEqual(error.reason._tag, "LockTimeoutError")
}))
})