@nilejs/client 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ //#region src/types.d.ts
1
2
  /** Standard Nile API response shape */
2
3
  interface ExternalResponse<T = Record<string, unknown>> {
3
4
  status: boolean;
@@ -58,7 +59,6 @@ interface ExploreActionsResponse {
58
59
  visibility?: {
59
60
  rest?: boolean;
60
61
  rpc?: boolean;
61
- ws?: boolean;
62
62
  };
63
63
  }>;
64
64
  }
@@ -75,6 +75,8 @@ interface NileClientConfig {
75
75
  headers?: Record<string, string>;
76
76
  /** Optional timeout in ms (default: 30000) */
77
77
  timeout?: number;
78
+ /** Socket settings for `client.socket`. The socket connects on first use. */
79
+ socket?: NileSocketConfig;
78
80
  }
79
81
  /** Options for individual fetch calls (body and method are managed internally) */
80
82
  interface FetchOptions extends Omit<RequestInit, "body" | "method"> {
@@ -97,7 +99,9 @@ interface UploadParams {
97
99
  fetchOptions?: FetchOptions;
98
100
  }
99
101
  /** Interface for the typed client */
100
- interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
102
+ interface NileClient<TPayloads = Record<string, Record<string, unknown>>, TTopics extends TopicShape<TTopics> = SocketTopics> {
103
+ /** Typed topics over Socket.IO: `send`, `receive` and `close`. */
104
+ socket: NileSocketClient<TTopics>;
101
105
  /** Invoke an action with full type-safety for payload */
102
106
  invoke: <S extends keyof TPayloads & string, A extends keyof TPayloads[S] & string>(params: {
103
107
  service: S;
@@ -112,42 +116,16 @@ interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
112
116
  /** Upload files to an action via multipart form-data */
113
117
  upload: (params: UploadParams) => Promise<ClientResult>;
114
118
  }
115
- /**
116
- * Creates a type-safe Nile client for interacting with backend services.
117
- *
118
- * @param config - The client configuration including baseUrl
119
- * @returns A factory object with invoke, explore, schema, and upload methods
120
- *
121
- * @example
122
- * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
123
- * const result = await nile.invoke({
124
- * service: "tasks",
125
- * action: "create",
126
- * payload: { title: "Buy milk" }
127
- * });
128
- * if (result.isErr) {
129
- * console.error(result.error);
130
- * } else {
131
- * console.log(result.value);
132
- * }
133
- */
134
- export declare function createNileClient<TPayloads = Record<string, Record<string, unknown>>>(config: NileClientConfig): NileClient<TPayloads>;
135
- /** Let the runtime set the multipart boundary; retain the same action Result contract. */
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>>;
137
- /**
138
- * Builds a FormData instance with RPC routing fields and file/field entries.
139
- * The server expects 'intent', 'service', 'action' as string fields.
140
- */
141
- export declare function buildFormData(service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>): FormData;
119
+ //#endregion
120
+ //#region src/socket-types.d.ts
121
+ type WireJson = object | string | number | boolean | null;
142
122
  interface TopicContract {
143
- data: object | string | number | boolean | null;
144
- input?: object | string | number | boolean | null;
145
- scope?: object | string | number | boolean | null;
123
+ data: WireJson;
124
+ input?: WireJson;
125
+ scope?: WireJson;
146
126
  }
147
127
  type TopicShape<T> = { [K in keyof T]: TopicContract; };
148
- type ActionShape<A> = { [S in keyof A]: { [K in keyof A[S]]: object; }; };
149
128
  type SocketTopics = Record<string, TopicContract>;
150
- type SocketActions = Record<string, Record<string, object>>;
151
129
  type SocketSelection<T, K extends keyof T> = {
152
130
  topic: K;
153
131
  } & (T[K] extends {
@@ -170,36 +148,97 @@ interface SocketMessage<K extends string, D> {
170
148
  id: string;
171
149
  topic: K;
172
150
  data: D;
173
- scope?: object | string | number | boolean | null;
151
+ scope?: WireJson;
174
152
  time: string;
175
- }
176
- interface NileSocketClientConfig {
177
- baseUrl: string;
153
+ /** Present when a client sent the message; absent for server messages. */
154
+ from?: {
155
+ connectionId: string;
156
+ userId?: string;
157
+ };
158
+ }
159
+ interface NileSocketConfig {
160
+ /** Socket server origin when it differs from `baseUrl`. Defaults to the origin of `baseUrl`. */
161
+ url?: string;
162
+ /** Socket.IO path. Default `/socket.io`; must match the server. */
178
163
  path?: string;
164
+ /** Connection and acknowledgement timeout in ms. Default 30000. */
179
165
  timeout?: number;
166
+ /** Sent when connecting, e.g. `{ token }`. A function is called again on every reconnect. */
180
167
  auth?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
181
- onSubscriptionError?: (input: {
182
- topic: string;
168
+ onConnect?: () => void;
169
+ onDisconnect?: (reason: string) => void;
170
+ /** Fires after a reconnect once active subscriptions are restored. */
171
+ onReconnect?: () => void;
172
+ /** A connection failure, or a subscription refused while restoring it after a reconnect. */
173
+ onError?: (error: {
183
174
  error: string;
175
+ topic?: string;
184
176
  }) => void;
185
177
  }
186
- interface NileSocketClient<A extends ActionShape<A>, T extends TopicShape<T>> {
187
- invoke: <S extends keyof A & string, K extends keyof A[S] & string>(input: {
188
- service: S;
189
- action: K;
190
- payload: A[S][K];
191
- }) => Promise<ClientResult>;
178
+ interface SocketSubscription {
179
+ topic: string;
180
+ /** Stop this listener; the server subscription ends when the last listener for it stops. */
181
+ unsubscribe: () => Promise<ClientResult<{
182
+ subscribed: boolean;
183
+ }>>;
184
+ }
185
+ interface NileSocketClient<T extends TopicShape<T> = SocketTopics> {
186
+ /** Send to a topic; the server checks the topic's `canSend` and validates the data. */
192
187
  send: <K extends keyof T & string>(input: SocketPublication<T, K>) => Promise<ClientResult<SocketMessage<K, T[K]["data"]>>>;
188
+ /** Subscribe to a topic and listen for its messages in one call. */
193
189
  receive: <K extends keyof T & string>(input: SocketSelection<T, K> & {
194
190
  onMessage: (message: SocketMessage<K, T[K]["data"]>) => void;
195
- }) => Promise<ClientResult<{
196
- unsubscribe: () => Promise<ClientResult<{
197
- subscribed: boolean;
198
- }>>;
199
- }>>;
191
+ }) => Promise<ClientResult<SocketSubscription>>;
192
+ /** Disconnect and drop every listener. The client cannot reconnect afterwards. */
200
193
  close: () => void;
201
194
  }
202
- /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
203
- export declare function createNileSocketClient<Actions extends ActionShape<Actions> = SocketActions, Topics extends TopicShape<Topics> = SocketTopics>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics>;
204
- export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketClientConfig, SocketActions, SocketMessage, SocketPublication, SocketSelection, SocketTopics, UploadParams, ValidationFailureData };
195
+ //#endregion
196
+ //#region src/create-client.d.ts
197
+ /**
198
+ * Creates a type-safe Nile client for interacting with backend services.
199
+ *
200
+ * @param config - The client configuration including baseUrl
201
+ * @returns A factory object with invoke, explore, schema and upload methods, plus
202
+ * `socket` for topics. The socket connects on its first `send` or `receive`.
203
+ *
204
+ * @example
205
+ * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
206
+ * const result = await nile.invoke({
207
+ * service: "tasks",
208
+ * action: "create",
209
+ * payload: { title: "Buy milk" }
210
+ * });
211
+ * if (result.isErr) {
212
+ * console.error(result.error);
213
+ * } else {
214
+ * console.log(result.value);
215
+ * }
216
+ *
217
+ * const received = await nile.socket.receive({
218
+ * topic: "tasks.updated",
219
+ * onMessage: (message) => console.log(message.data),
220
+ * });
221
+ */
222
+ export declare function createNileClient<TPayloads = Record<string, Record<string, unknown>>, TTopics extends TopicShape<TTopics> = SocketTopics>(config: NileClientConfig): NileClient<TPayloads, TTopics>;
223
+ //#endregion
224
+ //#region src/deprecated.d.ts
225
+ /**
226
+ * Kept as a throwing stub so an upgrade fails with guidance instead of a
227
+ * missing export.
228
+ *
229
+ * @deprecated Use `createNileClient({ baseUrl, socket: { auth } })` and call
230
+ * `socket.send`, `socket.receive` and `socket.close` on the returned client.
231
+ */
232
+ export declare function createNileSocketClient<Actions = unknown, Topics = unknown>(_config?: unknown): never;
233
+ //#endregion
234
+ //#region src/request.d.ts
235
+ /** Let the runtime set the multipart boundary; retain the same action Result contract. */
236
+ 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>>;
237
+ /**
238
+ * Builds a FormData instance with RPC routing fields and file/field entries.
239
+ * The server expects 'intent', 'service', 'action' as string fields.
240
+ */
241
+ export declare function buildFormData(service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>): FormData;
242
+ //#endregion
243
+ export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketConfig, SocketMessage, SocketPublication, SocketSelection, SocketSubscription, SocketTopics, TopicContract, UploadParams, ValidationFailureData };
205
244
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ //#region src/types.d.ts
1
2
  /** Standard Nile API response shape */
2
3
  interface ExternalResponse<T = Record<string, unknown>> {
3
4
  status: boolean;
@@ -58,7 +59,6 @@ interface ExploreActionsResponse {
58
59
  visibility?: {
59
60
  rest?: boolean;
60
61
  rpc?: boolean;
61
- ws?: boolean;
62
62
  };
63
63
  }>;
64
64
  }
@@ -75,6 +75,8 @@ interface NileClientConfig {
75
75
  headers?: Record<string, string>;
76
76
  /** Optional timeout in ms (default: 30000) */
77
77
  timeout?: number;
78
+ /** Socket settings for `client.socket`. The socket connects on first use. */
79
+ socket?: NileSocketConfig;
78
80
  }
79
81
  /** Options for individual fetch calls (body and method are managed internally) */
80
82
  interface FetchOptions extends Omit<RequestInit, "body" | "method"> {
@@ -97,7 +99,9 @@ interface UploadParams {
97
99
  fetchOptions?: FetchOptions;
98
100
  }
99
101
  /** Interface for the typed client */
100
- interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
102
+ interface NileClient<TPayloads = Record<string, Record<string, unknown>>, TTopics extends TopicShape<TTopics> = SocketTopics> {
103
+ /** Typed topics over Socket.IO: `send`, `receive` and `close`. */
104
+ socket: NileSocketClient<TTopics>;
101
105
  /** Invoke an action with full type-safety for payload */
102
106
  invoke: <S extends keyof TPayloads & string, A extends keyof TPayloads[S] & string>(params: {
103
107
  service: S;
@@ -112,42 +116,16 @@ interface NileClient<TPayloads = Record<string, Record<string, unknown>>> {
112
116
  /** Upload files to an action via multipart form-data */
113
117
  upload: (params: UploadParams) => Promise<ClientResult>;
114
118
  }
115
- /**
116
- * Creates a type-safe Nile client for interacting with backend services.
117
- *
118
- * @param config - The client configuration including baseUrl
119
- * @returns A factory object with invoke, explore, schema, and upload methods
120
- *
121
- * @example
122
- * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
123
- * const result = await nile.invoke({
124
- * service: "tasks",
125
- * action: "create",
126
- * payload: { title: "Buy milk" }
127
- * });
128
- * if (result.isErr) {
129
- * console.error(result.error);
130
- * } else {
131
- * console.log(result.value);
132
- * }
133
- */
134
- export declare function createNileClient<TPayloads = Record<string, Record<string, unknown>>>(config: NileClientConfig): NileClient<TPayloads>;
135
- /** Let the runtime set the multipart boundary; retain the same action Result contract. */
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>>;
137
- /**
138
- * Builds a FormData instance with RPC routing fields and file/field entries.
139
- * The server expects 'intent', 'service', 'action' as string fields.
140
- */
141
- export declare function buildFormData(service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>): FormData;
119
+ //#endregion
120
+ //#region src/socket-types.d.ts
121
+ type WireJson = object | string | number | boolean | null;
142
122
  interface TopicContract {
143
- data: object | string | number | boolean | null;
144
- input?: object | string | number | boolean | null;
145
- scope?: object | string | number | boolean | null;
123
+ data: WireJson;
124
+ input?: WireJson;
125
+ scope?: WireJson;
146
126
  }
147
127
  type TopicShape<T> = { [K in keyof T]: TopicContract; };
148
- type ActionShape<A> = { [S in keyof A]: { [K in keyof A[S]]: object; }; };
149
128
  type SocketTopics = Record<string, TopicContract>;
150
- type SocketActions = Record<string, Record<string, object>>;
151
129
  type SocketSelection<T, K extends keyof T> = {
152
130
  topic: K;
153
131
  } & (T[K] extends {
@@ -170,36 +148,97 @@ interface SocketMessage<K extends string, D> {
170
148
  id: string;
171
149
  topic: K;
172
150
  data: D;
173
- scope?: object | string | number | boolean | null;
151
+ scope?: WireJson;
174
152
  time: string;
175
- }
176
- interface NileSocketClientConfig {
177
- baseUrl: string;
153
+ /** Present when a client sent the message; absent for server messages. */
154
+ from?: {
155
+ connectionId: string;
156
+ userId?: string;
157
+ };
158
+ }
159
+ interface NileSocketConfig {
160
+ /** Socket server origin when it differs from `baseUrl`. Defaults to the origin of `baseUrl`. */
161
+ url?: string;
162
+ /** Socket.IO path. Default `/socket.io`; must match the server. */
178
163
  path?: string;
164
+ /** Connection and acknowledgement timeout in ms. Default 30000. */
179
165
  timeout?: number;
166
+ /** Sent when connecting, e.g. `{ token }`. A function is called again on every reconnect. */
180
167
  auth?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
181
- onSubscriptionError?: (input: {
182
- topic: string;
168
+ onConnect?: () => void;
169
+ onDisconnect?: (reason: string) => void;
170
+ /** Fires after a reconnect once active subscriptions are restored. */
171
+ onReconnect?: () => void;
172
+ /** A connection failure, or a subscription refused while restoring it after a reconnect. */
173
+ onError?: (error: {
183
174
  error: string;
175
+ topic?: string;
184
176
  }) => void;
185
177
  }
186
- interface NileSocketClient<A extends ActionShape<A>, T extends TopicShape<T>> {
187
- invoke: <S extends keyof A & string, K extends keyof A[S] & string>(input: {
188
- service: S;
189
- action: K;
190
- payload: A[S][K];
191
- }) => Promise<ClientResult>;
178
+ interface SocketSubscription {
179
+ topic: string;
180
+ /** Stop this listener; the server subscription ends when the last listener for it stops. */
181
+ unsubscribe: () => Promise<ClientResult<{
182
+ subscribed: boolean;
183
+ }>>;
184
+ }
185
+ interface NileSocketClient<T extends TopicShape<T> = SocketTopics> {
186
+ /** Send to a topic; the server checks the topic's `canSend` and validates the data. */
192
187
  send: <K extends keyof T & string>(input: SocketPublication<T, K>) => Promise<ClientResult<SocketMessage<K, T[K]["data"]>>>;
188
+ /** Subscribe to a topic and listen for its messages in one call. */
193
189
  receive: <K extends keyof T & string>(input: SocketSelection<T, K> & {
194
190
  onMessage: (message: SocketMessage<K, T[K]["data"]>) => void;
195
- }) => Promise<ClientResult<{
196
- unsubscribe: () => Promise<ClientResult<{
197
- subscribed: boolean;
198
- }>>;
199
- }>>;
191
+ }) => Promise<ClientResult<SocketSubscription>>;
192
+ /** Disconnect and drop every listener. The client cannot reconnect afterwards. */
200
193
  close: () => void;
201
194
  }
202
- /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
203
- export declare function createNileSocketClient<Actions extends ActionShape<Actions> = SocketActions, Topics extends TopicShape<Topics> = SocketTopics>(config: NileSocketClientConfig): NileSocketClient<Actions, Topics>;
204
- export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketClientConfig, SocketActions, SocketMessage, SocketPublication, SocketSelection, SocketTopics, UploadParams, ValidationFailureData };
195
+ //#endregion
196
+ //#region src/create-client.d.ts
197
+ /**
198
+ * Creates a type-safe Nile client for interacting with backend services.
199
+ *
200
+ * @param config - The client configuration including baseUrl
201
+ * @returns A factory object with invoke, explore, schema and upload methods, plus
202
+ * `socket` for topics. The socket connects on its first `send` or `receive`.
203
+ *
204
+ * @example
205
+ * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
206
+ * const result = await nile.invoke({
207
+ * service: "tasks",
208
+ * action: "create",
209
+ * payload: { title: "Buy milk" }
210
+ * });
211
+ * if (result.isErr) {
212
+ * console.error(result.error);
213
+ * } else {
214
+ * console.log(result.value);
215
+ * }
216
+ *
217
+ * const received = await nile.socket.receive({
218
+ * topic: "tasks.updated",
219
+ * onMessage: (message) => console.log(message.data),
220
+ * });
221
+ */
222
+ export declare function createNileClient<TPayloads = Record<string, Record<string, unknown>>, TTopics extends TopicShape<TTopics> = SocketTopics>(config: NileClientConfig): NileClient<TPayloads, TTopics>;
223
+ //#endregion
224
+ //#region src/deprecated.d.ts
225
+ /**
226
+ * Kept as a throwing stub so an upgrade fails with guidance instead of a
227
+ * missing export.
228
+ *
229
+ * @deprecated Use `createNileClient({ baseUrl, socket: { auth } })` and call
230
+ * `socket.send`, `socket.receive` and `socket.close` on the returned client.
231
+ */
232
+ export declare function createNileSocketClient<Actions = unknown, Topics = unknown>(_config?: unknown): never;
233
+ //#endregion
234
+ //#region src/request.d.ts
235
+ /** Let the runtime set the multipart boundary; retain the same action Result contract. */
236
+ 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>>;
237
+ /**
238
+ * Builds a FormData instance with RPC routing fields and file/field entries.
239
+ * The server expects 'intent', 'service', 'action' as string fields.
240
+ */
241
+ export declare function buildFormData(service: string, action: string, files: Record<string, File | File[]>, fields?: Record<string, string>): FormData;
242
+ //#endregion
243
+ export type { ClientErr, ClientOk, ClientResult, ExploreActionsResponse, ExploreServicesResponse, ExternalResponse, FetchOptions, NileClient, NileClientConfig, NileIntentParams, NileSocketClient, NileSocketConfig, SocketMessage, SocketPublication, SocketSelection, SocketSubscription, SocketTopics, TopicContract, UploadParams, ValidationFailureData };
205
244
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // src/result.ts
1
+ //#region 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,7 +27,8 @@ function clientErr(error, details = null) {
27
27
  data: null
28
28
  };
29
29
  }
30
- // src/safe-try.ts
30
+ //#endregion
31
+ //#region src/safe-try.ts
31
32
  /**
32
33
  * Gracefully handles both synchronous and asynchronous operations by returning a result object.
33
34
  * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.
@@ -67,7 +68,8 @@ function safeTry(fn) {
67
68
  };
68
69
  }
69
70
  }
70
- // src/request.ts
71
+ //#endregion
72
+ //#region src/request.ts
71
73
  /** Send one action call; the deadline covers both fetch and reading its JSON body. */
72
74
  async function sendRequest(config, body, multipart, options) {
73
75
  const { timeout = config.timeout ?? 3e4, headers, signal, ...fetchOptions } = options;
@@ -144,58 +146,24 @@ function buildFormData(service, action, files, fields) {
144
146
  function resolveError(error) {
145
147
  return clientErr(error instanceof Error ? error.message : String(error));
146
148
  }
147
- // src/create-client.ts
148
- /**
149
- * Creates a type-safe Nile client for interacting with backend services.
150
- *
151
- * @param config - The client configuration including baseUrl
152
- * @returns A factory object with invoke, explore, schema, and upload methods
153
- *
154
- * @example
155
- * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
156
- * const result = await nile.invoke({
157
- * service: "tasks",
158
- * action: "create",
159
- * payload: { title: "Buy milk" }
160
- * });
161
- * if (result.isErr) {
162
- * console.error(result.error);
163
- * } else {
164
- * console.log(result.value);
165
- * }
166
- */
167
- function createNileClient(config) {
168
- return {
169
- /** Invoke a service action through the backend engine pipeline */
170
- invoke: async (params) => {
171
- const { service, action, payload, fetchOptions } = params;
172
- return await sendNileRequest(config, "execute", service, action, payload, fetchOptions);
173
- },
174
- /** Discover available services and actions (supports "*" wildcard) */
175
- explore: async (params) => {
176
- const { service, action, fetchOptions } = params;
177
- return await sendNileRequest(config, "explore", service, action, {}, fetchOptions);
178
- },
179
- /** Get action schemas as JSON Schema (supports "*" wildcard) */
180
- schema: async (params) => {
181
- const { service, action, fetchOptions } = params;
182
- return await sendNileRequest(config, "schema", service, action, {}, fetchOptions);
183
- },
184
- /** Upload files to an action via multipart form-data */
185
- upload: async (params) => {
186
- const { service, action, files, fields, fetchOptions } = params;
187
- return await sendUploadRequest(config, service, action, files, fields, fetchOptions);
188
- }
189
- };
149
+ //#endregion
150
+ //#region src/socket-client.ts
151
+ /** The socket connects to an origin; any path in a URL would be read by Socket.IO as a namespace. */
152
+ function socketOrigin(url) {
153
+ try {
154
+ return new URL(url).origin;
155
+ } catch {
156
+ return;
157
+ }
190
158
  }
191
- // src/socket-client.ts
192
- /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
193
- function createNileSocketClient(config) {
159
+ /** Own a lazy Socket.IO connection with no offline write buffering. */
160
+ function createNileSocket(config) {
194
161
  const timeout = config.timeout ?? 3e4;
195
162
  if (!Number.isSafeInteger(timeout) || timeout < 1) throw new Error("Socket timeout must be positive");
196
163
  let socket;
197
164
  let connecting;
198
165
  let closed = false;
166
+ let everConnected = false;
199
167
  const listeners = /* @__PURE__ */ new Set();
200
168
  const pending = /* @__PURE__ */ new Set();
201
169
  const fail = (error) => clientErr(error);
@@ -244,7 +212,7 @@ function createNileSocketClient(config) {
244
212
  continue;
245
213
  }
246
214
  const message = result.error;
247
- settleCallback(() => config.onSubscriptionError?.({
215
+ settleCallback(() => config.onError?.({
248
216
  topic: listener.topic,
249
217
  error: message
250
218
  }));
@@ -259,7 +227,7 @@ function createNileSocketClient(config) {
259
227
  resolve(fail("Socket client is closed"));
260
228
  return;
261
229
  }
262
- const current = socket ?? io(config.baseUrl, {
230
+ const current = socket ?? io(config.url ? socketOrigin(config.url) : void 0, {
263
231
  path: config.path ?? "/socket.io",
264
232
  autoConnect: false,
265
233
  withCredentials: true,
@@ -270,11 +238,20 @@ function createNileSocketClient(config) {
270
238
  });
271
239
  if (!socket) {
272
240
  socket = current;
273
- current.on("disconnect", () => {
241
+ current.on("disconnect", (reason) => {
274
242
  for (const cancel of [...pending]) cancel();
243
+ settleCallback(() => config.onDisconnect?.(reason));
275
244
  });
276
245
  current.on("connect", () => {
277
- rejoin();
246
+ if (!everConnected) {
247
+ everConnected = true;
248
+ settleCallback(() => config.onConnect?.());
249
+ return;
250
+ }
251
+ rejoin().then(() => settleCallback(() => config.onReconnect?.()));
252
+ });
253
+ current.on("connect_error", () => {
254
+ settleCallback(() => config.onError?.({ error: "Socket connection failed" }));
278
255
  });
279
256
  current.on("nile:message", (message) => dispatchMessage(message, listeners));
280
257
  }
@@ -302,7 +279,6 @@ function createNileSocketClient(config) {
302
279
  return connecting;
303
280
  };
304
281
  return {
305
- invoke: (input) => request("nile:invoke", input),
306
282
  send: (input) => request("nile:send", input),
307
283
  async receive(input) {
308
284
  const listener = {
@@ -321,16 +297,19 @@ function createNileSocketClient(config) {
321
297
  return fail(result.error);
322
298
  }
323
299
  listener.normalizedScope = result.value?.scope;
324
- return clientOk({ unsubscribe: () => {
325
- listener.active = false;
326
- listeners.delete(listener);
327
- const shared = [...listeners].some((item) => item.topic === listener.topic && stableScope(item.scope) === stableScope(listener.scope));
328
- if (shared || closed) return Promise.resolve(clientOk({ subscribed: shared }));
329
- return request("nile:unsubscribe", {
330
- topic: listener.topic,
331
- scope: listener.scope
332
- });
333
- } });
300
+ return clientOk({
301
+ topic: input.topic,
302
+ unsubscribe: () => {
303
+ listener.active = false;
304
+ listeners.delete(listener);
305
+ const shared = [...listeners].some((item) => item.topic === listener.topic && stableScope(item.scope) === stableScope(listener.scope));
306
+ if (shared || closed) return Promise.resolve(clientOk({ subscribed: shared }));
307
+ return request("nile:unsubscribe", {
308
+ topic: listener.topic,
309
+ scope: listener.scope
310
+ });
311
+ }
312
+ });
334
313
  },
335
314
  close() {
336
315
  closed = true;
@@ -358,6 +337,82 @@ function dispatchMessage(message, listeners) {
358
337
  Promise.resolve().then(() => listener.onMessage(message)).catch(() => void 0);
359
338
  }
360
339
  }
340
+ //#endregion
341
+ //#region src/create-client.ts
342
+ /**
343
+ * Creates a type-safe Nile client for interacting with backend services.
344
+ *
345
+ * @param config - The client configuration including baseUrl
346
+ * @returns A factory object with invoke, explore, schema and upload methods, plus
347
+ * `socket` for topics. The socket connects on its first `send` or `receive`.
348
+ *
349
+ * @example
350
+ * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
351
+ * const result = await nile.invoke({
352
+ * service: "tasks",
353
+ * action: "create",
354
+ * payload: { title: "Buy milk" }
355
+ * });
356
+ * if (result.isErr) {
357
+ * console.error(result.error);
358
+ * } else {
359
+ * console.log(result.value);
360
+ * }
361
+ *
362
+ * const received = await nile.socket.receive({
363
+ * topic: "tasks.updated",
364
+ * onMessage: (message) => console.log(message.data),
365
+ * });
366
+ */
367
+ function createNileClient(config) {
368
+ return {
369
+ socket: createNileSocket({
370
+ ...config.socket,
371
+ url: config.socket?.url ?? config.baseUrl
372
+ }),
373
+ /** Invoke a service action through the backend engine pipeline */
374
+ invoke: async (params) => {
375
+ const { service, action, payload, fetchOptions } = params;
376
+ return await sendNileRequest(config, "execute", service, action, payload, fetchOptions);
377
+ },
378
+ /** Discover available services and actions (supports "*" wildcard) */
379
+ explore: async (params) => {
380
+ const { service, action, fetchOptions } = params;
381
+ return await sendNileRequest(config, "explore", service, action, {}, fetchOptions);
382
+ },
383
+ /** Get action schemas as JSON Schema (supports "*" wildcard) */
384
+ schema: async (params) => {
385
+ const { service, action, fetchOptions } = params;
386
+ return await sendNileRequest(config, "schema", service, action, {}, fetchOptions);
387
+ },
388
+ /** Upload files to an action via multipart form-data */
389
+ upload: async (params) => {
390
+ const { service, action, files, fields, fetchOptions } = params;
391
+ return await sendUploadRequest(config, service, action, files, fields, fetchOptions);
392
+ }
393
+ };
394
+ }
395
+ //#endregion
396
+ //#region src/deprecated.ts
397
+ const MIGRATION_MESSAGE = [
398
+ "createNileSocketClient was removed in @nilejs/client 1.1.0.",
399
+ "Create one client with createNileClient({ baseUrl, socket: { auth } })",
400
+ "and use nile.socket.send, nile.socket.receive and nile.socket.close.",
401
+ "Socket invoke is gone as well; call actions with nile.invoke over HTTP.",
402
+ "See https://nile-js.github.io/nile/guide/features/websockets/ and the",
403
+ "migration notes in packages/nile/docs/release-migration.md."
404
+ ].join(" ");
405
+ /**
406
+ * Kept as a throwing stub so an upgrade fails with guidance instead of a
407
+ * missing export.
408
+ *
409
+ * @deprecated Use `createNileClient({ baseUrl, socket: { auth } })` and call
410
+ * `socket.send`, `socket.receive` and `socket.close` on the returned client.
411
+ */
412
+ function createNileSocketClient(_config) {
413
+ throw new Error(MIGRATION_MESSAGE);
414
+ }
415
+ //#endregion
361
416
  export { buildFormData, createNileClient, createNileSocketClient, sendUploadRequest };
362
417
 
363
418
  //# sourceMappingURL=index.js.map