| /** | |
| * Runtime for writing typed events to an event journal. | |
| * | |
| * `EventLog` combines event groups, handlers, a journal, local identity, | |
| * optional remote replicas, and reactivity hooks. Writers send typed payloads | |
| * through a client; the matching handler runs first, and the journal entry is | |
| * committed only after the handler succeeds. This module also contains the | |
| * layers and helpers needed to assemble that runtime. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Context from "../../Context.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as FiberMap from "../../FiberMap.js"; | |
| import { constant, identity } from "../../Function.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import { pipeArguments } from "../../Pipeable.js"; | |
| import * as Predicate from "../../Predicate.js"; | |
| import * as PubSub from "../../PubSub.js"; | |
| import * as Queue from "../../Queue.js"; | |
| import * as Redacted from "../../Redacted.js"; | |
| import * as Schedule from "../../Schedule.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as SchemaGetter from "../../SchemaGetter.js"; | |
| import { Reactivity } from "../reactivity/Reactivity.js"; | |
| import * as ReactivityLayer from "../reactivity/Reactivity.js"; | |
| import { Entry, EventJournal, makeEntryIdUnsafe } from "./EventJournal.js"; | |
| import * as EventLogEncryption from "./EventLogEncryption.js"; | |
| import { StoreId } from "./EventLogMessage.js"; | |
| /** | |
| * Service for writing typed event-log events through registered handlers. | |
| * | |
| * **Details** | |
| * | |
| * `write` encodes the event payload, runs the matching handler, commits the entry | |
| * only when the handler succeeds, and exposes access to the underlying journal | |
| * entries and destroy operation. | |
| * | |
| * @category services | |
| * @since 4.0.0 | |
| */ | |
| export class EventLog extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLog") {} | |
| /** | |
| * Service that collects event handlers, compaction handlers, remote replicas, | |
| * and reactivity invalidation keys. | |
| * | |
| * @category services | |
| * @since 4.0.0 | |
| */ | |
| export class Registry extends /*#__PURE__*/Context.Service()("effect/unstable/eventlog/EventLog/Registry") {} | |
| /** | |
| * Provides an in-memory `Registry` for event handlers, compactors, remote | |
| * replicas, and reactivity keys. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerRegistry = /*#__PURE__*/Layer.effect(Registry, /*#__PURE__*/Effect.gen(function* () { | |
| const handlers = new Map(); | |
| const compactors = new Map(); | |
| const remoteFiberMap = yield* FiberMap.make(); | |
| const remotes = new Map(); | |
| let remoteHandler = _ => Effect.void; | |
| const reactivityKeys = {}; | |
| return Registry.of({ | |
| registerHandlerUnsafe(options) { | |
| handlers.set(options.event, options.handler); | |
| }, | |
| handlers, | |
| registerCompaction: options => Effect.sync(() => { | |
| const events = new Set(options.events); | |
| const compactor = { | |
| events, | |
| effect: options.effect | |
| }; | |
| for (const event of options.events) { | |
| compactors.set(event, compactor); | |
| } | |
| }), | |
| compactors, | |
| registerRemote: remote => Effect.acquireRelease(Effect.suspend(() => { | |
| remotes.set(remote.id, remote); | |
| return Effect.asVoid(FiberMap.run(remoteFiberMap, remote.id, remoteHandler(remote))); | |
| }), () => { | |
| remotes.delete(remote.id); | |
| return FiberMap.remove(remoteFiberMap, remote.id); | |
| }), | |
| handleRemote(handler) { | |
| remoteHandler = handler; | |
| return Effect.forEach(remotes, ([id, remote]) => FiberMap.run(remoteFiberMap, id, handler(remote)), { | |
| discard: true | |
| }); | |
| }, | |
| registerReactivity: keys => Effect.sync(() => { | |
| Object.assign(reactivityKeys, keys); | |
| }), | |
| reactivityKeys | |
| }); | |
| })); | |
| /** | |
| * Context service for an event-log identity containing a public key and redacted | |
| * private key material. | |
| * | |
| * **Details** | |
| * | |
| * The identity is used by remote replication for authentication and by the | |
| * encryption service to derive signing and encryption keys. | |
| * | |
| * @category services | |
| * @since 4.0.0 | |
| */ | |
| export class Identity extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLog/Identity") {} | |
| /** | |
| * Runtime property key used to identify `EventLogSchema` values. | |
| * | |
| * @category type IDs | |
| * @since 4.0.0 | |
| */ | |
| export const SchemaTypeId = "~effect/eventlog/EventLog/Schema"; | |
| /** | |
| * Returns `true` when a value carries the `EventLogSchema` marker. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const isEventLogSchema = u => Predicate.hasProperty(u, SchemaTypeId); | |
| /** | |
| * Creates an `EventLogSchema` from one or more event groups. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const schema = (...groups) => { | |
| const EventLog = Object.assign(function EventLog() {}, { | |
| [SchemaTypeId]: SchemaTypeId, | |
| groups | |
| }); | |
| return EventLog; | |
| }; | |
| /** | |
| * Runtime property key used to identify `Handlers` values. | |
| * | |
| * @category type IDs | |
| * @since 4.0.0 | |
| */ | |
| export const HandlersTypeId = "~effect/eventlog/EventLog/Handlers"; | |
| /** | |
| * Context reference for the store id used by event-log writes and remote | |
| * replication. | |
| * | |
| * **Details** | |
| * | |
| * Defaults to the branded store id `"default"`. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export class CurrentStoreId extends /*#__PURE__*/Context.Reference("effect/eventlog/EventLog/CurrentStoreId", { | |
| defaultValue: /*#__PURE__*/constant(/*#__PURE__*/StoreId.make("default")) | |
| }) {} | |
| const RedactedUint8Array = /*#__PURE__*/Schema.Uint8ArrayFromBase64.pipe(/*#__PURE__*/Schema.decodeTo(/*#__PURE__*/Schema.Redacted(Schema.Uint8Array), { | |
| decode: /*#__PURE__*/SchemaGetter.transform(value => Redacted.make(value)), | |
| encode: /*#__PURE__*/SchemaGetter.transform(value => Redacted.value(value)) | |
| })); | |
| /** | |
| * Schema for an event-log identity with a string public key and redacted | |
| * base64-encoded private key bytes. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const IdentitySchema = /*#__PURE__*/Schema.Struct({ | |
| publicKey: Schema.String, | |
| privateKey: RedactedUint8Array | |
| }); | |
| const IdentityEncodedSchema = /*#__PURE__*/Schema.Struct({ | |
| publicKey: Schema.String, | |
| privateKey: Schema.Uint8ArrayFromBase64 | |
| }); | |
| const IdentityStringSchema = /*#__PURE__*/Schema.StringFromBase64Url.pipe(/*#__PURE__*/Schema.decodeTo(/*#__PURE__*/Schema.fromJsonString(IdentityEncodedSchema))); | |
| /** | |
| * Decodes a base64url identity string produced by `encodeIdentityString`. | |
| * | |
| * **Gotchas** | |
| * | |
| * Invalid input throws a schema decoding error. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const decodeIdentityString = value => { | |
| const decoded = Schema.decodeUnknownSync(IdentityStringSchema)(value); | |
| return { | |
| publicKey: decoded.publicKey, | |
| privateKey: Redacted.make(decoded.privateKey) | |
| }; | |
| }; | |
| /** | |
| * Encodes an event-log identity as a base64url string containing the public key | |
| * and private key bytes. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const encodeIdentityString = identity => Schema.encodeSync(IdentityStringSchema)({ | |
| publicKey: identity.publicKey, | |
| privateKey: Redacted.value(identity.privateKey) | |
| }); | |
| /** | |
| * Generates a new event-log identity using the configured | |
| * `EventLogEncryption` service. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeIdentity = /*#__PURE__*/EventLogEncryption.EventLogEncryption.use(_ => _.generateIdentity); | |
| const handlersProto = { | |
| [HandlersTypeId]: { | |
| _Events: identity | |
| }, | |
| handle(tag, handler) { | |
| return makeHandlers({ | |
| group: this.group, | |
| context: this.context, | |
| handlers: { | |
| ...this.handlers, | |
| [tag]: { | |
| event: this.group.events[tag], | |
| context: this.context, | |
| handler | |
| } | |
| } | |
| }); | |
| }, | |
| pipe() { | |
| return pipeArguments(this, arguments); | |
| } | |
| }; | |
| const makeHandlers = options => Object.assign(Object.create(handlersProto), options); | |
| /** | |
| * Creates a layer that registers handlers for every event in an event group. | |
| * | |
| * **Details** | |
| * | |
| * The callback receives a `Handlers` builder; its return type is checked so every | |
| * event in the group is handled. | |
| * | |
| * @category handlers | |
| * @since 4.0.0 | |
| */ | |
| export const group = (group, f) => Layer.effectDiscard(Effect.gen(function* () { | |
| const registry = yield* Registry; | |
| const context = yield* Effect.context(); | |
| const result = f(makeHandlers({ | |
| group: group, | |
| handlers: {}, | |
| context | |
| })); | |
| const handlers = Effect.isEffect(result) ? yield* result : result; | |
| for (const tag in handlers.handlers) { | |
| registry.registerHandlerUnsafe({ | |
| event: tag, | |
| handler: handlers.handlers[tag] | |
| }); | |
| } | |
| })); | |
| /** | |
| * Registers a compaction handler for an event group. | |
| * | |
| * **Details** | |
| * | |
| * During remote replay, matching entries are decoded, grouped by primary key, and | |
| * passed to the compaction effect, which may write replacement entries. | |
| * | |
| * @category compaction | |
| * @since 4.0.0 | |
| */ | |
| export const groupCompaction = (group, effect) => Layer.effectDiscard(Effect.gen(function* () { | |
| const registry = yield* Registry; | |
| const services = yield* Effect.context(); | |
| yield* registry.registerCompaction({ | |
| events: Object.keys(group.events), | |
| effect: Effect.fnUntraced(function* ({ | |
| entries, | |
| write | |
| }) { | |
| const isEventTag = tag => tag in group.events; | |
| const decodePayload = (tag, payload) => Schema.decodeUnknownEffect(group.events[tag].payloadMsgPack)(payload).pipe(Effect.updateContext(input => Context.merge(services, input)), Effect.orDie); | |
| const writePayload = Effect.fnUntraced(function* (timestamp, tag, payload) { | |
| const event = group.events[tag]; | |
| const entry = new Entry({ | |
| id: makeEntryIdUnsafe({ | |
| msecs: timestamp | |
| }), | |
| event: tag, | |
| payload: yield* Schema.encodeUnknownEffect(event.payloadMsgPack)(payload).pipe(Effect.orDie), | |
| primaryKey: event.primaryKey(payload) | |
| }, { | |
| disableChecks: true | |
| }); | |
| yield* write(entry); | |
| }); | |
| const byPrimaryKey = new Map(); | |
| for (const entry of entries) { | |
| if (!isEventTag(entry.event)) { | |
| continue; | |
| } | |
| const payload = yield* decodePayload(entry.event, entry.payload); | |
| const record = byPrimaryKey.get(entry.primaryKey); | |
| const taggedPayload = { | |
| _tag: entry.event, | |
| payload | |
| }; | |
| if (record) { | |
| record.entries.push(entry); | |
| record.taggedPayloads.push(taggedPayload); | |
| } else { | |
| byPrimaryKey.set(entry.primaryKey, { | |
| entries: [entry], | |
| taggedPayloads: [taggedPayload] | |
| }); | |
| } | |
| } | |
| for (const [primaryKey, { | |
| entries, | |
| taggedPayloads | |
| }] of byPrimaryKey) { | |
| yield* Effect.orDie(effect({ | |
| primaryKey, | |
| entries, | |
| events: taggedPayloads, | |
| write(tag, payload) { | |
| return Effect.orDie(writePayload(entries[0].createdAtMillis, tag, payload)); | |
| } | |
| }).pipe(Effect.updateContext(input => Context.merge(services, input)))); | |
| } | |
| }) | |
| }); | |
| })); | |
| /** | |
| * Registers reactivity keys to invalidate when events from a group are written or | |
| * replayed. | |
| * | |
| * **Details** | |
| * | |
| * Pass a single key list for all events or a mapping from event tag to key list. | |
| * | |
| * @category reactivity | |
| * @since 4.0.0 | |
| */ | |
| export const groupReactivity = (group, keys) => Effect.gen(function* () { | |
| const registry = yield* Registry; | |
| if (!Array.isArray(keys)) { | |
| yield* registry.registerReactivity(keys); | |
| return; | |
| } | |
| const obj = {}; | |
| for (const tag in group.events) { | |
| obj[tag] = keys; | |
| } | |
| yield* registry.registerReactivity(obj); | |
| }).pipe(Layer.effectDiscard); | |
| /** | |
| * Builds the effect used to replay entries received from a remote event log. | |
| * | |
| * **Details** | |
| * | |
| * The returned handler decodes the entry and conflicts with the registered event | |
| * schema, runs the matching handler with the supplied identity and store id, logs | |
| * failures, and invalidates configured reactivity keys. | |
| * | |
| * @category handlers | |
| * @since 4.0.0 | |
| */ | |
| export const makeReplayFromRemote = options => Effect.fnUntraced(function* ({ | |
| conflicts, | |
| entry | |
| }) { | |
| const handler = options.handlers.get(entry.event); | |
| if (!handler) { | |
| return yield* Effect.logDebug(`Event handler not found for: "${entry.event}"`); | |
| } | |
| const decodePayload = Schema.decodeUnknownEffect(handler.event.payloadMsgPack); | |
| const decodedConflicts = new Array(conflicts.length); | |
| for (let i = 0; i < conflicts.length; i++) { | |
| decodedConflicts[i] = { | |
| entry: conflicts[i], | |
| payload: yield* decodePayload(conflicts[i].payload).pipe(Effect.updateContext(input => Context.merge(handler.context, input))) | |
| }; | |
| } | |
| yield* decodePayload(entry.payload).pipe(Effect.flatMap(payload => handler.handler({ | |
| storeId: options.storeId, | |
| payload, | |
| entry, | |
| conflicts: decodedConflicts | |
| })), Effect.provideService(Identity, options.identity), Effect.updateContext(input => Context.merge(handler.context, input)), Effect.asVoid); | |
| const keys = options.reactivityKeys[entry.event]; | |
| if (keys) { | |
| for (const key of keys) { | |
| options.reactivity.invalidateUnsafe({ | |
| [key]: [entry.primaryKey] | |
| }); | |
| } | |
| } | |
| }, Effect.catchCause(Effect.logError), (effect, { | |
| entry | |
| }) => Effect.annotateLogs(effect, { | |
| ...options.logAnnotations, | |
| entryId: entry.idString | |
| })); | |
| const make = /*#__PURE__*/Effect.gen(function* () { | |
| const storeId = yield* CurrentStoreId; | |
| const identity = yield* Identity; | |
| const journal = yield* EventJournal; | |
| const registry = yield* Registry; | |
| const reactivity = yield* Reactivity; | |
| const replayFromRemote = makeReplayFromRemote({ | |
| handlers: registry.handlers, | |
| storeId, | |
| identity, | |
| reactivity, | |
| reactivityKeys: registry.reactivityKeys, | |
| logAnnotations: { | |
| service: "EventLog", | |
| effect: "writeFromRemote" | |
| } | |
| }); | |
| const invalidateReactivityEntries = entries => Effect.sync(() => { | |
| for (const entry of entries) { | |
| const keys = registry.reactivityKeys[entry.event]; | |
| if (!keys) { | |
| continue; | |
| } | |
| for (const key of keys) { | |
| reactivity.invalidateUnsafe({ | |
| [key]: [entry.primaryKey] | |
| }); | |
| } | |
| } | |
| }); | |
| const runRemote = Effect.fnUntraced(function* (remote) { | |
| const startSequence = yield* journal.nextRemoteSequence(remote.id); | |
| yield* Effect.gen(function* () { | |
| const changes = yield* remote.changes({ | |
| identity, | |
| startSequence, | |
| storeId | |
| }); | |
| while (true) { | |
| const entries = yield* Queue.takeAll(changes); | |
| yield* journal.writeFromRemote({ | |
| remoteId: remote.id, | |
| entries: entries.flat(), | |
| compact: registry.compactors.size > 0 ? Effect.fnUntraced(function* (remoteEntries) { | |
| const finalEntries = []; | |
| const compactable = new Map(); | |
| for (let i = 0; i < remoteEntries.length; i++) { | |
| const remoteEntry = remoteEntries[i]; | |
| const entry = remoteEntry.entry; | |
| const compactor = registry.compactors.get(entry.event); | |
| if (!compactor) { | |
| finalEntries.push(entry); | |
| continue; | |
| } | |
| let arr = compactable.get(compactor.effect); | |
| if (!arr) { | |
| arr = []; | |
| compactable.set(compactor.effect, arr); | |
| } | |
| arr.push(entry); | |
| } | |
| for (const [compact, entries] of compactable) { | |
| yield* compact({ | |
| entries, | |
| write(entry) { | |
| return Effect.sync(() => { | |
| finalEntries.push(entry); | |
| }); | |
| } | |
| }); | |
| } | |
| return finalEntries.sort(Entry.Order); | |
| }) : undefined, | |
| effect: replayFromRemote | |
| }).pipe(Effect.tap(({ | |
| duplicateEntries | |
| }) => invalidateReactivityEntries(duplicateEntries)), journal.withLock(storeId)); | |
| } | |
| }).pipe(Effect.scoped, Effect.catchCause(Effect.logError), Effect.repeat(Schedule.exponential(200, 1.5).pipe(Schedule.either(Schedule.spaced({ | |
| seconds: 10 | |
| })))), Effect.annotateLogs({ | |
| service: "EventLog", | |
| effect: "runRemote consume" | |
| }), Effect.forkScoped); | |
| const write = journal.withRemoteUncommited(remote.id, entries => remote.write({ | |
| identity, | |
| entries, | |
| storeId | |
| })); | |
| yield* Effect.addFinalizer(() => Effect.ignore(write)); | |
| yield* write; | |
| const changesSub = yield* journal.changes; | |
| return yield* PubSub.takeAll(changesSub).pipe(Effect.andThen(write), Effect.catchCause(Effect.logError), Effect.forever); | |
| }, Effect.scoped, Effect.provideService(Identity, identity), Effect.orDie); | |
| const writeHandler = Effect.fnUntraced(function* (handler, options) { | |
| const payload = yield* Schema.encodeUnknownEffect(handler.event.payloadMsgPack)(options.payload).pipe(Effect.orDie); | |
| return yield* journal.withLock(storeId)(journal.write({ | |
| event: options.event, | |
| primaryKey: handler.event.primaryKey(options.payload), | |
| payload, | |
| effect: entry => handler.handler({ | |
| storeId, | |
| payload: options.payload, | |
| entry, | |
| conflicts: [] | |
| }).pipe(Effect.updateContext(input => Context.merge(handler.context, input)), Effect.provideService(Identity, identity), Effect.tap(() => { | |
| if (registry.reactivityKeys[entry.event]) { | |
| for (const key of registry.reactivityKeys[entry.event]) { | |
| reactivity.invalidateUnsafe({ | |
| [key]: [entry.primaryKey] | |
| }); | |
| } | |
| } | |
| return Effect.void; | |
| })) | |
| })); | |
| }); | |
| const eventLogWrite = options => { | |
| const handler = registry.handlers.get(options.event); | |
| if (handler === undefined) { | |
| return Effect.die(`Event handler not found for: "${options.event}"`); | |
| } | |
| return writeHandler(handler, options); | |
| }; | |
| yield* registry.handleRemote(runRemote); | |
| return EventLog.of({ | |
| write: eventLogWrite, | |
| entries: journal.entries, | |
| destroy: journal.destroy | |
| }); | |
| }); | |
| /** | |
| * Provides `EventLog` and `Registry` using the configured `EventJournal` and | |
| * `Identity`. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerEventLog = /*#__PURE__*/Layer.effect(EventLog, make).pipe(/*#__PURE__*/Layer.provide(ReactivityLayer.layer), /*#__PURE__*/Layer.provideMerge(layerRegistry)); | |
| /** | |
| * Combines event-group handler layers with the `EventLog` runtime for a schema. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need one layer that installs the shared `EventLog` runtime for | |
| * an `EventLogSchema` and registers an event-group handler layer for typed | |
| * writes. | |
| * | |
| * **Details** | |
| * | |
| * The supplied handler layer is provided with `layerEventLog`. The returned | |
| * layer provides `EventLog | Registry`, preserves the handler layer's error | |
| * type, and still requires its remaining services plus `EventJournal` and | |
| * `Identity`. | |
| * | |
| * **Gotchas** | |
| * | |
| * The schema argument does not register handlers by itself. Handler registration | |
| * comes from the supplied layer, and writing an event without a registered | |
| * handler dies with `Event handler not found for: "<tag>"`. | |
| * | |
| * @see {@link schema} for creating the schema argument from event groups | |
| * @see {@link group} for building the handler layer consumed by this layer | |
| * @see {@link layerEventLog} for installing the runtime and registry without combining a handler layer | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layer = (_schema, layer) => layer.pipe(Layer.provideMerge(layerEventLog)); | |
| /** | |
| * Creates a typed client function for writing events defined by an | |
| * `EventLogSchema`. | |
| * | |
| * **Details** | |
| * | |
| * The returned function delegates to the `EventLog` service and preserves each | |
| * event's success and error types. | |
| * | |
| * @category client | |
| * @since 4.0.0 | |
| */ | |
| export const makeClient = schema => Effect.gen(function* () { | |
| const log = yield* EventLog; | |
| return (event, payload) => log.write({ | |
| schema, | |
| event, | |
| payload | |
| }); | |
| }); | |
| //# sourceMappingURL=EventLog.js.map |
Xet Storage Details
- Size:
- 19.9 kB
- Xet hash:
- d520c8f144fde4bf1d8c53513e044e9352ff5c29da16902e3ab5ad8df0f52c6e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.