@net-mesh/sdk 0.19.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.
package/dist/node.d.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * NetNode — the main SDK handle.
3
+ *
4
+ * Every computer, device, and application is a NetNode.
5
+ * There are no clients, no servers, no coordinators.
6
+ */
7
+ import { Net as NapiNet } from '@net-mesh/core';
8
+ import type { NetNodeConfig, Receipt, PollRequest, PollResponseData, Stats, SubscribeOpts, StoredEvent } from './types';
9
+ import { EventStream, TypedEventStream } from './stream';
10
+ import { TypedChannel } from './channel';
11
+ /**
12
+ * A node on the Net mesh.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const node = await NetNode.create({ shards: 4 });
17
+ *
18
+ * node.emit({ token: 'hello', index: 0 });
19
+ *
20
+ * for await (const event of node.subscribe()) {
21
+ * console.log(event.raw);
22
+ * }
23
+ *
24
+ * await node.shutdown();
25
+ * ```
26
+ */
27
+ export declare class NetNode {
28
+ private bus;
29
+ private constructor();
30
+ /**
31
+ * Create a new NetNode.
32
+ */
33
+ static create(config?: NetNodeConfig): Promise<NetNode>;
34
+ /**
35
+ * Create a NetNode from an existing NAPI Net instance.
36
+ */
37
+ static fromNapi(bus: NapiNet): NetNode;
38
+ /**
39
+ * Emit a typed event (serializes to JSON).
40
+ */
41
+ emit(event: object): Receipt | null;
42
+ /**
43
+ * Emit a raw JSON string.
44
+ */
45
+ emitRaw(json: string): Receipt | null;
46
+ /**
47
+ * Emit a raw Buffer (fastest path, zero-copy to Rust).
48
+ */
49
+ emitBuffer(data: Buffer): boolean;
50
+ /**
51
+ * Emit a batch of typed events. Returns number ingested.
52
+ */
53
+ emitBatch(events: object[]): number;
54
+ /**
55
+ * Emit a batch of raw JSON strings. Returns number ingested.
56
+ */
57
+ emitRawBatch(jsons: string[]): number;
58
+ /**
59
+ * Fire-and-forget ingestion (no return value, maximum speed).
60
+ */
61
+ fire(json: string): boolean;
62
+ /**
63
+ * Fire-and-forget batch ingestion. Returns count ingested.
64
+ */
65
+ fireBatch(jsons: string[]): number;
66
+ /**
67
+ * One-shot poll for events.
68
+ */
69
+ poll(request: PollRequest): Promise<PollResponseData>;
70
+ /**
71
+ * Poll a single event (convenience).
72
+ */
73
+ pollOne(): Promise<StoredEvent | null>;
74
+ /**
75
+ * Subscribe to an async stream of events.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * for await (const event of node.subscribe({ limit: 100 })) {
80
+ * console.log(event.raw);
81
+ * }
82
+ * ```
83
+ */
84
+ subscribe(opts?: SubscribeOpts): EventStream;
85
+ /**
86
+ * Subscribe to a typed stream of events.
87
+ *
88
+ * Each event is automatically deserialized from JSON.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * for await (const token of node.subscribeTyped<TokenEvent>()) {
93
+ * console.log(token.token, token.index);
94
+ * }
95
+ * ```
96
+ */
97
+ subscribeTyped<T>(opts?: SubscribeOpts): TypedEventStream<T>;
98
+ /**
99
+ * Create a typed channel for pub/sub.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const temps = node.channel<TemperatureReading>('sensors/temperature');
104
+ * temps.publish({ sensor_id: 'A1', celsius: 22.5, timestamp: Date.now() });
105
+ * ```
106
+ */
107
+ channel<T>(name: string, validator?: (data: unknown) => T): TypedChannel<T>;
108
+ /** Get ingestion statistics. */
109
+ stats(): Stats;
110
+ /** Get the number of active shards. */
111
+ shards(): number;
112
+ /** Flush all pending batches to the adapter. */
113
+ flush(): Promise<void>;
114
+ /** Gracefully shut down the node. */
115
+ shutdown(): Promise<void>;
116
+ /**
117
+ * Get the underlying NAPI binding (escape hatch).
118
+ */
119
+ get napi(): NapiNet;
120
+ }
package/dist/node.js ADDED
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * NetNode — the main SDK handle.
4
+ *
5
+ * Every computer, device, and application is a NetNode.
6
+ * There are no clients, no servers, no coordinators.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.NetNode = void 0;
10
+ const core_1 = require("@net-mesh/core");
11
+ const stream_1 = require("./stream");
12
+ const channel_1 = require("./channel");
13
+ /**
14
+ * A node on the Net mesh.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const node = await NetNode.create({ shards: 4 });
19
+ *
20
+ * node.emit({ token: 'hello', index: 0 });
21
+ *
22
+ * for await (const event of node.subscribe()) {
23
+ * console.log(event.raw);
24
+ * }
25
+ *
26
+ * await node.shutdown();
27
+ * ```
28
+ */
29
+ class NetNode {
30
+ bus;
31
+ constructor(bus) {
32
+ this.bus = bus;
33
+ }
34
+ /**
35
+ * Create a new NetNode.
36
+ */
37
+ static async create(config = {}) {
38
+ const options = buildOptions(config);
39
+ const bus = await core_1.Net.create(options);
40
+ return new NetNode(bus);
41
+ }
42
+ /**
43
+ * Create a NetNode from an existing NAPI Net instance.
44
+ */
45
+ static fromNapi(bus) {
46
+ return new NetNode(bus);
47
+ }
48
+ // ---- Ingestion ----
49
+ /**
50
+ * Emit a typed event (serializes to JSON).
51
+ */
52
+ emit(event) {
53
+ const json = JSON.stringify(event);
54
+ const result = this.bus.ingestRawSync(json);
55
+ return { shardId: result.shardId, timestamp: result.timestamp };
56
+ }
57
+ /**
58
+ * Emit a raw JSON string.
59
+ */
60
+ emitRaw(json) {
61
+ const result = this.bus.ingestRawSync(json);
62
+ return { shardId: result.shardId, timestamp: result.timestamp };
63
+ }
64
+ /**
65
+ * Emit a raw Buffer (fastest path, zero-copy to Rust).
66
+ */
67
+ emitBuffer(data) {
68
+ return this.bus.push(data);
69
+ }
70
+ /**
71
+ * Emit a batch of typed events. Returns number ingested.
72
+ */
73
+ emitBatch(events) {
74
+ const jsons = events.map((e) => JSON.stringify(e));
75
+ return this.bus.ingestRawBatchSync(jsons);
76
+ }
77
+ /**
78
+ * Emit a batch of raw JSON strings. Returns number ingested.
79
+ */
80
+ emitRawBatch(jsons) {
81
+ return this.bus.ingestRawBatchSync(jsons);
82
+ }
83
+ /**
84
+ * Fire-and-forget ingestion (no return value, maximum speed).
85
+ */
86
+ fire(json) {
87
+ return this.bus.ingestFire(json);
88
+ }
89
+ /**
90
+ * Fire-and-forget batch ingestion. Returns count ingested.
91
+ */
92
+ fireBatch(jsons) {
93
+ return this.bus.ingestBatchFire(jsons);
94
+ }
95
+ // ---- Consumption ----
96
+ /**
97
+ * One-shot poll for events.
98
+ */
99
+ async poll(request) {
100
+ const response = await this.bus.poll({
101
+ limit: request.limit,
102
+ cursor: request.cursor,
103
+ filter: request.filter,
104
+ ordering: request.ordering,
105
+ });
106
+ return {
107
+ events: response.events.map((e) => ({
108
+ id: e.id,
109
+ raw: e.raw,
110
+ insertionTs: e.insertionTs,
111
+ shardId: e.shardId,
112
+ })),
113
+ nextId: response.nextId ?? undefined,
114
+ hasMore: response.hasMore,
115
+ };
116
+ }
117
+ /**
118
+ * Poll a single event (convenience).
119
+ */
120
+ async pollOne() {
121
+ const response = await this.poll({ limit: 1 });
122
+ return response.events[0] ?? null;
123
+ }
124
+ /**
125
+ * Subscribe to an async stream of events.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * for await (const event of node.subscribe({ limit: 100 })) {
130
+ * console.log(event.raw);
131
+ * }
132
+ * ```
133
+ */
134
+ subscribe(opts) {
135
+ return new stream_1.EventStream(this.bus, opts);
136
+ }
137
+ /**
138
+ * Subscribe to a typed stream of events.
139
+ *
140
+ * Each event is automatically deserialized from JSON.
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * for await (const token of node.subscribeTyped<TokenEvent>()) {
145
+ * console.log(token.token, token.index);
146
+ * }
147
+ * ```
148
+ */
149
+ subscribeTyped(opts) {
150
+ return new stream_1.TypedEventStream(this.bus, opts);
151
+ }
152
+ /**
153
+ * Create a typed channel for pub/sub.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const temps = node.channel<TemperatureReading>('sensors/temperature');
158
+ * temps.publish({ sensor_id: 'A1', celsius: 22.5, timestamp: Date.now() });
159
+ * ```
160
+ */
161
+ channel(name, validator) {
162
+ return new channel_1.TypedChannel(this.bus, name, validator);
163
+ }
164
+ // ---- Lifecycle ----
165
+ /** Get ingestion statistics. */
166
+ stats() {
167
+ return this.bus.stats();
168
+ }
169
+ /** Get the number of active shards. */
170
+ shards() {
171
+ return this.bus.numShards();
172
+ }
173
+ /** Flush all pending batches to the adapter. */
174
+ async flush() {
175
+ await this.bus.flush();
176
+ }
177
+ /** Gracefully shut down the node. */
178
+ async shutdown() {
179
+ await this.bus.shutdown();
180
+ }
181
+ /**
182
+ * Get the underlying NAPI binding (escape hatch).
183
+ */
184
+ get napi() {
185
+ return this.bus;
186
+ }
187
+ }
188
+ exports.NetNode = NetNode;
189
+ /** Convert SDK config to NAPI EventBusOptions. */
190
+ function buildOptions(config) {
191
+ const options = {};
192
+ if (config.shards !== undefined)
193
+ options.numShards = config.shards;
194
+ if (config.bufferCapacity !== undefined)
195
+ options.ringBufferCapacity = config.bufferCapacity;
196
+ if (config.backpressure !== undefined)
197
+ options.backpressureMode = config.backpressure;
198
+ if (config.transport) {
199
+ const t = config.transport;
200
+ switch (t.type) {
201
+ case 'memory':
202
+ // No adapter config — uses noop.
203
+ break;
204
+ case 'redis':
205
+ options.redis = {
206
+ url: t.url,
207
+ prefix: t.prefix,
208
+ pipelineSize: t.pipelineSize,
209
+ poolSize: t.poolSize,
210
+ connectTimeoutMs: t.connectTimeoutMs,
211
+ commandTimeoutMs: t.commandTimeoutMs,
212
+ maxStreamLen: t.maxStreamLen,
213
+ };
214
+ break;
215
+ case 'jetstream':
216
+ options.jetstream = {
217
+ url: t.url,
218
+ prefix: t.prefix,
219
+ connectTimeoutMs: t.connectTimeoutMs,
220
+ requestTimeoutMs: t.requestTimeoutMs,
221
+ maxMessages: t.maxMessages,
222
+ maxBytes: t.maxBytes,
223
+ maxAgeMs: t.maxAgeMs,
224
+ replicas: t.replicas,
225
+ };
226
+ break;
227
+ case 'mesh':
228
+ options.net = {
229
+ bindAddr: t.bind,
230
+ peerAddr: t.peer,
231
+ psk: t.psk,
232
+ role: t.role ?? 'initiator',
233
+ peerPublicKey: t.peerPublicKey,
234
+ secretKey: t.secretKey,
235
+ publicKey: t.publicKey,
236
+ reliability: t.reliability,
237
+ heartbeatIntervalMs: t.heartbeatIntervalMs,
238
+ sessionTimeoutMs: t.sessionTimeoutMs,
239
+ batchedIo: t.batchedIo,
240
+ packetPoolSize: t.packetPoolSize,
241
+ };
242
+ break;
243
+ }
244
+ }
245
+ return options;
246
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Redis Streams consumer-side dedup helper — re-export of the NAPI
3
+ * class from `@net-mesh/core`.
4
+ *
5
+ * The Redis adapter writes a stable `dedup_id` field on every XADD
6
+ * entry — see the producer-side contract in `sdk-ts/README.md`.
7
+ * The dedup helper filters producer-retry-induced duplicates at
8
+ * consume time. Default capacity is 4096; production callers should
9
+ * size to roughly `events_per_sec * dedup_window_seconds`.
10
+ *
11
+ * This shim re-exports the underlying NAPI class so users can
12
+ * `import { RedisStreamDedup } from '@net-mesh/sdk'` directly
13
+ * instead of reaching into `@net-mesh/core`.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { RedisStreamDedup } from '@net-mesh/sdk';
18
+ * import { createClient } from 'redis';
19
+ *
20
+ * // Sizing: ~10k events/sec * 1 min dedup window → ~600,000.
21
+ * const dedup = new RedisStreamDedup(600_000);
22
+ *
23
+ * const r = createClient();
24
+ * await r.connect();
25
+ *
26
+ * let cursor = '0';
27
+ * while (true) {
28
+ * // After the first page, use the exclusive form `(<id>` so we
29
+ * // don't re-read the entry the cursor points at.
30
+ * const start = cursor === '0' ? cursor : `(${cursor}`;
31
+ * const entries = await r.xRange('net:shard:0', start, '+', { COUNT: 100 });
32
+ * if (entries.length === 0) break;
33
+ * for (const entry of entries) {
34
+ * // Advance the cursor BEFORE the duplicate check. Skipping
35
+ * // the `cursor = entry.id` assignment on duplicate entries
36
+ * // (via `continue`) makes the loop spin forever re-reading
37
+ * // the same range when a window is full of duplicates.
38
+ * cursor = entry.id;
39
+ * const dedupId = entry.message.dedup_id;
40
+ * if (dedupId && dedup.isDuplicate(dedupId)) continue;
41
+ * await process(entry);
42
+ * }
43
+ * }
44
+ * ```
45
+ *
46
+ * @packageDocumentation
47
+ */
48
+ export { RedisStreamDedup } from '@net-mesh/core';
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * Redis Streams consumer-side dedup helper — re-export of the NAPI
4
+ * class from `@net-mesh/core`.
5
+ *
6
+ * The Redis adapter writes a stable `dedup_id` field on every XADD
7
+ * entry — see the producer-side contract in `sdk-ts/README.md`.
8
+ * The dedup helper filters producer-retry-induced duplicates at
9
+ * consume time. Default capacity is 4096; production callers should
10
+ * size to roughly `events_per_sec * dedup_window_seconds`.
11
+ *
12
+ * This shim re-exports the underlying NAPI class so users can
13
+ * `import { RedisStreamDedup } from '@net-mesh/sdk'` directly
14
+ * instead of reaching into `@net-mesh/core`.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { RedisStreamDedup } from '@net-mesh/sdk';
19
+ * import { createClient } from 'redis';
20
+ *
21
+ * // Sizing: ~10k events/sec * 1 min dedup window → ~600,000.
22
+ * const dedup = new RedisStreamDedup(600_000);
23
+ *
24
+ * const r = createClient();
25
+ * await r.connect();
26
+ *
27
+ * let cursor = '0';
28
+ * while (true) {
29
+ * // After the first page, use the exclusive form `(<id>` so we
30
+ * // don't re-read the entry the cursor points at.
31
+ * const start = cursor === '0' ? cursor : `(${cursor}`;
32
+ * const entries = await r.xRange('net:shard:0', start, '+', { COUNT: 100 });
33
+ * if (entries.length === 0) break;
34
+ * for (const entry of entries) {
35
+ * // Advance the cursor BEFORE the duplicate check. Skipping
36
+ * // the `cursor = entry.id` assignment on duplicate entries
37
+ * // (via `continue`) makes the loop spin forever re-reading
38
+ * // the same range when a window is full of duplicates.
39
+ * cursor = entry.id;
40
+ * const dedupId = entry.message.dedup_id;
41
+ * if (dedupId && dedup.isDuplicate(dedupId)) continue;
42
+ * await process(entry);
43
+ * }
44
+ * }
45
+ * ```
46
+ *
47
+ * @packageDocumentation
48
+ */
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.RedisStreamDedup = void 0;
51
+ var core_1 = require("@net-mesh/core");
52
+ Object.defineProperty(exports, "RedisStreamDedup", { enumerable: true, get: function () { return core_1.RedisStreamDedup; } });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Async streaming event consumption.
3
+ */
4
+ import type { Net as NapiNet } from '@net-mesh/core';
5
+ import type { StoredEvent, SubscribeOpts } from './types';
6
+ /**
7
+ * An async iterable stream of events from the bus.
8
+ *
9
+ * Uses adaptive polling — tight loop when events flow, exponential
10
+ * backoff when idle.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * for await (const event of node.subscribe({ limit: 100 })) {
15
+ * console.log(event.raw);
16
+ * }
17
+ * ```
18
+ */
19
+ export declare class EventStream implements AsyncIterable<StoredEvent> {
20
+ private bus;
21
+ private opts;
22
+ private cursor?;
23
+ private aborted;
24
+ constructor(bus: NapiNet, opts?: SubscribeOpts);
25
+ /** Stop the stream. */
26
+ stop(): void;
27
+ [Symbol.asyncIterator](): AsyncIterableIterator<StoredEvent>;
28
+ }
29
+ /**
30
+ * A typed async iterable stream that deserializes events into `T`.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * interface TokenEvent { token: string; index: number; }
35
+ * for await (const token of node.subscribe<TokenEvent>({ limit: 100 })) {
36
+ * console.log(token.token, token.index);
37
+ * }
38
+ * ```
39
+ */
40
+ export declare class TypedEventStream<T> implements AsyncIterable<T> {
41
+ private inner;
42
+ private parse;
43
+ constructor(bus: NapiNet, opts?: SubscribeOpts, parse?: (raw: string) => T);
44
+ /** Stop the stream. */
45
+ stop(): void;
46
+ [Symbol.asyncIterator](): AsyncIterableIterator<T>;
47
+ }
package/dist/stream.js ADDED
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * Async streaming event consumption.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TypedEventStream = exports.EventStream = void 0;
7
+ // Starting backoff for idle polls and the inter-poll wait on
8
+ // partial-batch responses. `1` would have us re-issue an FFI poll
9
+ // hundreds of times per second on a near-drained stream that returns
10
+ // 1-2 events per call; `5` keeps us at a 200/s ceiling on that path
11
+ // while still feeling instant. Saturated streams (full `limit`
12
+ // batches) skip the sleep entirely and continue to drain at full
13
+ // speed.
14
+ const DEFAULT_POLL_INTERVAL = 5;
15
+ const DEFAULT_MAX_BACKOFF = 100;
16
+ const DEFAULT_LIMIT = 100;
17
+ /**
18
+ * An async iterable stream of events from the bus.
19
+ *
20
+ * Uses adaptive polling — tight loop when events flow, exponential
21
+ * backoff when idle.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * for await (const event of node.subscribe({ limit: 100 })) {
26
+ * console.log(event.raw);
27
+ * }
28
+ * ```
29
+ */
30
+ class EventStream {
31
+ bus;
32
+ opts;
33
+ cursor;
34
+ aborted = false;
35
+ constructor(bus, opts = {}) {
36
+ this.bus = bus;
37
+ this.opts = {
38
+ limit: opts.limit ?? DEFAULT_LIMIT,
39
+ filter: opts.filter ?? '',
40
+ ordering: opts.ordering ?? 'none',
41
+ pollIntervalMs: opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL,
42
+ maxBackoffMs: opts.maxBackoffMs ?? DEFAULT_MAX_BACKOFF,
43
+ };
44
+ }
45
+ /** Stop the stream. */
46
+ stop() {
47
+ this.aborted = true;
48
+ }
49
+ async *[Symbol.asyncIterator]() {
50
+ let backoff = this.opts.pollIntervalMs;
51
+ while (!this.aborted) {
52
+ const response = await this.bus.poll({
53
+ limit: this.opts.limit,
54
+ cursor: this.cursor,
55
+ filter: this.opts.filter || undefined,
56
+ ordering: this.opts.ordering,
57
+ });
58
+ if (response.events.length > 0) {
59
+ backoff = this.opts.pollIntervalMs;
60
+ this.cursor = response.nextId ?? undefined;
61
+ for (const event of response.events) {
62
+ yield {
63
+ id: event.id,
64
+ raw: event.raw,
65
+ insertionTs: event.insertionTs,
66
+ shardId: event.shardId,
67
+ };
68
+ }
69
+ // Partial-batch sleep: a poll that returned fewer than `limit`
70
+ // events has drained (or nearly drained) the bus; re-issuing
71
+ // immediately would just spam FFI calls for trickle streams.
72
+ // A full-batch response means more events are queued, so we
73
+ // skip the sleep and loop tight to keep up.
74
+ if (response.events.length < this.opts.limit) {
75
+ await sleep(this.opts.pollIntervalMs);
76
+ }
77
+ }
78
+ else {
79
+ // Exponential backoff when idle.
80
+ await sleep(backoff);
81
+ backoff = Math.min(backoff * 2, this.opts.maxBackoffMs);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ exports.EventStream = EventStream;
87
+ /**
88
+ * A typed async iterable stream that deserializes events into `T`.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * interface TokenEvent { token: string; index: number; }
93
+ * for await (const token of node.subscribe<TokenEvent>({ limit: 100 })) {
94
+ * console.log(token.token, token.index);
95
+ * }
96
+ * ```
97
+ */
98
+ class TypedEventStream {
99
+ inner;
100
+ parse;
101
+ constructor(bus, opts = {}, parse) {
102
+ this.inner = new EventStream(bus, opts);
103
+ this.parse = parse ?? ((raw) => JSON.parse(raw));
104
+ }
105
+ /** Stop the stream. */
106
+ stop() {
107
+ this.inner.stop();
108
+ }
109
+ async *[Symbol.asyncIterator]() {
110
+ for await (const event of this.inner) {
111
+ yield this.parse(event.raw);
112
+ }
113
+ }
114
+ }
115
+ exports.TypedEventStream = TypedEventStream;
116
+ function sleep(ms) {
117
+ return new Promise((resolve) => setTimeout(resolve, ms));
118
+ }