EdgeAIG/opencode / .opencode /node_modules /effect /dist /unstable /eventlog /EventLogServerUnencrypted.js
EdgeAIG's picture
download
raw
22.7 kB
/**
* Plaintext server implementation for the event-log remote protocol.
*
* This module accepts unencrypted event batches from remote clients, runs the
* registered event handlers, stores journal entries, and streams backlog plus
* live changes through the shared `EventLogServer` RPC protocol. It is intended
* for trusted deployments, local development, and tests where event data does
* not need a server-side encryption layer. The module also provides the
* services and layers needed to authorize requests, map stores, persist entries,
* and install the plaintext server.
*
* @since 4.0.0
*/
import * as Arr from "../../Array.js";
import * as Context from "../../Context.js";
import * as Data from "../../Data.js";
import * as Effect from "../../Effect.js";
import * as Layer from "../../Layer.js";
import * as Option from "../../Option.js";
import * as PubSub from "../../PubSub.js";
import * as RcMap from "../../RcMap.js";
import * as Redacted from "../../Redacted.js";
import * as Schema from "../../Schema.js";
import * as Semaphore from "../../Semaphore.js";
import * as Stream from "../../Stream.js";
import * as RpcServer from "../rpc/RpcServer.js";
import * as EventJournal from "./EventJournal.js";
import { Entry, makeEntryIdUnsafe, makeRemoteIdUnsafe, RemoteEntry } from "./EventJournal.js";
import * as EventLog from "./EventLog.js";
import { ChangesRpc, EventLogProtocolError, EventLogRemoteRpcs, WriteEntriesUnencrypted } from "./EventLogMessage.js";
import * as EventLogServer from "./EventLogServer.js";
/**
* Service that writes plaintext event-log entries directly to
* unencrypted storage through registered event handlers.
*
* **When to use**
*
* Use to access or provide the server service that handles plaintext
* event-log writes.
*
* @category services
* @since 4.0.0
*/
export class EventLogServerUnencrypted extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLogServerUnencrypted") {}
/**
* Creates a typed server-side write function for events in the supplied
* `EventLogSchema`.
*
* @category EventLogServerUnencrypted
* @since 4.0.0
*/
export const makeWrite = schema => EventLogServerUnencrypted.useSync(_ => _.makeWrite(schema));
/**
* Provides RPC handlers for the unencrypted event-log server.
*
* **Details**
*
* Incoming plaintext entries are authorized, mapped to a server store, checked
* for conflicts, run through registered handlers, and persisted; change streams
* include compacted backlog entries when compactors are registered.
*
* @category layers
* @since 4.0.0
*/
export const layerRpcHandlers = /*#__PURE__*/Layer.unwrap(/*#__PURE__*/Effect.gen(function* () {
const storage = yield* Storage;
const mapping = yield* StoreMapping;
const auth = yield* EventLogServerAuthorization;
const registry = yield* EventLog.Registry;
const handler = yield* makeServerHandler;
const remoteId = yield* storage.getId;
const processEntries = Effect.fnUntraced(function* (options) {
const entries = Arr.sort(options.entries, Entry.Order);
let history = yield* storage.entriesAfter(options.storeId, entries[0]);
const persistedEntries = Arr.empty();
for (const entry of entries) {
const [duplicate, conflicts, newHistory] = toConflicts(history, entry);
if (duplicate) continue;
history = newHistory;
yield* handler({
publicKey: options.publicKey,
storeId: options.storeId,
entry,
conflicts
});
persistedEntries.push(entry);
}
yield* storage.write(options.storeId, persistedEntries);
}, storage.withTransaction);
return EventLogServer.layerRpcHandlers({
remoteId,
getOrCreateSessionAuthBinding: (publicKey, signingPublicKey) => storage.getOrCreateSessionAuthBinding(publicKey, signingPublicKey),
onWrite: Effect.fnUntraced(function* (data) {
const request = yield* WriteEntriesUnencrypted.decode(data).pipe(Effect.mapError(_ => new EventLogProtocolError({
requestTag: "WriteEntries",
publicKey: undefined,
code: "InternalServerError",
message: "Decoding failure"
})));
if (!Arr.isReadonlyArrayNonEmpty(request.entries)) return;
const resolvedStoreId = yield* mapping.resolve({
publicKey: request.publicKey,
storeId: request.storeId
}).pipe(Effect.mapError(_ => new EventLogProtocolError({
requestTag: "WriteEntries",
publicKey: request.publicKey,
storeId: request.storeId,
code: "Unauthorized",
message: _.message
})));
yield* auth.authorizeWrite({
publicKey: request.publicKey,
storeId: resolvedStoreId,
entries: request.entries
}).pipe(Effect.mapError(_ => new EventLogProtocolError({
requestTag: "WriteEntries",
publicKey: request.publicKey,
storeId: request.storeId,
code: "Unauthorized",
message: _.message
})));
yield* processEntries({
publicKey: request.publicKey,
storeId: resolvedStoreId,
entries: request.entries
}).pipe(Effect.catchCause(_ => Effect.fail(new EventLogProtocolError({
requestTag: "WriteEntries",
publicKey: request.publicKey,
code: "InternalServerError",
message: "Persistence failure"
}))), Effect.provideService(EventLog.Identity, makeClientIdentity(request.publicKey)));
}),
changes: Effect.fnUntraced(function* (request) {
const storeId = yield* mapping.resolve({
publicKey: request.publicKey,
storeId: request.storeId
});
yield* auth.authorizeRead({
publicKey: request.publicKey,
storeId
});
return storage.changes({
storeId,
startSequence: request.startSequence,
compactors: registry.compactors
}).pipe(Stream.mapArrayEffect(entries => Effect.map(ChangesRpc.encodeUnencrypted(entries), Arr.of)));
}, Stream.unwrap)
});
}));
/**
* Error raised by unencrypted server storage and store mapping operations.
*
* @category errors
* @since 4.0.0
*/
export class EventLogServerStoreError extends /*#__PURE__*/Data.TaggedError("EventLogServerStoreError") {}
/**
* Error raised when unencrypted server authorization rejects an identity or store
* operation.
*
* @category errors
* @since 4.0.0
*/
export class EventLogServerAuthError extends /*#__PURE__*/Data.TaggedError("EventLogServerAuthError") {}
/**
* Service that validates unencrypted event-log server
* write access, read access, and identities.
*
* **When to use**
*
* Use to provide authorization checks for plaintext event-log writes, reads,
* and identity authentication.
*
* @category services
* @since 4.0.0
*/
export class EventLogServerAuthorization extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLogServerUnencrypted/EventLogServerAuthorization") {}
/**
* Service that resolves client-requested store ids to server store ids and checks
* whether a store exists.
*
* **When to use**
*
* Use to map client-visible store identifiers to server storage identifiers
* before authorizing or serving unencrypted event-log requests.
*
* @category services
* @since 4.0.0
*/
export class StoreMapping extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLogServerUnencrypted/StoreMapping") {}
const toStoreNotFoundError = options => new EventLogServerStoreError({
reason: "NotFound",
publicKey: options.publicKey,
storeId: options.storeId,
message: options.publicKey === undefined ? `No provisioned store found for store id: ${options.storeId}` : `No provisioned store found for public key: ${options.publicKey} and store id: ${options.storeId}`
});
/**
* Provides a `StoreMapping` that accepts only one configured store id and fails
* all other store ids as not found.
*
* @category store
* @since 4.0.0
*/
export const layerStoreMappingStatic = options => Layer.succeed(StoreMapping, {
resolve(request) {
if (request.storeId === options.storeId) {
return Effect.succeed(options.storeId);
}
return Effect.fail(toStoreNotFoundError(request));
},
hasStore: ({
storeId
}) => Effect.succeed(storeId === options.storeId)
});
/**
* Defines the backing store service used by the unencrypted event-log server.
*
* **When to use**
*
* Use to provide durable event-log persistence for an unencrypted event-log
* server layer.
*
* **Details**
*
* It provides the server remote id, stores session authentication bindings,
* allocates remote sequence numbers, persists entries, streams changes, and
* exposes a transaction boundary.
*
* @category storage
* @since 4.0.0
*/
export class Storage extends /*#__PURE__*/Context.Service()("effect/eventlog/EventLogServerUnencrypted/Storage") {}
const makeClientIdentity = publicKey => ({
publicKey,
privateKey: constEmptyPrivateKey
});
const constEmptyPrivateKey = /*#__PURE__*/Redacted.make(/*#__PURE__*/new Uint8Array(32));
const makeServerWriteIdentityPublicKey = storeId => `effect-eventlog-server-write:${storeId}`;
const entriesAfter = (journal, startSequence) => journal.filter(entry => entry.remoteSequence >= startSequence);
const toConflicts = (history, originEntry) => {
let duplicate = false;
for (let i = 0; i < history.length; i++) {
const entry = history[i];
if (entry.createdAtMillis < originEntry.createdAtMillis) {
continue;
} else if (entry.idString === originEntry.idString) {
duplicate = true;
continue;
}
const newHistory = history.slice(i);
let conflicts = [];
for (let j = 0; j < newHistory.length; j++) {
const scannedEntry = history[j];
if (scannedEntry.event === originEntry.event && scannedEntry.primaryKey === originEntry.primaryKey) {
conflicts.push(scannedEntry);
}
}
return [duplicate, conflicts, newHistory];
}
return [duplicate, [], []];
};
const representativeSequences = options => {
if (options.compactedCount === 0) {
return [];
}
if (options.compactedCount > options.remoteEntries.length) {
return undefined;
}
const maxSequence = options.remoteEntries[options.remoteEntries.length - 1].remoteSequence;
if (options.compactedCount === 1) {
return [maxSequence];
}
const selected = options.remoteEntries.slice(0, options.compactedCount - 1).map(entry => entry.remoteSequence);
selected.push(maxSequence);
for (let i = 1; i < selected.length; i++) {
if (selected[i] <= selected[i - 1]) {
return undefined;
}
}
return selected;
};
const toCompactedRemoteEntries = options => {
const sequences = representativeSequences({
remoteEntries: options.remoteEntries,
compactedCount: options.compacted.length
});
if (sequences === undefined) {
return undefined;
}
return options.compacted.map((entry, index) => new RemoteEntry({
remoteSequence: sequences[index],
entry
}, {
disableChecks: true
}));
};
/**
* Runs the registered compactors over a backlog of remote entries.
*
* **When to use**
*
* Use to reduce stored remote entries before replaying them to an unencrypted
* event-log client.
*
* **Details**
*
* Contiguous entries handled by the same compactor may be replaced with compacted
* entries when the replacement count can be mapped back to increasing remote
* sequence numbers; otherwise the original entries are kept.
*
* @category compaction
* @since 4.0.0
*/
export const compactBacklog = /*#__PURE__*/Effect.fnUntraced(function* (options) {
if (options.compactors.size === 0 || options.remoteEntries.length === 0) {
return options.remoteEntries;
}
const compactedRemoteEntries = [];
let index = 0;
while (index < options.remoteEntries.length) {
const remoteEntry = options.remoteEntries[index];
const compactor = options.compactors.get(remoteEntry.entry.event);
if (compactor === undefined) {
compactedRemoteEntries.push(remoteEntry);
index++;
continue;
}
const entries = [remoteEntry.entry];
const remoteGroup = [remoteEntry];
const compacted = [];
index++;
while (index < options.remoteEntries.length) {
const nextRemoteEntry = options.remoteEntries[index];
const nextCompactor = options.compactors.get(nextRemoteEntry.entry.event);
if (nextCompactor !== compactor) {
break;
}
entries.push(nextRemoteEntry.entry);
remoteGroup.push(nextRemoteEntry);
index++;
}
yield* compactor.effect({
entries,
write(entry) {
return Effect.sync(() => {
compacted.push(entry);
});
}
}).pipe(Effect.orDie);
const projected = toCompactedRemoteEntries({
compacted,
remoteEntries: remoteGroup
});
if (projected === undefined) {
compactedRemoteEntries.push(...remoteGroup);
continue;
}
compactedRemoteEntries.push(...projected);
}
return compactedRemoteEntries;
});
/**
* Creates an in-memory unencrypted server `Storage`.
*
* **Details**
*
* The implementation keeps per-store journals and session authentication bindings
* in memory, publishes live changes, and serializes transactions with a
* semaphore.
*
* @category storage
* @since 4.0.0
*/
export const makeStorageMemory = /*#__PURE__*/Effect.gen(function* () {
const knownIds = new Map();
const journals = new Map();
const sessionAuthBindings = new Map();
const remoteId = makeRemoteIdUnsafe();
const ensureKnownIds = storeId => {
let storeKnownIds = knownIds.get(storeId);
if (storeKnownIds) return storeKnownIds;
storeKnownIds = new Map();
knownIds.set(storeId, storeKnownIds);
return storeKnownIds;
};
const ensureJournal = storeId => {
let journal = journals.get(storeId);
if (journal) return journal;
journal = [];
journals.set(storeId, journal);
return journal;
};
const pubsubs = yield* RcMap.make({
lookup: _storeId => Effect.acquireRelease(PubSub.unbounded(), PubSub.shutdown),
idleTimeToLive: 60000
});
const write = Effect.fnUntraced(function* (storeId, entries) {
const sequenceNumbers = [];
const committed = [];
const storeKnownIds = ensureKnownIds(storeId);
const journal = ensureJournal(storeId);
let lastSequenceNumber = Arr.last(journal).pipe(Option.map(entry => entry.remoteSequence), Option.getOrElse(() => 0));
if (entries.some(entry => storeKnownIds.has(entry.idString))) {
return yield* Effect.die("Duplicate entries");
}
for (const entry of entries) {
const remoteEntry = new RemoteEntry({
remoteSequence: ++lastSequenceNumber,
entry
}, {
disableChecks: true
});
sequenceNumbers.push(remoteEntry.remoteSequence);
committed.push(remoteEntry);
journal.push(remoteEntry);
storeKnownIds.set(entry.idString, remoteEntry.remoteSequence);
}
const pubsub = yield* RcMap.get(pubsubs, storeId);
yield* PubSub.publishAll(pubsub, committed);
return committed;
}, Effect.scoped);
const transactionSemaphore = yield* Semaphore.make(1);
return Storage.of({
getId: Effect.succeed(remoteId),
getOrCreateSessionAuthBinding: (publicKey, signingPublicKey) => Effect.sync(() => {
const existing = sessionAuthBindings.get(publicKey);
if (existing) return existing;
sessionAuthBindings.set(publicKey, signingPublicKey);
return signingPublicKey;
}),
entriesAfter: (storeId, entry) => Effect.sync(() => {
const journal = ensureJournal(storeId);
return journal.filter(e => Entry.Order(e.entry, entry) >= 0).map(e => e.entry);
}),
write,
changes: Effect.fnUntraced(function* ({
storeId,
startSequence,
compactors
}) {
const pubsub = yield* RcMap.get(pubsubs, storeId);
const subscription = yield* PubSub.subscribe(pubsub);
const backlog = yield* compactBacklog({
remoteEntries: entriesAfter(ensureJournal(storeId), startSequence),
compactors
});
const replayedUpTo = backlog.length > 0 ? backlog[backlog.length - 1].remoteSequence : startSequence - 1;
return Stream.fromArray(backlog).pipe(Stream.concat(Stream.fromSubscription(subscription).pipe(Stream.filter(entry => entry.remoteSequence > replayedUpTo))));
}, Stream.unwrap),
withTransaction: transactionSemaphore.withPermits(1)
});
});
/**
* Provides unencrypted server `Storage` using the in-memory implementation.
*
* @category storage
* @since 4.0.0
*/
export const layerStorageMemory = /*#__PURE__*/Layer.effect(Storage)(makeStorageMemory);
/**
* Creates the `EventLogServerUnencrypted` service from the configured storage and
* registered event handlers.
*
* **When to use**
*
* Use when you need the unencrypted event-log server service from provided
* `Storage` and an event-log `Registry`.
*
* **Details**
*
* The constructed service exposes `makeWrite`, which builds a typed server-side
* write function from an `EventLogSchema`. Each write encodes the payload with
* the event schema, runs the registered handler, and persists the generated
* entry inside `Storage.withTransaction`.
*
* **Gotchas**
*
* The write function dies if the requested event tag is not present in the
* schema passed to `makeWrite`; it does not report that case as a typed failure.
*
* @see {@link makeWrite} for the accessor that retrieves the typed server-side write function from the service environment
* @see {@link layerServer} for the layer form that provides this service together with an event-log `Registry`
*
* @category constructors
* @since 4.0.0
*/
export const make = /*#__PURE__*/Effect.gen(function* () {
const storage = yield* Storage;
const handler = yield* makeServerHandler;
return EventLogServerUnencrypted.of({
makeWrite(schema) {
const events = new Map();
for (const group of schema.groups) {
for (const [tag, event] of Object.entries(group.events)) {
events.set(tag, event);
}
}
return Effect.fnUntraced(function* (options) {
const publicKey = makeServerWriteIdentityPublicKey(options.storeId);
const schemaEvent = events.get(options.event);
if (schemaEvent === undefined) {
return yield* Effect.die(`Event schema not found for: "${options.event}"`);
}
const entry = new EventJournal.Entry({
id: makeEntryIdUnsafe(),
event: options.event,
primaryKey: schemaEvent.primaryKey(options.payload),
payload: yield* Schema.encodeUnknownEffect(schemaEvent.payloadMsgPack)(options.payload).pipe(Effect.mapError(_ => new EventLogServerStoreError({
reason: "PersistenceFailure",
publicKey: publicKey,
storeId: options.storeId,
message: "Failed to encode event"
})))
}, {
disableChecks: true
});
const result = yield* handler({
publicKey,
storeId: options.storeId,
entry,
conflicts: []
}).pipe(Effect.provideService(EventLog.Identity, makeClientIdentity(publicKey)));
yield* storage.write(options.storeId, [entry]);
return result;
}, storage.withTransaction);
}
});
});
/**
* Provides `EventLogServerUnencrypted` and an event-log `Registry` using the
* configured unencrypted server `Storage`.
*
* **When to use**
*
* Use to provide the unencrypted event-log server service together with the
* registry needed by event handlers.
*
* @category layers
* @since 4.0.0
*/
export const layerServer = /*#__PURE__*/Layer.effect(EventLogServerUnencrypted, make).pipe(/*#__PURE__*/Layer.provideMerge(EventLog.layerRegistry));
/**
* Builds a full unencrypted event-log RPC server for the supplied schema and
* event-group handler layer.
*
* **When to use**
*
* Use when you need the full unencrypted event-log RPC server layer with
* storage, authorization, RPC protocol, and event-group handler dependencies
* supplied externally.
*
* **Details**
*
* The layer installs `EventLogRemoteRpcs`, wires `layerRpcHandlers`, registers
* the supplied event-group handler layer, and provides `layerServer`, leaving
* only the required infrastructure services in the environment.
*
* **Gotchas**
*
* Entries are persisted and streamed in plaintext. Protect the backing
* `Storage` with the surrounding infrastructure, and use durable storage that
* preserves session authentication bindings when the server must survive
* restarts.
*
* @see {@link layerNoRpcServer} for installing the same unencrypted handlers when an `RpcServer.Protocol` is provided elsewhere
* @see {@link layerRpcHandlers} for wiring the unencrypted RPC handlers directly
* @see {@link layerServer} for constructing the server service and event-log registry without RPC handlers
*
* @category layers
* @since 4.0.0
*/
export const layer = (_schema, layer) => RpcServer.layer(EventLogRemoteRpcs).pipe(Layer.provide(layerRpcHandlers), Layer.provide(layer), Layer.provide(layerServer));
/**
* Builds the unencrypted event-log server handlers without installing an
* `RpcServer.Protocol` implementation.
*
* @category layers
* @since 4.0.0
*/
export const layerNoRpcServer = (_schema, layer) => layerRpcHandlers.pipe(Layer.merge(layer), Layer.provide(layerServer));
const makeServerHandler = /*#__PURE__*/Effect.gen(function* () {
const registry = yield* EventLog.Registry;
return Effect.fnUntraced(function* (options) {
const handler = registry.handlers.get(options.entry.event);
if (handler === undefined) {
return yield* Effect.logDebug(`Event handler not found for: "${options.entry.event}"`);
}
const decodePayload = Schema.decodeUnknownEffect(handler.event.payloadMsgPack);
const decodedConflicts = [];
for (const conflict of options.conflicts) {
decodedConflicts.push({
entry: conflict,
payload: yield* decodePayload(conflict.payload).pipe(Effect.updateContext(input => Context.merge(handler.context, input)))
});
}
const payloadEffect = "payload" in options ? Effect.succeed(options.payload) : decodePayload(options.entry.payload);
return yield* payloadEffect.pipe(Effect.mapError(_ => new EventLogServerStoreError({
reason: "PersistenceFailure",
publicKey: options.publicKey,
storeId: options.storeId,
message: "Failed to decode event"
})), Effect.flatMap(payload => handler.handler({
storeId: options.storeId,
payload,
entry: options.entry,
conflicts: decodedConflicts
})), Effect.updateContext(input => Context.merge(handler.context, input)));
});
});
//# sourceMappingURL=EventLogServerUnencrypted.js.map

Xet Storage Details

Size:
22.7 kB
·
Xet hash:
e85edc7ce561605b1111f18657378d966b04253b2e4b3497a0587b9ed985f1bb

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.