@restless-stream/core 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/README.md +465 -0
  2. package/package.json +22 -1
package/README.md ADDED
@@ -0,0 +1,465 @@
1
+ # @restless-stream/core
2
+
3
+ Official TypeScript SDK for Restless Stream core APIs: <https://restlessapi.stream>
4
+
5
+ Restless Stream turns REST APIs into live Server-Sent Events and WebSocket streams. This package provides the browser-compatible client, shared types, runtime URL builders, SSE parsing, and basic WebSocket iteration used by the other Restless Stream TypeScript packages.
6
+
7
+ Use this package when you need framework-neutral TypeScript support. Use `@restless-stream/node` for Node.js WebSocket subscriptions with handshake headers, and `@restless-stream/react` for React provider and hook APIs.
8
+
9
+ ## Requirements
10
+
11
+ - A Restless Stream account and API key.
12
+ - A runtime with `fetch` for REST and SSE support.
13
+ - A runtime with `WebSocket` for WebSocket helpers, or an explicit `WebSocket` implementation passed in options.
14
+ - Node.js `>=24` for local development and package tests.
15
+
16
+ Do not expose long-lived API keys in public browser applications. For browser clients, prefer backend-created stream or direct-session URLs.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @restless-stream/core
22
+ ```
23
+
24
+ ```bash
25
+ pnpm add @restless-stream/core
26
+ ```
27
+
28
+ ```bash
29
+ yarn add @restless-stream/core
30
+ ```
31
+
32
+ ## Getting Started
33
+
34
+ Create a stream with the REST API, then subscribe to the returned SSE URL.
35
+
36
+ ```ts
37
+ import { createRestlessClient } from '@restless-stream/core';
38
+
39
+ const client = createRestlessClient({
40
+ apiKey: process.env.RESTLESS_API_KEY,
41
+ });
42
+
43
+ const stream = await client.streams.create({
44
+ name: 'Orders',
45
+ description: 'Live order feed',
46
+ status: 'ACTIVE',
47
+ method: 'GET',
48
+ url: 'https://api.example.com/orders',
49
+ payloadMode: 'FULL_DATA',
50
+ pollingInterval: 30,
51
+ });
52
+
53
+ for await (const event of client.streams.subscribeSse({ sseUrl: stream.sseUrl })) {
54
+ if (event.type === 'update') {
55
+ console.log('data', event.data);
56
+ }
57
+
58
+ if (event.type === 'error') {
59
+ console.error(event.error.message);
60
+ }
61
+ }
62
+ ```
63
+
64
+ The top-level client methods and the `client.streams` resource expose the same management operations. For example, `client.createStream(input)` and `client.streams.create(input)` both create a stream.
65
+
66
+ ## Client Configuration
67
+
68
+ ```ts
69
+ const client = createRestlessClient({
70
+ apiKey: process.env.RESTLESS_API_KEY,
71
+ apiBaseUrl: 'https://api.restlessapi.stream',
72
+ streamBaseUrl: 'https://stream.restlessapi.stream',
73
+ headers: { 'X-App': 'orders-service' },
74
+ fetch: globalThis.fetch,
75
+ });
76
+ ```
77
+
78
+ | Option | Description |
79
+ | --- | --- |
80
+ | `apiKey` | Sends `x-api-key` on REST requests and `Authorization: Bearer <key>` on SSE runtime requests. |
81
+ | `apiBaseUrl` | REST API base URL. Defaults to `https://api.restlessapi.stream`. |
82
+ | `streamBaseUrl` | Runtime stream base URL. Defaults to `https://stream.restlessapi.stream`. |
83
+ | `headers` | Default headers for REST API requests. |
84
+ | `fetch` | Custom `fetch` implementation. Required only when the runtime does not provide global `fetch`. |
85
+
86
+ `createStream` and `updateStream` automatically add `apiKey` to the JSON body when the client has an API key and the input does not already provide one.
87
+
88
+ ## Stream Management
89
+
90
+ ```ts
91
+ const page = await client.streams.list({ limit: 20, offset: 0 });
92
+ const stream = await client.streams.get('stream_123');
93
+
94
+ await client.streams.update(stream.id, {
95
+ name: 'Orders v2',
96
+ pollingInterval: 60,
97
+ });
98
+
99
+ await client.streams.stop(stream.id);
100
+ await client.streams.start(stream.id);
101
+
102
+ const usage = await client.streams.creditUsageStats(stream.id);
103
+ const snippets = await client.streams.connectionSnippets({
104
+ streamId: stream.id,
105
+ language: 'javascript',
106
+ });
107
+ ```
108
+
109
+ | Method | Description |
110
+ | --- | --- |
111
+ | `streams.list(input?)` / `listStreams(input?)` | List streams with optional `limit` and `offset`. |
112
+ | `streams.get(id)` / `getStream(id)` | Get one stream by ID. |
113
+ | `streams.create(input)` / `createStream(input)` | Create a persisted stream. |
114
+ | `streams.update(id, input)` / `updateStream(id, input)` | Patch stream configuration. |
115
+ | `streams.start(id)` / `startStream(id)` | Mark a stream active. |
116
+ | `streams.stop(id)` / `stopStream(id)` | Mark a stream inactive. |
117
+ | `streams.delete(id)` / `deleteStream(id)` | Delete a stream. |
118
+ | `streams.validateApiKey(apiKey?)` / `validateApiKey(apiKey?)` | Validate an API key. Uses the client API key when omitted. |
119
+ | `streams.creditUsageStats(id)` / `creditUsageStats(id)` | Fetch credit usage totals and daily usage. |
120
+ | `streams.connectionSnippets(input)` / `connectionSnippets(input)` | Generate runtime URLs and connection snippets. |
121
+
122
+ ## Direct Streams
123
+
124
+ Direct streams let you subscribe to a REST endpoint without creating a persisted stream first.
125
+
126
+ ```ts
127
+ for await (const event of client.direct.subscribeSse({
128
+ url: 'https://api.example.com/orders',
129
+ method: 'GET',
130
+ pollingInterval: 30,
131
+ })) {
132
+ console.log(event);
133
+ }
134
+ ```
135
+
136
+ Create a reusable direct session when you want a stable runtime URL.
137
+
138
+ ```ts
139
+ const session = await client.direct.createSession({
140
+ dedupeKey: 'orders-feed-v1',
141
+ method: 'GET',
142
+ url: 'https://api.example.com/orders',
143
+ pollingInterval: 30,
144
+ });
145
+
146
+ for await (const event of client.streams.subscribeSse({ sseUrl: session.sseUrl })) {
147
+ console.log(event);
148
+ }
149
+ ```
150
+
151
+ Generate setup commands and snippets without opening a stream connection.
152
+
153
+ ```ts
154
+ const setup = await client.direct.setup({
155
+ method: 'POST',
156
+ url: 'https://api.example.com/search',
157
+ body: { query: 'restless' },
158
+ language: 'javascript',
159
+ });
160
+
161
+ console.log(setup.commands.header.curl);
162
+ console.log(setup.runtime.directSseUrl);
163
+ ```
164
+
165
+ Direct stream input supports:
166
+
167
+ | Field | Description |
168
+ | --- | --- |
169
+ | `url` | Target REST URL. Required. |
170
+ | `method` | HTTP method. Defaults to `GET` when omitted from runtime URLs. |
171
+ | `headers` | JSON object of upstream request headers. |
172
+ | `body` | JSON object request body. |
173
+ | `jqFilter` | Optional jq filter applied by Restless Stream. |
174
+ | `payloadMode` | `FULL_DATA` or `JSON_PATCH`. |
175
+ | `pollingInterval` | Polling interval in seconds. |
176
+ | `pollingStrategies` | Time-windowed polling strategies. |
177
+ | `apiKey` | Optional API key for query-parameter auth in generated direct URLs. |
178
+
179
+ ## SSE Streaming
180
+
181
+ `streams.subscribeSse` yields parsed Restless Stream runtime events and skips non-JSON SSE messages.
182
+
183
+ ```ts
184
+ const controller = new AbortController();
185
+
186
+ for await (const event of client.streams.subscribeSse({
187
+ sseUrl: stream.sseUrl,
188
+ cursor: '1700000000000',
189
+ reconnect: true,
190
+ signal: controller.signal,
191
+ })) {
192
+ console.log(event.type, event.meta?.timestamp);
193
+ }
194
+ ```
195
+
196
+ Use `client.sse` or `streamSse` when you need raw SSE message metadata such as `id`, `event`, `retry`, and `data`.
197
+
198
+ ```ts
199
+ for await (const message of client.sse(stream.sseUrl, { reconnect: false })) {
200
+ console.log(message.id, message.parsed ?? message.data);
201
+ }
202
+ ```
203
+
204
+ SSE options include:
205
+
206
+ | Option | Description |
207
+ | --- | --- |
208
+ | `sseUrl` | Runtime SSE URL for `subscribeSse`. |
209
+ | `headers` | Additional runtime request headers. |
210
+ | `signal` | `AbortSignal` used to stop iteration. |
211
+ | `reconnect` | Set `false` to stop after the first connection ends or fails. Defaults to reconnecting. |
212
+ | `reconnectDelayMs` | Initial reconnect delay. Defaults to `1000`. |
213
+ | `maxReconnectDelayMs` | Maximum reconnect delay. Defaults to `30000`. |
214
+ | `cursor` | Resume from a cursor or event ID. |
215
+ | `since` | Resume from a timestamp or cursor when no event cursor is known yet. |
216
+ | `allowApiKeyInUrl` | Adds the client API key as `apiKey` in the URL. Disabled by default because URLs can leak. |
217
+
218
+ ## WebSocket Streaming
219
+
220
+ Core WebSocket helpers use the runtime `WebSocket` API and yield parsed JSON messages when possible, otherwise raw strings.
221
+
222
+ ```ts
223
+ for await (const event of client.direct.subscribeWebSocket(
224
+ {
225
+ url: 'https://api.example.com/orders',
226
+ method: 'GET',
227
+ },
228
+ {
229
+ cursor: '42',
230
+ },
231
+ )) {
232
+ console.log(event);
233
+ }
234
+ ```
235
+
236
+ ```ts
237
+ for await (const message of client.websocket('wss://stream.restlessapi.stream/ws?streamId=stream_123')) {
238
+ console.log(message);
239
+ }
240
+ ```
241
+
242
+ Browser WebSocket constructors do not support custom handshake headers. Use `@restless-stream/node` when you need `Authorization` headers for WebSocket subscriptions in Node.js.
243
+
244
+ ## Runtime URL Builders
245
+
246
+ ```ts
247
+ import {
248
+ buildDirectSseUrl,
249
+ buildDirectWebSocketUrl,
250
+ buildSseUrl,
251
+ buildWebSocketUrl,
252
+ toWebSocketStreamUrl,
253
+ } from '@restless-stream/core';
254
+
255
+ const sseUrl = buildSseUrl(undefined, { streamId: 'stream_123' }, { since: '2026-01-01T00:00:00Z' });
256
+ const wsUrl = buildWebSocketUrl(undefined, { sessionId: 'session_123' });
257
+
258
+ const directSseUrl = buildDirectSseUrl(undefined, {
259
+ url: 'https://api.example.com/orders',
260
+ method: 'POST',
261
+ body: { page: 1 },
262
+ });
263
+
264
+ const directWsUrl = buildDirectWebSocketUrl(undefined, {
265
+ url: 'https://api.example.com/orders',
266
+ });
267
+
268
+ console.log(toWebSocketStreamUrl(sseUrl));
269
+ console.log(wsUrl, directSseUrl, directWsUrl);
270
+ ```
271
+
272
+ The client also exposes URL builders bound to its `streamBaseUrl`.
273
+
274
+ ```ts
275
+ const sse = client.urls.sse({ streamId: 'stream_123' });
276
+ const websocket = client.urls.websocket({ streamId: 'stream_123' });
277
+ const directSse = client.urls.directSse({ url: 'https://api.example.com/orders' });
278
+ const directWebSocket = client.urls.directWebSocket({ url: 'https://api.example.com/orders' });
279
+ ```
280
+
281
+ ## Error Handling
282
+
283
+ REST API failures throw `RestlessApiError`.
284
+
285
+ ```ts
286
+ import { RestlessApiError } from '@restless-stream/core';
287
+
288
+ try {
289
+ await client.streams.get('missing');
290
+ } catch (error) {
291
+ if (error instanceof RestlessApiError) {
292
+ console.error(error.status, error.statusText, error.body);
293
+ }
294
+ }
295
+ ```
296
+
297
+ ## Runtime Events
298
+
299
+ Update events contain data from the upstream REST endpoint.
300
+
301
+ ```ts
302
+ type UpdateEvent<TData> = {
303
+ type: 'update';
304
+ meta: {
305
+ timestamp: string;
306
+ attempt_id: string;
307
+ status?: number;
308
+ latency_ms: number;
309
+ payload_mode_fallback?: 'FULL_DATA' | 'JSON_PATCH';
310
+ };
311
+ data: TData;
312
+ signature?: string;
313
+ };
314
+ ```
315
+
316
+ Error events contain a code and message.
317
+
318
+ ```ts
319
+ type ErrorEvent = {
320
+ type: 'error';
321
+ meta?: Partial<UpdateEvent<unknown>['meta']>;
322
+ error: {
323
+ code: string;
324
+ message: string;
325
+ };
326
+ };
327
+ ```
328
+
329
+ ## Public Exports
330
+
331
+ | Export | Purpose |
332
+ | --- | --- |
333
+ | `createRestlessClient` | Creates the core REST and runtime client. |
334
+ | `DEFAULT_API_BASE_URL` | Default REST API base URL. |
335
+ | `DEFAULT_STREAM_BASE_URL` | Default runtime stream base URL. |
336
+ | `RestlessApiError` | Error type thrown for non-2xx REST API responses. |
337
+ | `streamSse` | Low-level SSE async iterator. |
338
+ | `parseSseStream` | Parses a `ReadableStream<Uint8Array>` into SSE messages. |
339
+ | `streamWebSocket` | Low-level WebSocket async iterator. |
340
+ | `buildSseUrl`, `buildWebSocketUrl` | Build managed stream runtime URLs. |
341
+ | `buildDirectSseUrl`, `buildDirectWebSocketUrl` | Build direct stream runtime URLs. |
342
+ | `appendDirectQueryParams` | Append direct stream query params to `URLSearchParams`. |
343
+ | `normalizeBaseUrl` | Trim a trailing slash from a base URL. |
344
+ | `toWebSocketStreamUrl` | Convert an SSE URL to a WebSocket URL. |
345
+ | `withRuntimeCursor` | Add `cursor` or `since` resume params to a runtime URL. |
346
+ | `RestlessClient`, `RestlessStreamsClient`, `RestlessDirectClient`, `RestlessUrlBuilder` | Client interfaces. |
347
+ | Stream, direct, snippet, and runtime types | Request, response, event, URL, and snippet types exported from `types.ts`. |
348
+
349
+ ## Appendix: Creating an API Key
350
+
351
+ API keys authenticate requests to the Restless Stream management API and runtime streams. Keys start with `rs_` followed by a hex string:
352
+
353
+ ```
354
+ rs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
355
+ ```
356
+
357
+ ### Create a key in the dashboard
358
+
359
+ 1. Sign in at [restlessapi.stream](https://restlessapi.stream) and go to **API Keys** in the dashboard.
360
+
361
+ ![API Keys dashboard showing existing keys](docs/api-keys-dashboard.png)
362
+
363
+ 2. Click **Create API Key**, enter a name and optional description, and set an optional expiry date.
364
+
365
+ ![Create API Key modal form](docs/api-keys-create-modal.png)
366
+
367
+ 3. Copy the key immediately — it is shown only once after creation.
368
+
369
+ ```bash
370
+ export RESTLESS_API_KEY="rs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
371
+ ```
372
+
373
+ ### Pass the key to the client
374
+
375
+ ```ts
376
+ import { createRestlessClient } from '@restless-stream/core';
377
+
378
+ const client = createRestlessClient({
379
+ apiKey: process.env.RESTLESS_API_KEY,
380
+ });
381
+ ```
382
+
383
+ ## Sample Responses
384
+
385
+ ### `streams.list()`
386
+
387
+ ```ts
388
+ const page = await client.streams.list({ limit: 5, offset: 0 });
389
+ console.log(page);
390
+ ```
391
+
392
+ **Response:**
393
+
394
+ ```json
395
+ {
396
+ "streams": [],
397
+ "paginationInfo": {
398
+ "hasNextPage": false
399
+ }
400
+ }
401
+ ```
402
+
403
+ ### `streams.validateApiKey()`
404
+
405
+ ```ts
406
+ const result = await client.streams.validateApiKey();
407
+ ```
408
+
409
+ **Response:**
410
+
411
+ ```json
412
+ {
413
+ "id": "rs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
414
+ "affected": true
415
+ }
416
+ ```
417
+
418
+ ### `streams.subscribeSse()` — direct mode, live event
419
+
420
+ ```ts
421
+ const sseUrl =
422
+ 'https://stream.restlessapi.stream/?url=' +
423
+ encodeURIComponent('https://httpbin.org/get') +
424
+ '&pollingInterval=15';
425
+
426
+ for await (const event of client.streams.subscribeSse({ sseUrl, reconnect: false })) {
427
+ console.log(event);
428
+ break; // receive one event then stop
429
+ }
430
+ ```
431
+
432
+ **Event received:**
433
+
434
+ ```json
435
+ {
436
+ "type": "update",
437
+ "meta": {
438
+ "timestamp": "2026-06-07T00:49:36.562+00:00",
439
+ "attempt_id": "a59d80d4-7d36-4019-b2c6-40cc8b8742ce",
440
+ "status": 200,
441
+ "latency_ms": 43
442
+ },
443
+ "data": {
444
+ "args": {},
445
+ "headers": {
446
+ "Accept": "*/*",
447
+ "Host": "httpbin.org",
448
+ "User-Agent": "Mozilla/5.0 (compatible; OurInternalService/1.0)",
449
+ "X-Amzn-Trace-Id": "Root=1-6a24c020-316d1d396470c0900ce68495"
450
+ },
451
+ "origin": "140.82.14.127",
452
+ "url": "https://httpbin.org/get"
453
+ }
454
+ }
455
+ ```
456
+
457
+ ## Development
458
+
459
+ ```bash
460
+ cd packages/typescript
461
+ pnpm install
462
+ pnpm --filter @restless-stream/core typecheck
463
+ pnpm --filter @restless-stream/core test
464
+ pnpm --filter @restless-stream/core build
465
+ ```
package/package.json CHANGED
@@ -1,7 +1,25 @@
1
1
  {
2
2
  "name": "@restless-stream/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "TypeScript SDK for Restless Stream.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/restless-stream/sdk.git",
9
+ "directory": "packages/typescript/core"
10
+ },
11
+ "homepage": "https://github.com/restless-stream/sdk/tree/main/packages/typescript/core#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/restless-stream/sdk/issues"
14
+ },
15
+ "keywords": [
16
+ "restless-stream",
17
+ "sdk",
18
+ "sse",
19
+ "websocket",
20
+ "streaming",
21
+ "typescript"
22
+ ],
5
23
  "type": "module",
6
24
  "main": "./dist/index.cjs",
7
25
  "module": "./dist/index.js",
@@ -16,6 +34,9 @@
16
34
  "files": [
17
35
  "dist"
18
36
  ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
19
40
  "sideEffects": false,
20
41
  "devDependencies": {
21
42
  "tsup": "^8.5.1",