@nilejs/client 1.0.0 → 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/README.md CHANGED
@@ -4,7 +4,7 @@ A type-safe frontend client for [Nile Js](https://nile-js.github.io/nile/) backe
4
4
 
5
5
  ## Features
6
6
 
7
- - **Type-safe:** Generates and consumes types from your Nile backend for full compile-time payload validation.
7
+ - **Type-safe:** Consumes shared or CLI-generated backend types for service, action and payload checking.
8
8
  - **Graceful Error Handling:** Returns the same Result pattern as the Nile Js backend (`isOk`, `isErr`, `value`, `error`) instead of throwing exceptions.
9
9
  - **Small:** HTTP calls use the platform `fetch`. The only runtime dependency is `socket.io-client`, for the socket client.
10
10
  - **Flexible:** Supports custom headers, credentials, and timeouts per request.
@@ -116,7 +116,92 @@ Check `isErr` first, then read `error` or `value`. The older `data` field is sti
116
116
  - `explore(params)`: Discover services/actions using wildcards.
117
117
  - `schema(params)`: Fetch action schemas as JSON Schema.
118
118
 
119
- Each method accepts an optional `timeout` and any standard fetch `headers` in its parameters.
119
+ Pass per-call settings in `fetchOptions`. Headers merge with the client’s configured headers; a per-call value overrides the same header regardless of case. Both `Headers` instances and header tuples are supported. The timeout covers reading the response body as well as connecting; an optional `signal` can cancel the call.
120
+
121
+ ```ts
122
+ const controller = new AbortController();
123
+ const result = await nile.invoke({
124
+ service: "tasks",
125
+ action: "create",
126
+ payload: { title: "Ship the next feature" },
127
+ fetchOptions: {
128
+ headers: { Authorization: "Bearer your-session-token" },
129
+ timeout: 5000,
130
+ signal: controller.signal,
131
+ },
132
+ });
133
+ ```
134
+
135
+ ## Nile 1.0.3 responses
136
+
137
+ HTTP responses contain exactly `status`, `message` and `data`. The client maps those into its existing Result API; it preserves custom messages, Zod’s prettified validation message, and generated diagnostic references. The default success message is `Action completed.`
138
+
139
+ For a validation failure, `result.error` is the readable message and `result.details` contains `{ errorCode, issues }`. `ValidationFailureData` is exported for applications that narrow this unknown failure data. Other failures can carry different details, so check the shape before using it. Diagnostic failures may include `Error Code: <unique-code> - <message>` in the message. Treat the code as a reference to share with your backend logs; do not use it as a fixed error category.
140
+
141
+ ## Socket.IO actions and topics
142
+
143
+ Create a separate socket client. It owns the connection; you do not pass a socket into each invocation. Use shared types, generated action types, or define your frontend contracts explicitly. Topic definitions and authorization policies must also be configured on the backend.
144
+
145
+ ```ts
146
+ import { createNileSocketClient } from "@nilejs/client";
147
+ import type { ServicePayloads } from "./generated/types";
148
+
149
+ type Topics = {
150
+ "tasks.changed": {
151
+ data: { taskId: string; status: string };
152
+ };
153
+ };
154
+
155
+ const socket = createNileSocketClient<ServicePayloads, Topics>({
156
+ baseUrl: "http://localhost:8000",
157
+ auth: () => {
158
+ return { token: "your-session-token" };
159
+ },
160
+ });
161
+
162
+ const subscription = await socket.receive({
163
+ topic: "tasks.changed",
164
+ onMessage: (message) => {
165
+ console.log(message.data.taskId, message.data.status);
166
+ },
167
+ });
168
+
169
+ const result = await socket.invoke({
170
+ service: "tasks",
171
+ action: "create",
172
+ payload: { title: "Ship the next feature" },
173
+ });
174
+
175
+ const publication = await socket.send({
176
+ topic: "tasks.changed",
177
+ data: { taskId: "task-123", status: "done" },
178
+ });
179
+
180
+ if (subscription.isOk) {
181
+ await subscription.value.unsubscribe();
182
+ }
183
+ socket.close();
184
+ ```
185
+
186
+ Keep the client open for as long as your application needs live updates. The connection is lazy, subscriptions rejoin after reconnecting, and disconnected writes are not buffered or retried automatically. Topic names, data and any required scope are checked from the supplied types; Nile validates them and enforces policies on the backend. The server can expose HTTP and sockets together. See [WebSockets](https://nile-js.github.io/nile/guide/features/websockets/) for server setup.
187
+
188
+ ## Uploads
189
+
190
+ ```ts
191
+ const result = await nile.upload({
192
+ service: "documents",
193
+ action: "upload",
194
+ files: { document: file },
195
+ fields: { title: "Invoice" },
196
+ fetchOptions: { timeout: 60000 },
197
+ });
198
+ ```
199
+
200
+ Multipart calls share the same cancellation, deadline and Result behavior as JSON calls. Content-Type is set by the runtime so the multipart boundary is correct.
201
+
202
+ ## Compatibility
203
+
204
+ Version 1.0.1 supports Nile 1.0.3 and 1.0.4 while keeping the existing HTTP and socket client APIs. It fixes header merging, cancellation and timeout cleanup; it does not add fields to Nile’s wire contract.
120
205
 
121
206
  ## License
122
207
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/result.ts
2
+ // src/result.ts
3
3
  /**
4
4
  * A successful client result. `data` mirrors `value` so code written for
5
5
  * earlier clients keeps working. `message` is present only when the server sent one.
@@ -28,8 +28,7 @@ function clientErr(error, details = null) {
28
28
  data: null
29
29
  };
30
30
  }
31
- //#endregion
32
- //#region src/safe-try.ts
31
+ // src/safe-try.ts
33
32
  /**
34
33
  * Gracefully handles both synchronous and asynchronous operations by returning a result object.
35
34
  * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.
@@ -69,76 +68,64 @@ function safeTry(fn) {
69
68
  };
70
69
  }
71
70
  }
72
- //#endregion
73
- //#region src/request.ts
74
- /**
75
- * Handles JSON communication with the Nile /services endpoint.
76
- * Uses fetch + safeTry for crash safety and returns a Result that never throws.
77
- */
78
- async function sendNileRequest(config, intent, service, action, payload, options = {}) {
79
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
71
+ // src/request.ts
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.");
80
76
  const controller = new AbortController();
81
- const timeoutId = setTimeout(() => controller.abort(), timeout);
82
- const requestResult = await safeTry(async () => {
83
- const response = await fetch(`${config.baseUrl}/services`, {
84
- method: "POST",
85
- headers: {
86
- "Content-Type": "application/json",
87
- ...config.headers,
88
- ...fetchOptions.headers
89
- },
90
- credentials: config.credentials,
91
- body: JSON.stringify({
92
- intent,
93
- service,
94
- action,
95
- payload: payload ?? {}
96
- }),
97
- signal: controller.signal,
98
- ...fetchOptions
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;
99
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 {
100
113
  clearTimeout(timeoutId);
101
- return await response.json();
102
- });
103
- if (requestResult.isErr) return resolveError(requestResult.error);
104
- const result = requestResult.value;
105
- if (!result.status) return clientErr(result.message, result.data ?? null);
106
- return clientOk(result.data, result.message);
114
+ signal?.removeEventListener("abort", abort);
115
+ }
107
116
  }
108
- /**
109
- * Handles multipart form-data uploads to the Nile /services endpoint.
110
- * Builds a FormData body with RPC routing fields (intent, service, action),
111
- * user-provided fields, and file attachments.
112
- *
113
- * Content-Type header is intentionally omitted so the runtime sets the
114
- * correct multipart boundary automatically.
115
- */
116
- async function sendUploadRequest(config, service, action, files, fields, options = {}) {
117
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
118
- const controller = new AbortController();
119
- const timeoutId = setTimeout(() => controller.abort(), timeout);
120
- const formData = buildFormData(service, action, files, fields);
121
- const requestResult = await safeTry(async () => {
122
- const { "Content-Type": _ct, ...safeHeaders } = config.headers ?? {};
123
- const { "Content-Type": _ct2, ...safeFetchHeaders } = fetchOptions.headers ?? {};
124
- const response = await fetch(`${config.baseUrl}/services`, {
125
- method: "POST",
126
- headers: {
127
- ...safeHeaders,
128
- ...safeFetchHeaders
129
- },
130
- credentials: config.credentials,
131
- body: formData,
132
- signal: controller.signal,
133
- ...fetchOptions
134
- });
135
- clearTimeout(timeoutId);
136
- return await response.json();
137
- });
138
- if (requestResult.isErr) return resolveError(requestResult.error);
139
- const result = requestResult.value;
140
- if (!result.status) return clientErr(result.message, result.data ?? null);
141
- return clientOk(result.data, result.message);
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);
125
+ }
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);
142
129
  }
143
130
  /**
144
131
  * Builds a FormData instance with RPC routing fields and file/field entries.
@@ -154,15 +141,11 @@ function buildFormData(service, action, files, fields) {
154
141
  else formData.append(key, value);
155
142
  return formData;
156
143
  }
157
- /** Resolve error from safeTry into a ClientResult */
144
+ /** Convert local transport errors to the same client failure shape. */
158
145
  function resolveError(error) {
159
- let errorMsg = "An unknown error occurred";
160
- if (error instanceof Error) errorMsg = error.name === "AbortError" ? "Request timed out" : error.message;
161
- else errorMsg = String(error);
162
- return clientErr(errorMsg);
146
+ return clientErr(error instanceof Error ? error.message : String(error));
163
147
  }
164
- //#endregion
165
- //#region src/create-client.ts
148
+ // src/create-client.ts
166
149
  /**
167
150
  * Creates a type-safe Nile client for interacting with backend services.
168
151
  *
@@ -206,8 +189,7 @@ function createNileClient(config) {
206
189
  }
207
190
  };
208
191
  }
209
- //#endregion
210
- //#region src/socket-client.ts
192
+ // src/socket-client.ts
211
193
  /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
212
194
  function createNileSocketClient(config) {
213
195
  const timeout = config.timeout ?? 3e4;
@@ -377,7 +359,6 @@ function dispatchMessage(message, listeners) {
377
359
  Promise.resolve().then(() => listener.onMessage(message)).catch(() => void 0);
378
360
  }
379
361
  }
380
- //#endregion
381
362
  exports.buildFormData = buildFormData;
382
363
  exports.createNileClient = createNileClient;
383
364
  exports.createNileSocketClient = createNileSocketClient;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/result.ts","../src/safe-try.ts","../src/request.ts","../src/create-client.ts","../src/socket-client.ts"],"sourcesContent":["import type { ClientErr, ClientOk } from \"./types.js\";\n\n/**\n * A successful client result. `data` mirrors `value` so code written for\n * earlier clients keeps working. `message` is present only when the server sent one.\n */\nexport function clientOk<T>(value: T, message?: string): ClientOk<T> {\n return {\n isOk: true,\n isErr: false,\n value: value,\n error: null,\n ...(message === undefined ? {} : { message: message }),\n data: value,\n };\n}\n\n/**\n * A failed client result with a readable message and any data the server\n * attached to the failure. `data` stays null, as it was for earlier clients.\n */\nexport function clientErr(error: string, details: unknown = null): ClientErr {\n return {\n isOk: false,\n isErr: true,\n value: null,\n error: error,\n details: details,\n data: null,\n };\n}\n","/**\n * Internal result type for safeTry\n */\nexport type SafeResult<T, E = unknown> =\n | { isOk: true; value: T; isErr: false; error: null }\n | { isOk: false; value: null; isErr: true; error: E };\n\n/**\n * Gracefully handles both synchronous and asynchronous operations by returning a result object.\n * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.\n *\n * @param fn - The function to execute (sync or async)\n * @returns A result object containing either the value or the error\n *\n * @example\n * const { isOk, value, error } = await safeTry(() => fetch(url));\n */\nexport function safeTry<T>(\n fn: () => T | Promise<T>\n): Promise<SafeResult<T>> | SafeResult<T> {\n try {\n const result = fn();\n\n if (result instanceof Promise) {\n return result\n .then(\n (value): SafeResult<T> => ({\n isOk: true,\n value,\n isErr: false,\n error: null,\n })\n )\n .catch(\n (error): SafeResult<T> => ({\n isOk: false,\n value: null,\n isErr: true,\n error,\n })\n );\n }\n\n return { isOk: true, value: result, isErr: false, error: null };\n } catch (error) {\n return { isOk: false, value: null, isErr: true, error };\n }\n}\n","import { clientErr, clientOk } from \"./result.js\";\nimport { safeTry } from \"./safe-try.js\";\nimport type {\n ClientResult,\n ExternalResponse,\n FetchOptions,\n NileClientConfig,\n} from \"./types.js\";\n\n/**\n * Handles JSON communication with the Nile /services endpoint.\n * Uses fetch + safeTry for crash safety and returns a Result that never throws.\n */\nexport async function sendNileRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n intent: \"explore\" | \"execute\" | \"schema\",\n service: string,\n action: string,\n payload: unknown,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n const { timeout = config.timeout ?? 30_000, ...fetchOptions } = options;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const requestResult = await safeTry(async () => {\n const response = await fetch(`${config.baseUrl}/services`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...config.headers,\n ...(fetchOptions.headers as Record<string, string>),\n },\n credentials: config.credentials,\n body: JSON.stringify({\n intent,\n service,\n action,\n payload: payload ?? {},\n }),\n signal: controller.signal,\n ...fetchOptions,\n });\n\n clearTimeout(timeoutId);\n\n const json = (await response.json()) as ExternalResponse<T>;\n return json;\n });\n\n if (requestResult.isErr) {\n return resolveError(requestResult.error);\n }\n\n const result = requestResult.value;\n\n if (!result.status) {\n return clientErr(result.message, result.data ?? null);\n }\n\n return clientOk(result.data, result.message);\n}\n\n/**\n * Handles multipart form-data uploads to the Nile /services endpoint.\n * Builds a FormData body with RPC routing fields (intent, service, action),\n * user-provided fields, and file attachments.\n *\n * Content-Type header is intentionally omitted so the runtime sets the\n * correct multipart boundary automatically.\n */\nexport async function sendUploadRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n const { timeout = config.timeout ?? 30_000, ...fetchOptions } = options;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const formData = buildFormData(service, action, files, fields);\n\n const requestResult = await safeTry(async () => {\n // Spread config.headers but strip Content-Type — let the runtime set multipart boundary\n const { \"Content-Type\": _ct, ...safeHeaders } = config.headers ?? {};\n const { \"Content-Type\": _ct2, ...safeFetchHeaders } =\n (fetchOptions.headers as Record<string, string>) ?? {};\n\n const response = await fetch(`${config.baseUrl}/services`, {\n method: \"POST\",\n headers: {\n ...safeHeaders,\n ...safeFetchHeaders,\n },\n credentials: config.credentials,\n body: formData,\n signal: controller.signal,\n ...fetchOptions,\n });\n\n clearTimeout(timeoutId);\n\n const json = (await response.json()) as ExternalResponse<T>;\n return json;\n });\n\n if (requestResult.isErr) {\n return resolveError(requestResult.error);\n }\n\n const result = requestResult.value;\n\n if (!result.status) {\n return clientErr(result.message, result.data ?? null);\n }\n\n return clientOk(result.data, result.message);\n}\n\n/**\n * Builds a FormData instance with RPC routing fields and file/field entries.\n * The server expects 'intent', 'service', 'action' as string fields.\n */\nexport function buildFormData(\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>\n): FormData {\n const formData = new FormData();\n\n // RPC routing fields the server expects\n formData.append(\"intent\", \"execute\");\n formData.append(\"service\", service);\n formData.append(\"action\", action);\n\n // User-provided string fields\n if (fields) {\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n }\n\n // File attachments — arrays append multiple entries under the same key\n for (const [key, value] of Object.entries(files)) {\n if (Array.isArray(value)) {\n for (const file of value) {\n formData.append(key, file);\n }\n } else {\n formData.append(key, value);\n }\n }\n\n return formData;\n}\n\n/** Resolve error from safeTry into a ClientResult */\nfunction resolveError(error: unknown): ClientResult<never> {\n let errorMsg = \"An unknown error occurred\";\n\n if (error instanceof Error) {\n errorMsg =\n error.name === \"AbortError\" ? \"Request timed out\" : error.message;\n } else {\n errorMsg = String(error);\n }\n\n return clientErr(errorMsg);\n}\n","import { sendNileRequest, sendUploadRequest } from \"./request.js\";\nimport type {\n ClientResult,\n NileClient,\n NileClientConfig,\n UploadParams,\n} from \"./types.js\";\n\n/**\n * Creates a type-safe Nile client for interacting with backend services.\n *\n * @param config - The client configuration including baseUrl\n * @returns A factory object with invoke, explore, schema, and upload methods\n *\n * @example\n * const nile = createNileClient<ServicePayloads>({ baseUrl: \"/api\" });\n * const result = await nile.invoke({\n * service: \"tasks\",\n * action: \"create\",\n * payload: { title: \"Buy milk\" }\n * });\n * if (result.isErr) {\n * console.error(result.error);\n * } else {\n * console.log(result.value);\n * }\n */\nexport function createNileClient<\n TPayloads = Record<string, Record<string, unknown>>,\n>(config: NileClientConfig): NileClient<TPayloads> {\n return {\n /** Invoke a service action through the backend engine pipeline */\n invoke: async <\n S extends keyof TPayloads & string,\n A extends keyof TPayloads[S] & string,\n >(\n params: {\n service: S;\n action: A;\n payload: TPayloads[S][A];\n fetchOptions?: Parameters<typeof sendNileRequest>[5];\n }\n ) => {\n const { service, action, payload, fetchOptions } = params;\n const result = await sendNileRequest<Record<string, unknown>>(\n config,\n \"execute\",\n service,\n action,\n payload,\n fetchOptions\n );\n return result;\n },\n\n /** Discover available services and actions (supports \"*\" wildcard) */\n explore: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"explore\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Get action schemas as JSON Schema (supports \"*\" wildcard) */\n schema: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"schema\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Upload files to an action via multipart form-data */\n upload: async (params: UploadParams) => {\n const { service, action, files, fields, fetchOptions } = params;\n return await sendUploadRequest(\n config,\n service,\n action,\n files,\n fields,\n fetchOptions\n );\n },\n };\n}\n","import type { Socket } from \"socket.io-client\";\nimport type {\n ActionShape,\n NileSocketClient,\n NileSocketClientConfig,\n SocketActions,\n SocketTopics,\n TopicShape,\n} from \"./socket-types.js\";\nimport { clientErr, clientOk } from \"./result.js\";\nimport type { ClientResult } from \"./types.js\";\n\ntype WireValue = object | string | number | boolean | null | undefined;\n/** Acknowledgements keep the server's `{ error, data }` wire shape; the client turns them into Results. */\ninterface WireResult {\n error: string | null;\n data: unknown;\n}\ninterface Listener {\n topic: string;\n scope?: WireValue;\n onMessage: (message: WireValue) => void;\n active: boolean;\n normalizedScope?: WireValue;\n}\n\n/** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */\nexport function createNileSocketClient<\n Actions extends ActionShape<Actions> = SocketActions,\n Topics extends TopicShape<Topics> = SocketTopics,\n>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics> {\n const timeout = config.timeout ?? 30_000;\n if (!Number.isSafeInteger(timeout) || timeout < 1) {\n throw new Error(\"Socket timeout must be positive\");\n }\n let socket: Socket | undefined;\n let connecting: Promise<ClientResult<Socket>> | undefined;\n let closed = false;\n const listeners = new Set<Listener>();\n const pending = new Set<() => void>();\n const fail = (error: string) => clientErr(error);\n const settleCallback = (fn: () => void) => {\n Promise.resolve()\n .then(fn)\n .catch(() => undefined);\n };\n const request = async <T>(\n event: string,\n input: WireValue\n ): Promise<ClientResult<T>> => {\n const connected = await connect();\n if (connected.isErr) {\n return fail(connected.error);\n }\n const current = connected.value;\n if (!current.connected || closed) {\n return fail(\"Socket disconnected\");\n }\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: ClientResult<T>) => {\n if (settled) {\n return;\n }\n settled = true;\n pending.delete(cancel);\n clearTimeout(timer);\n resolve(result);\n };\n const cancel = () => finish(fail(\"Socket disconnected\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket acknowledgement timed out\")),\n timeout\n );\n pending.add(cancel);\n // The connection check and emit are synchronous; disconnected calls never enter the offline queue.\n current.emit(event, input, (result: WireResult) => {\n if (\n !result ||\n typeof result !== \"object\" ||\n !(\"error\" in result) ||\n !(\"data\" in result)\n ) {\n finish(fail(\"Invalid socket response\"));\n return;\n }\n if (typeof result.error === \"string\") {\n finish(clientErr(result.error, result.data));\n return;\n }\n // The server validated this acknowledgement for the requested event.\n finish(clientOk(result.data as T));\n });\n });\n };\n const rejoin = async () => {\n for (const listener of listeners) {\n if (!listener.active) {\n continue;\n }\n const result = await request<{ scope?: WireValue }>(\"nile:receive\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n if (result.isOk) {\n listener.normalizedScope = result.value?.scope;\n continue;\n }\n const message = result.error;\n settleCallback(() =>\n config.onSubscriptionError?.({\n topic: listener.topic,\n error: message,\n })\n );\n }\n };\n const connect = (): Promise<ClientResult<Socket>> => {\n if (closed) {\n return Promise.resolve(fail(\"Socket client is closed\"));\n }\n if (socket?.connected) {\n return Promise.resolve(clientOk(socket));\n }\n if (connecting) {\n return connecting;\n }\n connecting = import(\"socket.io-client\")\n .then(\n ({ io }) =>\n new Promise<ClientResult<Socket>>((resolve) => {\n if (closed) {\n resolve(fail(\"Socket client is closed\"));\n return;\n }\n const current =\n socket ??\n io(config.baseUrl, {\n path: config.path ?? \"/socket.io\",\n autoConnect: false,\n withCredentials: true,\n timeout,\n auth: (done) => {\n Promise.resolve()\n .then(() =>\n typeof config.auth === \"function\"\n ? config.auth()\n : (config.auth ?? {})\n )\n .then(done)\n .catch(() => done({}));\n },\n });\n if (!socket) {\n socket = current;\n current.on(\"disconnect\", () => {\n for (const cancel of [...pending]) {\n cancel();\n }\n });\n current.on(\"connect\", () => {\n rejoin();\n });\n current.on(\"nile:message\", (message: WireValue) =>\n dispatchMessage(message, listeners)\n );\n }\n let settled = false;\n const finish = (result: ClientResult<Socket>) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n current.off(\"connect\", ready);\n current.off(\"connect_error\", failed);\n pending.delete(cancel);\n resolve(result);\n };\n const ready = () => finish(clientOk(current));\n const failed = () => finish(fail(\"Socket connection failed\"));\n const cancel = () => finish(fail(\"Socket client closed\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket connection timed out\")),\n timeout\n );\n pending.add(cancel);\n current.once(\"connect\", ready);\n current.once(\"connect_error\", failed);\n current.connect();\n })\n )\n .catch(() => fail(\"Could not initialize socket client\"))\n .finally(() => {\n connecting = undefined;\n });\n return connecting;\n };\n return {\n invoke: (input) => request(\"nile:invoke\", input),\n send: (input) => request(\"nile:send\", input),\n async receive(input) {\n const listener: Listener = {\n ...input,\n scope: input.scope as WireValue,\n onMessage: input.onMessage as Listener[\"onMessage\"],\n active: true,\n };\n listeners.add(listener);\n const result = await request<{ subscribed: boolean; scope?: WireValue }>(\n \"nile:receive\",\n { topic: input.topic, scope: input.scope }\n );\n if (result.isErr) {\n listeners.delete(listener);\n return fail(result.error);\n }\n listener.normalizedScope = result.value?.scope;\n return clientOk({\n unsubscribe: () => {\n listener.active = false;\n listeners.delete(listener);\n const shared = [...listeners].some(\n (item) =>\n item.topic === listener.topic &&\n stableScope(item.scope) === stableScope(listener.scope)\n );\n if (shared || closed) {\n return Promise.resolve(clientOk({ subscribed: shared }));\n }\n return request(\"nile:unsubscribe\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n },\n });\n },\n close() {\n closed = true;\n listeners.clear();\n for (const cancel of [...pending]) {\n cancel();\n }\n socket?.disconnect();\n socket?.removeAllListeners();\n },\n };\n}\n\n/** Match scoped messages independently of object property insertion order. */\nfunction stableScope(scope: WireValue): string {\n if (scope === undefined || scope === null) {\n return \"null\";\n }\n if (Array.isArray(scope)) {\n return `[${scope.map(stableScope).join(\",\")}]`;\n }\n if (typeof scope === \"object\") {\n return `{${Object.keys(scope)\n .sort()\n .map(\n (key) =>\n `${JSON.stringify(key)}:${stableScope((scope as Record<string, WireValue>)[key])}`\n )\n .join(\",\")}}`;\n }\n return JSON.stringify(scope);\n}\n\n/** Deliver messages only to listeners for the same validated topic and scope. */\nfunction dispatchMessage(message: WireValue, listeners: Set<Listener>) {\n if (!message || typeof message !== \"object\" || !(\"topic\" in message)) {\n return;\n }\n const scope = stableScope(\n \"scope\" in message ? (message.scope as WireValue) : undefined\n );\n for (const listener of listeners) {\n if (!listener.active || listener.topic !== message.topic) {\n continue;\n }\n if (stableScope(listener.normalizedScope ?? listener.scope) !== scope) {\n continue;\n }\n Promise.resolve()\n .then(() => listener.onMessage(message))\n .catch(() => undefined);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/result.ts","../src/safe-try.ts","../src/request.ts","../src/create-client.ts","../src/socket-client.ts"],"sourcesContent":["import type { ClientErr, ClientOk } from \"./types.js\";\n\n/**\n * A successful client result. `data` mirrors `value` so code written for\n * earlier clients keeps working. `message` is present only when the server sent one.\n */\nexport function clientOk<T>(value: T, message?: string): ClientOk<T> {\n return {\n isOk: true,\n isErr: false,\n value: value,\n error: null,\n ...(message === undefined ? {} : { message: message }),\n data: value,\n };\n}\n\n/**\n * A failed client result with a readable message and any data the server\n * attached to the failure. `data` stays null, as it was for earlier clients.\n */\nexport function clientErr(error: string, details: unknown = null): ClientErr {\n return {\n isOk: false,\n isErr: true,\n value: null,\n error: error,\n details: details,\n data: null,\n };\n}\n","/**\n * Internal result type for safeTry\n */\nexport type SafeResult<T, E = unknown> =\n | { isOk: true; value: T; isErr: false; error: null }\n | { isOk: false; value: null; isErr: true; error: E };\n\n/**\n * Gracefully handles both synchronous and asynchronous operations by returning a result object.\n * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.\n *\n * @param fn - The function to execute (sync or async)\n * @returns A result object containing either the value or the error\n *\n * @example\n * const { isOk, value, error } = await safeTry(() => fetch(url));\n */\nexport function safeTry<T>(\n fn: () => T | Promise<T>\n): Promise<SafeResult<T>> | SafeResult<T> {\n try {\n const result = fn();\n\n if (result instanceof Promise) {\n return result\n .then(\n (value): SafeResult<T> => ({\n isOk: true,\n value,\n isErr: false,\n error: null,\n })\n )\n .catch(\n (error): SafeResult<T> => ({\n isOk: false,\n value: null,\n isErr: true,\n error,\n })\n );\n }\n\n return { isOk: true, value: result, isErr: false, error: null };\n } catch (error) {\n return { isOk: false, value: null, isErr: true, error };\n }\n}\n","import { clientErr, clientOk } from \"./result.js\";\nimport { safeTry } from \"./safe-try.js\";\nimport type {\n ClientResult,\n ExternalResponse,\n FetchOptions,\n NileClientConfig,\n} from \"./types.js\";\n\n/** Send one action call; the deadline covers both fetch and reading its JSON body. */\nasync function sendRequest<T>(\n config: NileClientConfig,\n body: () => BodyInit,\n multipart: boolean,\n options: FetchOptions\n): Promise<ClientResult<T>> {\n const {\n timeout = config.timeout ?? 30_000,\n headers,\n signal,\n ...fetchOptions\n } = options;\n if (\n !Number.isSafeInteger(timeout) ||\n timeout < 1 ||\n timeout > 2_147_483_647\n ) {\n return clientErr(\n \"Action timeout must be a positive integer up to 2147483647 milliseconds.\"\n );\n }\n const controller = new AbortController();\n const abort = () => controller.abort(signal?.reason);\n if (signal?.aborted) {\n abort();\n } else {\n signal?.addEventListener(\"abort\", abort, { once: true });\n }\n let timedOut = false;\n const timeoutId = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, timeout);\n\n try {\n const requestResult = await safeTry(async () => {\n const mergedHeaders = new Headers(config.headers);\n new Headers(headers).forEach((value, key) => {\n mergedHeaders.set(key, value);\n });\n if (multipart) {\n mergedHeaders.delete(\"content-type\");\n } else {\n mergedHeaders.set(\"content-type\", \"application/json\");\n }\n const response = await fetch(\n `${config.baseUrl.replace(/\\/+$/, \"\")}/services`,\n {\n ...fetchOptions,\n method: \"POST\",\n headers: mergedHeaders,\n credentials: fetchOptions.credentials ?? config.credentials,\n body: body(),\n signal: controller.signal,\n }\n );\n const json: unknown = await response.json();\n if (\n json === null ||\n typeof json !== \"object\" ||\n !(\"status\" in json) ||\n typeof json.status !== \"boolean\" ||\n !(\"message\" in json) ||\n typeof json.message !== \"string\" ||\n !(\"data\" in json)\n ) {\n throw new Error(\"Invalid Nile action response.\");\n }\n return json as ExternalResponse<T>;\n });\n if (requestResult.isErr) {\n if (timedOut) {\n return clientErr(\"Action timed out.\");\n }\n if (signal?.aborted) {\n return clientErr(\"Action cancelled.\");\n }\n return resolveError(requestResult.error);\n }\n const result = requestResult.value;\n if (!result.status) {\n return clientErr(result.message, result.data ?? null);\n }\n return clientOk(result.data, result.message);\n } finally {\n clearTimeout(timeoutId);\n signal?.removeEventListener(\"abort\", abort);\n }\n}\n\n/** JSON action calls preserve the server's message and failure data unchanged. */\nexport function sendNileRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n intent: \"explore\" | \"execute\" | \"schema\",\n service: string,\n action: string,\n payload: unknown,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n return sendRequest<T>(\n config,\n () =>\n JSON.stringify({\n intent: intent,\n service: service,\n action: action,\n payload: payload ?? {},\n }),\n false,\n options\n );\n}\n\n/** Let the runtime set the multipart boundary; retain the same action Result contract. */\nexport function sendUploadRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n return sendRequest<T>(\n config,\n () => buildFormData(service, action, files, fields),\n true,\n options\n );\n}\n\n/**\n * Builds a FormData instance with RPC routing fields and file/field entries.\n * The server expects 'intent', 'service', 'action' as string fields.\n */\nexport function buildFormData(\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>\n): FormData {\n const formData = new FormData();\n\n // RPC routing fields the server expects\n formData.append(\"intent\", \"execute\");\n formData.append(\"service\", service);\n formData.append(\"action\", action);\n\n // User-provided string fields\n if (fields) {\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n }\n\n // File attachments — arrays append multiple entries under the same key\n for (const [key, value] of Object.entries(files)) {\n if (Array.isArray(value)) {\n for (const file of value) {\n formData.append(key, file);\n }\n } else {\n formData.append(key, value);\n }\n }\n\n return formData;\n}\n\n/** Convert local transport errors to the same client failure shape. */\nfunction resolveError(error: unknown): ClientResult<never> {\n return clientErr(error instanceof Error ? error.message : String(error));\n}\n","import { sendNileRequest, sendUploadRequest } from \"./request.js\";\nimport type {\n ClientResult,\n NileClient,\n NileClientConfig,\n UploadParams,\n} from \"./types.js\";\n\n/**\n * Creates a type-safe Nile client for interacting with backend services.\n *\n * @param config - The client configuration including baseUrl\n * @returns A factory object with invoke, explore, schema, and upload methods\n *\n * @example\n * const nile = createNileClient<ServicePayloads>({ baseUrl: \"/api\" });\n * const result = await nile.invoke({\n * service: \"tasks\",\n * action: \"create\",\n * payload: { title: \"Buy milk\" }\n * });\n * if (result.isErr) {\n * console.error(result.error);\n * } else {\n * console.log(result.value);\n * }\n */\nexport function createNileClient<\n TPayloads = Record<string, Record<string, unknown>>,\n>(config: NileClientConfig): NileClient<TPayloads> {\n return {\n /** Invoke a service action through the backend engine pipeline */\n invoke: async <\n S extends keyof TPayloads & string,\n A extends keyof TPayloads[S] & string,\n >(\n params: {\n service: S;\n action: A;\n payload: TPayloads[S][A];\n fetchOptions?: Parameters<typeof sendNileRequest>[5];\n }\n ) => {\n const { service, action, payload, fetchOptions } = params;\n const result = await sendNileRequest<Record<string, unknown>>(\n config,\n \"execute\",\n service,\n action,\n payload,\n fetchOptions\n );\n return result;\n },\n\n /** Discover available services and actions (supports \"*\" wildcard) */\n explore: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"explore\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Get action schemas as JSON Schema (supports \"*\" wildcard) */\n schema: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"schema\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Upload files to an action via multipart form-data */\n upload: async (params: UploadParams) => {\n const { service, action, files, fields, fetchOptions } = params;\n return await sendUploadRequest(\n config,\n service,\n action,\n files,\n fields,\n fetchOptions\n );\n },\n };\n}\n","import type { Socket } from \"socket.io-client\";\nimport type {\n ActionShape,\n NileSocketClient,\n NileSocketClientConfig,\n SocketActions,\n SocketTopics,\n TopicShape,\n} from \"./socket-types.js\";\nimport { clientErr, clientOk } from \"./result.js\";\nimport type { ClientResult } from \"./types.js\";\n\ntype WireValue = object | string | number | boolean | null | undefined;\n/** Acknowledgements keep the server's `{ error, data }` wire shape; the client turns them into Results. */\ninterface WireResult {\n error: string | null;\n data: unknown;\n}\ninterface Listener {\n topic: string;\n scope?: WireValue;\n onMessage: (message: WireValue) => void;\n active: boolean;\n normalizedScope?: WireValue;\n}\n\n/** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */\nexport function createNileSocketClient<\n Actions extends ActionShape<Actions> = SocketActions,\n Topics extends TopicShape<Topics> = SocketTopics,\n>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics> {\n const timeout = config.timeout ?? 30_000;\n if (!Number.isSafeInteger(timeout) || timeout < 1) {\n throw new Error(\"Socket timeout must be positive\");\n }\n let socket: Socket | undefined;\n let connecting: Promise<ClientResult<Socket>> | undefined;\n let closed = false;\n const listeners = new Set<Listener>();\n const pending = new Set<() => void>();\n const fail = (error: string) => clientErr(error);\n const settleCallback = (fn: () => void) => {\n Promise.resolve()\n .then(fn)\n .catch(() => undefined);\n };\n const request = async <T>(\n event: string,\n input: WireValue\n ): Promise<ClientResult<T>> => {\n const connected = await connect();\n if (connected.isErr) {\n return fail(connected.error);\n }\n const current = connected.value;\n if (!current.connected || closed) {\n return fail(\"Socket disconnected\");\n }\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: ClientResult<T>) => {\n if (settled) {\n return;\n }\n settled = true;\n pending.delete(cancel);\n clearTimeout(timer);\n resolve(result);\n };\n const cancel = () => finish(fail(\"Socket disconnected\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket acknowledgement timed out\")),\n timeout\n );\n pending.add(cancel);\n // The connection check and emit are synchronous; disconnected calls never enter the offline queue.\n current.emit(event, input, (result: WireResult) => {\n if (\n !result ||\n typeof result !== \"object\" ||\n !(\"error\" in result) ||\n !(\"data\" in result)\n ) {\n finish(fail(\"Invalid socket response\"));\n return;\n }\n if (typeof result.error === \"string\") {\n finish(clientErr(result.error, result.data));\n return;\n }\n // The server validated this acknowledgement for the requested event.\n finish(clientOk(result.data as T));\n });\n });\n };\n const rejoin = async () => {\n for (const listener of listeners) {\n if (!listener.active) {\n continue;\n }\n const result = await request<{ scope?: WireValue }>(\"nile:receive\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n if (result.isOk) {\n listener.normalizedScope = result.value?.scope;\n continue;\n }\n const message = result.error;\n settleCallback(() =>\n config.onSubscriptionError?.({\n topic: listener.topic,\n error: message,\n })\n );\n }\n };\n const connect = (): Promise<ClientResult<Socket>> => {\n if (closed) {\n return Promise.resolve(fail(\"Socket client is closed\"));\n }\n if (socket?.connected) {\n return Promise.resolve(clientOk(socket));\n }\n if (connecting) {\n return connecting;\n }\n connecting = import(\"socket.io-client\")\n .then(\n ({ io }) =>\n new Promise<ClientResult<Socket>>((resolve) => {\n if (closed) {\n resolve(fail(\"Socket client is closed\"));\n return;\n }\n const current =\n socket ??\n io(config.baseUrl, {\n path: config.path ?? \"/socket.io\",\n autoConnect: false,\n withCredentials: true,\n timeout,\n auth: (done) => {\n Promise.resolve()\n .then(() =>\n typeof config.auth === \"function\"\n ? config.auth()\n : (config.auth ?? {})\n )\n .then(done)\n .catch(() => done({}));\n },\n });\n if (!socket) {\n socket = current;\n current.on(\"disconnect\", () => {\n for (const cancel of [...pending]) {\n cancel();\n }\n });\n current.on(\"connect\", () => {\n rejoin();\n });\n current.on(\"nile:message\", (message: WireValue) =>\n dispatchMessage(message, listeners)\n );\n }\n let settled = false;\n const finish = (result: ClientResult<Socket>) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n current.off(\"connect\", ready);\n current.off(\"connect_error\", failed);\n pending.delete(cancel);\n resolve(result);\n };\n const ready = () => finish(clientOk(current));\n const failed = () => finish(fail(\"Socket connection failed\"));\n const cancel = () => finish(fail(\"Socket client closed\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket connection timed out\")),\n timeout\n );\n pending.add(cancel);\n current.once(\"connect\", ready);\n current.once(\"connect_error\", failed);\n current.connect();\n })\n )\n .catch(() => fail(\"Could not initialize socket client\"))\n .finally(() => {\n connecting = undefined;\n });\n return connecting;\n };\n return {\n invoke: (input) => request(\"nile:invoke\", input),\n send: (input) => request(\"nile:send\", input),\n async receive(input) {\n const listener: Listener = {\n ...input,\n scope: input.scope as WireValue,\n onMessage: input.onMessage as Listener[\"onMessage\"],\n active: true,\n };\n listeners.add(listener);\n const result = await request<{ subscribed: boolean; scope?: WireValue }>(\n \"nile:receive\",\n { topic: input.topic, scope: input.scope }\n );\n if (result.isErr) {\n listeners.delete(listener);\n return fail(result.error);\n }\n listener.normalizedScope = result.value?.scope;\n return clientOk({\n unsubscribe: () => {\n listener.active = false;\n listeners.delete(listener);\n const shared = [...listeners].some(\n (item) =>\n item.topic === listener.topic &&\n stableScope(item.scope) === stableScope(listener.scope)\n );\n if (shared || closed) {\n return Promise.resolve(clientOk({ subscribed: shared }));\n }\n return request(\"nile:unsubscribe\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n },\n });\n },\n close() {\n closed = true;\n listeners.clear();\n for (const cancel of [...pending]) {\n cancel();\n }\n socket?.disconnect();\n socket?.removeAllListeners();\n },\n };\n}\n\n/** Match scoped messages independently of object property insertion order. */\nfunction stableScope(scope: WireValue): string {\n if (scope === undefined || scope === null) {\n return \"null\";\n }\n if (Array.isArray(scope)) {\n return `[${scope.map(stableScope).join(\",\")}]`;\n }\n if (typeof scope === \"object\") {\n return `{${Object.keys(scope)\n .sort()\n .map(\n (key) =>\n `${JSON.stringify(key)}:${stableScope((scope as Record<string, WireValue>)[key])}`\n )\n .join(\",\")}}`;\n }\n return JSON.stringify(scope);\n}\n\n/** Deliver messages only to listeners for the same validated topic and scope. */\nfunction dispatchMessage(message: WireValue, listeners: Set<Listener>) {\n if (!message || typeof message !== \"object\" || !(\"topic\" in message)) {\n return;\n }\n const scope = stableScope(\n \"scope\" in message ? (message.scope as WireValue) : undefined\n );\n for (const listener of listeners) {\n if (!listener.active || listener.topic !== message.topic) {\n continue;\n }\n if (stableScope(listener.normalizedScope ?? listener.scope) !== scope) {\n continue;\n }\n Promise.resolve()\n .then(() => listener.onMessage(message))\n .catch(() => undefined);\n }\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,10 +1,17 @@
1
- //#region src/types.d.ts
2
1
  /** Standard Nile API response shape */
3
2
  interface ExternalResponse<T = Record<string, unknown>> {
4
3
  status: boolean;
5
4
  message: string;
6
5
  data: T;
7
6
  }
7
+ /** Validation details returned by Nile HTTP actions; codes identify individual failures. */
8
+ interface ValidationFailureData {
9
+ errorCode: string;
10
+ issues: Array<{
11
+ path: string;
12
+ message: string;
13
+ }>;
14
+ }
8
15
  /** A successful call: read the action's data from `value`. */
9
16
  interface ClientOk<T> {
10
17
  isOk: true;
@@ -51,6 +58,7 @@ interface ExploreActionsResponse {
51
58
  visibility?: {
52
59
  rest?: boolean;
53
60
  rpc?: boolean;
61
+ ws?: boolean;
54
62
  };
55
63
  }>;
56
64
  }
@@ -104,8 +112,6 @@ interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
104
112
  /** Upload files to an action via multipart form-data */
105
113
  upload: (params: UploadParams) => Promise<ClientResult>;
106
114
  }
107
- //#endregion
108
- //#region src/create-client.d.ts
109
115
  /**
110
116
  * Creates a type-safe Nile client for interacting with backend services.
111
117
  *
@@ -126,24 +132,13 @@ interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
126
132
  * }
127
133
  */
128
134
  export declare function createNileClient<TPayloads = Record<string, Record<string, unknown>>>(config: NileClientConfig): NileClient<TPayloads>;
129
- //#endregion
130
- //#region src/request.d.ts
131
- /**
132
- * Handles multipart form-data uploads to the Nile /services endpoint.
133
- * Builds a FormData body with RPC routing fields (intent, service, action),
134
- * user-provided fields, and file attachments.
135
- *
136
- * Content-Type header is intentionally omitted so the runtime sets the
137
- * correct multipart boundary automatically.
138
- */
135
+ /** Let the runtime set the multipart boundary; retain the same action Result contract. */
139
136
  export declare function sendUploadRequest<T = Record<string, unknown>>(config: NileClientConfig, service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>, options?: FetchOptions): Promise<ClientResult<T>>;
140
137
  /**
141
138
  * Builds a FormData instance with RPC routing fields and file/field entries.
142
139
  * The server expects 'intent', 'service', 'action' as string fields.
143
140
  */
144
141
  export declare function buildFormData(service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>): FormData;
145
- //#endregion
146
- //#region src/socket-types.d.ts
147
142
  interface TopicContract {
148
143
  data: object | string | number | boolean | null;
149
144
  input?: object | string | number | boolean | null;
@@ -204,10 +199,7 @@ interface NileSocketClient<A extends ActionShape<A>, T extends TopicShape<T>> {
204
199
  }>>;
205
200
  close: () => void;
206
201
  }
207
- //#endregion
208
- //#region src/socket-client.d.ts
209
202
  /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
210
203
  export declare function createNileSocketClient<Actions extends ActionShape<Actions> = SocketActions, Topics extends TopicShape<Topics> = SocketTopics>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics>;
211
- //#endregion
212
- export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketClientConfig, SocketActions, SocketMessage, SocketPublication, SocketSelection, SocketTopics, UploadParams };
204
+ export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketClientConfig, SocketActions, SocketMessage, SocketPublication, SocketSelection, SocketTopics, UploadParams, ValidationFailureData };
213
205
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
- //#region src/types.d.ts
2
1
  /** Standard Nile API response shape */
3
2
  interface ExternalResponse<T = Record<string, unknown>> {
4
3
  status: boolean;
5
4
  message: string;
6
5
  data: T;
7
6
  }
7
+ /** Validation details returned by Nile HTTP actions; codes identify individual failures. */
8
+ interface ValidationFailureData {
9
+ errorCode: string;
10
+ issues: Array<{
11
+ path: string;
12
+ message: string;
13
+ }>;
14
+ }
8
15
  /** A successful call: read the action's data from `value`. */
9
16
  interface ClientOk<T> {
10
17
  isOk: true;
@@ -51,6 +58,7 @@ interface ExploreActionsResponse {
51
58
  visibility?: {
52
59
  rest?: boolean;
53
60
  rpc?: boolean;
61
+ ws?: boolean;
54
62
  };
55
63
  }>;
56
64
  }
@@ -104,8 +112,6 @@ interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
104
112
  /** Upload files to an action via multipart form-data */
105
113
  upload: (params: UploadParams) => Promise<ClientResult>;
106
114
  }
107
- //#endregion
108
- //#region src/create-client.d.ts
109
115
  /**
110
116
  * Creates a type-safe Nile client for interacting with backend services.
111
117
  *
@@ -126,24 +132,13 @@ interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
126
132
  * }
127
133
  */
128
134
  export declare function createNileClient<TPayloads = Record<string, Record<string, unknown>>>(config: NileClientConfig): NileClient<TPayloads>;
129
- //#endregion
130
- //#region src/request.d.ts
131
- /**
132
- * Handles multipart form-data uploads to the Nile /services endpoint.
133
- * Builds a FormData body with RPC routing fields (intent, service, action),
134
- * user-provided fields, and file attachments.
135
- *
136
- * Content-Type header is intentionally omitted so the runtime sets the
137
- * correct multipart boundary automatically.
138
- */
135
+ /** Let the runtime set the multipart boundary; retain the same action Result contract. */
139
136
  export declare function sendUploadRequest<T = Record<string, unknown>>(config: NileClientConfig, service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>, options?: FetchOptions): Promise<ClientResult<T>>;
140
137
  /**
141
138
  * Builds a FormData instance with RPC routing fields and file/field entries.
142
139
  * The server expects 'intent', 'service', 'action' as string fields.
143
140
  */
144
141
  export declare function buildFormData(service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>): FormData;
145
- //#endregion
146
- //#region src/socket-types.d.ts
147
142
  interface TopicContract {
148
143
  data: object | string | number | boolean | null;
149
144
  input?: object | string | number | boolean | null;
@@ -204,10 +199,7 @@ interface NileSocketClient<A extends ActionShape<A>, T extends TopicShape<T>> {
204
199
  }>>;
205
200
  close: () => void;
206
201
  }
207
- //#endregion
208
- //#region src/socket-client.d.ts
209
202
  /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
210
203
  export declare function createNileSocketClient<Actions extends ActionShape<Actions> = SocketActions, Topics extends TopicShape<Topics> = SocketTopics>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics>;
211
- //#endregion
212
- export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketClientConfig, SocketActions, SocketMessage, SocketPublication, SocketSelection, SocketTopics, UploadParams };
204
+ export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketClientConfig, SocketActions, SocketMessage, SocketPublication, SocketSelection, SocketTopics, UploadParams, ValidationFailureData };
213
205
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/result.ts
1
+ // src/result.ts
2
2
  /**
3
3
  * A successful client result. `data` mirrors `value` so code written for
4
4
  * earlier clients keeps working. `message` is present only when the server sent one.
@@ -27,8 +27,7 @@ function clientErr(error, details = null) {
27
27
  data: null
28
28
  };
29
29
  }
30
- //#endregion
31
- //#region src/safe-try.ts
30
+ // src/safe-try.ts
32
31
  /**
33
32
  * Gracefully handles both synchronous and asynchronous operations by returning a result object.
34
33
  * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.
@@ -68,76 +67,64 @@ function safeTry(fn) {
68
67
  };
69
68
  }
70
69
  }
71
- //#endregion
72
- //#region src/request.ts
73
- /**
74
- * Handles JSON communication with the Nile /services endpoint.
75
- * Uses fetch + safeTry for crash safety and returns a Result that never throws.
76
- */
77
- async function sendNileRequest(config, intent, service, action, payload, options = {}) {
78
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
70
+ // src/request.ts
71
+ /** Send one action call; the deadline covers both fetch and reading its JSON body. */
72
+ async function sendRequest(config, body, multipart, options) {
73
+ const { timeout = config.timeout ?? 3e4, headers, signal, ...fetchOptions } = options;
74
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2147483647) return clientErr("Action timeout must be a positive integer up to 2147483647 milliseconds.");
79
75
  const controller = new AbortController();
80
- const timeoutId = setTimeout(() => controller.abort(), timeout);
81
- const requestResult = await safeTry(async () => {
82
- const response = await fetch(`${config.baseUrl}/services`, {
83
- method: "POST",
84
- headers: {
85
- "Content-Type": "application/json",
86
- ...config.headers,
87
- ...fetchOptions.headers
88
- },
89
- credentials: config.credentials,
90
- body: JSON.stringify({
91
- intent,
92
- service,
93
- action,
94
- payload: payload ?? {}
95
- }),
96
- signal: controller.signal,
97
- ...fetchOptions
76
+ const abort = () => controller.abort(signal?.reason);
77
+ if (signal?.aborted) abort();
78
+ else signal?.addEventListener("abort", abort, { once: true });
79
+ let timedOut = false;
80
+ const timeoutId = setTimeout(() => {
81
+ timedOut = true;
82
+ controller.abort();
83
+ }, timeout);
84
+ try {
85
+ const requestResult = await safeTry(async () => {
86
+ const mergedHeaders = new Headers(config.headers);
87
+ new Headers(headers).forEach((value, key) => {
88
+ mergedHeaders.set(key, value);
89
+ });
90
+ if (multipart) mergedHeaders.delete("content-type");
91
+ else mergedHeaders.set("content-type", "application/json");
92
+ const json = await (await fetch(`${config.baseUrl.replace(/\/+$/, "")}/services`, {
93
+ ...fetchOptions,
94
+ method: "POST",
95
+ headers: mergedHeaders,
96
+ credentials: fetchOptions.credentials ?? config.credentials,
97
+ body: body(),
98
+ signal: controller.signal
99
+ })).json();
100
+ 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.");
101
+ return json;
98
102
  });
103
+ if (requestResult.isErr) {
104
+ if (timedOut) return clientErr("Action timed out.");
105
+ if (signal?.aborted) return clientErr("Action cancelled.");
106
+ return resolveError(requestResult.error);
107
+ }
108
+ const result = requestResult.value;
109
+ if (!result.status) return clientErr(result.message, result.data ?? null);
110
+ return clientOk(result.data, result.message);
111
+ } finally {
99
112
  clearTimeout(timeoutId);
100
- return await response.json();
101
- });
102
- if (requestResult.isErr) return resolveError(requestResult.error);
103
- const result = requestResult.value;
104
- if (!result.status) return clientErr(result.message, result.data ?? null);
105
- return clientOk(result.data, result.message);
113
+ signal?.removeEventListener("abort", abort);
114
+ }
106
115
  }
107
- /**
108
- * Handles multipart form-data uploads to the Nile /services endpoint.
109
- * Builds a FormData body with RPC routing fields (intent, service, action),
110
- * user-provided fields, and file attachments.
111
- *
112
- * Content-Type header is intentionally omitted so the runtime sets the
113
- * correct multipart boundary automatically.
114
- */
115
- async function sendUploadRequest(config, service, action, files, fields, options = {}) {
116
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
117
- const controller = new AbortController();
118
- const timeoutId = setTimeout(() => controller.abort(), timeout);
119
- const formData = buildFormData(service, action, files, fields);
120
- const requestResult = await safeTry(async () => {
121
- const { "Content-Type": _ct, ...safeHeaders } = config.headers ?? {};
122
- const { "Content-Type": _ct2, ...safeFetchHeaders } = fetchOptions.headers ?? {};
123
- const response = await fetch(`${config.baseUrl}/services`, {
124
- method: "POST",
125
- headers: {
126
- ...safeHeaders,
127
- ...safeFetchHeaders
128
- },
129
- credentials: config.credentials,
130
- body: formData,
131
- signal: controller.signal,
132
- ...fetchOptions
133
- });
134
- clearTimeout(timeoutId);
135
- return await response.json();
136
- });
137
- if (requestResult.isErr) return resolveError(requestResult.error);
138
- const result = requestResult.value;
139
- if (!result.status) return clientErr(result.message, result.data ?? null);
140
- return clientOk(result.data, result.message);
116
+ /** JSON action calls preserve the server's message and failure data unchanged. */
117
+ function sendNileRequest(config, intent, service, action, payload, options = {}) {
118
+ return sendRequest(config, () => JSON.stringify({
119
+ intent,
120
+ service,
121
+ action,
122
+ payload: payload ?? {}
123
+ }), false, options);
124
+ }
125
+ /** Let the runtime set the multipart boundary; retain the same action Result contract. */
126
+ function sendUploadRequest(config, service, action, files, fields, options = {}) {
127
+ return sendRequest(config, () => buildFormData(service, action, files, fields), true, options);
141
128
  }
142
129
  /**
143
130
  * Builds a FormData instance with RPC routing fields and file/field entries.
@@ -153,15 +140,11 @@ function buildFormData(service, action, files, fields) {
153
140
  else formData.append(key, value);
154
141
  return formData;
155
142
  }
156
- /** Resolve error from safeTry into a ClientResult */
143
+ /** Convert local transport errors to the same client failure shape. */
157
144
  function resolveError(error) {
158
- let errorMsg = "An unknown error occurred";
159
- if (error instanceof Error) errorMsg = error.name === "AbortError" ? "Request timed out" : error.message;
160
- else errorMsg = String(error);
161
- return clientErr(errorMsg);
145
+ return clientErr(error instanceof Error ? error.message : String(error));
162
146
  }
163
- //#endregion
164
- //#region src/create-client.ts
147
+ // src/create-client.ts
165
148
  /**
166
149
  * Creates a type-safe Nile client for interacting with backend services.
167
150
  *
@@ -205,8 +188,7 @@ function createNileClient(config) {
205
188
  }
206
189
  };
207
190
  }
208
- //#endregion
209
- //#region src/socket-client.ts
191
+ // src/socket-client.ts
210
192
  /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
211
193
  function createNileSocketClient(config) {
212
194
  const timeout = config.timeout ?? 3e4;
@@ -376,7 +358,6 @@ function dispatchMessage(message, listeners) {
376
358
  Promise.resolve().then(() => listener.onMessage(message)).catch(() => void 0);
377
359
  }
378
360
  }
379
- //#endregion
380
361
  export { buildFormData, createNileClient, createNileSocketClient, sendUploadRequest };
381
362
 
382
363
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/result.ts","../src/safe-try.ts","../src/request.ts","../src/create-client.ts","../src/socket-client.ts"],"sourcesContent":["import type { ClientErr, ClientOk } from \"./types.js\";\n\n/**\n * A successful client result. `data` mirrors `value` so code written for\n * earlier clients keeps working. `message` is present only when the server sent one.\n */\nexport function clientOk<T>(value: T, message?: string): ClientOk<T> {\n return {\n isOk: true,\n isErr: false,\n value: value,\n error: null,\n ...(message === undefined ? {} : { message: message }),\n data: value,\n };\n}\n\n/**\n * A failed client result with a readable message and any data the server\n * attached to the failure. `data` stays null, as it was for earlier clients.\n */\nexport function clientErr(error: string, details: unknown = null): ClientErr {\n return {\n isOk: false,\n isErr: true,\n value: null,\n error: error,\n details: details,\n data: null,\n };\n}\n","/**\n * Internal result type for safeTry\n */\nexport type SafeResult<T, E = unknown> =\n | { isOk: true; value: T; isErr: false; error: null }\n | { isOk: false; value: null; isErr: true; error: E };\n\n/**\n * Gracefully handles both synchronous and asynchronous operations by returning a result object.\n * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.\n *\n * @param fn - The function to execute (sync or async)\n * @returns A result object containing either the value or the error\n *\n * @example\n * const { isOk, value, error } = await safeTry(() => fetch(url));\n */\nexport function safeTry<T>(\n fn: () => T | Promise<T>\n): Promise<SafeResult<T>> | SafeResult<T> {\n try {\n const result = fn();\n\n if (result instanceof Promise) {\n return result\n .then(\n (value): SafeResult<T> => ({\n isOk: true,\n value,\n isErr: false,\n error: null,\n })\n )\n .catch(\n (error): SafeResult<T> => ({\n isOk: false,\n value: null,\n isErr: true,\n error,\n })\n );\n }\n\n return { isOk: true, value: result, isErr: false, error: null };\n } catch (error) {\n return { isOk: false, value: null, isErr: true, error };\n }\n}\n","import { clientErr, clientOk } from \"./result.js\";\nimport { safeTry } from \"./safe-try.js\";\nimport type {\n ClientResult,\n ExternalResponse,\n FetchOptions,\n NileClientConfig,\n} from \"./types.js\";\n\n/**\n * Handles JSON communication with the Nile /services endpoint.\n * Uses fetch + safeTry for crash safety and returns a Result that never throws.\n */\nexport async function sendNileRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n intent: \"explore\" | \"execute\" | \"schema\",\n service: string,\n action: string,\n payload: unknown,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n const { timeout = config.timeout ?? 30_000, ...fetchOptions } = options;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const requestResult = await safeTry(async () => {\n const response = await fetch(`${config.baseUrl}/services`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...config.headers,\n ...(fetchOptions.headers as Record<string, string>),\n },\n credentials: config.credentials,\n body: JSON.stringify({\n intent,\n service,\n action,\n payload: payload ?? {},\n }),\n signal: controller.signal,\n ...fetchOptions,\n });\n\n clearTimeout(timeoutId);\n\n const json = (await response.json()) as ExternalResponse<T>;\n return json;\n });\n\n if (requestResult.isErr) {\n return resolveError(requestResult.error);\n }\n\n const result = requestResult.value;\n\n if (!result.status) {\n return clientErr(result.message, result.data ?? null);\n }\n\n return clientOk(result.data, result.message);\n}\n\n/**\n * Handles multipart form-data uploads to the Nile /services endpoint.\n * Builds a FormData body with RPC routing fields (intent, service, action),\n * user-provided fields, and file attachments.\n *\n * Content-Type header is intentionally omitted so the runtime sets the\n * correct multipart boundary automatically.\n */\nexport async function sendUploadRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n const { timeout = config.timeout ?? 30_000, ...fetchOptions } = options;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const formData = buildFormData(service, action, files, fields);\n\n const requestResult = await safeTry(async () => {\n // Spread config.headers but strip Content-Type — let the runtime set multipart boundary\n const { \"Content-Type\": _ct, ...safeHeaders } = config.headers ?? {};\n const { \"Content-Type\": _ct2, ...safeFetchHeaders } =\n (fetchOptions.headers as Record<string, string>) ?? {};\n\n const response = await fetch(`${config.baseUrl}/services`, {\n method: \"POST\",\n headers: {\n ...safeHeaders,\n ...safeFetchHeaders,\n },\n credentials: config.credentials,\n body: formData,\n signal: controller.signal,\n ...fetchOptions,\n });\n\n clearTimeout(timeoutId);\n\n const json = (await response.json()) as ExternalResponse<T>;\n return json;\n });\n\n if (requestResult.isErr) {\n return resolveError(requestResult.error);\n }\n\n const result = requestResult.value;\n\n if (!result.status) {\n return clientErr(result.message, result.data ?? null);\n }\n\n return clientOk(result.data, result.message);\n}\n\n/**\n * Builds a FormData instance with RPC routing fields and file/field entries.\n * The server expects 'intent', 'service', 'action' as string fields.\n */\nexport function buildFormData(\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>\n): FormData {\n const formData = new FormData();\n\n // RPC routing fields the server expects\n formData.append(\"intent\", \"execute\");\n formData.append(\"service\", service);\n formData.append(\"action\", action);\n\n // User-provided string fields\n if (fields) {\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n }\n\n // File attachments — arrays append multiple entries under the same key\n for (const [key, value] of Object.entries(files)) {\n if (Array.isArray(value)) {\n for (const file of value) {\n formData.append(key, file);\n }\n } else {\n formData.append(key, value);\n }\n }\n\n return formData;\n}\n\n/** Resolve error from safeTry into a ClientResult */\nfunction resolveError(error: unknown): ClientResult<never> {\n let errorMsg = \"An unknown error occurred\";\n\n if (error instanceof Error) {\n errorMsg =\n error.name === \"AbortError\" ? \"Request timed out\" : error.message;\n } else {\n errorMsg = String(error);\n }\n\n return clientErr(errorMsg);\n}\n","import { sendNileRequest, sendUploadRequest } from \"./request.js\";\nimport type {\n ClientResult,\n NileClient,\n NileClientConfig,\n UploadParams,\n} from \"./types.js\";\n\n/**\n * Creates a type-safe Nile client for interacting with backend services.\n *\n * @param config - The client configuration including baseUrl\n * @returns A factory object with invoke, explore, schema, and upload methods\n *\n * @example\n * const nile = createNileClient<ServicePayloads>({ baseUrl: \"/api\" });\n * const result = await nile.invoke({\n * service: \"tasks\",\n * action: \"create\",\n * payload: { title: \"Buy milk\" }\n * });\n * if (result.isErr) {\n * console.error(result.error);\n * } else {\n * console.log(result.value);\n * }\n */\nexport function createNileClient<\n TPayloads = Record<string, Record<string, unknown>>,\n>(config: NileClientConfig): NileClient<TPayloads> {\n return {\n /** Invoke a service action through the backend engine pipeline */\n invoke: async <\n S extends keyof TPayloads & string,\n A extends keyof TPayloads[S] & string,\n >(\n params: {\n service: S;\n action: A;\n payload: TPayloads[S][A];\n fetchOptions?: Parameters<typeof sendNileRequest>[5];\n }\n ) => {\n const { service, action, payload, fetchOptions } = params;\n const result = await sendNileRequest<Record<string, unknown>>(\n config,\n \"execute\",\n service,\n action,\n payload,\n fetchOptions\n );\n return result;\n },\n\n /** Discover available services and actions (supports \"*\" wildcard) */\n explore: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"explore\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Get action schemas as JSON Schema (supports \"*\" wildcard) */\n schema: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"schema\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Upload files to an action via multipart form-data */\n upload: async (params: UploadParams) => {\n const { service, action, files, fields, fetchOptions } = params;\n return await sendUploadRequest(\n config,\n service,\n action,\n files,\n fields,\n fetchOptions\n );\n },\n };\n}\n","import type { Socket } from \"socket.io-client\";\nimport type {\n ActionShape,\n NileSocketClient,\n NileSocketClientConfig,\n SocketActions,\n SocketTopics,\n TopicShape,\n} from \"./socket-types.js\";\nimport { clientErr, clientOk } from \"./result.js\";\nimport type { ClientResult } from \"./types.js\";\n\ntype WireValue = object | string | number | boolean | null | undefined;\n/** Acknowledgements keep the server's `{ error, data }` wire shape; the client turns them into Results. */\ninterface WireResult {\n error: string | null;\n data: unknown;\n}\ninterface Listener {\n topic: string;\n scope?: WireValue;\n onMessage: (message: WireValue) => void;\n active: boolean;\n normalizedScope?: WireValue;\n}\n\n/** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */\nexport function createNileSocketClient<\n Actions extends ActionShape<Actions> = SocketActions,\n Topics extends TopicShape<Topics> = SocketTopics,\n>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics> {\n const timeout = config.timeout ?? 30_000;\n if (!Number.isSafeInteger(timeout) || timeout < 1) {\n throw new Error(\"Socket timeout must be positive\");\n }\n let socket: Socket | undefined;\n let connecting: Promise<ClientResult<Socket>> | undefined;\n let closed = false;\n const listeners = new Set<Listener>();\n const pending = new Set<() => void>();\n const fail = (error: string) => clientErr(error);\n const settleCallback = (fn: () => void) => {\n Promise.resolve()\n .then(fn)\n .catch(() => undefined);\n };\n const request = async <T>(\n event: string,\n input: WireValue\n ): Promise<ClientResult<T>> => {\n const connected = await connect();\n if (connected.isErr) {\n return fail(connected.error);\n }\n const current = connected.value;\n if (!current.connected || closed) {\n return fail(\"Socket disconnected\");\n }\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: ClientResult<T>) => {\n if (settled) {\n return;\n }\n settled = true;\n pending.delete(cancel);\n clearTimeout(timer);\n resolve(result);\n };\n const cancel = () => finish(fail(\"Socket disconnected\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket acknowledgement timed out\")),\n timeout\n );\n pending.add(cancel);\n // The connection check and emit are synchronous; disconnected calls never enter the offline queue.\n current.emit(event, input, (result: WireResult) => {\n if (\n !result ||\n typeof result !== \"object\" ||\n !(\"error\" in result) ||\n !(\"data\" in result)\n ) {\n finish(fail(\"Invalid socket response\"));\n return;\n }\n if (typeof result.error === \"string\") {\n finish(clientErr(result.error, result.data));\n return;\n }\n // The server validated this acknowledgement for the requested event.\n finish(clientOk(result.data as T));\n });\n });\n };\n const rejoin = async () => {\n for (const listener of listeners) {\n if (!listener.active) {\n continue;\n }\n const result = await request<{ scope?: WireValue }>(\"nile:receive\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n if (result.isOk) {\n listener.normalizedScope = result.value?.scope;\n continue;\n }\n const message = result.error;\n settleCallback(() =>\n config.onSubscriptionError?.({\n topic: listener.topic,\n error: message,\n })\n );\n }\n };\n const connect = (): Promise<ClientResult<Socket>> => {\n if (closed) {\n return Promise.resolve(fail(\"Socket client is closed\"));\n }\n if (socket?.connected) {\n return Promise.resolve(clientOk(socket));\n }\n if (connecting) {\n return connecting;\n }\n connecting = import(\"socket.io-client\")\n .then(\n ({ io }) =>\n new Promise<ClientResult<Socket>>((resolve) => {\n if (closed) {\n resolve(fail(\"Socket client is closed\"));\n return;\n }\n const current =\n socket ??\n io(config.baseUrl, {\n path: config.path ?? \"/socket.io\",\n autoConnect: false,\n withCredentials: true,\n timeout,\n auth: (done) => {\n Promise.resolve()\n .then(() =>\n typeof config.auth === \"function\"\n ? config.auth()\n : (config.auth ?? {})\n )\n .then(done)\n .catch(() => done({}));\n },\n });\n if (!socket) {\n socket = current;\n current.on(\"disconnect\", () => {\n for (const cancel of [...pending]) {\n cancel();\n }\n });\n current.on(\"connect\", () => {\n rejoin();\n });\n current.on(\"nile:message\", (message: WireValue) =>\n dispatchMessage(message, listeners)\n );\n }\n let settled = false;\n const finish = (result: ClientResult<Socket>) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n current.off(\"connect\", ready);\n current.off(\"connect_error\", failed);\n pending.delete(cancel);\n resolve(result);\n };\n const ready = () => finish(clientOk(current));\n const failed = () => finish(fail(\"Socket connection failed\"));\n const cancel = () => finish(fail(\"Socket client closed\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket connection timed out\")),\n timeout\n );\n pending.add(cancel);\n current.once(\"connect\", ready);\n current.once(\"connect_error\", failed);\n current.connect();\n })\n )\n .catch(() => fail(\"Could not initialize socket client\"))\n .finally(() => {\n connecting = undefined;\n });\n return connecting;\n };\n return {\n invoke: (input) => request(\"nile:invoke\", input),\n send: (input) => request(\"nile:send\", input),\n async receive(input) {\n const listener: Listener = {\n ...input,\n scope: input.scope as WireValue,\n onMessage: input.onMessage as Listener[\"onMessage\"],\n active: true,\n };\n listeners.add(listener);\n const result = await request<{ subscribed: boolean; scope?: WireValue }>(\n \"nile:receive\",\n { topic: input.topic, scope: input.scope }\n );\n if (result.isErr) {\n listeners.delete(listener);\n return fail(result.error);\n }\n listener.normalizedScope = result.value?.scope;\n return clientOk({\n unsubscribe: () => {\n listener.active = false;\n listeners.delete(listener);\n const shared = [...listeners].some(\n (item) =>\n item.topic === listener.topic &&\n stableScope(item.scope) === stableScope(listener.scope)\n );\n if (shared || closed) {\n return Promise.resolve(clientOk({ subscribed: shared }));\n }\n return request(\"nile:unsubscribe\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n },\n });\n },\n close() {\n closed = true;\n listeners.clear();\n for (const cancel of [...pending]) {\n cancel();\n }\n socket?.disconnect();\n socket?.removeAllListeners();\n },\n };\n}\n\n/** Match scoped messages independently of object property insertion order. */\nfunction stableScope(scope: WireValue): string {\n if (scope === undefined || scope === null) {\n return \"null\";\n }\n if (Array.isArray(scope)) {\n return `[${scope.map(stableScope).join(\",\")}]`;\n }\n if (typeof scope === \"object\") {\n return `{${Object.keys(scope)\n .sort()\n .map(\n (key) =>\n `${JSON.stringify(key)}:${stableScope((scope as Record<string, WireValue>)[key])}`\n )\n .join(\",\")}}`;\n }\n return JSON.stringify(scope);\n}\n\n/** Deliver messages only to listeners for the same validated topic and scope. */\nfunction dispatchMessage(message: WireValue, listeners: Set<Listener>) {\n if (!message || typeof message !== \"object\" || !(\"topic\" in message)) {\n return;\n }\n const scope = stableScope(\n \"scope\" in message ? (message.scope as WireValue) : undefined\n );\n for (const listener of listeners) {\n if (!listener.active || listener.topic !== message.topic) {\n continue;\n }\n if (stableScope(listener.normalizedScope ?? listener.scope) !== scope) {\n continue;\n }\n Promise.resolve()\n .then(() => listener.onMessage(message))\n .catch(() => undefined);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/result.ts","../src/safe-try.ts","../src/request.ts","../src/create-client.ts","../src/socket-client.ts"],"sourcesContent":["import type { ClientErr, ClientOk } from \"./types.js\";\n\n/**\n * A successful client result. `data` mirrors `value` so code written for\n * earlier clients keeps working. `message` is present only when the server sent one.\n */\nexport function clientOk<T>(value: T, message?: string): ClientOk<T> {\n return {\n isOk: true,\n isErr: false,\n value: value,\n error: null,\n ...(message === undefined ? {} : { message: message }),\n data: value,\n };\n}\n\n/**\n * A failed client result with a readable message and any data the server\n * attached to the failure. `data` stays null, as it was for earlier clients.\n */\nexport function clientErr(error: string, details: unknown = null): ClientErr {\n return {\n isOk: false,\n isErr: true,\n value: null,\n error: error,\n details: details,\n data: null,\n };\n}\n","/**\n * Internal result type for safeTry\n */\nexport type SafeResult<T, E = unknown> =\n | { isOk: true; value: T; isErr: false; error: null }\n | { isOk: false; value: null; isErr: true; error: E };\n\n/**\n * Gracefully handles both synchronous and asynchronous operations by returning a result object.\n * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.\n *\n * @param fn - The function to execute (sync or async)\n * @returns A result object containing either the value or the error\n *\n * @example\n * const { isOk, value, error } = await safeTry(() => fetch(url));\n */\nexport function safeTry<T>(\n fn: () => T | Promise<T>\n): Promise<SafeResult<T>> | SafeResult<T> {\n try {\n const result = fn();\n\n if (result instanceof Promise) {\n return result\n .then(\n (value): SafeResult<T> => ({\n isOk: true,\n value,\n isErr: false,\n error: null,\n })\n )\n .catch(\n (error): SafeResult<T> => ({\n isOk: false,\n value: null,\n isErr: true,\n error,\n })\n );\n }\n\n return { isOk: true, value: result, isErr: false, error: null };\n } catch (error) {\n return { isOk: false, value: null, isErr: true, error };\n }\n}\n","import { clientErr, clientOk } from \"./result.js\";\nimport { safeTry } from \"./safe-try.js\";\nimport type {\n ClientResult,\n ExternalResponse,\n FetchOptions,\n NileClientConfig,\n} from \"./types.js\";\n\n/** Send one action call; the deadline covers both fetch and reading its JSON body. */\nasync function sendRequest<T>(\n config: NileClientConfig,\n body: () => BodyInit,\n multipart: boolean,\n options: FetchOptions\n): Promise<ClientResult<T>> {\n const {\n timeout = config.timeout ?? 30_000,\n headers,\n signal,\n ...fetchOptions\n } = options;\n if (\n !Number.isSafeInteger(timeout) ||\n timeout < 1 ||\n timeout > 2_147_483_647\n ) {\n return clientErr(\n \"Action timeout must be a positive integer up to 2147483647 milliseconds.\"\n );\n }\n const controller = new AbortController();\n const abort = () => controller.abort(signal?.reason);\n if (signal?.aborted) {\n abort();\n } else {\n signal?.addEventListener(\"abort\", abort, { once: true });\n }\n let timedOut = false;\n const timeoutId = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, timeout);\n\n try {\n const requestResult = await safeTry(async () => {\n const mergedHeaders = new Headers(config.headers);\n new Headers(headers).forEach((value, key) => {\n mergedHeaders.set(key, value);\n });\n if (multipart) {\n mergedHeaders.delete(\"content-type\");\n } else {\n mergedHeaders.set(\"content-type\", \"application/json\");\n }\n const response = await fetch(\n `${config.baseUrl.replace(/\\/+$/, \"\")}/services`,\n {\n ...fetchOptions,\n method: \"POST\",\n headers: mergedHeaders,\n credentials: fetchOptions.credentials ?? config.credentials,\n body: body(),\n signal: controller.signal,\n }\n );\n const json: unknown = await response.json();\n if (\n json === null ||\n typeof json !== \"object\" ||\n !(\"status\" in json) ||\n typeof json.status !== \"boolean\" ||\n !(\"message\" in json) ||\n typeof json.message !== \"string\" ||\n !(\"data\" in json)\n ) {\n throw new Error(\"Invalid Nile action response.\");\n }\n return json as ExternalResponse<T>;\n });\n if (requestResult.isErr) {\n if (timedOut) {\n return clientErr(\"Action timed out.\");\n }\n if (signal?.aborted) {\n return clientErr(\"Action cancelled.\");\n }\n return resolveError(requestResult.error);\n }\n const result = requestResult.value;\n if (!result.status) {\n return clientErr(result.message, result.data ?? null);\n }\n return clientOk(result.data, result.message);\n } finally {\n clearTimeout(timeoutId);\n signal?.removeEventListener(\"abort\", abort);\n }\n}\n\n/** JSON action calls preserve the server's message and failure data unchanged. */\nexport function sendNileRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n intent: \"explore\" | \"execute\" | \"schema\",\n service: string,\n action: string,\n payload: unknown,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n return sendRequest<T>(\n config,\n () =>\n JSON.stringify({\n intent: intent,\n service: service,\n action: action,\n payload: payload ?? {},\n }),\n false,\n options\n );\n}\n\n/** Let the runtime set the multipart boundary; retain the same action Result contract. */\nexport function sendUploadRequest<T = Record<string, unknown>>(\n config: NileClientConfig,\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>,\n options: FetchOptions = {}\n): Promise<ClientResult<T>> {\n return sendRequest<T>(\n config,\n () => buildFormData(service, action, files, fields),\n true,\n options\n );\n}\n\n/**\n * Builds a FormData instance with RPC routing fields and file/field entries.\n * The server expects 'intent', 'service', 'action' as string fields.\n */\nexport function buildFormData(\n service: string,\n action: string,\n files: Record<string, File | File[]>,\n fields?: Record<string, string>\n): FormData {\n const formData = new FormData();\n\n // RPC routing fields the server expects\n formData.append(\"intent\", \"execute\");\n formData.append(\"service\", service);\n formData.append(\"action\", action);\n\n // User-provided string fields\n if (fields) {\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n }\n\n // File attachments — arrays append multiple entries under the same key\n for (const [key, value] of Object.entries(files)) {\n if (Array.isArray(value)) {\n for (const file of value) {\n formData.append(key, file);\n }\n } else {\n formData.append(key, value);\n }\n }\n\n return formData;\n}\n\n/** Convert local transport errors to the same client failure shape. */\nfunction resolveError(error: unknown): ClientResult<never> {\n return clientErr(error instanceof Error ? error.message : String(error));\n}\n","import { sendNileRequest, sendUploadRequest } from \"./request.js\";\nimport type {\n ClientResult,\n NileClient,\n NileClientConfig,\n UploadParams,\n} from \"./types.js\";\n\n/**\n * Creates a type-safe Nile client for interacting with backend services.\n *\n * @param config - The client configuration including baseUrl\n * @returns A factory object with invoke, explore, schema, and upload methods\n *\n * @example\n * const nile = createNileClient<ServicePayloads>({ baseUrl: \"/api\" });\n * const result = await nile.invoke({\n * service: \"tasks\",\n * action: \"create\",\n * payload: { title: \"Buy milk\" }\n * });\n * if (result.isErr) {\n * console.error(result.error);\n * } else {\n * console.log(result.value);\n * }\n */\nexport function createNileClient<\n TPayloads = Record<string, Record<string, unknown>>,\n>(config: NileClientConfig): NileClient<TPayloads> {\n return {\n /** Invoke a service action through the backend engine pipeline */\n invoke: async <\n S extends keyof TPayloads & string,\n A extends keyof TPayloads[S] & string,\n >(\n params: {\n service: S;\n action: A;\n payload: TPayloads[S][A];\n fetchOptions?: Parameters<typeof sendNileRequest>[5];\n }\n ) => {\n const { service, action, payload, fetchOptions } = params;\n const result = await sendNileRequest<Record<string, unknown>>(\n config,\n \"execute\",\n service,\n action,\n payload,\n fetchOptions\n );\n return result;\n },\n\n /** Discover available services and actions (supports \"*\" wildcard) */\n explore: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"explore\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Get action schemas as JSON Schema (supports \"*\" wildcard) */\n schema: async (params) => {\n const { service, action, fetchOptions } = params;\n return await sendNileRequest(\n config,\n \"schema\",\n service,\n action,\n {},\n fetchOptions\n );\n },\n\n /** Upload files to an action via multipart form-data */\n upload: async (params: UploadParams) => {\n const { service, action, files, fields, fetchOptions } = params;\n return await sendUploadRequest(\n config,\n service,\n action,\n files,\n fields,\n fetchOptions\n );\n },\n };\n}\n","import type { Socket } from \"socket.io-client\";\nimport type {\n ActionShape,\n NileSocketClient,\n NileSocketClientConfig,\n SocketActions,\n SocketTopics,\n TopicShape,\n} from \"./socket-types.js\";\nimport { clientErr, clientOk } from \"./result.js\";\nimport type { ClientResult } from \"./types.js\";\n\ntype WireValue = object | string | number | boolean | null | undefined;\n/** Acknowledgements keep the server's `{ error, data }` wire shape; the client turns them into Results. */\ninterface WireResult {\n error: string | null;\n data: unknown;\n}\ninterface Listener {\n topic: string;\n scope?: WireValue;\n onMessage: (message: WireValue) => void;\n active: boolean;\n normalizedScope?: WireValue;\n}\n\n/** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */\nexport function createNileSocketClient<\n Actions extends ActionShape<Actions> = SocketActions,\n Topics extends TopicShape<Topics> = SocketTopics,\n>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics> {\n const timeout = config.timeout ?? 30_000;\n if (!Number.isSafeInteger(timeout) || timeout < 1) {\n throw new Error(\"Socket timeout must be positive\");\n }\n let socket: Socket | undefined;\n let connecting: Promise<ClientResult<Socket>> | undefined;\n let closed = false;\n const listeners = new Set<Listener>();\n const pending = new Set<() => void>();\n const fail = (error: string) => clientErr(error);\n const settleCallback = (fn: () => void) => {\n Promise.resolve()\n .then(fn)\n .catch(() => undefined);\n };\n const request = async <T>(\n event: string,\n input: WireValue\n ): Promise<ClientResult<T>> => {\n const connected = await connect();\n if (connected.isErr) {\n return fail(connected.error);\n }\n const current = connected.value;\n if (!current.connected || closed) {\n return fail(\"Socket disconnected\");\n }\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: ClientResult<T>) => {\n if (settled) {\n return;\n }\n settled = true;\n pending.delete(cancel);\n clearTimeout(timer);\n resolve(result);\n };\n const cancel = () => finish(fail(\"Socket disconnected\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket acknowledgement timed out\")),\n timeout\n );\n pending.add(cancel);\n // The connection check and emit are synchronous; disconnected calls never enter the offline queue.\n current.emit(event, input, (result: WireResult) => {\n if (\n !result ||\n typeof result !== \"object\" ||\n !(\"error\" in result) ||\n !(\"data\" in result)\n ) {\n finish(fail(\"Invalid socket response\"));\n return;\n }\n if (typeof result.error === \"string\") {\n finish(clientErr(result.error, result.data));\n return;\n }\n // The server validated this acknowledgement for the requested event.\n finish(clientOk(result.data as T));\n });\n });\n };\n const rejoin = async () => {\n for (const listener of listeners) {\n if (!listener.active) {\n continue;\n }\n const result = await request<{ scope?: WireValue }>(\"nile:receive\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n if (result.isOk) {\n listener.normalizedScope = result.value?.scope;\n continue;\n }\n const message = result.error;\n settleCallback(() =>\n config.onSubscriptionError?.({\n topic: listener.topic,\n error: message,\n })\n );\n }\n };\n const connect = (): Promise<ClientResult<Socket>> => {\n if (closed) {\n return Promise.resolve(fail(\"Socket client is closed\"));\n }\n if (socket?.connected) {\n return Promise.resolve(clientOk(socket));\n }\n if (connecting) {\n return connecting;\n }\n connecting = import(\"socket.io-client\")\n .then(\n ({ io }) =>\n new Promise<ClientResult<Socket>>((resolve) => {\n if (closed) {\n resolve(fail(\"Socket client is closed\"));\n return;\n }\n const current =\n socket ??\n io(config.baseUrl, {\n path: config.path ?? \"/socket.io\",\n autoConnect: false,\n withCredentials: true,\n timeout,\n auth: (done) => {\n Promise.resolve()\n .then(() =>\n typeof config.auth === \"function\"\n ? config.auth()\n : (config.auth ?? {})\n )\n .then(done)\n .catch(() => done({}));\n },\n });\n if (!socket) {\n socket = current;\n current.on(\"disconnect\", () => {\n for (const cancel of [...pending]) {\n cancel();\n }\n });\n current.on(\"connect\", () => {\n rejoin();\n });\n current.on(\"nile:message\", (message: WireValue) =>\n dispatchMessage(message, listeners)\n );\n }\n let settled = false;\n const finish = (result: ClientResult<Socket>) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timer);\n current.off(\"connect\", ready);\n current.off(\"connect_error\", failed);\n pending.delete(cancel);\n resolve(result);\n };\n const ready = () => finish(clientOk(current));\n const failed = () => finish(fail(\"Socket connection failed\"));\n const cancel = () => finish(fail(\"Socket client closed\"));\n const timer = setTimeout(\n () => finish(fail(\"Socket connection timed out\")),\n timeout\n );\n pending.add(cancel);\n current.once(\"connect\", ready);\n current.once(\"connect_error\", failed);\n current.connect();\n })\n )\n .catch(() => fail(\"Could not initialize socket client\"))\n .finally(() => {\n connecting = undefined;\n });\n return connecting;\n };\n return {\n invoke: (input) => request(\"nile:invoke\", input),\n send: (input) => request(\"nile:send\", input),\n async receive(input) {\n const listener: Listener = {\n ...input,\n scope: input.scope as WireValue,\n onMessage: input.onMessage as Listener[\"onMessage\"],\n active: true,\n };\n listeners.add(listener);\n const result = await request<{ subscribed: boolean; scope?: WireValue }>(\n \"nile:receive\",\n { topic: input.topic, scope: input.scope }\n );\n if (result.isErr) {\n listeners.delete(listener);\n return fail(result.error);\n }\n listener.normalizedScope = result.value?.scope;\n return clientOk({\n unsubscribe: () => {\n listener.active = false;\n listeners.delete(listener);\n const shared = [...listeners].some(\n (item) =>\n item.topic === listener.topic &&\n stableScope(item.scope) === stableScope(listener.scope)\n );\n if (shared || closed) {\n return Promise.resolve(clientOk({ subscribed: shared }));\n }\n return request(\"nile:unsubscribe\", {\n topic: listener.topic,\n scope: listener.scope,\n });\n },\n });\n },\n close() {\n closed = true;\n listeners.clear();\n for (const cancel of [...pending]) {\n cancel();\n }\n socket?.disconnect();\n socket?.removeAllListeners();\n },\n };\n}\n\n/** Match scoped messages independently of object property insertion order. */\nfunction stableScope(scope: WireValue): string {\n if (scope === undefined || scope === null) {\n return \"null\";\n }\n if (Array.isArray(scope)) {\n return `[${scope.map(stableScope).join(\",\")}]`;\n }\n if (typeof scope === \"object\") {\n return `{${Object.keys(scope)\n .sort()\n .map(\n (key) =>\n `${JSON.stringify(key)}:${stableScope((scope as Record<string, WireValue>)[key])}`\n )\n .join(\",\")}}`;\n }\n return JSON.stringify(scope);\n}\n\n/** Deliver messages only to listeners for the same validated topic and scope. */\nfunction dispatchMessage(message: WireValue, listeners: Set<Listener>) {\n if (!message || typeof message !== \"object\" || !(\"topic\" in message)) {\n return;\n }\n const scope = stableScope(\n \"scope\" in message ? (message.scope as WireValue) : undefined\n );\n for (const listener of listeners) {\n if (!listener.active || listener.topic !== message.topic) {\n continue;\n }\n if (stableScope(listener.normalizedScope ?? listener.scope) !== scope) {\n continue;\n }\n Promise.resolve()\n .then(() => listener.onMessage(message))\n .catch(() => undefined);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nilejs/client",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "Standalone, type-safe client for Nile backend framework",
6
6
  "main": "./dist/index.cjs",