@nilejs/client 0.0.3 → 1.0.1

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.cjs CHANGED
@@ -1,215 +1,367 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- buildFormData: () => buildFormData,
24
- createNileClient: () => createNileClient,
25
- sendUploadRequest: () => sendUploadRequest
26
- });
27
- module.exports = __toCommonJS(index_exports);
28
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/result.ts
3
+ /**
4
+ * A successful client result. `data` mirrors `value` so code written for
5
+ * earlier clients keeps working. `message` is present only when the server sent one.
6
+ */
7
+ function clientOk(value, message) {
8
+ return {
9
+ isOk: true,
10
+ isErr: false,
11
+ value,
12
+ error: null,
13
+ ...message === void 0 ? {} : { message },
14
+ data: value
15
+ };
16
+ }
17
+ /**
18
+ * A failed client result with a readable message and any data the server
19
+ * attached to the failure. `data` stays null, as it was for earlier clients.
20
+ */
21
+ function clientErr(error, details = null) {
22
+ return {
23
+ isOk: false,
24
+ isErr: true,
25
+ value: null,
26
+ error,
27
+ details,
28
+ data: null
29
+ };
30
+ }
29
31
  // src/safe-try.ts
32
+ /**
33
+ * Gracefully handles both synchronous and asynchronous operations by returning a result object.
34
+ * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.
35
+ *
36
+ * @param fn - The function to execute (sync or async)
37
+ * @returns A result object containing either the value or the error
38
+ *
39
+ * @example
40
+ * const { isOk, value, error } = await safeTry(() => fetch(url));
41
+ */
30
42
  function safeTry(fn) {
31
- try {
32
- const result = fn();
33
- if (result instanceof Promise) {
34
- return result.then(
35
- (value) => ({
36
- isOk: true,
37
- value,
38
- isErr: false,
39
- error: null
40
- })
41
- ).catch(
42
- (error) => ({
43
- isOk: false,
44
- value: null,
45
- isErr: true,
46
- error
47
- })
48
- );
49
- }
50
- return { isOk: true, value: result, isErr: false, error: null };
51
- } catch (error) {
52
- return { isOk: false, value: null, isErr: true, error };
53
- }
43
+ try {
44
+ const result = fn();
45
+ if (result instanceof Promise) return result.then((value) => ({
46
+ isOk: true,
47
+ value,
48
+ isErr: false,
49
+ error: null
50
+ })).catch((error) => ({
51
+ isOk: false,
52
+ value: null,
53
+ isErr: true,
54
+ error
55
+ }));
56
+ return {
57
+ isOk: true,
58
+ value: result,
59
+ isErr: false,
60
+ error: null
61
+ };
62
+ } catch (error) {
63
+ return {
64
+ isOk: false,
65
+ value: null,
66
+ isErr: true,
67
+ error
68
+ };
69
+ }
54
70
  }
55
-
56
71
  // src/request.ts
57
- async function sendNileRequest(config, intent, service, action, payload, options = {}) {
58
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
59
- const controller = new AbortController();
60
- const timeoutId = setTimeout(() => controller.abort(), timeout);
61
- const requestResult = await safeTry(async () => {
62
- const response = await fetch(`${config.baseUrl}/services`, {
63
- method: "POST",
64
- headers: {
65
- "Content-Type": "application/json",
66
- ...config.headers,
67
- ...fetchOptions.headers
68
- },
69
- credentials: config.credentials,
70
- body: JSON.stringify({
71
- intent,
72
- service,
73
- action,
74
- payload: payload ?? {}
75
- }),
76
- signal: controller.signal,
77
- ...fetchOptions
78
- });
79
- clearTimeout(timeoutId);
80
- const json = await response.json();
81
- return json;
82
- });
83
- if (requestResult.isErr) {
84
- return resolveError(requestResult.error);
85
- }
86
- const result = requestResult.value;
87
- if (!result.status) {
88
- return { error: result.message, data: result.data };
89
- }
90
- return { error: null, data: result.data };
72
+ /** Send one action call; the deadline covers both fetch and reading its JSON body. */
73
+ async function sendRequest(config, body, multipart, options) {
74
+ const { timeout = config.timeout ?? 3e4, headers, signal, ...fetchOptions } = options;
75
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2147483647) return clientErr("Action timeout must be a positive integer up to 2147483647 milliseconds.");
76
+ const controller = new AbortController();
77
+ const abort = () => controller.abort(signal?.reason);
78
+ if (signal?.aborted) abort();
79
+ else signal?.addEventListener("abort", abort, { once: true });
80
+ let timedOut = false;
81
+ const timeoutId = setTimeout(() => {
82
+ timedOut = true;
83
+ controller.abort();
84
+ }, timeout);
85
+ try {
86
+ const requestResult = await safeTry(async () => {
87
+ const mergedHeaders = new Headers(config.headers);
88
+ new Headers(headers).forEach((value, key) => {
89
+ mergedHeaders.set(key, value);
90
+ });
91
+ if (multipart) mergedHeaders.delete("content-type");
92
+ else mergedHeaders.set("content-type", "application/json");
93
+ const json = await (await fetch(`${config.baseUrl.replace(/\/+$/, "")}/services`, {
94
+ ...fetchOptions,
95
+ method: "POST",
96
+ headers: mergedHeaders,
97
+ credentials: fetchOptions.credentials ?? config.credentials,
98
+ body: body(),
99
+ signal: controller.signal
100
+ })).json();
101
+ if (json === null || typeof json !== "object" || !("status" in json) || typeof json.status !== "boolean" || !("message" in json) || typeof json.message !== "string" || !("data" in json)) throw new Error("Invalid Nile action response.");
102
+ return json;
103
+ });
104
+ if (requestResult.isErr) {
105
+ if (timedOut) return clientErr("Action timed out.");
106
+ if (signal?.aborted) return clientErr("Action cancelled.");
107
+ return resolveError(requestResult.error);
108
+ }
109
+ const result = requestResult.value;
110
+ if (!result.status) return clientErr(result.message, result.data ?? null);
111
+ return clientOk(result.data, result.message);
112
+ } finally {
113
+ clearTimeout(timeoutId);
114
+ signal?.removeEventListener("abort", abort);
115
+ }
116
+ }
117
+ /** JSON action calls preserve the server's message and failure data unchanged. */
118
+ function sendNileRequest(config, intent, service, action, payload, options = {}) {
119
+ return sendRequest(config, () => JSON.stringify({
120
+ intent,
121
+ service,
122
+ action,
123
+ payload: payload ?? {}
124
+ }), false, options);
91
125
  }
92
- async function sendUploadRequest(config, service, action, files, fields, options = {}) {
93
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
94
- const controller = new AbortController();
95
- const timeoutId = setTimeout(() => controller.abort(), timeout);
96
- const formData = buildFormData(service, action, files, fields);
97
- const requestResult = await safeTry(async () => {
98
- const { "Content-Type": _ct, ...safeHeaders } = config.headers ?? {};
99
- const { "Content-Type": _ct2, ...safeFetchHeaders } = fetchOptions.headers ?? {};
100
- const response = await fetch(`${config.baseUrl}/services`, {
101
- method: "POST",
102
- headers: {
103
- ...safeHeaders,
104
- ...safeFetchHeaders
105
- },
106
- credentials: config.credentials,
107
- body: formData,
108
- signal: controller.signal,
109
- ...fetchOptions
110
- });
111
- clearTimeout(timeoutId);
112
- const json = await response.json();
113
- return json;
114
- });
115
- if (requestResult.isErr) {
116
- return resolveError(requestResult.error);
117
- }
118
- const result = requestResult.value;
119
- if (!result.status) {
120
- return { error: result.message, data: result.data };
121
- }
122
- return { error: null, data: result.data };
126
+ /** Let the runtime set the multipart boundary; retain the same action Result contract. */
127
+ function sendUploadRequest(config, service, action, files, fields, options = {}) {
128
+ return sendRequest(config, () => buildFormData(service, action, files, fields), true, options);
123
129
  }
130
+ /**
131
+ * Builds a FormData instance with RPC routing fields and file/field entries.
132
+ * The server expects 'intent', 'service', 'action' as string fields.
133
+ */
124
134
  function buildFormData(service, action, files, fields) {
125
- const formData = new FormData();
126
- formData.append("intent", "execute");
127
- formData.append("service", service);
128
- formData.append("action", action);
129
- if (fields) {
130
- for (const [key, value] of Object.entries(fields)) {
131
- formData.append(key, value);
132
- }
133
- }
134
- for (const [key, value] of Object.entries(files)) {
135
- if (Array.isArray(value)) {
136
- for (const file of value) {
137
- formData.append(key, file);
138
- }
139
- } else {
140
- formData.append(key, value);
141
- }
142
- }
143
- return formData;
135
+ const formData = new FormData();
136
+ formData.append("intent", "execute");
137
+ formData.append("service", service);
138
+ formData.append("action", action);
139
+ if (fields) for (const [key, value] of Object.entries(fields)) formData.append(key, value);
140
+ for (const [key, value] of Object.entries(files)) if (Array.isArray(value)) for (const file of value) formData.append(key, file);
141
+ else formData.append(key, value);
142
+ return formData;
144
143
  }
144
+ /** Convert local transport errors to the same client failure shape. */
145
145
  function resolveError(error) {
146
- let errorMsg = "An unknown error occurred";
147
- if (error instanceof Error) {
148
- errorMsg = error.name === "AbortError" ? "Request timed out" : error.message;
149
- } else {
150
- errorMsg = String(error);
151
- }
152
- return { error: errorMsg, data: null };
146
+ return clientErr(error instanceof Error ? error.message : String(error));
153
147
  }
154
-
155
148
  // src/create-client.ts
149
+ /**
150
+ * Creates a type-safe Nile client for interacting with backend services.
151
+ *
152
+ * @param config - The client configuration including baseUrl
153
+ * @returns A factory object with invoke, explore, schema, and upload methods
154
+ *
155
+ * @example
156
+ * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
157
+ * const result = await nile.invoke({
158
+ * service: "tasks",
159
+ * action: "create",
160
+ * payload: { title: "Buy milk" }
161
+ * });
162
+ * if (result.isErr) {
163
+ * console.error(result.error);
164
+ * } else {
165
+ * console.log(result.value);
166
+ * }
167
+ */
156
168
  function createNileClient(config) {
157
- return {
158
- /** Invoke a service action through the backend engine pipeline */
159
- invoke: async (params) => {
160
- const { service, action, payload, fetchOptions } = params;
161
- const result = await sendNileRequest(
162
- config,
163
- "execute",
164
- service,
165
- action,
166
- payload,
167
- fetchOptions
168
- );
169
- return result;
170
- },
171
- /** Discover available services and actions (supports "*" wildcard) */
172
- explore: async (params) => {
173
- const { service, action, fetchOptions } = params;
174
- return await sendNileRequest(
175
- config,
176
- "explore",
177
- service,
178
- action,
179
- {},
180
- fetchOptions
181
- );
182
- },
183
- /** Get action schemas as JSON Schema (supports "*" wildcard) */
184
- schema: async (params) => {
185
- const { service, action, fetchOptions } = params;
186
- return await sendNileRequest(
187
- config,
188
- "schema",
189
- service,
190
- action,
191
- {},
192
- fetchOptions
193
- );
194
- },
195
- /** Upload files to an action via multipart form-data */
196
- upload: async (params) => {
197
- const { service, action, files, fields, fetchOptions } = params;
198
- return await sendUploadRequest(
199
- config,
200
- service,
201
- action,
202
- files,
203
- fields,
204
- fetchOptions
205
- );
206
- }
207
- };
169
+ return {
170
+ /** Invoke a service action through the backend engine pipeline */
171
+ invoke: async (params) => {
172
+ const { service, action, payload, fetchOptions } = params;
173
+ return await sendNileRequest(config, "execute", service, action, payload, fetchOptions);
174
+ },
175
+ /** Discover available services and actions (supports "*" wildcard) */
176
+ explore: async (params) => {
177
+ const { service, action, fetchOptions } = params;
178
+ return await sendNileRequest(config, "explore", service, action, {}, fetchOptions);
179
+ },
180
+ /** Get action schemas as JSON Schema (supports "*" wildcard) */
181
+ schema: async (params) => {
182
+ const { service, action, fetchOptions } = params;
183
+ return await sendNileRequest(config, "schema", service, action, {}, fetchOptions);
184
+ },
185
+ /** Upload files to an action via multipart form-data */
186
+ upload: async (params) => {
187
+ const { service, action, files, fields, fetchOptions } = params;
188
+ return await sendUploadRequest(config, service, action, files, fields, fetchOptions);
189
+ }
190
+ };
191
+ }
192
+ // src/socket-client.ts
193
+ /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
194
+ function createNileSocketClient(config) {
195
+ const timeout = config.timeout ?? 3e4;
196
+ if (!Number.isSafeInteger(timeout) || timeout < 1) throw new Error("Socket timeout must be positive");
197
+ let socket;
198
+ let connecting;
199
+ let closed = false;
200
+ const listeners = /* @__PURE__ */ new Set();
201
+ const pending = /* @__PURE__ */ new Set();
202
+ const fail = (error) => clientErr(error);
203
+ const settleCallback = (fn) => {
204
+ Promise.resolve().then(fn).catch(() => void 0);
205
+ };
206
+ const request = async (event, input) => {
207
+ const connected = await connect();
208
+ if (connected.isErr) return fail(connected.error);
209
+ const current = connected.value;
210
+ if (!current.connected || closed) return fail("Socket disconnected");
211
+ return new Promise((resolve) => {
212
+ let settled = false;
213
+ const finish = (result) => {
214
+ if (settled) return;
215
+ settled = true;
216
+ pending.delete(cancel);
217
+ clearTimeout(timer);
218
+ resolve(result);
219
+ };
220
+ const cancel = () => finish(fail("Socket disconnected"));
221
+ const timer = setTimeout(() => finish(fail("Socket acknowledgement timed out")), timeout);
222
+ pending.add(cancel);
223
+ current.emit(event, input, (result) => {
224
+ if (!result || typeof result !== "object" || !("error" in result) || !("data" in result)) {
225
+ finish(fail("Invalid socket response"));
226
+ return;
227
+ }
228
+ if (typeof result.error === "string") {
229
+ finish(clientErr(result.error, result.data));
230
+ return;
231
+ }
232
+ finish(clientOk(result.data));
233
+ });
234
+ });
235
+ };
236
+ const rejoin = async () => {
237
+ for (const listener of listeners) {
238
+ if (!listener.active) continue;
239
+ const result = await request("nile:receive", {
240
+ topic: listener.topic,
241
+ scope: listener.scope
242
+ });
243
+ if (result.isOk) {
244
+ listener.normalizedScope = result.value?.scope;
245
+ continue;
246
+ }
247
+ const message = result.error;
248
+ settleCallback(() => config.onSubscriptionError?.({
249
+ topic: listener.topic,
250
+ error: message
251
+ }));
252
+ }
253
+ };
254
+ const connect = () => {
255
+ if (closed) return Promise.resolve(fail("Socket client is closed"));
256
+ if (socket?.connected) return Promise.resolve(clientOk(socket));
257
+ if (connecting) return connecting;
258
+ connecting = import("socket.io-client").then(({ io }) => new Promise((resolve) => {
259
+ if (closed) {
260
+ resolve(fail("Socket client is closed"));
261
+ return;
262
+ }
263
+ const current = socket ?? io(config.baseUrl, {
264
+ path: config.path ?? "/socket.io",
265
+ autoConnect: false,
266
+ withCredentials: true,
267
+ timeout,
268
+ auth: (done) => {
269
+ Promise.resolve().then(() => typeof config.auth === "function" ? config.auth() : config.auth ?? {}).then(done).catch(() => done({}));
270
+ }
271
+ });
272
+ if (!socket) {
273
+ socket = current;
274
+ current.on("disconnect", () => {
275
+ for (const cancel of [...pending]) cancel();
276
+ });
277
+ current.on("connect", () => {
278
+ rejoin();
279
+ });
280
+ current.on("nile:message", (message) => dispatchMessage(message, listeners));
281
+ }
282
+ let settled = false;
283
+ const finish = (result) => {
284
+ if (settled) return;
285
+ settled = true;
286
+ clearTimeout(timer);
287
+ current.off("connect", ready);
288
+ current.off("connect_error", failed);
289
+ pending.delete(cancel);
290
+ resolve(result);
291
+ };
292
+ const ready = () => finish(clientOk(current));
293
+ const failed = () => finish(fail("Socket connection failed"));
294
+ const cancel = () => finish(fail("Socket client closed"));
295
+ const timer = setTimeout(() => finish(fail("Socket connection timed out")), timeout);
296
+ pending.add(cancel);
297
+ current.once("connect", ready);
298
+ current.once("connect_error", failed);
299
+ current.connect();
300
+ })).catch(() => fail("Could not initialize socket client")).finally(() => {
301
+ connecting = void 0;
302
+ });
303
+ return connecting;
304
+ };
305
+ return {
306
+ invoke: (input) => request("nile:invoke", input),
307
+ send: (input) => request("nile:send", input),
308
+ async receive(input) {
309
+ const listener = {
310
+ ...input,
311
+ scope: input.scope,
312
+ onMessage: input.onMessage,
313
+ active: true
314
+ };
315
+ listeners.add(listener);
316
+ const result = await request("nile:receive", {
317
+ topic: input.topic,
318
+ scope: input.scope
319
+ });
320
+ if (result.isErr) {
321
+ listeners.delete(listener);
322
+ return fail(result.error);
323
+ }
324
+ listener.normalizedScope = result.value?.scope;
325
+ return clientOk({ unsubscribe: () => {
326
+ listener.active = false;
327
+ listeners.delete(listener);
328
+ const shared = [...listeners].some((item) => item.topic === listener.topic && stableScope(item.scope) === stableScope(listener.scope));
329
+ if (shared || closed) return Promise.resolve(clientOk({ subscribed: shared }));
330
+ return request("nile:unsubscribe", {
331
+ topic: listener.topic,
332
+ scope: listener.scope
333
+ });
334
+ } });
335
+ },
336
+ close() {
337
+ closed = true;
338
+ listeners.clear();
339
+ for (const cancel of [...pending]) cancel();
340
+ socket?.disconnect();
341
+ socket?.removeAllListeners();
342
+ }
343
+ };
344
+ }
345
+ /** Match scoped messages independently of object property insertion order. */
346
+ function stableScope(scope) {
347
+ if (scope === void 0 || scope === null) return "null";
348
+ if (Array.isArray(scope)) return `[${scope.map(stableScope).join(",")}]`;
349
+ if (typeof scope === "object") return `{${Object.keys(scope).sort().map((key) => `${JSON.stringify(key)}:${stableScope(scope[key])}`).join(",")}}`;
350
+ return JSON.stringify(scope);
208
351
  }
209
- // Annotate the CommonJS export names for ESM import in node:
210
- 0 && (module.exports = {
211
- buildFormData,
212
- createNileClient,
213
- sendUploadRequest
214
- });
352
+ /** Deliver messages only to listeners for the same validated topic and scope. */
353
+ function dispatchMessage(message, listeners) {
354
+ if (!message || typeof message !== "object" || !("topic" in message)) return;
355
+ const scope = stableScope("scope" in message ? message.scope : void 0);
356
+ for (const listener of listeners) {
357
+ if (!listener.active || listener.topic !== message.topic) continue;
358
+ if (stableScope(listener.normalizedScope ?? listener.scope) !== scope) continue;
359
+ Promise.resolve().then(() => listener.onMessage(message)).catch(() => void 0);
360
+ }
361
+ }
362
+ exports.buildFormData = buildFormData;
363
+ exports.createNileClient = createNileClient;
364
+ exports.createNileSocketClient = createNileSocketClient;
365
+ exports.sendUploadRequest = sendUploadRequest;
366
+
215
367
  //# sourceMappingURL=index.cjs.map