| /** | |
| * Connects a local event log to a remote replica. | |
| * | |
| * `EventLogRemote` writes local entries to another journal, receives remote | |
| * change streams from a sequence number, and can wait until the current | |
| * event-log identity has completed remote authentication. The encrypted | |
| * constructor is the default for browser, edge, or service replicas crossing an | |
| * untrusted network. The unencrypted constructor is intended for trusted | |
| * transports or tests. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Cache from "../../Cache.ts" | |
| import * as Context from "../../Context.ts" | |
| import * as Data from "../../Data.ts" | |
| import * as Effect from "../../Effect.ts" | |
| import * as Layer from "../../Layer.ts" | |
| import * as Predicate from "../../Predicate.ts" | |
| import * as Queue from "../../Queue.ts" | |
| import * as Redacted from "../../Redacted.ts" | |
| import type * as Schema from "../../Schema.ts" | |
| import type * as Scope from "../../Scope.ts" | |
| import * as RpcClient from "../rpc/RpcClient.ts" | |
| import type { RpcClientError } from "../rpc/RpcClientError.ts" | |
| import type * as RpcGroup from "../rpc/RpcGroup.ts" | |
| import type { Entry, RemoteEntry, RemoteId } from "./EventJournal.ts" | |
| import { type Identity, Registry } from "./EventLog.ts" | |
| import { EventLogEncryption, layerSubtle } from "./EventLogEncryption.ts" | |
| import { | |
| Authenticate, | |
| ChangesRpc, | |
| ChunkedMessage, | |
| type EventLogProtocolError, | |
| EventLogRemoteRpcs, | |
| type HelloResponse, | |
| type StoreId, | |
| WriteEntries, | |
| WriteEntriesUnencrypted | |
| } from "./EventLogMessage.ts" | |
| import { encodeSessionAuthPayload, signSessionAuthPayloadBytes } from "./EventLogSessionAuth.ts" | |
| import { makeGetIdentityRootSecretMaterial } from "./internal/identityRootSecretDerivation.ts" | |
| /** | |
| * Service that represents a remote event-log replica. | |
| * | |
| * **When to use** | |
| * | |
| * Use to access or provide a remote event-log replica that can write local | |
| * entries and stream remote changes. | |
| * | |
| * **Details** | |
| * | |
| * It can write local entries to the remote, stream remote changes from a sequence | |
| * number, and run effects only after the supplied identity has authenticated. | |
| * | |
| * @category services | |
| * @since 4.0.0 | |
| */ | |
| export class EventLogRemote extends Context.Service<EventLogRemote, { | |
| readonly id: RemoteId | |
| readonly changes: (options: { | |
| readonly identity: Identity["Service"] | |
| readonly storeId: StoreId | |
| readonly startSequence: number | |
| }) => Effect.Effect<Queue.Dequeue<RemoteEntry, EventLogRemoteError>, never, Scope.Scope> | |
| readonly write: (options: { | |
| readonly identity: Identity["Service"] | |
| readonly storeId: StoreId | |
| readonly entries: ReadonlyArray<Entry> | |
| }) => Effect.Effect<void, EventLogRemoteError> | |
| readonly whenAuthenticated: <A, E, R>( | |
| effect: Effect.Effect<A, E, R> | |
| ) => Effect.Effect<A, E | EventLogRemoteError, R | Identity> | |
| }>()("effect/eventlog/EventLogRemote") {} | |
| /** | |
| * Error raised by `EventLogRemote` operations, recording the failed method and | |
| * underlying cause. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class EventLogRemoteError extends Data.TaggedError("EventLogRemoteError")<{ | |
| readonly method: string | |
| readonly cause: unknown | |
| }> {} | |
| const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto) | |
| const makeAuthenticate = Effect.fnUntraced(function*(options: { | |
| readonly identity: Identity["Service"] | |
| readonly hello: HelloResponse | |
| }) { | |
| const rootSecretMaterial = yield* getIdentityRootSecretMaterial(options.identity) | |
| const payload = yield* encodeSessionAuthPayload({ | |
| remoteId: options.hello.remoteId, | |
| challenge: options.hello.challenge, | |
| publicKey: options.identity.publicKey, | |
| signingPublicKey: rootSecretMaterial.signingPublicKey | |
| }) | |
| const signature = yield* signSessionAuthPayloadBytes({ | |
| payload, | |
| signingPrivateKey: Redacted.value(rootSecretMaterial.signingPrivateKey) | |
| }) | |
| return new Authenticate({ | |
| publicKey: options.identity.publicKey, | |
| signingPublicKey: rootSecretMaterial.signingPublicKey, | |
| signature, | |
| algorithm: "Ed25519" | |
| }) | |
| }) | |
| /** | |
| * Service that provides a typed RPC client for the `EventLogRemoteRpcs` protocol. | |
| * | |
| * **When to use** | |
| * | |
| * Use to provide the RPC client used by remote event-log replicas to | |
| * authenticate, write entries, and subscribe to changes. | |
| * | |
| * @category RPC client | |
| * @since 4.0.0 | |
| */ | |
| export class EventLogRemoteClient extends Context.Service< | |
| EventLogRemoteClient, | |
| RpcClient.RpcClient<RpcGroup.Rpcs<typeof EventLogRemoteRpcs>, RpcClientError> | |
| >()( | |
| "effect/unstable/eventlog/EventLogRemote/EventLogRemoteClient" | |
| ) { | |
| static readonly layer = Layer.effect( | |
| EventLogRemoteClient, | |
| RpcClient.make(EventLogRemoteRpcs, { | |
| disableTracing: true | |
| }) | |
| ) | |
| } | |
| /** | |
| * Creates an `EventLogRemote` from custom write encoding and change decoding | |
| * functions. | |
| * | |
| * **Details** | |
| * | |
| * The remote performs the hello/authentication handshake, retries after forbidden | |
| * responses by re-authenticating, chunks large writes, and registers itself with | |
| * the `Registry` for the current scope. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeWith = Effect.fnUntraced(function*({ encodeWrite, decodeChanges }: { | |
| readonly encodeWrite: (options: { | |
| readonly identity: Identity["Service"] | |
| readonly entries: ReadonlyArray<Entry> | |
| readonly storeId: StoreId | |
| }) => Effect.Effect<Uint8Array<ArrayBuffer>, Schema.SchemaError> | |
| readonly decodeChanges: ( | |
| identity: Identity["Service"], | |
| data: Uint8Array<ArrayBuffer> | |
| ) => Effect.Effect<ReadonlyArray<RemoteEntry>, Schema.SchemaError> | |
| }): Effect.fn.Return<EventLogRemote["Service"], EventLogRemoteError, Scope.Scope | EventLogRemoteClient | Registry> { | |
| const client = yield* EventLogRemoteClient | |
| const registry = yield* Registry | |
| let hello: HelloResponse | null = yield* client["EventLog.Hello"]().pipe( | |
| Effect.mapError((cause) => new EventLogRemoteError({ method: "hello", cause })) | |
| ) | |
| const identities = new Map<string, Identity["Service"]>() | |
| const ensureIdentity = (identity: Identity["Service"]) => { | |
| let entry = identities.get(identity.publicKey) | |
| if (!entry) { | |
| entry = identity | |
| identities.set(identity.publicKey, entry) | |
| } | |
| return entry | |
| } | |
| const authCache = yield* Cache.make({ | |
| lookup: Effect.fnUntraced(function*(publicKey: string) { | |
| const identity = identities.get(publicKey)! | |
| hello ??= yield* client["EventLog.Hello"]().pipe( | |
| Effect.mapError((cause) => new EventLogRemoteError({ method: "hello", cause })) | |
| ) | |
| const authenticate = yield* makeAuthenticate({ | |
| identity, | |
| hello | |
| }) | |
| yield* client["EventLog.Authenticate"](authenticate) | |
| }, Effect.mapError((cause) => new EventLogRemoteError({ method: "authenticate", cause }))), | |
| capacity: Number.MAX_SAFE_INTEGER | |
| }) | |
| const ensureAuthenticated = (identity: Identity["Service"]) => { | |
| ensureIdentity(identity) | |
| return Cache.get(authCache, identity.publicKey) | |
| } | |
| const retryForbidden = <A, E, R>( | |
| effect: Effect.Effect<A, E, R>, | |
| options: { | |
| readonly identity: Identity["Service"] | |
| } | |
| ) => | |
| Effect.retry(effect, { | |
| while(e) { | |
| hello = null | |
| const isForbidden = Predicate.isTagged(e, "EventLogProtocolError") && | |
| (e as any as EventLogProtocolError).code === "Forbidden" | |
| return Cache.invalidate(authCache, options.identity.publicKey).pipe( | |
| Effect.as(isForbidden) | |
| ) | |
| }, | |
| times: 5 | |
| }) | |
| let chunkedIdCounter = 0 | |
| const remote = EventLogRemote.of({ | |
| id: hello.remoteId, | |
| write: Effect.fnUntraced( | |
| function*(options) { | |
| yield* ensureAuthenticated(options.identity) | |
| const encoded = yield* encodeWrite(options) | |
| if (encoded.byteLength <= ChunkedMessage.chunkSize) { | |
| return yield* client["EventLog.WriteSingle"]({ data: encoded }) | |
| } | |
| for (const part of ChunkedMessage.split(chunkedIdCounter++, encoded)) { | |
| yield* client["EventLog.WriteChunked"](part) | |
| } | |
| }, | |
| retryForbidden, | |
| Effect.mapError((cause) => new EventLogRemoteError({ method: "write", cause })) | |
| ), | |
| changes: Effect.fnUntraced(function*(options) { | |
| const outgoing = yield* Queue.make<RemoteEntry, EventLogRemoteError>() | |
| yield* Effect.gen(function*() { | |
| yield* ensureAuthenticated(options.identity) | |
| const chunkedState = ChunkedMessage.initialJoinState() | |
| const incoming = yield* client["EventLog.Changes"]({ | |
| publicKey: options.identity.publicKey, | |
| storeId: options.storeId, | |
| startSequence: options.startSequence | |
| }, { asQueue: true }) | |
| while (true) { | |
| const parts = yield* Queue.takeAll(incoming) | |
| for (let i = 0; i < parts.length; i++) { | |
| const part = parts[i] | |
| if (part._tag === "Single") { | |
| yield* Queue.offerAll(outgoing, yield* decodeChanges(options.identity, part.data)) | |
| continue | |
| } | |
| const data = ChunkedMessage.join(chunkedState, part) | |
| if (!data) continue | |
| yield* Queue.offerAll(outgoing, yield* decodeChanges(options.identity, data)) | |
| } | |
| } | |
| }).pipe( | |
| (effect) => retryForbidden(effect, options), | |
| Effect.mapError((cause) => { | |
| if (cause._tag === "EventLogRemoteError") { | |
| return cause | |
| } | |
| return new EventLogRemoteError({ | |
| method: "changes", | |
| cause | |
| }) | |
| }), | |
| Effect.catchCause((cause) => Queue.failCause(outgoing, cause)), | |
| Effect.forkScoped | |
| ) | |
| return outgoing | |
| }), | |
| whenAuthenticated: (effect) => | |
| IdentityService.use((identity) => Effect.flatMap(ensureAuthenticated(identity), () => effect)) | |
| }) | |
| yield* registry.registerRemote(remote) | |
| return remote | |
| }) | |
| /** @effect-diagnostics-next-line classSelfMismatch:off */ | |
| class IdentityService extends Context.Service<Identity, Identity["Service"]>()( | |
| "effect/eventlog/EventLog/Identity" satisfies Identity["key"] | |
| ) {} | |
| /** | |
| * Creates an `EventLogRemote` that encrypts outgoing entries and decrypts | |
| * incoming changes with `EventLogEncryption`. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeEncrypted = Effect.gen(function*(): Effect.fn.Return< | |
| EventLogRemote["Service"], | |
| EventLogRemoteError, | |
| Scope.Scope | EventLogRemoteClient | EventLogEncryption | Registry | |
| > { | |
| const encryption = yield* EventLogEncryption | |
| return yield* makeWith({ | |
| encodeWrite: (options) => | |
| encryption.encrypt(options.identity, options.entries).pipe( | |
| Effect.flatMap((msg) => | |
| new WriteEntries({ | |
| publicKey: options.identity.publicKey, | |
| storeId: options.storeId, | |
| iv: msg.iv, | |
| encryptedEntries: msg.encryptedEntries.map((entry, i) => ({ | |
| entryId: options.entries[i].id, | |
| encryptedEntry: entry | |
| })) | |
| }).encoded | |
| ) | |
| ), | |
| decodeChanges: (identity, data) => | |
| ChangesRpc.decodeEncrypted(data).pipe( | |
| Effect.flatMap((entries) => encryption.decrypt(identity, entries)) | |
| ) | |
| }) | |
| }) | |
| /** | |
| * Creates an `EventLogRemote` that sends and receives plaintext entry payloads. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeUnencrypted: Effect.Effect< | |
| EventLogRemote["Service"], | |
| EventLogRemoteError, | |
| Scope.Scope | EventLogRemoteClient | Registry | |
| > = makeWith({ | |
| encodeWrite: (options) => | |
| new WriteEntriesUnencrypted({ | |
| publicKey: options.identity.publicKey, | |
| storeId: options.storeId, | |
| entries: options.entries | |
| }).encoded, | |
| decodeChanges: (_identity, data) => ChangesRpc.decodeUnencrypted(data) | |
| }) | |
| /** | |
| * Provides an encrypted `EventLogRemote` using the remote RPC client and the | |
| * default Web Crypto encryption layer. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerEncrypted: Layer.Layer< | |
| EventLogRemote, | |
| EventLogRemoteError, | |
| RpcClient.Protocol | Registry | |
| > = Layer.effect(EventLogRemote, makeEncrypted).pipe( | |
| Layer.provide(EventLogRemoteClient.layer), | |
| Layer.provide(layerSubtle) | |
| ) | |
| /** | |
| * Provides an unencrypted `EventLogRemote` using the remote RPC client. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerUnencrypted: Layer.Layer< | |
| EventLogRemote, | |
| EventLogRemoteError, | |
| RpcClient.Protocol | Registry | |
| > = Layer.effect(EventLogRemote, makeUnencrypted).pipe( | |
| Layer.provide(EventLogRemoteClient.layer) | |
| ) | |
Xet Storage Details
- Size:
- 12.4 kB
- Xet hash:
- 2f24ee562e783ce8f43a2f9e25d73cf138cdac2ab5866e192c5b74170cb9f904
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.