nats.do 0.1.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/README.md +298 -0
- package/dist/env-Ds3wIkQg.d.cts +28 -0
- package/dist/env-Ds3wIkQg.d.ts +28 -0
- package/dist/index.cjs +235 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +230 -0
- package/dist/index.js.map +1 -0
- package/dist/types.cjs +169 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +444 -0
- package/dist/types.d.ts +444 -0
- package/dist/types.js +155 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.cjs +362 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.cts +214 -0
- package/dist/utils.d.ts +214 -0
- package/dist/utils.js +338 -0
- package/dist/utils.js.map +1 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# NATS.do
|
|
2
|
+
|
|
3
|
+
NATS/JetStream on Cloudflare Durable Objects
|
|
4
|
+
|
|
5
|
+
NATS.do implements [NATS](https://nats.io/) Core messaging and [JetStream](https://docs.nats.io/nats-concepts/jetstream) persistence using Cloudflare Workers and Durable Objects with SQLite storage. It provides a `nats.js`-compatible API accessible via JSON-RPC 2.0 over HTTP, WebSockets, or Cloudflare Workers Service Bindings.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **NATS Core**: Publish/subscribe messaging with subject wildcards (`*`, `>`)
|
|
10
|
+
- **JetStream Streams**: Persistent message storage with configurable retention policies
|
|
11
|
+
- **JetStream Consumers**: Pull and push consumers with acknowledgment tracking
|
|
12
|
+
- **MCP Integration**: Model Context Protocol tools for AI agent access
|
|
13
|
+
- **Edge-Native**: Runs entirely on Cloudflare's global network
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install nats.do
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Basic Pub/Sub
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { StringCodec, JSONCodec } from 'nats.do'
|
|
27
|
+
|
|
28
|
+
const sc = StringCodec()
|
|
29
|
+
const jc = JSONCodec()
|
|
30
|
+
|
|
31
|
+
// Publish a message
|
|
32
|
+
nc.publish('orders.new', sc.encode('Hello NATS!'))
|
|
33
|
+
|
|
34
|
+
// Subscribe with wildcards
|
|
35
|
+
const sub = nc.subscribe('orders.*')
|
|
36
|
+
for await (const msg of sub) {
|
|
37
|
+
console.log(`Received: ${sc.decode(msg.data)}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Request/Reply pattern
|
|
41
|
+
const response = await nc.request('api.users.get', jc.encode({ id: 123 }))
|
|
42
|
+
console.log(jc.decode(response.data))
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### JetStream Streams
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Create a stream
|
|
49
|
+
const jsm = nc.jetstreamManager()
|
|
50
|
+
await jsm.streams.add({
|
|
51
|
+
name: 'ORDERS',
|
|
52
|
+
subjects: ['orders.*'],
|
|
53
|
+
retention: 'workqueue',
|
|
54
|
+
max_msgs: 10000,
|
|
55
|
+
max_age: 24 * 60 * 60 * 1_000_000_000, // 24 hours in nanoseconds
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Publish with acknowledgment
|
|
59
|
+
const js = nc.jetstream()
|
|
60
|
+
const ack = await js.publish('orders.new', sc.encode('{"id": 456}'))
|
|
61
|
+
console.log(`Published to ${ack.stream} at seq ${ack.seq}`)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### JetStream Consumers
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Create a durable consumer
|
|
68
|
+
await jsm.consumers.add('ORDERS', {
|
|
69
|
+
durable_name: 'order-processor',
|
|
70
|
+
ack_policy: 'explicit',
|
|
71
|
+
deliver_policy: 'all',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Fetch messages
|
|
75
|
+
const consumer = await js.consumers.get('ORDERS', 'order-processor')
|
|
76
|
+
const messages = await consumer.fetch({ max_messages: 10 })
|
|
77
|
+
|
|
78
|
+
for await (const msg of messages) {
|
|
79
|
+
console.log(`Processing: ${sc.decode(msg.data)}`)
|
|
80
|
+
msg.ack()
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## API Reference
|
|
85
|
+
|
|
86
|
+
### Core Types
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// Connection options
|
|
90
|
+
interface ConnectionOptions {
|
|
91
|
+
servers: string | string[]
|
|
92
|
+
name?: string
|
|
93
|
+
token?: string
|
|
94
|
+
timeout?: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Message
|
|
98
|
+
interface Msg {
|
|
99
|
+
subject: string
|
|
100
|
+
data: Uint8Array
|
|
101
|
+
reply?: string
|
|
102
|
+
headers?: MsgHdrs
|
|
103
|
+
respond(data?: Uint8Array): boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Subscription
|
|
107
|
+
interface Subscription extends AsyncIterable<Msg> {
|
|
108
|
+
getSubject(): string
|
|
109
|
+
unsubscribe(max?: number): void
|
|
110
|
+
drain(): Promise<void>
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### JetStream Types
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Stream configuration
|
|
118
|
+
interface StreamConfig {
|
|
119
|
+
name: string
|
|
120
|
+
subjects: string[]
|
|
121
|
+
retention?: 'limits' | 'interest' | 'workqueue'
|
|
122
|
+
storage?: 'file' | 'memory'
|
|
123
|
+
max_msgs?: number
|
|
124
|
+
max_bytes?: number
|
|
125
|
+
max_age?: number // nanoseconds
|
|
126
|
+
discard?: 'old' | 'new'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Consumer configuration
|
|
130
|
+
interface ConsumerConfig {
|
|
131
|
+
name?: string
|
|
132
|
+
durable_name?: string
|
|
133
|
+
ack_policy: 'none' | 'all' | 'explicit'
|
|
134
|
+
deliver_policy?: 'all' | 'last' | 'new' | 'by_start_sequence' | 'by_start_time'
|
|
135
|
+
filter_subject?: string
|
|
136
|
+
max_deliver?: number
|
|
137
|
+
ack_wait?: number // nanoseconds
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Publish acknowledgment
|
|
141
|
+
interface PubAck {
|
|
142
|
+
stream: string
|
|
143
|
+
seq: number
|
|
144
|
+
duplicate?: boolean
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Codecs
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { StringCodec, JSONCodec, Empty } from 'nats.do'
|
|
152
|
+
|
|
153
|
+
const sc = StringCodec()
|
|
154
|
+
sc.encode('hello') // Uint8Array
|
|
155
|
+
sc.decode(data) // string
|
|
156
|
+
|
|
157
|
+
const jc = JSONCodec<MyType>()
|
|
158
|
+
jc.encode({ key: 'value' }) // Uint8Array
|
|
159
|
+
jc.decode(data) // MyType
|
|
160
|
+
|
|
161
|
+
Empty // Empty Uint8Array for messages without payload
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Subject Wildcards
|
|
165
|
+
|
|
166
|
+
NATS.do supports NATS subject wildcards for subscriptions:
|
|
167
|
+
|
|
168
|
+
- `*` matches exactly one token: `orders.*` matches `orders.new` but not `orders.us.new`
|
|
169
|
+
- `>` matches one or more tokens (must be last): `orders.>` matches `orders.new` and `orders.us.new`
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { matchSubject, isValidSubject, isValidWildcard } from 'nats.do/utils'
|
|
173
|
+
|
|
174
|
+
matchSubject('orders.*', 'orders.new') // true
|
|
175
|
+
matchSubject('orders.*', 'orders.us.new') // false
|
|
176
|
+
matchSubject('orders.>', 'orders.us.new') // true
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Architecture
|
|
180
|
+
|
|
181
|
+
NATS.do uses three Durable Object classes:
|
|
182
|
+
|
|
183
|
+
| Durable Object | Scope | Responsibility |
|
|
184
|
+
|---------------|-------|----------------|
|
|
185
|
+
| `NatsCoordinator` | Global singleton | Stream registry, consumer discovery, cluster metadata |
|
|
186
|
+
| `NatsPubSub` | Per region | Core NATS pub/sub, WebSocket connections, request/reply |
|
|
187
|
+
| `StreamDO` | Per stream | Message storage, consumer state, ack tracking, retention |
|
|
188
|
+
|
|
189
|
+
### RPC Protocol
|
|
190
|
+
|
|
191
|
+
NATS.do uses JSON-RPC 2.0 for communication:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Request
|
|
195
|
+
{
|
|
196
|
+
"jsonrpc": "2.0",
|
|
197
|
+
"method": "nats.publish",
|
|
198
|
+
"params": { "subject": "orders.new", "data": "base64..." },
|
|
199
|
+
"id": 1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Response
|
|
203
|
+
{
|
|
204
|
+
"jsonrpc": "2.0",
|
|
205
|
+
"result": { "success": true },
|
|
206
|
+
"id": 1
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## MCP Tools
|
|
211
|
+
|
|
212
|
+
NATS.do exposes MCP (Model Context Protocol) tools for AI agent integration:
|
|
213
|
+
|
|
214
|
+
| Tool | Description |
|
|
215
|
+
|------|-------------|
|
|
216
|
+
| `nats_publish` | Publish a message to a subject |
|
|
217
|
+
| `nats_subscribe` | Subscribe to a subject |
|
|
218
|
+
| `nats_request` | Send a request and wait for response |
|
|
219
|
+
| `jetstream_publish` | Publish to JetStream with acknowledgment |
|
|
220
|
+
| `jetstream_stream_create` | Create a new stream |
|
|
221
|
+
| `jetstream_stream_info` | Get stream information |
|
|
222
|
+
| `jetstream_consumer_create` | Create a consumer |
|
|
223
|
+
| `jetstream_consumer_fetch` | Fetch messages from a consumer |
|
|
224
|
+
|
|
225
|
+
## Cloudflare Workers Deployment
|
|
226
|
+
|
|
227
|
+
### wrangler.jsonc
|
|
228
|
+
|
|
229
|
+
```jsonc
|
|
230
|
+
{
|
|
231
|
+
"name": "nats.do",
|
|
232
|
+
"main": "src/index.ts",
|
|
233
|
+
"compatibility_date": "2024-01-01",
|
|
234
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
235
|
+
|
|
236
|
+
"durable_objects": {
|
|
237
|
+
"bindings": [
|
|
238
|
+
{ "name": "NATS_COORDINATOR", "class_name": "NatsCoordinator" },
|
|
239
|
+
{ "name": "NATS_PUBSUB", "class_name": "NatsPubSub" },
|
|
240
|
+
{ "name": "STREAM_DO", "class_name": "StreamDO" }
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
"migrations": [
|
|
245
|
+
{
|
|
246
|
+
"tag": "v1",
|
|
247
|
+
"new_sqlite_classes": ["NatsCoordinator", "NatsPubSub", "StreamDO"]
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Service Binding Usage
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// In another Worker
|
|
257
|
+
export default {
|
|
258
|
+
async fetch(request: Request, env: Env) {
|
|
259
|
+
const id = env.NATS_COORDINATOR.idFromName('global')
|
|
260
|
+
const stub = env.NATS_COORDINATOR.get(id)
|
|
261
|
+
|
|
262
|
+
const response = await stub.fetch(new Request('http://internal/rpc', {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
jsonrpc: '2.0',
|
|
266
|
+
method: 'consumers.list',
|
|
267
|
+
params: { streamName: 'ORDERS' },
|
|
268
|
+
id: 1
|
|
269
|
+
})
|
|
270
|
+
}))
|
|
271
|
+
|
|
272
|
+
return response
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Development
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# Install dependencies
|
|
281
|
+
npm install
|
|
282
|
+
|
|
283
|
+
# Run tests
|
|
284
|
+
npm test
|
|
285
|
+
|
|
286
|
+
# Run tests in watch mode
|
|
287
|
+
npm run test:watch
|
|
288
|
+
|
|
289
|
+
# Type checking
|
|
290
|
+
npm run typecheck
|
|
291
|
+
|
|
292
|
+
# Local development
|
|
293
|
+
npm run dev
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers Environment Type
|
|
3
|
+
*
|
|
4
|
+
* Defines the environment bindings for the NatDO worker,
|
|
5
|
+
* including Durable Object namespace bindings.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Environment interface for NatDO worker.
|
|
9
|
+
* Contains bindings for Durable Objects used in the application.
|
|
10
|
+
*/
|
|
11
|
+
interface Env {
|
|
12
|
+
/**
|
|
13
|
+
* Durable Object namespace for NATS coordination.
|
|
14
|
+
* Manages consumer registry with SQLite storage.
|
|
15
|
+
*/
|
|
16
|
+
NATS_COORDINATOR: DurableObjectNamespace;
|
|
17
|
+
/**
|
|
18
|
+
* Durable Object namespace for NATS publish/subscribe operations.
|
|
19
|
+
*/
|
|
20
|
+
NATS_PUBSUB: DurableObjectNamespace;
|
|
21
|
+
/**
|
|
22
|
+
* Durable Object namespace for stream-specific operations.
|
|
23
|
+
* Each stream gets its own instance for fetching and acking messages.
|
|
24
|
+
*/
|
|
25
|
+
STREAM_DO: DurableObjectNamespace;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type { Env as E };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers Environment Type
|
|
3
|
+
*
|
|
4
|
+
* Defines the environment bindings for the NatDO worker,
|
|
5
|
+
* including Durable Object namespace bindings.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Environment interface for NatDO worker.
|
|
9
|
+
* Contains bindings for Durable Objects used in the application.
|
|
10
|
+
*/
|
|
11
|
+
interface Env {
|
|
12
|
+
/**
|
|
13
|
+
* Durable Object namespace for NATS coordination.
|
|
14
|
+
* Manages consumer registry with SQLite storage.
|
|
15
|
+
*/
|
|
16
|
+
NATS_COORDINATOR: DurableObjectNamespace;
|
|
17
|
+
/**
|
|
18
|
+
* Durable Object namespace for NATS publish/subscribe operations.
|
|
19
|
+
*/
|
|
20
|
+
NATS_PUBSUB: DurableObjectNamespace;
|
|
21
|
+
/**
|
|
22
|
+
* Durable Object namespace for stream-specific operations.
|
|
23
|
+
* Each stream gets its own instance for fetching and acking messages.
|
|
24
|
+
*/
|
|
25
|
+
STREAM_DO: DurableObjectNamespace;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type { Env as E };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cloudflare_workers = require('cloudflare:workers');
|
|
6
|
+
|
|
7
|
+
// src/durable-objects/nats-coordinator.ts
|
|
8
|
+
|
|
9
|
+
// src/types/rpc.ts
|
|
10
|
+
var RPC_ERROR_CODES = {
|
|
11
|
+
// Standard JSON-RPC errors
|
|
12
|
+
PARSE_ERROR: -32700,
|
|
13
|
+
METHOD_NOT_FOUND: -32601,
|
|
14
|
+
INVALID_PARAMS: -32602,
|
|
15
|
+
CONSUMER_NOT_FOUND: -32002,
|
|
16
|
+
CONSUMER_EXISTS: -32007};
|
|
17
|
+
function createRpcError(code, message, id, data) {
|
|
18
|
+
return {
|
|
19
|
+
jsonrpc: "2.0",
|
|
20
|
+
error: {
|
|
21
|
+
code,
|
|
22
|
+
message,
|
|
23
|
+
...data !== void 0
|
|
24
|
+
},
|
|
25
|
+
id: id ?? null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createRpcSuccess(result, id) {
|
|
29
|
+
return {
|
|
30
|
+
jsonrpc: "2.0",
|
|
31
|
+
result,
|
|
32
|
+
id
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/durable-objects/nats-coordinator.ts
|
|
37
|
+
var NatsCoordinator = class extends cloudflare_workers.DurableObject {
|
|
38
|
+
sql;
|
|
39
|
+
constructor(ctx, env) {
|
|
40
|
+
super(ctx, env);
|
|
41
|
+
this.sql = ctx.storage.sql;
|
|
42
|
+
this.initSchema();
|
|
43
|
+
}
|
|
44
|
+
initSchema() {
|
|
45
|
+
this.sql.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS consumers (
|
|
47
|
+
stream_name TEXT NOT NULL,
|
|
48
|
+
name TEXT NOT NULL,
|
|
49
|
+
config TEXT NOT NULL,
|
|
50
|
+
durable INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
created_at INTEGER NOT NULL,
|
|
52
|
+
last_active_at INTEGER,
|
|
53
|
+
PRIMARY KEY (stream_name, name)
|
|
54
|
+
)
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
async fetch(request) {
|
|
58
|
+
if (request.method !== "POST") {
|
|
59
|
+
return new Response("Method not allowed", { status: 405 });
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const body = await request.json();
|
|
63
|
+
const { method, params, id } = body;
|
|
64
|
+
const result = await this.handleRpc(method, params || {}, id);
|
|
65
|
+
return new Response(JSON.stringify(result), {
|
|
66
|
+
headers: { "Content-Type": "application/json" }
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const errorResponse = createRpcError(
|
|
70
|
+
RPC_ERROR_CODES.PARSE_ERROR,
|
|
71
|
+
"Invalid JSON",
|
|
72
|
+
null
|
|
73
|
+
);
|
|
74
|
+
return new Response(JSON.stringify(errorResponse), {
|
|
75
|
+
headers: { "Content-Type": "application/json" }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async handleRpc(method, params, id) {
|
|
80
|
+
switch (method) {
|
|
81
|
+
case "consumers.register":
|
|
82
|
+
return this.registerConsumer(params, id);
|
|
83
|
+
case "consumers.get":
|
|
84
|
+
return this.getConsumer(params, id);
|
|
85
|
+
case "consumers.delete":
|
|
86
|
+
return this.deleteConsumer(params, id);
|
|
87
|
+
case "consumers.list":
|
|
88
|
+
return this.listConsumers(params, id);
|
|
89
|
+
case "consumers.updateLastActive":
|
|
90
|
+
return this.updateConsumerLastActive(params, id);
|
|
91
|
+
default:
|
|
92
|
+
return createRpcError(
|
|
93
|
+
RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
94
|
+
`Method not found: ${method}`,
|
|
95
|
+
id
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
registerConsumer(params, id) {
|
|
100
|
+
const streamName = params.streamName;
|
|
101
|
+
const config = params.config;
|
|
102
|
+
const consumerName = config.name || config.durable_name;
|
|
103
|
+
if (!consumerName) {
|
|
104
|
+
return createRpcError(
|
|
105
|
+
RPC_ERROR_CODES.INVALID_PARAMS,
|
|
106
|
+
"Consumer name is required (name or durable_name)",
|
|
107
|
+
id
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const existing = this.sql.exec("SELECT 1 FROM consumers WHERE stream_name = ? AND name = ?", streamName, consumerName).toArray();
|
|
111
|
+
if (existing.length > 0) {
|
|
112
|
+
return createRpcError(
|
|
113
|
+
RPC_ERROR_CODES.CONSUMER_EXISTS,
|
|
114
|
+
`Consumer ${consumerName} already exists on stream ${streamName}`,
|
|
115
|
+
id
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const isDurable = !!config.durable_name;
|
|
120
|
+
this.sql.exec(
|
|
121
|
+
`INSERT INTO consumers (stream_name, name, config, durable, created_at, last_active_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
123
|
+
streamName,
|
|
124
|
+
consumerName,
|
|
125
|
+
JSON.stringify(config),
|
|
126
|
+
isDurable ? 1 : 0,
|
|
127
|
+
now,
|
|
128
|
+
null
|
|
129
|
+
);
|
|
130
|
+
const entry = {
|
|
131
|
+
stream_name: streamName,
|
|
132
|
+
name: consumerName,
|
|
133
|
+
config,
|
|
134
|
+
durable: isDurable,
|
|
135
|
+
created_at: now,
|
|
136
|
+
last_active_at: null
|
|
137
|
+
};
|
|
138
|
+
return createRpcSuccess(entry, id);
|
|
139
|
+
}
|
|
140
|
+
getConsumer(params, id) {
|
|
141
|
+
const streamName = params.streamName;
|
|
142
|
+
const consumerName = params.consumerName;
|
|
143
|
+
const rows = this.sql.exec(
|
|
144
|
+
"SELECT stream_name, name, config, durable, created_at, last_active_at FROM consumers WHERE stream_name = ? AND name = ?",
|
|
145
|
+
streamName,
|
|
146
|
+
consumerName
|
|
147
|
+
).toArray();
|
|
148
|
+
if (rows.length === 0) {
|
|
149
|
+
return createRpcError(
|
|
150
|
+
RPC_ERROR_CODES.CONSUMER_NOT_FOUND,
|
|
151
|
+
`Consumer ${consumerName} not found on stream ${streamName}`,
|
|
152
|
+
id
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const row = rows[0];
|
|
156
|
+
const entry = {
|
|
157
|
+
stream_name: row.stream_name,
|
|
158
|
+
name: row.name,
|
|
159
|
+
config: JSON.parse(row.config),
|
|
160
|
+
durable: row.durable === 1,
|
|
161
|
+
created_at: row.created_at,
|
|
162
|
+
last_active_at: row.last_active_at
|
|
163
|
+
};
|
|
164
|
+
return createRpcSuccess(entry, id);
|
|
165
|
+
}
|
|
166
|
+
deleteConsumer(params, id) {
|
|
167
|
+
const streamName = params.streamName;
|
|
168
|
+
const consumerName = params.consumerName;
|
|
169
|
+
const existing = this.sql.exec("SELECT 1 FROM consumers WHERE stream_name = ? AND name = ?", streamName, consumerName).toArray();
|
|
170
|
+
if (existing.length === 0) {
|
|
171
|
+
return createRpcError(
|
|
172
|
+
RPC_ERROR_CODES.CONSUMER_NOT_FOUND,
|
|
173
|
+
`Consumer ${consumerName} not found on stream ${streamName}`,
|
|
174
|
+
id
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
this.sql.exec(
|
|
178
|
+
"DELETE FROM consumers WHERE stream_name = ? AND name = ?",
|
|
179
|
+
streamName,
|
|
180
|
+
consumerName
|
|
181
|
+
);
|
|
182
|
+
return createRpcSuccess({ success: true }, id);
|
|
183
|
+
}
|
|
184
|
+
listConsumers(params, id) {
|
|
185
|
+
const streamName = params.streamName;
|
|
186
|
+
const rows = this.sql.exec(
|
|
187
|
+
"SELECT stream_name, name, config, durable, created_at, last_active_at FROM consumers WHERE stream_name = ? ORDER BY name",
|
|
188
|
+
streamName
|
|
189
|
+
).toArray();
|
|
190
|
+
const consumers = rows.map((row) => {
|
|
191
|
+
const r = row;
|
|
192
|
+
return {
|
|
193
|
+
stream_name: r.stream_name,
|
|
194
|
+
name: r.name,
|
|
195
|
+
config: JSON.parse(r.config),
|
|
196
|
+
durable: r.durable === 1,
|
|
197
|
+
created_at: r.created_at,
|
|
198
|
+
last_active_at: r.last_active_at
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
return createRpcSuccess(consumers, id);
|
|
202
|
+
}
|
|
203
|
+
updateConsumerLastActive(params, id) {
|
|
204
|
+
const streamName = params.streamName;
|
|
205
|
+
const consumerName = params.consumerName;
|
|
206
|
+
const existing = this.sql.exec("SELECT 1 FROM consumers WHERE stream_name = ? AND name = ?", streamName, consumerName).toArray();
|
|
207
|
+
if (existing.length === 0) {
|
|
208
|
+
return createRpcError(
|
|
209
|
+
RPC_ERROR_CODES.CONSUMER_NOT_FOUND,
|
|
210
|
+
`Consumer ${consumerName} not found on stream ${streamName}`,
|
|
211
|
+
id
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
this.sql.exec(
|
|
216
|
+
"UPDATE consumers SET last_active_at = ? WHERE stream_name = ? AND name = ?",
|
|
217
|
+
now,
|
|
218
|
+
streamName,
|
|
219
|
+
consumerName
|
|
220
|
+
);
|
|
221
|
+
return createRpcSuccess({ last_active_at: now }, id);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/index.ts
|
|
226
|
+
var src_default = {
|
|
227
|
+
async fetch(_request, _env) {
|
|
228
|
+
return new Response("NatDO - Not yet implemented", { status: 501 });
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
exports.NatsCoordinator = NatsCoordinator;
|
|
233
|
+
exports.default = src_default;
|
|
234
|
+
//# sourceMappingURL=index.cjs.map
|
|
235
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types/rpc.ts","../src/durable-objects/nats-coordinator.ts","../src/index.ts"],"names":["DurableObject"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * RPC Types\n *\n * JSON-RPC 2.0 types for NatDO communication.\n */\n\n// Request ID type\nexport type RpcId = string | number\n\n// RPC Request\nexport interface RpcRequest {\n jsonrpc: '2.0'\n method: string\n params?: Record<string, unknown> | unknown[]\n id: RpcId\n}\n\n// RPC Notification (no id)\nexport interface RpcNotification {\n jsonrpc: '2.0'\n method: string\n params?: Record<string, unknown> | unknown[]\n}\n\n// RPC Error\nexport interface RpcError {\n code: number\n message: string\n data?: unknown\n}\n\n// RPC Response\nexport interface RpcResponse {\n jsonrpc: '2.0'\n result?: unknown\n error?: RpcError\n id: RpcId | null\n}\n\n// Batch types\nexport type RpcBatchRequest = Array<RpcRequest | RpcNotification>\nexport type RpcBatchResponse = RpcResponse[]\n\n// Standard JSON-RPC 2.0 error codes\nexport const RPC_ERROR_CODES = {\n // Standard JSON-RPC errors\n PARSE_ERROR: -32700,\n INVALID_REQUEST: -32600,\n METHOD_NOT_FOUND: -32601,\n INVALID_PARAMS: -32602,\n INTERNAL_ERROR: -32603,\n\n // NATS-specific error codes (-32001 to -32099)\n STREAM_NOT_FOUND: -32001,\n CONSUMER_NOT_FOUND: -32002,\n NO_RESPONDERS: -32003,\n TIMEOUT: -32004,\n MESSAGE_NOT_FOUND: -32005,\n STREAM_EXISTS: -32006,\n CONSUMER_EXISTS: -32007,\n INVALID_SUBJECT: -32008,\n PERMISSION_DENIED: -32009,\n MAX_PAYLOAD_EXCEEDED: -32010,\n DUPLICATE_MESSAGE: -32011,\n STREAM_SEALED: -32012,\n CONSUMER_DELETED: -32013,\n} as const\n\n// Type guard for error responses\nexport function isRpcError(response: RpcResponse): response is RpcResponse & { error: RpcError } {\n return 'error' in response && response.error !== undefined\n}\n\n// Type guard for success responses\nexport function isRpcSuccess(response: RpcResponse): response is RpcResponse & { result: unknown } {\n return !isRpcError(response)\n}\n\n// Request ID counter for auto-incrementing IDs\nlet requestIdCounter = 0\n\n// Create RPC request helper\nexport function createRpcRequest(\n method: string,\n opts?: { params?: Record<string, unknown> | unknown[]; id?: RpcId }\n): RpcRequest {\n return {\n jsonrpc: '2.0',\n method,\n ...(opts?.params && { params: opts.params }),\n id: opts?.id ?? ++requestIdCounter,\n }\n}\n\n// Create RPC error response helper\nexport function createRpcError(\n code: number,\n message: string,\n id?: RpcId | null,\n data?: unknown\n): RpcResponse {\n return {\n jsonrpc: '2.0',\n error: {\n code,\n message,\n ...(data !== undefined && { data }),\n },\n id: id ?? null,\n }\n}\n\n// Create RPC success response helper\nexport function createRpcSuccess(result: unknown, id: RpcId): RpcResponse {\n return {\n jsonrpc: '2.0',\n result,\n id,\n }\n}\n\n// Reset request ID counter (for testing)\nexport function resetRequestIdCounter(): void {\n requestIdCounter = 0\n}\n","/**\n * NatsCoordinator Durable Object\n *\n * Central coordinator for NATS/JetStream operations.\n * Manages consumer registry with SQLite storage.\n */\n\nimport { DurableObject } from 'cloudflare:workers'\nimport type { ConsumerConfig } from '../types/jetstream'\nimport { RPC_ERROR_CODES, createRpcError, createRpcSuccess } from '../types/rpc'\nimport type { Env } from '../types/env'\n\n// Consumer registry entry stored in SQLite\ninterface ConsumerEntry {\n stream_name: string\n name: string\n config: ConsumerConfig\n durable: boolean\n created_at: number\n last_active_at: number | null\n}\n\n// RPC request structure\ninterface RpcRequest {\n jsonrpc: '2.0'\n method: string\n params?: Record<string, unknown>\n id: number | string\n}\n\nexport class NatsCoordinator extends DurableObject<Env> {\n private sql: SqlStorage\n\n constructor(ctx: DurableObjectState, env: Env) {\n super(ctx, env)\n this.sql = ctx.storage.sql\n\n // Initialize schema\n this.initSchema()\n }\n\n private initSchema(): void {\n this.sql.exec(`\n CREATE TABLE IF NOT EXISTS consumers (\n stream_name TEXT NOT NULL,\n name TEXT NOT NULL,\n config TEXT NOT NULL,\n durable INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n last_active_at INTEGER,\n PRIMARY KEY (stream_name, name)\n )\n `)\n }\n\n async fetch(request: Request): Promise<Response> {\n if (request.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n try {\n const body = await request.json() as RpcRequest\n const { method, params, id } = body\n\n const result = await this.handleRpc(method, params || {}, id)\n return new Response(JSON.stringify(result), {\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n const errorResponse = createRpcError(\n RPC_ERROR_CODES.PARSE_ERROR,\n 'Invalid JSON',\n null\n )\n return new Response(JSON.stringify(errorResponse), {\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n private async handleRpc(\n method: string,\n params: Record<string, unknown>,\n id: number | string\n ) {\n switch (method) {\n case 'consumers.register':\n return this.registerConsumer(params, id)\n case 'consumers.get':\n return this.getConsumer(params, id)\n case 'consumers.delete':\n return this.deleteConsumer(params, id)\n case 'consumers.list':\n return this.listConsumers(params, id)\n case 'consumers.updateLastActive':\n return this.updateConsumerLastActive(params, id)\n default:\n return createRpcError(\n RPC_ERROR_CODES.METHOD_NOT_FOUND,\n `Method not found: ${method}`,\n id\n )\n }\n }\n\n private registerConsumer(params: Record<string, unknown>, id: number | string) {\n const streamName = params.streamName as string\n const config = params.config as ConsumerConfig\n\n // Validate consumer name\n const consumerName = config.name || config.durable_name\n if (!consumerName) {\n return createRpcError(\n RPC_ERROR_CODES.INVALID_PARAMS,\n 'Consumer name is required (name or durable_name)',\n id\n )\n }\n\n // Check if consumer already exists\n const existing = this.sql\n .exec('SELECT 1 FROM consumers WHERE stream_name = ? AND name = ?', streamName, consumerName)\n .toArray()\n\n if (existing.length > 0) {\n return createRpcError(\n RPC_ERROR_CODES.CONSUMER_EXISTS,\n `Consumer ${consumerName} already exists on stream ${streamName}`,\n id\n )\n }\n\n const now = Date.now()\n const isDurable = !!config.durable_name\n\n // Store the consumer\n this.sql.exec(\n `INSERT INTO consumers (stream_name, name, config, durable, created_at, last_active_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n streamName,\n consumerName,\n JSON.stringify(config),\n isDurable ? 1 : 0,\n now,\n null\n )\n\n const entry: ConsumerEntry = {\n stream_name: streamName,\n name: consumerName,\n config,\n durable: isDurable,\n created_at: now,\n last_active_at: null,\n }\n\n return createRpcSuccess(entry, id)\n }\n\n private getConsumer(params: Record<string, unknown>, id: number | string) {\n const streamName = params.streamName as string\n const consumerName = params.consumerName as string\n\n const rows = this.sql\n .exec(\n 'SELECT stream_name, name, config, durable, created_at, last_active_at FROM consumers WHERE stream_name = ? AND name = ?',\n streamName,\n consumerName\n )\n .toArray()\n\n if (rows.length === 0) {\n return createRpcError(\n RPC_ERROR_CODES.CONSUMER_NOT_FOUND,\n `Consumer ${consumerName} not found on stream ${streamName}`,\n id\n )\n }\n\n const row = rows[0] as {\n stream_name: string\n name: string\n config: string\n durable: number\n created_at: number\n last_active_at: number | null\n }\n\n const entry: ConsumerEntry = {\n stream_name: row.stream_name,\n name: row.name,\n config: JSON.parse(row.config) as ConsumerConfig,\n durable: row.durable === 1,\n created_at: row.created_at,\n last_active_at: row.last_active_at,\n }\n\n return createRpcSuccess(entry, id)\n }\n\n private deleteConsumer(params: Record<string, unknown>, id: number | string) {\n const streamName = params.streamName as string\n const consumerName = params.consumerName as string\n\n // Check if consumer exists\n const existing = this.sql\n .exec('SELECT 1 FROM consumers WHERE stream_name = ? AND name = ?', streamName, consumerName)\n .toArray()\n\n if (existing.length === 0) {\n return createRpcError(\n RPC_ERROR_CODES.CONSUMER_NOT_FOUND,\n `Consumer ${consumerName} not found on stream ${streamName}`,\n id\n )\n }\n\n this.sql.exec(\n 'DELETE FROM consumers WHERE stream_name = ? AND name = ?',\n streamName,\n consumerName\n )\n\n return createRpcSuccess({ success: true }, id)\n }\n\n private listConsumers(params: Record<string, unknown>, id: number | string) {\n const streamName = params.streamName as string\n\n const rows = this.sql\n .exec(\n 'SELECT stream_name, name, config, durable, created_at, last_active_at FROM consumers WHERE stream_name = ? ORDER BY name',\n streamName\n )\n .toArray()\n\n const consumers: ConsumerEntry[] = rows.map((row) => {\n const r = row as {\n stream_name: string\n name: string\n config: string\n durable: number\n created_at: number\n last_active_at: number | null\n }\n return {\n stream_name: r.stream_name,\n name: r.name,\n config: JSON.parse(r.config) as ConsumerConfig,\n durable: r.durable === 1,\n created_at: r.created_at,\n last_active_at: r.last_active_at,\n }\n })\n\n return createRpcSuccess(consumers, id)\n }\n\n private updateConsumerLastActive(params: Record<string, unknown>, id: number | string) {\n const streamName = params.streamName as string\n const consumerName = params.consumerName as string\n\n // Check if consumer exists\n const existing = this.sql\n .exec('SELECT 1 FROM consumers WHERE stream_name = ? AND name = ?', streamName, consumerName)\n .toArray()\n\n if (existing.length === 0) {\n return createRpcError(\n RPC_ERROR_CODES.CONSUMER_NOT_FOUND,\n `Consumer ${consumerName} not found on stream ${streamName}`,\n id\n )\n }\n\n const now = Date.now()\n this.sql.exec(\n 'UPDATE consumers SET last_active_at = ? WHERE stream_name = ? AND name = ?',\n now,\n streamName,\n consumerName\n )\n\n return createRpcSuccess({ last_active_at: now }, id)\n }\n}\n","/**\n * NatDO - NATS/JetStream on Cloudflare Durable Objects\n *\n * This is the main entry point for the worker.\n */\n\nexport { NatsCoordinator } from './durable-objects/nats-coordinator'\n\nexport default {\n async fetch(_request: Request, _env: unknown): Promise<Response> {\n return new Response('NatDO - Not yet implemented', { status: 501 })\n },\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
import { E as Env } from './env-Ds3wIkQg.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NatsCoordinator Durable Object
|
|
6
|
+
*
|
|
7
|
+
* Central coordinator for NATS/JetStream operations.
|
|
8
|
+
* Manages consumer registry with SQLite storage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare class NatsCoordinator extends DurableObject<Env> {
|
|
12
|
+
private sql;
|
|
13
|
+
constructor(ctx: DurableObjectState, env: Env);
|
|
14
|
+
private initSchema;
|
|
15
|
+
fetch(request: Request): Promise<Response>;
|
|
16
|
+
private handleRpc;
|
|
17
|
+
private registerConsumer;
|
|
18
|
+
private getConsumer;
|
|
19
|
+
private deleteConsumer;
|
|
20
|
+
private listConsumers;
|
|
21
|
+
private updateConsumerLastActive;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* NatDO - NATS/JetStream on Cloudflare Durable Objects
|
|
26
|
+
*
|
|
27
|
+
* This is the main entry point for the worker.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
declare const _default: {
|
|
31
|
+
fetch(_request: Request, _env: unknown): Promise<Response>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { NatsCoordinator, _default as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
import { E as Env } from './env-Ds3wIkQg.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NatsCoordinator Durable Object
|
|
6
|
+
*
|
|
7
|
+
* Central coordinator for NATS/JetStream operations.
|
|
8
|
+
* Manages consumer registry with SQLite storage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare class NatsCoordinator extends DurableObject<Env> {
|
|
12
|
+
private sql;
|
|
13
|
+
constructor(ctx: DurableObjectState, env: Env);
|
|
14
|
+
private initSchema;
|
|
15
|
+
fetch(request: Request): Promise<Response>;
|
|
16
|
+
private handleRpc;
|
|
17
|
+
private registerConsumer;
|
|
18
|
+
private getConsumer;
|
|
19
|
+
private deleteConsumer;
|
|
20
|
+
private listConsumers;
|
|
21
|
+
private updateConsumerLastActive;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* NatDO - NATS/JetStream on Cloudflare Durable Objects
|
|
26
|
+
*
|
|
27
|
+
* This is the main entry point for the worker.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
declare const _default: {
|
|
31
|
+
fetch(_request: Request, _env: unknown): Promise<Response>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { NatsCoordinator, _default as default };
|