@saltify/milky-node-sdk 0.1.0-beta.5 → 0.1.0-beta.6

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/index.d.ts CHANGED
@@ -69,20 +69,19 @@ type EventCollection = {
69
69
  };
70
70
 
71
71
  declare class MilkyClient {
72
- private readonly accessToken?;
72
+ private readonly eventEmitter;
73
73
  private readonly httpApiUrl;
74
74
  private readonly eventUrl;
75
- private readonly wsClient;
76
75
  private readonly fetchHeader;
77
- private readonly eventEmitter;
76
+ private disposeCore?;
78
77
  /**
79
- * @param address The address of the Milky API server
80
- * @param port The port of the Milky API server
81
- * @param base The base path for the Milky API
78
+ * @param authority The authority of the Milky API (value of `new URL('https://example.com:443/some-path').host`)
79
+ * @param basePath The base path for the Milky API
82
80
  * @param accessToken The access token for authentication (optional)
83
- * @param useHttps Whether to use HTTPS (default: false)
81
+ * @param useTLS Whether to use HTTPS and WSS (default: false)
82
+ * @param useSSE Whether to use Server-Sent Events for event streaming (default: false)
84
83
  */
85
- constructor(address: string, port: number, base: string, accessToken?: string | undefined, useHttps?: boolean);
84
+ constructor(authority: string, basePath?: `/${string}/` | '/', accessToken?: string, useTLS?: boolean, useSSE?: boolean);
86
85
  /**
87
86
  * Call a Milky API method.
88
87
  * @param method The API method to call
@@ -96,6 +95,13 @@ declare class MilkyClient {
96
95
  * @param listener The listener function to call when the event is emitted
97
96
  */
98
97
  onEvent<K extends keyof EventCollection>(eventType: K, listener: (data: EventCollection[K]) => void): Promise<void>;
98
+ private createSSE;
99
+ private createWebsocket;
100
+ /**
101
+ * Release the WebSocket / Server Sent Event connection.
102
+ */
103
+ dispose(): void;
104
+ [Symbol.dispose]: () => void;
99
105
  }
100
106
 
101
107
  export { MilkyClient };
package/dist/index.js CHANGED
@@ -1,40 +1,350 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+ var __commonJS = (cb, mod) => function __require2() {
14
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // ../../node_modules/.pnpm/eventsource-parser@3.0.6/node_modules/eventsource-parser/dist/index.cjs
34
+ var require_dist = __commonJS({
35
+ "../../node_modules/.pnpm/eventsource-parser@3.0.6/node_modules/eventsource-parser/dist/index.cjs"(exports) {
36
+ "use strict";
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ var ParseError = class extends Error {
39
+ constructor(message, options) {
40
+ super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
41
+ }
42
+ };
43
+ function noop(_arg) {
44
+ }
45
+ function createParser(callbacks) {
46
+ if (typeof callbacks == "function")
47
+ throw new TypeError(
48
+ "`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?"
49
+ );
50
+ const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks;
51
+ let incompleteLine = "", isFirstChunk = true, id, data = "", eventType = "";
52
+ function feed(newChunk) {
53
+ const chunk = isFirstChunk ? newChunk.replace(/^\xEF\xBB\xBF/, "") : newChunk, [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`);
54
+ for (const line of complete)
55
+ parseLine(line);
56
+ incompleteLine = incomplete, isFirstChunk = false;
57
+ }
58
+ function parseLine(line) {
59
+ if (line === "") {
60
+ dispatchEvent();
61
+ return;
62
+ }
63
+ if (line.startsWith(":")) {
64
+ onComment && onComment(line.slice(line.startsWith(": ") ? 2 : 1));
65
+ return;
66
+ }
67
+ const fieldSeparatorIndex = line.indexOf(":");
68
+ if (fieldSeparatorIndex !== -1) {
69
+ const field = line.slice(0, fieldSeparatorIndex), offset = line[fieldSeparatorIndex + 1] === " " ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset);
70
+ processField(field, value, line);
71
+ return;
72
+ }
73
+ processField(line, "", line);
74
+ }
75
+ function processField(field, value, line) {
76
+ switch (field) {
77
+ case "event":
78
+ eventType = value;
79
+ break;
80
+ case "data":
81
+ data = `${data}${value}
82
+ `;
83
+ break;
84
+ case "id":
85
+ id = value.includes("\0") ? void 0 : value;
86
+ break;
87
+ case "retry":
88
+ /^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
89
+ new ParseError(`Invalid \`retry\` value: "${value}"`, {
90
+ type: "invalid-retry",
91
+ value,
92
+ line
93
+ })
94
+ );
95
+ break;
96
+ default:
97
+ onError(
98
+ new ParseError(
99
+ `Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`,
100
+ { type: "unknown-field", field, value, line }
101
+ )
102
+ );
103
+ break;
104
+ }
105
+ }
106
+ function dispatchEvent() {
107
+ data.length > 0 && onEvent({
108
+ id,
109
+ event: eventType || void 0,
110
+ // If the data buffer's last character is a U+000A LINE FEED (LF) character,
111
+ // then remove the last character from the data buffer.
112
+ data: data.endsWith(`
113
+ `) ? data.slice(0, -1) : data
114
+ }), id = void 0, data = "", eventType = "";
115
+ }
116
+ function reset(options = {}) {
117
+ incompleteLine && options.consume && parseLine(incompleteLine), isFirstChunk = true, id = void 0, data = "", eventType = "", incompleteLine = "";
118
+ }
119
+ return { feed, reset };
120
+ }
121
+ function splitLines(chunk) {
122
+ const lines = [];
123
+ let incompleteLine = "", searchIndex = 0;
124
+ for (; searchIndex < chunk.length; ) {
125
+ const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
126
+ `, searchIndex);
127
+ let lineEnd = -1;
128
+ if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = Math.min(crIndex, lfIndex) : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) {
129
+ incompleteLine = chunk.slice(searchIndex);
130
+ break;
131
+ } else {
132
+ const line = chunk.slice(searchIndex, lineEnd);
133
+ lines.push(line), searchIndex = lineEnd + 1, chunk[searchIndex - 1] === "\r" && chunk[searchIndex] === `
134
+ ` && searchIndex++;
135
+ }
136
+ }
137
+ return [lines, incompleteLine];
138
+ }
139
+ exports.ParseError = ParseError;
140
+ exports.createParser = createParser;
141
+ }
142
+ });
143
+
144
+ // ../../node_modules/.pnpm/eventsource-client@1.1.4/node_modules/eventsource-client/dist/node.js
145
+ var require_node = __commonJS({
146
+ "../../node_modules/.pnpm/eventsource-client@1.1.4/node_modules/eventsource-client/dist/node.js"(exports) {
147
+ "use strict";
148
+ Object.defineProperty(exports, "__esModule", { value: true });
149
+ var node_stream = __require("stream");
150
+ var eventsourceParser = require_dist();
151
+ var CONNECTING2 = "connecting";
152
+ var OPEN2 = "open";
153
+ var CLOSED2 = "closed";
154
+ var noop = () => {
155
+ };
156
+ function createEventSource$1(optionsOrUrl, { getStream: getStream2 }) {
157
+ const options = typeof optionsOrUrl == "string" || optionsOrUrl instanceof URL ? { url: optionsOrUrl } : optionsOrUrl, { onMessage, onConnect = noop, onDisconnect = noop, onScheduleReconnect = noop } = options, { fetch: fetch2, url, initialLastEventId } = validate(options), requestHeaders = { ...options.headers }, onCloseSubscribers = [], subscribers = onMessage ? [onMessage] : [], emit = (event) => subscribers.forEach((fn) => fn(event)), parser = eventsourceParser.createParser({ onEvent, onRetry });
158
+ let request, currentUrl = url.toString(), controller = new AbortController(), lastEventId = initialLastEventId, reconnectMs = 2e3, reconnectTimer, readyState = CLOSED2;
159
+ return connect(), {
160
+ close,
161
+ connect,
162
+ [Symbol.iterator]: () => {
163
+ throw new Error(
164
+ "EventSource does not support synchronous iteration. Use `for await` instead."
165
+ );
166
+ },
167
+ [Symbol.asyncIterator]: getEventIterator,
168
+ get lastEventId() {
169
+ return lastEventId;
170
+ },
171
+ get url() {
172
+ return currentUrl;
173
+ },
174
+ get readyState() {
175
+ return readyState;
176
+ }
177
+ };
178
+ function connect() {
179
+ request || (readyState = CONNECTING2, controller = new AbortController(), request = fetch2(url, getRequestOptions()).then(onFetchResponse).catch((err) => {
180
+ request = null, !(err.name === "AbortError" || err.type === "aborted" || controller.signal.aborted) && scheduleReconnect();
181
+ }));
182
+ }
183
+ function close() {
184
+ readyState = CLOSED2, controller.abort(), parser.reset(), clearTimeout(reconnectTimer), onCloseSubscribers.forEach((fn) => fn());
185
+ }
186
+ function getEventIterator() {
187
+ const pullQueue = [], pushQueue = [];
188
+ function pullValue() {
189
+ return new Promise((resolve) => {
190
+ const value = pushQueue.shift();
191
+ value ? resolve({ value, done: false }) : pullQueue.push(resolve);
192
+ });
193
+ }
194
+ const pushValue = function(value) {
195
+ const resolve = pullQueue.shift();
196
+ resolve ? resolve({ value, done: false }) : pushQueue.push(value);
197
+ };
198
+ function unsubscribe() {
199
+ for (subscribers.splice(subscribers.indexOf(pushValue), 1); pullQueue.shift(); )
200
+ ;
201
+ for (; pushQueue.shift(); )
202
+ ;
203
+ }
204
+ function onClose() {
205
+ const resolve = pullQueue.shift();
206
+ resolve && (resolve({ done: true, value: void 0 }), unsubscribe());
207
+ }
208
+ return onCloseSubscribers.push(onClose), subscribers.push(pushValue), {
209
+ next() {
210
+ return readyState === CLOSED2 ? this.return() : pullValue();
211
+ },
212
+ return() {
213
+ return unsubscribe(), Promise.resolve({ done: true, value: void 0 });
214
+ },
215
+ throw(error) {
216
+ return unsubscribe(), Promise.reject(error);
217
+ },
218
+ [Symbol.asyncIterator]() {
219
+ return this;
220
+ }
221
+ };
222
+ }
223
+ function scheduleReconnect() {
224
+ onScheduleReconnect({ delay: reconnectMs }), readyState = CONNECTING2, reconnectTimer = setTimeout(connect, reconnectMs);
225
+ }
226
+ async function onFetchResponse(response) {
227
+ onConnect(), parser.reset();
228
+ const { body, redirected, status } = response;
229
+ if (status === 204) {
230
+ onDisconnect(), close();
231
+ return;
232
+ }
233
+ if (!body)
234
+ throw new Error("Missing response body");
235
+ redirected && (currentUrl = response.url);
236
+ const stream = getStream2(body), decoder = new TextDecoder(), reader = stream.getReader();
237
+ let open = true;
238
+ readyState = OPEN2;
239
+ do {
240
+ const { done, value } = await reader.read();
241
+ value && parser.feed(decoder.decode(value, { stream: !done })), done && (open = false, request = null, parser.reset(), scheduleReconnect(), onDisconnect());
242
+ } while (open);
243
+ }
244
+ function onEvent(msg) {
245
+ typeof msg.id == "string" && (lastEventId = msg.id), emit(msg);
246
+ }
247
+ function onRetry(ms) {
248
+ reconnectMs = ms;
249
+ }
250
+ function getRequestOptions() {
251
+ const { mode, credentials, body, method, redirect, referrer, referrerPolicy } = options, headers = { Accept: "text/event-stream", ...requestHeaders, ...lastEventId ? { "Last-Event-ID": lastEventId } : void 0 };
252
+ return {
253
+ mode,
254
+ credentials,
255
+ body,
256
+ method,
257
+ redirect,
258
+ referrer,
259
+ referrerPolicy,
260
+ headers,
261
+ cache: "no-store",
262
+ signal: controller.signal
263
+ };
264
+ }
265
+ }
266
+ function validate(options) {
267
+ const fetch2 = options.fetch || globalThis.fetch;
268
+ if (!isFetchLike(fetch2))
269
+ throw new Error("No fetch implementation provided, and one was not found on the global object.");
270
+ if (typeof AbortController != "function")
271
+ throw new Error("Missing AbortController implementation");
272
+ const { url, initialLastEventId } = options;
273
+ if (typeof url != "string" && !(url instanceof URL))
274
+ throw new Error("Invalid URL provided - must be string or URL instance");
275
+ if (typeof initialLastEventId != "string" && initialLastEventId !== void 0)
276
+ throw new Error("Invalid initialLastEventId provided - must be string or undefined");
277
+ return { fetch: fetch2, url, initialLastEventId };
278
+ }
279
+ function isFetchLike(fetch2) {
280
+ return typeof fetch2 == "function";
281
+ }
282
+ var nodeAbstractions = {
283
+ getStream
284
+ };
285
+ function createEventSource2(optionsOrUrl) {
286
+ return createEventSource$1(optionsOrUrl, nodeAbstractions);
287
+ }
288
+ function getStream(body) {
289
+ if ("getReader" in body)
290
+ return body;
291
+ if (typeof body.pipe != "function" || typeof body.on != "function")
292
+ throw new Error("Invalid response body, expected a web or node.js stream");
293
+ if (typeof node_stream.Readable.toWeb != "function")
294
+ throw new Error("Node.js 18 or higher required (`Readable.toWeb()` not defined)");
295
+ return node_stream.Readable.toWeb(node_stream.Readable.from(body));
296
+ }
297
+ exports.CLOSED = CLOSED2;
298
+ exports.CONNECTING = CONNECTING2;
299
+ exports.OPEN = OPEN2;
300
+ exports.createEventSource = createEventSource2;
301
+ }
302
+ });
303
+
1
304
  // src/index.ts
2
305
  import EventEmitter from "events";
3
- if (typeof globalThis.WebSocket === "undefined") {
4
- console.error("WebSocket is not supported in this environment.");
5
- process.exit(1);
6
- }
306
+
307
+ // ../../node_modules/.pnpm/eventsource-client@1.1.4/node_modules/eventsource-client/dist/node.cjs.mjs
308
+ var import_node = __toESM(require_node(), 1);
309
+ var CLOSED = import_node.default.CLOSED;
310
+ var CONNECTING = import_node.default.CONNECTING;
311
+ var OPEN = import_node.default.OPEN;
312
+ var createEventSource = import_node.default.createEventSource;
313
+
314
+ // src/index.ts
315
+ var combineUrl = (base, path) => {
316
+ const baseUrl = base.endsWith("/") ? base.slice(0, -1) : base;
317
+ const pathUrl = path.startsWith("/") ? path.slice(1) : path;
318
+ return `${baseUrl}/${pathUrl}`;
319
+ };
7
320
  var MilkyClient = class {
321
+ eventEmitter;
322
+ httpApiUrl;
323
+ eventUrl;
324
+ fetchHeader;
325
+ disposeCore;
8
326
  /**
9
- * @param address The address of the Milky API server
10
- * @param port The port of the Milky API server
11
- * @param base The base path for the Milky API
327
+ * @param authority The authority of the Milky API (value of `new URL('https://example.com:443/some-path').host`)
328
+ * @param basePath The base path for the Milky API
12
329
  * @param accessToken The access token for authentication (optional)
13
- * @param useHttps Whether to use HTTPS (default: false)
330
+ * @param useTLS Whether to use HTTPS and WSS (default: false)
331
+ * @param useSSE Whether to use Server-Sent Events for event streaming (default: false)
14
332
  */
15
- constructor(address, port, base, accessToken, useHttps = false) {
16
- this.accessToken = accessToken;
17
- base = base.endsWith("/") ? base.slice(0, -1) : base;
18
- this.httpApiUrl = `${useHttps ? "https" : "http"}://${address}:${port}${base}/api`;
19
- this.eventUrl = accessToken ? `${useHttps ? "wss" : "ws"}://${address}:${port}${base}/event?access_token=${accessToken}` : `${useHttps ? "wss" : "ws"}://${address}:${port}${base}/event`;
20
- this.fetchHeader = this.accessToken ? {
21
- "Content-Type": "application/json",
22
- "Authorization": `Bearer ${this.accessToken}`
23
- } : {
24
- "Content-Type": "application/json"
25
- };
26
- this.wsClient = new WebSocket(this.eventUrl);
333
+ constructor(authority, basePath, accessToken, useTLS, useSSE) {
334
+ const httpProtocol = useTLS ? "https" : "http";
335
+ const urlFragment = `${authority}${basePath}`;
336
+ const httpUrlBase = `${httpProtocol}://${urlFragment}`;
337
+ this.fetchHeader = {};
338
+ if (accessToken) this.fetchHeader["Authorization"] = `Bearer ${accessToken}`;
27
339
  this.eventEmitter = new EventEmitter();
28
- this.wsClient.addEventListener("message", (event) => {
29
- const data = JSON.parse(event.data);
30
- this.eventEmitter.emit(data.event_type, data);
31
- });
340
+ this.httpApiUrl = combineUrl(httpUrlBase, "api");
341
+ this.eventUrl = combineUrl(httpUrlBase, "api");
342
+ if (!useSSE) {
343
+ this.createWebsocket();
344
+ } else {
345
+ this.createSSE();
346
+ }
32
347
  }
33
- httpApiUrl;
34
- eventUrl;
35
- wsClient;
36
- fetchHeader;
37
- eventEmitter;
38
348
  /**
39
349
  * Call a Milky API method.
40
350
  * @param method The API method to call
@@ -42,9 +352,13 @@ var MilkyClient = class {
42
352
  * If the API does not require any input, you can leave it empty.
43
353
  */
44
354
  async callApi(method, ...input) {
45
- const response = await fetch(`${this.httpApiUrl}/${method}`, {
355
+ const response = await fetch(combineUrl(this.httpApiUrl, method), {
46
356
  method: "POST",
47
- headers: this.fetchHeader,
357
+ headers: {
358
+ Accept: "application/json",
359
+ "Content-Type": "application/json",
360
+ ...this.fetchHeader
361
+ },
48
362
  body: JSON.stringify(input[0] ?? {})
49
363
  });
50
364
  const callResult = await response.json();
@@ -61,6 +375,42 @@ var MilkyClient = class {
61
375
  async onEvent(eventType, listener) {
62
376
  this.eventEmitter.on(eventType, listener);
63
377
  }
378
+ createSSE() {
379
+ const sse = createEventSource({
380
+ url: this.eventUrl,
381
+ headers: {
382
+ Accept: "text/event-stream",
383
+ ...this.fetchHeader
384
+ },
385
+ onMessage: ({ event, data }) => {
386
+ if (event !== "milky_event") return;
387
+ const parsedData = JSON.parse(data);
388
+ this.eventEmitter.emit(parsedData.event_type, parsedData);
389
+ }
390
+ });
391
+ sse.connect();
392
+ this.disposeCore = sse.close;
393
+ }
394
+ createWebsocket() {
395
+ if (!globalThis.WebSocket) throw new Error("WebSocket is not supported in this environment.");
396
+ const ws = new WebSocket(this.eventUrl, {
397
+ headers: {
398
+ ...this.fetchHeader
399
+ }
400
+ });
401
+ ws.addEventListener("message", (event) => {
402
+ const data = JSON.parse(event.data);
403
+ this.eventEmitter.emit(data.event_type, data);
404
+ });
405
+ this.disposeCore = ws.close;
406
+ }
407
+ /**
408
+ * Release the WebSocket / Server Sent Event connection.
409
+ */
410
+ dispose() {
411
+ this.disposeCore?.();
412
+ }
413
+ [Symbol.dispose] = this.dispose;
64
414
  };
65
415
  export {
66
416
  MilkyClient
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltify/milky-node-sdk",
3
3
  "type": "module",
4
- "version": "0.1.0-beta.5",
4
+ "version": "0.1.0-beta.6",
5
5
  "description": "Node.js SDK for Milky protocol",
6
6
  "main": "dist/index.js",
7
7
  "typings": "dist/index.d.ts",
@@ -16,13 +16,15 @@
16
16
  },
17
17
  "license": "CC0-1.0",
18
18
  "devDependencies": {
19
- "@types/node": "^22.17.2"
19
+ "@types/node": "^22.17.2",
20
+ "eventsource-client": "^1.1.4"
20
21
  },
21
22
  "dependencies": {
22
- "@saltify/milky-types": "^1.0.0-draft.12",
23
+ "@saltify/milky-types": "^1.0.0-draft.14",
23
24
  "zod": "^4.1.5"
24
25
  },
25
26
  "scripts": {
26
- "build": "tsup"
27
+ "build": "npm run gen:apis && tsup",
28
+ "gen:apis": "tsx ./scripts/gen_code.ts"
27
29
  }
28
30
  }