@prosody-events/prosody 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The event context passed to handlers, and the demand that started it.
3
+ * @module lib/context
4
+ */
5
+
6
+ import type { Message } from "./payload";
7
+ import type {
8
+ DequeDefinition,
9
+ MapDefinition,
10
+ MessageDequeDefinition,
11
+ MessageMapDefinition,
12
+ MessageValueDefinition,
13
+ SetDefinition,
14
+ ValueDefinition,
15
+ } from "./state/definitions";
16
+ import type { DequeState } from "./state/deque";
17
+ import type { MapState } from "./state/map";
18
+ import type { SetState } from "./state/set";
19
+ import type { ValueState } from "./state/value";
20
+
21
+ /**
22
+ * The demand that started a handler invocation.
23
+ */
24
+ export interface Demand {
25
+ /** `"normal"` for a first attempt, or `"failure"` for a retry. */
26
+ readonly kind: "normal" | "failure";
27
+ /**
28
+ * The retry count: 0 for normal demand and 1 on the first retry. It is
29
+ * an estimate. Keep an exact attempt count in keyed state if you need one.
30
+ */
31
+ readonly retry: number;
32
+ }
33
+
34
+ /**
35
+ * The event context that a handler receives. Each call propagates the active
36
+ * trace context.
37
+ *
38
+ * `state()` caches one handle per definition object, so repeated calls with
39
+ * the same definition return the same handle. It throws a
40
+ * {@link TransientStateError} for an object that no definition constructor
41
+ * made.
42
+ */
43
+ export declare class Context {
44
+ private constructor();
45
+
46
+ /**
47
+ * The demand that started this handler invocation. The value is frozen.
48
+ */
49
+ get demand(): Demand;
50
+
51
+ /**
52
+ * Checks whether cancellation has been signaled.
53
+ * Cancellation includes message-level cancellation (e.g., timeout) and partition shutdown. During shutdown, cancellation is delayed until near the end of the shutdown timeout to allow in-flight work to complete.
54
+ *
55
+ * @returns True if cancellation was requested, otherwise false.
56
+ */
57
+ get shouldCancel(): boolean;
58
+
59
+ /**
60
+ * Waits for a cancellation signal.
61
+ * Cancellation includes message-level cancellation (e.g., timeout) and partition shutdown. During shutdown, cancellation is delayed until near the end of the shutdown timeout to allow in-flight work to complete.
62
+ *
63
+ * @returns A promise that resolves when cancellation is signaled.
64
+ */
65
+ onCancel(): Promise<void>;
66
+
67
+ /**
68
+ * Schedule a timer at the given time.
69
+ *
70
+ * @param time - The UTC timestamp to schedule.
71
+ * @returns A promise that resolves when the timer has been scheduled.
72
+ * @throws Error if time conversion or scheduling fails.
73
+ */
74
+ schedule(time: Date): Promise<void>;
75
+
76
+ /**
77
+ * Clear existing timers and schedule a new one at the given time.
78
+ *
79
+ * @param time - The UTC timestamp to schedule.
80
+ * @returns A promise that resolves when the timer has been scheduled.
81
+ * @throws Error if time conversion or scheduling fails.
82
+ */
83
+ clearAndSchedule(time: Date): Promise<void>;
84
+
85
+ /**
86
+ * Unschedules the timer for the specified time.
87
+ * @param time - The time to unschedule.
88
+ * @returns A promise that resolves when the timer has been unscheduled.
89
+ * @throws Error if unscheduling fails.
90
+ */
91
+ unschedule(time: Date): Promise<void>;
92
+
93
+ /**
94
+ * Clears all scheduled timers.
95
+ * @returns A promise that resolves when all timers have been cleared.
96
+ * @throws Error if clearing schedules fails.
97
+ */
98
+ clearScheduled(): Promise<void>;
99
+
100
+ /**
101
+ * Retrieves all scheduled times.
102
+ * @returns An array of scheduled times as Date objects.
103
+ * @throws Error if retrieval fails.
104
+ */
105
+ scheduled(): Promise<Array<Date>>;
106
+
107
+ /**
108
+ * Binds a registered message single-value collection, vending a handle whose
109
+ * item is the full `Message<P>`.
110
+ *
111
+ * The handle and any iterator it opens are valid only within this event
112
+ * attempt. Throws a {@link PermanentStateError} if the collection name is
113
+ * unregistered or its registered identity mismatches.
114
+ * @param definition - A definition from {@link messageValue}.
115
+ */
116
+ state<P>(definition: MessageValueDefinition<P>): ValueState<Message<P>>;
117
+ /**
118
+ * Binds a registered message ordered-map collection, vending a handle whose
119
+ * values are the full `Message<P>`. Valid only within this event attempt;
120
+ * throws {@link PermanentStateError} on an unregistered name or identity
121
+ * mismatch.
122
+ * @param definition - A definition from {@link messageMap}.
123
+ */
124
+ state<P>(definition: MessageMapDefinition<P>): MapState<Message<P>>;
125
+ /**
126
+ * Binds a registered message deque collection, vending a handle whose
127
+ * elements are the full `Message<P>`. Valid only within this event attempt;
128
+ * throws {@link PermanentStateError} on an unregistered name or identity
129
+ * mismatch.
130
+ * @param definition - A definition from {@link messageDeque}.
131
+ */
132
+ state<P>(definition: MessageDequeDefinition<P>): DequeState<Message<P>>;
133
+ /**
134
+ * Binds a registered single-value JSON collection. Valid only within this
135
+ * event attempt; throws {@link PermanentStateError} on an unregistered name
136
+ * or identity mismatch.
137
+ * @param definition - A definition from {@link value}.
138
+ */
139
+ state<T>(definition: ValueDefinition<T>): ValueState<T>;
140
+ /**
141
+ * Binds a registered ordered-map JSON collection (string keys). Valid only
142
+ * within this event attempt; throws {@link PermanentStateError} on an
143
+ * unregistered name or identity mismatch.
144
+ * @param definition - A definition from {@link map}.
145
+ */
146
+ state<V>(definition: MapDefinition<V>): MapState<V>;
147
+ /**
148
+ * Binds a registered set collection of string members. Valid only within
149
+ * this event attempt; throws {@link PermanentStateError} on an unregistered
150
+ * name or identity mismatch.
151
+ * @param definition - A definition from {@link set}.
152
+ */
153
+ state(definition: SetDefinition): SetState;
154
+ /**
155
+ * Binds a registered deque JSON collection. Valid only within this event
156
+ * attempt; throws {@link PermanentStateError} on an unregistered name or
157
+ * identity mismatch.
158
+ * @param definition - A definition from {@link deque}.
159
+ */
160
+ state<T>(definition: DequeDefinition<T>): DequeState<T>;
161
+ }
package/lib/context.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The event context passed to handlers: cancellation, timers, and state.
3
+ * @module lib/context
4
+ * @private
5
+ */
6
+
7
+ const { TransientStateError } = require("./errors");
8
+ const {
9
+ defineHidden,
10
+ injectedCarrier,
11
+ toStateError,
12
+ } = require("./state/bridge");
13
+ const { stateDefinitionAccess } = require("./state/definitions");
14
+
15
+ // The frozen demand of each native context, read on first access. The key is
16
+ // the native context, so the cache also works through a Proxy with a `set`
17
+ // trap. An entry goes away with its native context.
18
+ const demands = new WeakMap();
19
+
20
+ /** The event context that handlers receive. Each call propagates the trace. */
21
+ class Context {
22
+ constructor(nativeContext) {
23
+ // The vended state handles, cached by definition object, so repeated
24
+ // state(definition) calls within one event return the same handle.
25
+ defineHidden(this, { nativeContext, stateHandles: new Map() });
26
+ }
27
+
28
+ get shouldCancel() {
29
+ return this.nativeContext.shouldCancel;
30
+ }
31
+
32
+ get demand() {
33
+ let demand = demands.get(this.nativeContext);
34
+ if (demand === undefined) {
35
+ demand = Object.freeze({ ...this.nativeContext.demand });
36
+ demands.set(this.nativeContext, demand);
37
+ }
38
+ return demand;
39
+ }
40
+
41
+ async onCancel() {
42
+ return this.nativeContext.onCancel();
43
+ }
44
+
45
+ async schedule(time) {
46
+ return this.nativeContext.schedule(time, injectedCarrier());
47
+ }
48
+
49
+ async clearAndSchedule(time) {
50
+ return this.nativeContext.clearAndSchedule(time, injectedCarrier());
51
+ }
52
+
53
+ async unschedule(time) {
54
+ return this.nativeContext.unschedule(time, injectedCarrier());
55
+ }
56
+
57
+ async clearScheduled() {
58
+ return this.nativeContext.clearScheduled(injectedCarrier());
59
+ }
60
+
61
+ async scheduled() {
62
+ return this.nativeContext.scheduled(injectedCarrier());
63
+ }
64
+
65
+ state(definition) {
66
+ const access = stateDefinitionAccess.get(definition);
67
+ if (access === undefined) {
68
+ throw new TransientStateError(
69
+ "state: definition must come from a Prosody state definition constructor",
70
+ );
71
+ }
72
+ const cached = this.stateHandles.get(definition);
73
+ if (cached !== undefined) return cached;
74
+ try {
75
+ const handle = access.owned(this.nativeContext, definition.name);
76
+ this.stateHandles.set(definition, handle);
77
+ return handle;
78
+ } catch (error) {
79
+ throw toStateError(error);
80
+ }
81
+ }
82
+ }
83
+
84
+ module.exports = {
85
+ Context,
86
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The handler error classes, the state error classes, and the error decorators.
3
+ * @module lib/errors
4
+ */
5
+
6
+ /**
7
+ * Base class for event handler errors.
8
+ * Provides a common interface for determining if an error is permanent.
9
+ */
10
+ export abstract class EventHandlerError extends Error {
11
+ /**
12
+ * @param message - The error message.
13
+ * @param options - The standard error options, such as `cause`.
14
+ */
15
+ constructor(message?: string, options?: ErrorOptions);
16
+
17
+ /**
18
+ * Indicates whether the error is permanent and should not be retried.
19
+ */
20
+ abstract get isPermanent(): boolean;
21
+ }
22
+
23
+ /**
24
+ * Represents a transient error that may be resolved by retrying.
25
+ * These errors are temporary and the operation should be retried.
26
+ */
27
+ export class TransientError extends EventHandlerError {
28
+ /**
29
+ * @returns Always false, indicating the error is not permanent.
30
+ */
31
+ get isPermanent(): false;
32
+ }
33
+
34
+ /**
35
+ * Represents a permanent error that should not be retried.
36
+ * These errors indicate unrecoverable failures.
37
+ */
38
+ export class PermanentError extends EventHandlerError {
39
+ /**
40
+ * @returns Always true, indicating the error is permanent.
41
+ */
42
+ get isPermanent(): true;
43
+ }
44
+
45
+ /**
46
+ * A keyed-state failure that a later attempt can resolve.
47
+ *
48
+ * A store timeout is transient. A caller mistake is transient too: an
49
+ * unrepresentable write, a wrong item type, or a bad index. The event then
50
+ * retries, so the mistake stays visible and no message is discarded. State
51
+ * handles and scan iterators throw it. It extends {@link TransientError}, so a
52
+ * handler that rethrows it retries the event.
53
+ */
54
+ export class TransientStateError extends TransientError {}
55
+
56
+ /**
57
+ * A keyed-state failure that no retry in this process can resolve.
58
+ *
59
+ * Examples are an unregistered collection name, a registered identity that
60
+ * does not match, a JSON `null` write, and a stored value that cannot be
61
+ * decoded. A caller mistake is a {@link TransientStateError} instead. A
62
+ * handler can also throw this error to mark its own failure permanent. It extends {@link PermanentError},
63
+ * so a handler that rethrows it does not retry the event.
64
+ */
65
+ export class PermanentStateError extends PermanentError {}
66
+
67
+ /**
68
+ * Checks whether a value is a keyed-state error of either category. It
69
+ * matches the error `name`, so it also recognizes an error from another realm.
70
+ *
71
+ * @param error - The value to test.
72
+ * @returns True when the value is a keyed-state error.
73
+ */
74
+ export function isStateError(
75
+ error: unknown,
76
+ ): error is PermanentStateError | TransientStateError;
77
+
78
+ /** Type alias for a constructor of an Error subclass. */
79
+ type ErrorClass<T extends Error> = new (...args: never[]) => T;
80
+
81
+ /** A decorator for a class method. */
82
+ type DecoratorFunction = (
83
+ target: Function,
84
+ context: ClassMethodDecoratorContext,
85
+ ) => Function | void;
86
+
87
+ /**
88
+ * Decorator factory for marking errors as transient.
89
+ * Apply it to a method. The wrapper keeps the original error as its `cause`.
90
+ * @param exceptionTypes The error types to be treated as transient.
91
+ */
92
+ export declare function transient<E extends Error>(
93
+ ...exceptionTypes: ErrorClass<E>[]
94
+ ): DecoratorFunction;
95
+
96
+ /**
97
+ * Decorator factory for marking errors as permanent.
98
+ * Apply it to a method. The wrapper keeps the original error as its `cause`.
99
+ * @param exceptionTypes The error types to be treated as permanent.
100
+ */
101
+ export declare function permanent<E extends Error>(
102
+ ...exceptionTypes: ErrorClass<E>[]
103
+ ): DecoratorFunction;
package/lib/errors.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The handler error classes, the state error classes, and the error decorators.
3
+ * @module lib/errors
4
+ * @private
5
+ */
6
+
7
+ const { types } = require("node:util");
8
+
9
+ /** Base class for event handler errors. */
10
+ class EventHandlerError extends Error {
11
+ constructor(message, options) {
12
+ super(message, options);
13
+ this.name = this.constructor.name;
14
+ }
15
+
16
+ get isPermanent() {
17
+ throw new Error("Subclasses must implement isPermanent");
18
+ }
19
+ }
20
+
21
+ /** An event handler error that a retry can resolve. */
22
+ class TransientError extends EventHandlerError {
23
+ get isPermanent() {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /** An event handler error that no retry can resolve. */
29
+ class PermanentError extends EventHandlerError {
30
+ get isPermanent() {
31
+ return true;
32
+ }
33
+ }
34
+
35
+ /** A keyed-state failure that a later attempt can resolve. */
36
+ class TransientStateError extends TransientError {}
37
+
38
+ /** A keyed-state failure that no retry in this process can resolve. */
39
+ class PermanentStateError extends PermanentError {}
40
+
41
+ const STATE_ERROR_NAMES = new Set([
42
+ "PermanentStateError",
43
+ "TransientStateError",
44
+ ]);
45
+
46
+ function isStateError(error) {
47
+ return types.isNativeError(error) && STATE_ERROR_NAMES.has(error.name);
48
+ }
49
+
50
+ /**
51
+ * Helper function to create error decorators.
52
+ * @param {Function} ErrorClass - The error class to wrap exceptions with.
53
+ * @returns {Function} A decorator function that wraps specified exceptions.
54
+ * @private
55
+ */
56
+ function createErrorDecorator(ErrorClass) {
57
+ return function decorator(...exceptionTypes) {
58
+ return function (originalMethod, context) {
59
+ if (context.kind !== "method") {
60
+ throw new TypeError(`@${ErrorClass.name} can only decorate methods`);
61
+ }
62
+
63
+ function handleError(error) {
64
+ if (exceptionTypes.some((type) => error instanceof type)) {
65
+ return new ErrorClass(error.message, { cause: error });
66
+ }
67
+ return error;
68
+ }
69
+
70
+ // A method that returns a promise is classified the same way whether
71
+ // or not it is a native async function, because a transpiler can
72
+ // compile an async method into a plain function.
73
+ return function (...args) {
74
+ let result;
75
+ try {
76
+ result = originalMethod.apply(this, args);
77
+ } catch (error) {
78
+ throw handleError(error);
79
+ }
80
+ if (typeof result?.then !== "function") return result;
81
+ return result.then(undefined, (error) => {
82
+ throw handleError(error);
83
+ });
84
+ };
85
+ };
86
+ };
87
+ }
88
+
89
+ const transient = createErrorDecorator(TransientError);
90
+
91
+ const permanent = createErrorDecorator(PermanentError);
92
+
93
+ module.exports = {
94
+ EventHandlerError,
95
+ PermanentError,
96
+ PermanentStateError,
97
+ TransientError,
98
+ TransientStateError,
99
+ isStateError,
100
+ permanent,
101
+ transient,
102
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The logger interface and the logger setters.
3
+ * @module lib/logging
4
+ */
5
+
6
+ /**
7
+ * JavaScript logger interface for use with Prosody client.
8
+ *
9
+ * Each logging method receives a message string and optional metadata object.
10
+ */
11
+ export interface Logger {
12
+ /** Logs error-level messages. */
13
+ error: (
14
+ message: string | undefined | null,
15
+ metadata?: Record<string, unknown>,
16
+ ) => void;
17
+ /** Logs warning-level messages. */
18
+ warn: (
19
+ message: string | undefined | null,
20
+ metadata?: Record<string, unknown>,
21
+ ) => void;
22
+ /** Logs info-level messages. */
23
+ info: (
24
+ message: string | undefined | null,
25
+ metadata?: Record<string, unknown>,
26
+ ) => void;
27
+ /** Logs debug-level messages. */
28
+ debug: (
29
+ message: string | undefined | null,
30
+ metadata?: Record<string, unknown>,
31
+ ) => void;
32
+ /** Logs trace-level messages. */
33
+ trace: (
34
+ message: string | undefined | null,
35
+ metadata?: Record<string, unknown>,
36
+ ) => void;
37
+ }
38
+
39
+ /**
40
+ * Sets a new JavaScript logger for the Prosody client.
41
+ *
42
+ * This function configures the logging system to use the provided JavaScript logger
43
+ * for all log output. The logger must implement all required log levels.
44
+ *
45
+ * @param logger - The JavaScript logger object with error, warn, info, debug, and trace methods.
46
+ * @throws Error if creating the new JavaScript logger fails.
47
+ */
48
+ export function setLogger(logger: Logger): void;
49
+
50
+ /**
51
+ * Checks whether the application set a logger. The default console logger
52
+ * does not count.
53
+ *
54
+ * @returns True after a call to `setLogger` or a successful `setLoggerIfUnset`.
55
+ */
56
+ export function loggerIsSet(): boolean;
57
+
58
+ /**
59
+ * Sets a JavaScript logger only if the application has not set one.
60
+ *
61
+ * A library that embeds Prosody can use it to supply a logger without
62
+ * replacing the logger of the host application.
63
+ *
64
+ * @param logger - The JavaScript logger object with error, warn, info, debug, and trace methods.
65
+ * @returns True if the logger was set, false if the application already set one.
66
+ * @throws Error if creating the new JavaScript logger fails.
67
+ */
68
+ export function setLoggerIfUnset(logger: Logger): boolean;
69
+
70
+ /**
71
+ * Gets the current logger.
72
+ *
73
+ * @returns The logger that the application set, or the default console logger.
74
+ */
75
+ export function getCurrentLogger(): Logger;
package/lib/logging.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The JavaScript logger bridge and the optional Sentry reporter.
3
+ * @module lib/logging
4
+ * @private
5
+ */
6
+
7
+ const { initialize, setLogger: setLoggerInternal } = require("../bindings");
8
+
9
+ let _sentry = undefined;
10
+ function getSentry() {
11
+ if (_sentry !== undefined) return _sentry;
12
+ _sentry = null;
13
+ if (!process.env.SENTRY_DSN) return null;
14
+ try {
15
+ const Sentry = require("@sentry/node");
16
+ if (!Sentry.isInitialized()) {
17
+ Sentry.init({ dsn: process.env.SENTRY_DSN });
18
+ }
19
+ _sentry = Sentry;
20
+ return Sentry;
21
+ } catch (err) {
22
+ const isMissing =
23
+ err?.code === "MODULE_NOT_FOUND" && err.message?.includes("@sentry/node");
24
+ if (isMissing) {
25
+ getCurrentLogger().error(
26
+ "SENTRY_DSN is set but @sentry/node is not installed. Run: npm install @sentry/node",
27
+ );
28
+ } else {
29
+ getCurrentLogger().warn("Unexpected error loading @sentry/node", err);
30
+ }
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function captureException(error, eventType, context) {
36
+ const Sentry = getSentry();
37
+ if (!Sentry) return;
38
+ Sentry.withScope((scope) => {
39
+ scope.setTag("prosody.event_type", eventType);
40
+ scope.setContext("prosody", context);
41
+ Sentry.captureException(error.cause ?? error);
42
+ });
43
+ }
44
+
45
+ // Passes the metadata to the console only when the caller gives it.
46
+ const write = (log) => (message, metadata) =>
47
+ metadata === undefined ? log(message) : log(message, metadata);
48
+
49
+ const defaultLogger = {
50
+ error: write(console.error),
51
+ warn: write(console.warn),
52
+ info: write(console.info),
53
+ debug: write(console.debug),
54
+ trace: write(console.debug),
55
+ };
56
+
57
+ // The logger that handlers use. It is the default console logger until the
58
+ // application sets one.
59
+ let currentLogger = defaultLogger;
60
+
61
+ function transformLogger(logger) {
62
+ return {
63
+ info: ([msg, meta]) => logger.info(msg, meta),
64
+ error: ([msg, meta]) => logger.error(msg, meta),
65
+ debug: ([msg, meta]) => logger.debug(msg, meta),
66
+ warn: ([msg, meta]) => logger.warn(msg, meta),
67
+ trace: ([msg, meta]) => logger.trace(msg, meta),
68
+ };
69
+ }
70
+
71
+ function getCurrentLogger() {
72
+ return currentLogger;
73
+ }
74
+
75
+ initialize();
76
+ setLoggerInternal(transformLogger(defaultLogger));
77
+
78
+ function setLogger(logger) {
79
+ currentLogger = logger;
80
+ setLoggerInternal(transformLogger(logger));
81
+ }
82
+
83
+ function loggerIsSet() {
84
+ return currentLogger !== defaultLogger;
85
+ }
86
+
87
+ function setLoggerIfUnset(logger) {
88
+ if (loggerIsSet()) return false;
89
+ setLogger(logger);
90
+ return true;
91
+ }
92
+
93
+ module.exports = {
94
+ captureException,
95
+ getCurrentLogger,
96
+ loggerIsSet,
97
+ setLogger,
98
+ setLoggerIfUnset,
99
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * JSON payload types and the message types that carry them.
3
+ * @module lib/payload
4
+ */
5
+
6
+ import type {
7
+ ExciseMessage as NativeExciseMessage,
8
+ Message as NativeMessage,
9
+ } from "../bindings";
10
+
11
+ /** A primitive value representable in JSON. */
12
+ export type JsonPrimitive = null | boolean | number | string;
13
+
14
+ /**
15
+ * Any value representable in JSON.
16
+ *
17
+ * This is a compile-time contract. Prosody serializes payloads at runtime but
18
+ * does not validate them against a more specific application payload type.
19
+ */
20
+ export type JsonValue =
21
+ JsonPrimitive | readonly JsonValue[] | { readonly [key: string]: JsonValue };
22
+
23
+ /**
24
+ * Maps an application type to its JSON-compatible shape. This lets APIs
25
+ * accept ordinary interfaces and type aliases without requiring an index
26
+ * signature, while rejecting non-JSON members wherever they are nested.
27
+ */
28
+ export type JsonCompatible<T> = T extends JsonPrimitive
29
+ ? T
30
+ : T extends bigint | symbol | undefined | ((...args: never[]) => unknown)
31
+ ? never
32
+ : T extends readonly (infer Item)[]
33
+ ? readonly JsonCompatible<Item>[]
34
+ : T extends object
35
+ ? { readonly [Key in keyof T]: JsonCompatible<T[Key]> }
36
+ : never;
37
+
38
+ /** A value that a handler can return now or through a promise. */
39
+ export type MaybePromise<T> = T | PromiseLike<T>;
40
+
41
+ /**
42
+ * Represents a message consumed from a Kafka topic.
43
+ *
44
+ * The optional payload type parameter is annotation-level only: the runtime
45
+ * payload is unchanged. An unparameterized `Message` uses {@link JsonValue};
46
+ * provide an application payload type for precise field-level checking.
47
+ * Message-backed keyed-state collections vend their items as `Message<P>`.
48
+ *
49
+ * A message can be stored into a message collection, whether it arrived from the
50
+ * topic or was read back out of a collection. What is stored is the message
51
+ * itself, so mutating its parsed `payload` does not change what is written.
52
+ */
53
+ export interface Message<P = JsonValue> extends Omit<NativeMessage, "payload"> {
54
+ /** The message payload as a JSON-serializable value. */
55
+ payload: P;
56
+ }
57
+
58
+ /** An excise record with Kafka metadata and no payload. */
59
+ export type ExciseMessage = NativeExciseMessage;