@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,103 @@
1
+ /**
2
+ * Calls into native code: trace carriers, error categories, value rendering,
3
+ * and the hidden native handle properties.
4
+ * @module lib/state/bridge
5
+ * @private
6
+ */
7
+
8
+ const { types } = require("node:util");
9
+ const { context: otelContext, propagation } = require("@opentelemetry/api");
10
+ const { PermanentStateError, TransientStateError } = require("../errors");
11
+
12
+ /**
13
+ * Injects the active OpenTelemetry context into a fresh carrier for one native
14
+ * operation. Native glue activates the carrier without recording a binding
15
+ * span, so the core semantic operation span joins the JavaScript trace directly.
16
+ * @returns {Record<string, string>} The populated carrier.
17
+ * @private
18
+ */
19
+ function injectedCarrier() {
20
+ const carrier = {};
21
+ propagation.inject(otelContext.active(), carrier);
22
+ return carrier;
23
+ }
24
+
25
+ /**
26
+ * Converts a category-tagged native state error into the matching typed state
27
+ * error. The native layer sets the error's `cause` to an error whose message
28
+ * is exactly `"permanent"` or `"transient"`. The human message is never
29
+ * parsed. An untagged error, such as an argument conversion `TypeError`,
30
+ * passes through unchanged.
31
+ *
32
+ * The checks use `util.types.isNativeError`, which works across realms. A napi
33
+ * error comes from the addon's realm. Under a vm context, a worker thread, or a
34
+ * second module copy, `instanceof Error` is false for it.
35
+ * @param {unknown} error - The error thrown by the native layer.
36
+ * @returns {unknown} The typed state error, or the original error if untagged.
37
+ * @private
38
+ */
39
+ function toStateError(error) {
40
+ if (!types.isNativeError(error)) return error;
41
+ const cause = error.cause;
42
+ const category = types.isNativeError(cause) ? cause.message : undefined;
43
+ if (category !== "permanent" && category !== "transient") return error;
44
+ const StateError =
45
+ category === "permanent" ? PermanentStateError : TransientStateError;
46
+ return new StateError(error.message, { cause: error });
47
+ }
48
+
49
+ /**
50
+ * Renders a value for a diagnostic message without ever throwing.
51
+ * `JSON.stringify` throws on a BigInt or a cycle, and a template literal throws
52
+ * on a Symbol. A raw `TypeError` from either would skip the state error
53
+ * classes. The function returns the value's `typeof` when it cannot serialize
54
+ * the value.
55
+ * @param {*} value - The value to describe.
56
+ * @returns {string} A safe, human-readable rendering.
57
+ * @private
58
+ */
59
+ function describeValue(value) {
60
+ try {
61
+ const rendered = JSON.stringify(value);
62
+ return rendered === undefined ? typeof value : rendered;
63
+ } catch {
64
+ return typeof value;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Runs one async native state operation with a fresh carrier, translating a
70
+ * category-tagged failure into the matching typed state error.
71
+ * @param {(carrier: Record<string, string>) => Promise<*>} operation - The native call.
72
+ * @returns {Promise<*>} The operation's resolved value.
73
+ * @private
74
+ */
75
+ async function stateOp(operation) {
76
+ try {
77
+ return await operation(injectedCarrier());
78
+ } catch (error) {
79
+ throw toStateError(error);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Stores internal fields on a wrapper as writable properties that do not
85
+ * enumerate, so `console.log` shows no internals. A data property also reads
86
+ * correctly when a getter or method runs with a Proxy as `this`.
87
+ * @param {object} object - The wrapper object.
88
+ * @param {Record<string, *>} fields - The internal fields and their values.
89
+ * @private
90
+ */
91
+ function defineHidden(object, fields) {
92
+ for (const [name, value] of Object.entries(fields)) {
93
+ Object.defineProperty(object, name, { value, writable: true });
94
+ }
95
+ }
96
+
97
+ module.exports = {
98
+ defineHidden,
99
+ describeValue,
100
+ injectedCarrier,
101
+ stateOp,
102
+ toStateError,
103
+ };
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Encodes and decodes the items of JSON and message collections.
3
+ * @module lib/state/codec
4
+ * @private
5
+ */
6
+
7
+ const { Message: NativeMessage } = require("../../bindings");
8
+ const {
9
+ PermanentError,
10
+ PermanentStateError,
11
+ TransientStateError,
12
+ } = require("../errors");
13
+
14
+ /**
15
+ * The native `payload` getter, which answers the raw JSON text read from the
16
+ * wire. Captured before {@link withParsedPayload} shadows it per instance.
17
+ * @private
18
+ */
19
+ const rawPayload = Object.getOwnPropertyDescriptor(
20
+ NativeMessage.prototype,
21
+ "payload",
22
+ ).get;
23
+
24
+ /**
25
+ * Replaces a message's raw-text `payload` with the parsed document.
26
+ *
27
+ * Rust hands the payload across as the bytes it read from the wire, never
28
+ * parsing them; the parse happens here, on first read, and the result is kept.
29
+ * A handler that only inspects metadata therefore never pays for one.
30
+ *
31
+ * A payload that is not JSON raises a permanent error: no retry makes bytes
32
+ * parse. Reading `payload` is where that surfaces, since nothing before it
33
+ * looks at the document. That outcome is kept too, so a handler reading a bad
34
+ * payload twice re-raises rather than re-parsing.
35
+ * @param {object} message - The native message.
36
+ * @returns {object} The same message, with `payload` parsed on demand.
37
+ * @private
38
+ */
39
+ function withParsedPayload(message) {
40
+ let document;
41
+ let failure;
42
+ let parsed = false;
43
+ // Not enumerable, matching the five native getters: none of a message's
44
+ // fields are own properties, so spread, `Object.keys`, and `JSON.stringify`
45
+ // see none of them. An enumerable own `payload` would make `JSON.stringify`
46
+ // of a message serialize the whole parsed document.
47
+ Object.defineProperty(message, "payload", {
48
+ configurable: true,
49
+ enumerable: false,
50
+ get() {
51
+ if (!parsed) {
52
+ // The native read fails for bytes that are not UTF-8, and JSON.parse
53
+ // fails for text that is not JSON. Both are the same permanent error.
54
+ try {
55
+ document = JSON.parse(rawPayload.call(this));
56
+ } catch (error) {
57
+ failure = new PermanentError(
58
+ `message payload is not JSON: ${error.message}`,
59
+ { cause: error },
60
+ );
61
+ }
62
+ parsed = true;
63
+ }
64
+ if (failure !== undefined) throw failure;
65
+ return document;
66
+ },
67
+ });
68
+ return message;
69
+ }
70
+
71
+ /**
72
+ * Serializes a JSON value.
73
+ *
74
+ * It returns `undefined` for a function, a symbol, or `undefined` itself. It
75
+ * throws for a BigInt or a cycle.
76
+ * @param {*} value - The value to serialize.
77
+ * @param {Function} ErrorClass - The error to raise on failure.
78
+ * @returns {string} The JSON text.
79
+ * @private
80
+ */
81
+ function toJson(value, ErrorClass) {
82
+ let json;
83
+ try {
84
+ json = JSON.stringify(value);
85
+ } catch (error) {
86
+ throw new ErrorClass(
87
+ `value is not representable as JSON: ${error.message}`,
88
+ );
89
+ }
90
+ if (json === undefined) {
91
+ throw new ErrorClass(
92
+ "value is not representable as JSON (functions, symbols, and " +
93
+ "`undefined` have no JSON form)",
94
+ );
95
+ }
96
+ return json;
97
+ }
98
+
99
+ /**
100
+ * Serializes a value for a JSON collection.
101
+ *
102
+ * A collection stores whatever JSON it is given, so the only rejections here
103
+ * are values with no JSON form at all. Both are caller mistakes and reject
104
+ * transient: the event retries and the mistake stays visible rather than
105
+ * discarding the message.
106
+ *
107
+ * Everything else follows `JSON.stringify`, which is now the serializer of
108
+ * record. Inside a container a function-valued property is dropped and an
109
+ * `undefined` element becomes `null`. `NaN` and the infinities become `null`
110
+ * anywhere they appear. A `toJSON` method decides what its value serializes to.
111
+ * The previous Rust-side conversion rejected each of those instead.
112
+ * @param {*} value - The value to store.
113
+ * @returns {string} The document's JSON text.
114
+ * @throws {TransientStateError} If the value has no JSON representation.
115
+ * @private
116
+ */
117
+ function encodeJson(value) {
118
+ if (value instanceof NativeMessage) {
119
+ throw new TransientStateError(
120
+ "a Kafka message cannot be stored in a JSON collection; declare it with " +
121
+ "messageValue/messageMap/messageDeque instead",
122
+ );
123
+ }
124
+ return toJson(value, TransientStateError);
125
+ }
126
+
127
+ /**
128
+ * Reads the event metadata a payload carries.
129
+ *
130
+ * When the payload is an object with a string `id` or `type`, prosody uses
131
+ * them. Anything else carries no metadata. Each field is read once, so a getter
132
+ * that answers differently on a second read cannot make the metadata disagree
133
+ * with itself.
134
+ * @param {*} payload - The payload about to be sent.
135
+ * @returns {{eventId?: string, eventType?: string}} The metadata.
136
+ * @private
137
+ */
138
+ function eventMetadata(payload) {
139
+ const id = payload?.id;
140
+ const type = payload?.type;
141
+ return {
142
+ eventId: typeof id === "string" ? id : undefined,
143
+ eventType: typeof type === "string" ? type : undefined,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Checks that a value for a message collection is a real Kafka message.
149
+ *
150
+ * A message collection stores the Kafka message itself, so it accepts only a
151
+ * `Message` — an object merely shaped like one is rejected. A message read back
152
+ * out of a collection is a `Message` and stores fine.
153
+ * What it stores is the message's own wire bytes, so assigning to a message's
154
+ * fields, or mutating its parsed `payload`, does not change what is written.
155
+ * @param {*} value - The value to store.
156
+ * @returns {object} The message.
157
+ * @throws {TransientStateError} If the value is not a Kafka message.
158
+ * @private
159
+ */
160
+ function requireMessage(value) {
161
+ if (!(value instanceof NativeMessage)) {
162
+ throw new TransientStateError(
163
+ "expected a Kafka message; a JSON value cannot be stored in a message collection",
164
+ );
165
+ }
166
+ return value;
167
+ }
168
+
169
+ /**
170
+ * Builds the item codec for one collection payload type.
171
+ *
172
+ * The native handle has one payload type. `encode` prepares an item for a
173
+ * write and rejects an item of the other type.
174
+ * @param {(item: *) => *} encode - Prepares an item for a write.
175
+ * @param {(item: *) => *} decode - Turns a read item into what the caller asked for.
176
+ * @returns {Readonly<object>} The frozen codec.
177
+ * @private
178
+ */
179
+ function itemCodec(encode, decode) {
180
+ return Object.freeze({
181
+ encode,
182
+ decode: (item) => (item === null ? null : decode(item)),
183
+ });
184
+ }
185
+
186
+ /** JSON documents cross as their text. @private */
187
+ const jsonItems = itemCodec(encodeJson, (text) => {
188
+ try {
189
+ return JSON.parse(text);
190
+ } catch (error) {
191
+ throw new PermanentStateError(
192
+ `stored JSON document could not be parsed: ${error.message}`,
193
+ );
194
+ }
195
+ });
196
+
197
+ /** Kafka messages cross as the `Message` object itself. @private */
198
+ const messageItems = itemCodec(requireMessage, withParsedPayload);
199
+
200
+ module.exports = {
201
+ eventMetadata,
202
+ jsonItems,
203
+ messageItems,
204
+ toJson,
205
+ withParsedPayload,
206
+ };
@@ -0,0 +1,233 @@
1
+ /**
2
+ * The keyed-state definition types, their options, and their constructors.
3
+ * @module lib/state/definitions
4
+ */
5
+
6
+ import type { JsonValue } from "../payload";
7
+ import type { ReadCacheOptions } from "./published";
8
+
9
+ /** Options accepted by every keyed-state definition constructor. */
10
+ export interface StateDefinitionOptions {
11
+ /**
12
+ * Optional per-write TTL in whole seconds. Must be at least 1 and must
13
+ * stay within the Cassandra TTL limit.
14
+ */
15
+ ttlSeconds?: number;
16
+ /**
17
+ * Opt out of transactional staging (read-uncommitted, at-least-once).
18
+ * Defaults to transactional.
19
+ */
20
+ readUncommitted?: boolean;
21
+ }
22
+
23
+ /** Options accepted by JSON collections that may be published. */
24
+ export interface PublishedStateDefinitionOptions extends StateDefinitionOptions {
25
+ /** Allow read-only access from other consumer groups. */
26
+ published?: boolean;
27
+ /** Override caching when this descriptor opens a published reader. */
28
+ readCache?: ReadCacheOptions | false;
29
+ }
30
+
31
+ /** The keyset option of map and set collections. */
32
+ interface KeysetOptions {
33
+ /**
34
+ * Keyset bound for ordered scans (`0..=4096`; default 128 core-side; `0`
35
+ * disables ordered-scan tracking). Map and set collections only.
36
+ */
37
+ keysetLimit?: number;
38
+ }
39
+
40
+ /** The capacity option of deque collections. */
41
+ interface CapacityOptions {
42
+ /**
43
+ * Optional maximum value count, at least 1. Prosody does not store it and it
44
+ * is not part of the collection identity, so a deploy can change it. A push
45
+ * past the bound removes values from the opposite end. A deque above a new,
46
+ * smaller bound keeps its length until the next push trims it.
47
+ */
48
+ capacity?: number;
49
+ }
50
+
51
+ /** Options accepted by the map definition constructors. */
52
+ export interface MapDefinitionOptions
53
+ extends PublishedStateDefinitionOptions, KeysetOptions {}
54
+
55
+ /** Options accepted by the set definition constructor. */
56
+ export type SetDefinitionOptions = MapDefinitionOptions;
57
+
58
+ /** Options accepted by the deque definition constructors. */
59
+ export interface DequeDefinitionOptions
60
+ extends PublishedStateDefinitionOptions, CapacityOptions {}
61
+
62
+ /** Options accepted by message-map definitions, which cannot be published. */
63
+ export interface MessageMapDefinitionOptions
64
+ extends StateDefinitionOptions, KeysetOptions {}
65
+
66
+ /** Options accepted by message-deque definitions, which cannot be published. */
67
+ export interface MessageDequeDefinitionOptions
68
+ extends StateDefinitionOptions, CapacityOptions {}
69
+
70
+ /**
71
+ * Phantom brand carrying a definition's item type. Never present at runtime —
72
+ * it exists only so the item type survives on the frozen definition object and
73
+ * flows into the vended handle.
74
+ */
75
+ declare const StateItem: unique symbol;
76
+ declare const StateDescriptor: unique symbol;
77
+
78
+ interface DefinitionBrand {
79
+ readonly [StateDescriptor]: true;
80
+ }
81
+
82
+ /** A frozen single-value JSON collection definition. */
83
+ export interface ValueDefinition<T = JsonValue>
84
+ extends DefinitionBrand, Readonly<PublishedStateDefinitionOptions> {
85
+ readonly name: string;
86
+ readonly kind: "value";
87
+ readonly payload: "json";
88
+ readonly [StateItem]?: T;
89
+ }
90
+
91
+ /** A frozen ordered-map JSON collection definition (string keys). */
92
+ export interface MapDefinition<V = JsonValue>
93
+ extends DefinitionBrand, Readonly<MapDefinitionOptions> {
94
+ readonly name: string;
95
+ readonly kind: "map";
96
+ readonly payload: "json";
97
+ readonly [StateItem]?: V;
98
+ }
99
+
100
+ /** A frozen set collection definition. A set stores string members only. */
101
+ export interface SetDefinition
102
+ extends DefinitionBrand, Readonly<SetDefinitionOptions> {
103
+ readonly name: string;
104
+ readonly kind: "set";
105
+ }
106
+
107
+ /** A frozen deque JSON collection definition. */
108
+ export interface DequeDefinition<T = JsonValue>
109
+ extends DefinitionBrand, Readonly<DequeDefinitionOptions> {
110
+ readonly name: string;
111
+ readonly kind: "deque";
112
+ readonly payload: "json";
113
+ readonly [StateItem]?: T;
114
+ }
115
+
116
+ /** A frozen single-value message collection definition (items are `Message<P>`). */
117
+ export interface MessageValueDefinition<P = JsonValue>
118
+ extends DefinitionBrand, Readonly<StateDefinitionOptions> {
119
+ readonly name: string;
120
+ readonly kind: "value";
121
+ readonly payload: "message";
122
+ readonly [StateItem]?: P;
123
+ }
124
+
125
+ /** A frozen ordered-map message collection definition (values are `Message<P>`). */
126
+ export interface MessageMapDefinition<P = JsonValue>
127
+ extends DefinitionBrand, Readonly<MessageMapDefinitionOptions> {
128
+ readonly name: string;
129
+ readonly kind: "map";
130
+ readonly payload: "message";
131
+ readonly [StateItem]?: P;
132
+ }
133
+
134
+ /** A frozen deque message collection definition (elements are `Message<P>`). */
135
+ export interface MessageDequeDefinition<P = JsonValue>
136
+ extends DefinitionBrand, Readonly<MessageDequeDefinitionOptions> {
137
+ readonly name: string;
138
+ readonly kind: "deque";
139
+ readonly payload: "message";
140
+ readonly [StateItem]?: P;
141
+ }
142
+
143
+ /**
144
+ * Declares a single-value JSON collection. The returned frozen definition is
145
+ * the single source of typing: place it in `Configuration.stateCollections` to
146
+ * register the collection, and pass it to `Context.state()` to vend a typed
147
+ * handle. The type parameter annotates the stored value and is compile-time
148
+ * only — payloads cross as plain JSON with no runtime validation.
149
+ * @param name - The collection name (unique per client).
150
+ * @param options - Optional retention, transaction, publication, and read-cache
151
+ * settings.
152
+ */
153
+ export function value<T = JsonValue>(
154
+ name: string,
155
+ options?: PublishedStateDefinitionOptions,
156
+ ): ValueDefinition<T>;
157
+
158
+ /**
159
+ * Declares an ordered-map JSON collection. Map keys are always `string`. The
160
+ * returned frozen definition is used both in `Configuration.stateCollections`
161
+ * and with `Context.state()`. The type parameter annotates the stored value
162
+ * (compile-time only).
163
+ * @param name - The collection name (unique per client).
164
+ * @param options - Optional retention, transaction, publication, read-cache,
165
+ * and `keysetLimit` settings.
166
+ */
167
+ export function map<V = JsonValue>(
168
+ name: string,
169
+ options?: MapDefinitionOptions,
170
+ ): MapDefinition<V>;
171
+
172
+ /**
173
+ * Declares a presence-only ordered set of string members. The returned frozen
174
+ * definition is used both in `Configuration.stateCollections` and with
175
+ * `Context.state()`. A set has no payload.
176
+ * @param name - The collection name (unique per client).
177
+ * @param options - Optional retention, transaction, publication, read-cache,
178
+ * and `keysetLimit` settings.
179
+ */
180
+ export function set(
181
+ name: string,
182
+ options?: SetDefinitionOptions,
183
+ ): SetDefinition;
184
+
185
+ /**
186
+ * Declares a double-ended-queue JSON collection. The returned frozen definition
187
+ * is used both in `Configuration.stateCollections` and with `Context.state()`.
188
+ * The type parameter annotates the stored element (compile-time only).
189
+ * @param name - The collection name (unique per client).
190
+ * @param options - Optional retention, transaction, publication, read-cache,
191
+ * and `capacity` settings.
192
+ */
193
+ export function deque<T = JsonValue>(
194
+ name: string,
195
+ options?: DequeDefinitionOptions,
196
+ ): DequeDefinition<T>;
197
+
198
+ /**
199
+ * Declares a single-value message collection: each stored item is the full
200
+ * Kafka `Message<P>` the handler received. The type parameter annotates the
201
+ * message payload (compile-time only).
202
+ * @param name - The collection name (unique per client).
203
+ * @param options - Optional `ttlSeconds` (whole seconds) and `readUncommitted`.
204
+ */
205
+ export function messageValue<P = JsonValue>(
206
+ name: string,
207
+ options?: StateDefinitionOptions,
208
+ ): MessageValueDefinition<P>;
209
+
210
+ /**
211
+ * Declares an ordered-map message collection (string keys; values are the full
212
+ * Kafka `Message<P>`). The type parameter annotates the message payload
213
+ * (compile-time only).
214
+ * @param name - The collection name (unique per client).
215
+ * @param options - Optional `ttlSeconds`, `readUncommitted`, and `keysetLimit`.
216
+ */
217
+ export function messageMap<P = JsonValue>(
218
+ name: string,
219
+ options?: MessageMapDefinitionOptions,
220
+ ): MessageMapDefinition<P>;
221
+
222
+ /**
223
+ * Declares a double-ended-queue message collection: each stored element is the
224
+ * full Kafka `Message<P>`. The type parameter annotates the message payload
225
+ * (compile-time only).
226
+ * @param name - The collection name (unique per client).
227
+ * @param options - Optional `ttlSeconds` (whole seconds), `readUncommitted`, and
228
+ * `capacity` (bounded backlog; enforced lazily on push).
229
+ */
230
+ export function messageDeque<P = JsonValue>(
231
+ name: string,
232
+ options?: MessageDequeDefinitionOptions,
233
+ ): MessageDequeDefinition<P>;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The keyed-state definition constructors and the handle and reader factories they carry.
3
+ * @module lib/state/definitions
4
+ * @private
5
+ */
6
+
7
+ const { jsonItems, messageItems } = require("./codec");
8
+ const { DequeState } = require("./deque");
9
+ const { MapState } = require("./map");
10
+ const {
11
+ PublishedDeque,
12
+ PublishedMap,
13
+ PublishedSet,
14
+ PublishedValue,
15
+ } = require("./published");
16
+ const { SetState } = require("./set");
17
+ const { ValueState } = require("./value");
18
+
19
+ /**
20
+ * Maps each frozen definition to the factories that open its owned handle and
21
+ * its published reader. Only the definition constructors add entries.
22
+ * @private
23
+ */
24
+ const stateDefinitionAccess = new WeakMap();
25
+ const VALUE_ACCESS = Object.freeze({
26
+ owned: (context, collection) =>
27
+ new ValueState(context.valueState(collection), jsonItems),
28
+ published: async (client, subsystem, collection, readCache) =>
29
+ new PublishedValue(
30
+ await client.publishedValue(subsystem, collection, readCache),
31
+ ),
32
+ });
33
+ const MAP_ACCESS = Object.freeze({
34
+ owned: (context, collection) =>
35
+ new MapState(context.mapState(collection), jsonItems),
36
+ published: async (client, subsystem, collection, readCache) =>
37
+ new PublishedMap(
38
+ await client.publishedMap(subsystem, collection, readCache),
39
+ ),
40
+ });
41
+ const SET_ACCESS = Object.freeze({
42
+ owned: (context, collection) => new SetState(context.setState(collection)),
43
+ published: async (client, subsystem, collection, readCache) =>
44
+ new PublishedSet(
45
+ await client.publishedSet(subsystem, collection, readCache),
46
+ ),
47
+ });
48
+ const DEQUE_ACCESS = Object.freeze({
49
+ owned: (context, collection) =>
50
+ new DequeState(context.dequeState(collection), jsonItems),
51
+ published: async (client, subsystem, collection, readCache) =>
52
+ new PublishedDeque(
53
+ await client.publishedDeque(subsystem, collection, readCache),
54
+ ),
55
+ });
56
+ const MESSAGE_VALUE_ACCESS = Object.freeze({
57
+ owned: (context, collection) =>
58
+ new ValueState(context.messageValueState(collection), messageItems),
59
+ });
60
+ const MESSAGE_MAP_ACCESS = Object.freeze({
61
+ owned: (context, collection) =>
62
+ new MapState(context.messageMapState(collection), messageItems),
63
+ });
64
+ const MESSAGE_DEQUE_ACCESS = Object.freeze({
65
+ owned: (context, collection) =>
66
+ new DequeState(context.messageDequeState(collection), messageItems),
67
+ });
68
+
69
+ /**
70
+ * Builds a frozen state-collection definition. The definition is the single
71
+ * source of typing: the same frozen object is placed in
72
+ * `Configuration.stateCollections` (so the collection is registered once) and
73
+ * passed to `Context.state()` to vend a typed handle. Validation (name/ttl/
74
+ * keyset rules, duplicate names) is core-owned and happens at client
75
+ * construction and registration — this layer only shapes and freezes.
76
+ * @param {string} name - The collection name.
77
+ * @param {string} kind - `"value"`, `"map"`, `"set"`, or `"deque"`.
78
+ * @param {string|undefined} payload - `"json"` or `"message"`; a set has none.
79
+ * @param {Readonly<object>} access - The factories that open the owned handle
80
+ * and, for a JSON or set collection, the published reader.
81
+ * @param {object} [options] - The definition options of the kind.
82
+ * @returns {Readonly<object>} The frozen definition.
83
+ * @private
84
+ */
85
+ function stateDefinition(name, kind, payload, access, options = {}) {
86
+ const definition = { name, kind };
87
+ if (payload !== undefined) definition.payload = payload;
88
+ if (options.ttlSeconds !== undefined)
89
+ definition.ttlSeconds = options.ttlSeconds;
90
+ if (options.readUncommitted !== undefined)
91
+ definition.readUncommitted = options.readUncommitted;
92
+ if (options.published !== undefined) definition.published = options.published;
93
+ if (options.readCache !== undefined) definition.readCache = options.readCache;
94
+ if (options.keysetLimit !== undefined)
95
+ definition.keysetLimit = options.keysetLimit;
96
+ if (options.capacity !== undefined) definition.capacity = options.capacity;
97
+ const frozen = Object.freeze(definition);
98
+ stateDefinitionAccess.set(frozen, access);
99
+ return frozen;
100
+ }
101
+
102
+ function value(name, options) {
103
+ return stateDefinition(name, "value", "json", VALUE_ACCESS, options);
104
+ }
105
+
106
+ function map(name, options) {
107
+ return stateDefinition(name, "map", "json", MAP_ACCESS, options);
108
+ }
109
+
110
+ function set(name, options) {
111
+ return stateDefinition(name, "set", undefined, SET_ACCESS, options);
112
+ }
113
+
114
+ function deque(name, options) {
115
+ return stateDefinition(name, "deque", "json", DEQUE_ACCESS, options);
116
+ }
117
+
118
+ function messageValue(name, options) {
119
+ return stateDefinition(
120
+ name,
121
+ "value",
122
+ "message",
123
+ MESSAGE_VALUE_ACCESS,
124
+ options,
125
+ );
126
+ }
127
+
128
+ function messageMap(name, options) {
129
+ return stateDefinition(name, "map", "message", MESSAGE_MAP_ACCESS, options);
130
+ }
131
+
132
+ function messageDeque(name, options) {
133
+ return stateDefinition(
134
+ name,
135
+ "deque",
136
+ "message",
137
+ MESSAGE_DEQUE_ACCESS,
138
+ options,
139
+ );
140
+ }
141
+
142
+ module.exports = {
143
+ deque,
144
+ map,
145
+ messageDeque,
146
+ messageMap,
147
+ messageValue,
148
+ set,
149
+ stateDefinitionAccess,
150
+ value,
151
+ };