@nilejs/client 0.0.3 → 1.0.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.js CHANGED
@@ -1,186 +1,382 @@
1
- // src/safe-try.ts
1
+ //#region src/result.ts
2
+ /**
3
+ * A successful client result. `data` mirrors `value` so code written for
4
+ * earlier clients keeps working. `message` is present only when the server sent one.
5
+ */
6
+ function clientOk(value, message) {
7
+ return {
8
+ isOk: true,
9
+ isErr: false,
10
+ value,
11
+ error: null,
12
+ ...message === void 0 ? {} : { message },
13
+ data: value
14
+ };
15
+ }
16
+ /**
17
+ * A failed client result with a readable message and any data the server
18
+ * attached to the failure. `data` stays null, as it was for earlier clients.
19
+ */
20
+ function clientErr(error, details = null) {
21
+ return {
22
+ isOk: false,
23
+ isErr: true,
24
+ value: null,
25
+ error,
26
+ details,
27
+ data: null
28
+ };
29
+ }
30
+ //#endregion
31
+ //#region src/safe-try.ts
32
+ /**
33
+ * Gracefully handles both synchronous and asynchronous operations by returning a result object.
34
+ * This replaces the need for external slang-ts dependency and avoids try/catch boilerplate.
35
+ *
36
+ * @param fn - The function to execute (sync or async)
37
+ * @returns A result object containing either the value or the error
38
+ *
39
+ * @example
40
+ * const { isOk, value, error } = await safeTry(() => fetch(url));
41
+ */
2
42
  function safeTry(fn) {
3
- try {
4
- const result = fn();
5
- if (result instanceof Promise) {
6
- return result.then(
7
- (value) => ({
8
- isOk: true,
9
- value,
10
- isErr: false,
11
- error: null
12
- })
13
- ).catch(
14
- (error) => ({
15
- isOk: false,
16
- value: null,
17
- isErr: true,
18
- error
19
- })
20
- );
21
- }
22
- return { isOk: true, value: result, isErr: false, error: null };
23
- } catch (error) {
24
- return { isOk: false, value: null, isErr: true, error };
25
- }
43
+ try {
44
+ const result = fn();
45
+ if (result instanceof Promise) return result.then((value) => ({
46
+ isOk: true,
47
+ value,
48
+ isErr: false,
49
+ error: null
50
+ })).catch((error) => ({
51
+ isOk: false,
52
+ value: null,
53
+ isErr: true,
54
+ error
55
+ }));
56
+ return {
57
+ isOk: true,
58
+ value: result,
59
+ isErr: false,
60
+ error: null
61
+ };
62
+ } catch (error) {
63
+ return {
64
+ isOk: false,
65
+ value: null,
66
+ isErr: true,
67
+ error
68
+ };
69
+ }
26
70
  }
27
-
28
- // src/request.ts
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
+ */
29
77
  async function sendNileRequest(config, intent, service, action, payload, options = {}) {
30
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
31
- const controller = new AbortController();
32
- const timeoutId = setTimeout(() => controller.abort(), timeout);
33
- const requestResult = await safeTry(async () => {
34
- const response = await fetch(`${config.baseUrl}/services`, {
35
- method: "POST",
36
- headers: {
37
- "Content-Type": "application/json",
38
- ...config.headers,
39
- ...fetchOptions.headers
40
- },
41
- credentials: config.credentials,
42
- body: JSON.stringify({
43
- intent,
44
- service,
45
- action,
46
- payload: payload ?? {}
47
- }),
48
- signal: controller.signal,
49
- ...fetchOptions
50
- });
51
- clearTimeout(timeoutId);
52
- const json = await response.json();
53
- return json;
54
- });
55
- if (requestResult.isErr) {
56
- return resolveError(requestResult.error);
57
- }
58
- const result = requestResult.value;
59
- if (!result.status) {
60
- return { error: result.message, data: result.data };
61
- }
62
- return { error: null, data: result.data };
78
+ const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
79
+ 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
98
+ });
99
+ 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);
63
106
  }
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
+ */
64
115
  async function sendUploadRequest(config, service, action, files, fields, options = {}) {
65
- const { timeout = config.timeout ?? 3e4, ...fetchOptions } = options;
66
- const controller = new AbortController();
67
- const timeoutId = setTimeout(() => controller.abort(), timeout);
68
- const formData = buildFormData(service, action, files, fields);
69
- const requestResult = await safeTry(async () => {
70
- const { "Content-Type": _ct, ...safeHeaders } = config.headers ?? {};
71
- const { "Content-Type": _ct2, ...safeFetchHeaders } = fetchOptions.headers ?? {};
72
- const response = await fetch(`${config.baseUrl}/services`, {
73
- method: "POST",
74
- headers: {
75
- ...safeHeaders,
76
- ...safeFetchHeaders
77
- },
78
- credentials: config.credentials,
79
- body: formData,
80
- signal: controller.signal,
81
- ...fetchOptions
82
- });
83
- clearTimeout(timeoutId);
84
- const json = await response.json();
85
- return json;
86
- });
87
- if (requestResult.isErr) {
88
- return resolveError(requestResult.error);
89
- }
90
- const result = requestResult.value;
91
- if (!result.status) {
92
- return { error: result.message, data: result.data };
93
- }
94
- return { error: null, data: result.data };
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);
95
141
  }
142
+ /**
143
+ * Builds a FormData instance with RPC routing fields and file/field entries.
144
+ * The server expects 'intent', 'service', 'action' as string fields.
145
+ */
96
146
  function buildFormData(service, action, files, fields) {
97
- const formData = new FormData();
98
- formData.append("intent", "execute");
99
- formData.append("service", service);
100
- formData.append("action", action);
101
- if (fields) {
102
- for (const [key, value] of Object.entries(fields)) {
103
- formData.append(key, value);
104
- }
105
- }
106
- for (const [key, value] of Object.entries(files)) {
107
- if (Array.isArray(value)) {
108
- for (const file of value) {
109
- formData.append(key, file);
110
- }
111
- } else {
112
- formData.append(key, value);
113
- }
114
- }
115
- return formData;
147
+ const formData = new FormData();
148
+ formData.append("intent", "execute");
149
+ formData.append("service", service);
150
+ formData.append("action", action);
151
+ if (fields) for (const [key, value] of Object.entries(fields)) formData.append(key, value);
152
+ for (const [key, value] of Object.entries(files)) if (Array.isArray(value)) for (const file of value) formData.append(key, file);
153
+ else formData.append(key, value);
154
+ return formData;
116
155
  }
156
+ /** Resolve error from safeTry into a ClientResult */
117
157
  function resolveError(error) {
118
- let errorMsg = "An unknown error occurred";
119
- if (error instanceof Error) {
120
- errorMsg = error.name === "AbortError" ? "Request timed out" : error.message;
121
- } else {
122
- errorMsg = String(error);
123
- }
124
- return { error: errorMsg, data: null };
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);
125
162
  }
126
-
127
- // src/create-client.ts
163
+ //#endregion
164
+ //#region src/create-client.ts
165
+ /**
166
+ * Creates a type-safe Nile client for interacting with backend services.
167
+ *
168
+ * @param config - The client configuration including baseUrl
169
+ * @returns A factory object with invoke, explore, schema, and upload methods
170
+ *
171
+ * @example
172
+ * const nile = createNileClient<ServicePayloads>({ baseUrl: "/api" });
173
+ * const result = await nile.invoke({
174
+ * service: "tasks",
175
+ * action: "create",
176
+ * payload: { title: "Buy milk" }
177
+ * });
178
+ * if (result.isErr) {
179
+ * console.error(result.error);
180
+ * } else {
181
+ * console.log(result.value);
182
+ * }
183
+ */
128
184
  function createNileClient(config) {
129
- return {
130
- /** Invoke a service action through the backend engine pipeline */
131
- invoke: async (params) => {
132
- const { service, action, payload, fetchOptions } = params;
133
- const result = await sendNileRequest(
134
- config,
135
- "execute",
136
- service,
137
- action,
138
- payload,
139
- fetchOptions
140
- );
141
- return result;
142
- },
143
- /** Discover available services and actions (supports "*" wildcard) */
144
- explore: async (params) => {
145
- const { service, action, fetchOptions } = params;
146
- return await sendNileRequest(
147
- config,
148
- "explore",
149
- service,
150
- action,
151
- {},
152
- fetchOptions
153
- );
154
- },
155
- /** Get action schemas as JSON Schema (supports "*" wildcard) */
156
- schema: async (params) => {
157
- const { service, action, fetchOptions } = params;
158
- return await sendNileRequest(
159
- config,
160
- "schema",
161
- service,
162
- action,
163
- {},
164
- fetchOptions
165
- );
166
- },
167
- /** Upload files to an action via multipart form-data */
168
- upload: async (params) => {
169
- const { service, action, files, fields, fetchOptions } = params;
170
- return await sendUploadRequest(
171
- config,
172
- service,
173
- action,
174
- files,
175
- fields,
176
- fetchOptions
177
- );
178
- }
179
- };
185
+ return {
186
+ /** Invoke a service action through the backend engine pipeline */
187
+ invoke: async (params) => {
188
+ const { service, action, payload, fetchOptions } = params;
189
+ return await sendNileRequest(config, "execute", service, action, payload, fetchOptions);
190
+ },
191
+ /** Discover available services and actions (supports "*" wildcard) */
192
+ explore: async (params) => {
193
+ const { service, action, fetchOptions } = params;
194
+ return await sendNileRequest(config, "explore", service, action, {}, fetchOptions);
195
+ },
196
+ /** Get action schemas as JSON Schema (supports "*" wildcard) */
197
+ schema: async (params) => {
198
+ const { service, action, fetchOptions } = params;
199
+ return await sendNileRequest(config, "schema", service, action, {}, fetchOptions);
200
+ },
201
+ /** Upload files to an action via multipart form-data */
202
+ upload: async (params) => {
203
+ const { service, action, files, fields, fetchOptions } = params;
204
+ return await sendUploadRequest(config, service, action, files, fields, fetchOptions);
205
+ }
206
+ };
180
207
  }
181
- export {
182
- buildFormData,
183
- createNileClient,
184
- sendUploadRequest
185
- };
208
+ //#endregion
209
+ //#region src/socket-client.ts
210
+ /** Own a lazy Socket.IO connection with no offline write buffering or automatic invocation retries. */
211
+ function createNileSocketClient(config) {
212
+ const timeout = config.timeout ?? 3e4;
213
+ if (!Number.isSafeInteger(timeout) || timeout < 1) throw new Error("Socket timeout must be positive");
214
+ let socket;
215
+ let connecting;
216
+ let closed = false;
217
+ const listeners = /* @__PURE__ */ new Set();
218
+ const pending = /* @__PURE__ */ new Set();
219
+ const fail = (error) => clientErr(error);
220
+ const settleCallback = (fn) => {
221
+ Promise.resolve().then(fn).catch(() => void 0);
222
+ };
223
+ const request = async (event, input) => {
224
+ const connected = await connect();
225
+ if (connected.isErr) return fail(connected.error);
226
+ const current = connected.value;
227
+ if (!current.connected || closed) return fail("Socket disconnected");
228
+ return new Promise((resolve) => {
229
+ let settled = false;
230
+ const finish = (result) => {
231
+ if (settled) return;
232
+ settled = true;
233
+ pending.delete(cancel);
234
+ clearTimeout(timer);
235
+ resolve(result);
236
+ };
237
+ const cancel = () => finish(fail("Socket disconnected"));
238
+ const timer = setTimeout(() => finish(fail("Socket acknowledgement timed out")), timeout);
239
+ pending.add(cancel);
240
+ current.emit(event, input, (result) => {
241
+ if (!result || typeof result !== "object" || !("error" in result) || !("data" in result)) {
242
+ finish(fail("Invalid socket response"));
243
+ return;
244
+ }
245
+ if (typeof result.error === "string") {
246
+ finish(clientErr(result.error, result.data));
247
+ return;
248
+ }
249
+ finish(clientOk(result.data));
250
+ });
251
+ });
252
+ };
253
+ const rejoin = async () => {
254
+ for (const listener of listeners) {
255
+ if (!listener.active) continue;
256
+ const result = await request("nile:receive", {
257
+ topic: listener.topic,
258
+ scope: listener.scope
259
+ });
260
+ if (result.isOk) {
261
+ listener.normalizedScope = result.value?.scope;
262
+ continue;
263
+ }
264
+ const message = result.error;
265
+ settleCallback(() => config.onSubscriptionError?.({
266
+ topic: listener.topic,
267
+ error: message
268
+ }));
269
+ }
270
+ };
271
+ const connect = () => {
272
+ if (closed) return Promise.resolve(fail("Socket client is closed"));
273
+ if (socket?.connected) return Promise.resolve(clientOk(socket));
274
+ if (connecting) return connecting;
275
+ connecting = import("socket.io-client").then(({ io }) => new Promise((resolve) => {
276
+ if (closed) {
277
+ resolve(fail("Socket client is closed"));
278
+ return;
279
+ }
280
+ const current = socket ?? io(config.baseUrl, {
281
+ path: config.path ?? "/socket.io",
282
+ autoConnect: false,
283
+ withCredentials: true,
284
+ timeout,
285
+ auth: (done) => {
286
+ Promise.resolve().then(() => typeof config.auth === "function" ? config.auth() : config.auth ?? {}).then(done).catch(() => done({}));
287
+ }
288
+ });
289
+ if (!socket) {
290
+ socket = current;
291
+ current.on("disconnect", () => {
292
+ for (const cancel of [...pending]) cancel();
293
+ });
294
+ current.on("connect", () => {
295
+ rejoin();
296
+ });
297
+ current.on("nile:message", (message) => dispatchMessage(message, listeners));
298
+ }
299
+ let settled = false;
300
+ const finish = (result) => {
301
+ if (settled) return;
302
+ settled = true;
303
+ clearTimeout(timer);
304
+ current.off("connect", ready);
305
+ current.off("connect_error", failed);
306
+ pending.delete(cancel);
307
+ resolve(result);
308
+ };
309
+ const ready = () => finish(clientOk(current));
310
+ const failed = () => finish(fail("Socket connection failed"));
311
+ const cancel = () => finish(fail("Socket client closed"));
312
+ const timer = setTimeout(() => finish(fail("Socket connection timed out")), timeout);
313
+ pending.add(cancel);
314
+ current.once("connect", ready);
315
+ current.once("connect_error", failed);
316
+ current.connect();
317
+ })).catch(() => fail("Could not initialize socket client")).finally(() => {
318
+ connecting = void 0;
319
+ });
320
+ return connecting;
321
+ };
322
+ return {
323
+ invoke: (input) => request("nile:invoke", input),
324
+ send: (input) => request("nile:send", input),
325
+ async receive(input) {
326
+ const listener = {
327
+ ...input,
328
+ scope: input.scope,
329
+ onMessage: input.onMessage,
330
+ active: true
331
+ };
332
+ listeners.add(listener);
333
+ const result = await request("nile:receive", {
334
+ topic: input.topic,
335
+ scope: input.scope
336
+ });
337
+ if (result.isErr) {
338
+ listeners.delete(listener);
339
+ return fail(result.error);
340
+ }
341
+ listener.normalizedScope = result.value?.scope;
342
+ return clientOk({ unsubscribe: () => {
343
+ listener.active = false;
344
+ listeners.delete(listener);
345
+ const shared = [...listeners].some((item) => item.topic === listener.topic && stableScope(item.scope) === stableScope(listener.scope));
346
+ if (shared || closed) return Promise.resolve(clientOk({ subscribed: shared }));
347
+ return request("nile:unsubscribe", {
348
+ topic: listener.topic,
349
+ scope: listener.scope
350
+ });
351
+ } });
352
+ },
353
+ close() {
354
+ closed = true;
355
+ listeners.clear();
356
+ for (const cancel of [...pending]) cancel();
357
+ socket?.disconnect();
358
+ socket?.removeAllListeners();
359
+ }
360
+ };
361
+ }
362
+ /** Match scoped messages independently of object property insertion order. */
363
+ function stableScope(scope) {
364
+ if (scope === void 0 || scope === null) return "null";
365
+ if (Array.isArray(scope)) return `[${scope.map(stableScope).join(",")}]`;
366
+ if (typeof scope === "object") return `{${Object.keys(scope).sort().map((key) => `${JSON.stringify(key)}:${stableScope(scope[key])}`).join(",")}}`;
367
+ return JSON.stringify(scope);
368
+ }
369
+ /** Deliver messages only to listeners for the same validated topic and scope. */
370
+ function dispatchMessage(message, listeners) {
371
+ if (!message || typeof message !== "object" || !("topic" in message)) return;
372
+ const scope = stableScope("scope" in message ? message.scope : void 0);
373
+ for (const listener of listeners) {
374
+ if (!listener.active || listener.topic !== message.topic) continue;
375
+ if (stableScope(listener.normalizedScope ?? listener.scope) !== scope) continue;
376
+ Promise.resolve().then(() => listener.onMessage(message)).catch(() => void 0);
377
+ }
378
+ }
379
+ //#endregion
380
+ export { buildFormData, createNileClient, createNileSocketClient, sendUploadRequest };
381
+
186
382
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/safe-try.ts","../src/request.ts","../src/create-client.ts"],"sourcesContent":["/**\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 { 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 consistent { error, data } results.\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 { error: result.message, data: result.data };\n }\n\n return { error: null, data: result.data };\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 { error: result.message, data: result.data };\n }\n\n return { error: null, data: result.data };\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<T>(error: unknown): ClientResult<T> {\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 { error: errorMsg, data: null };\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 { error, data } = await nile.invoke({\n * service: \"tasks\",\n * action: \"create\",\n * payload: { title: \"Buy milk\" }\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 as ClientResult;\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"],"mappings":";AAiBO,SAAS,QACd,IACwC;AACxC,MAAI;AACF,UAAM,SAAS,GAAG;AAElB,QAAI,kBAAkB,SAAS;AAC7B,aAAO,OACJ;AAAA,QACC,CAAC,WAA0B;AAAA,UACzB,MAAM;AAAA,UACN;AAAA,UACA,OAAO;AAAA,UACP,OAAO;AAAA,QACT;AAAA,MACF,EACC;AAAA,QACC,CAAC,WAA0B;AAAA,UACzB,MAAM;AAAA,UACN,OAAO;AAAA,UACP,OAAO;AAAA,UACP;AAAA,QACF;AAAA,MACF;AAAA,IACJ;AAEA,WAAO,EAAE,MAAM,MAAM,OAAO,QAAQ,OAAO,OAAO,OAAO,KAAK;AAAA,EAChE,SAAS,OAAO;AACd,WAAO,EAAE,MAAM,OAAO,OAAO,MAAM,OAAO,MAAM,MAAM;AAAA,EACxD;AACF;;;ACnCA,eAAsB,gBACpB,QACA,QACA,SACA,QACA,SACA,UAAwB,CAAC,GACC;AAC1B,QAAM,EAAE,UAAU,OAAO,WAAW,KAAQ,GAAG,aAAa,IAAI;AAEhE,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO;AAE9D,QAAM,gBAAgB,MAAM,QAAQ,YAAY;AAC9C,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,OAAO,aAAa;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,OAAO;AAAA,QACV,GAAI,aAAa;AAAA,MACnB;AAAA,MACA,aAAa,OAAO;AAAA,MACpB,MAAM,KAAK,UAAU;AAAA,QACnB;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,WAAW,CAAC;AAAA,MACvB,CAAC;AAAA,MACD,QAAQ,WAAW;AAAA,MACnB,GAAG;AAAA,IACL,CAAC;AAED,iBAAa,SAAS;AAEtB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO;AAAA,EACT,CAAC;AAED,MAAI,cAAc,OAAO;AACvB,WAAO,aAAa,cAAc,KAAK;AAAA,EACzC;AAEA,QAAM,SAAS,cAAc;AAE7B,MAAI,CAAC,OAAO,QAAQ;AAClB,WAAO,EAAE,OAAO,OAAO,SAAS,MAAM,OAAO,KAAK;AAAA,EACpD;AAEA,SAAO,EAAE,OAAO,MAAM,MAAM,OAAO,KAAK;AAC1C;AAUA,eAAsB,kBACpB,QACA,SACA,QACA,OACA,QACA,UAAwB,CAAC,GACC;AAC1B,QAAM,EAAE,UAAU,OAAO,WAAW,KAAQ,GAAG,aAAa,IAAI;AAEhE,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO;AAE9D,QAAM,WAAW,cAAc,SAAS,QAAQ,OAAO,MAAM;AAE7D,QAAM,gBAAgB,MAAM,QAAQ,YAAY;AAE9C,UAAM,EAAE,gBAAgB,KAAK,GAAG,YAAY,IAAI,OAAO,WAAW,CAAC;AACnE,UAAM,EAAE,gBAAgB,MAAM,GAAG,iBAAiB,IAC/C,aAAa,WAAsC,CAAC;AAEvD,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,OAAO,aAAa;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,GAAG;AAAA,QACH,GAAG;AAAA,MACL;AAAA,MACA,aAAa,OAAO;AAAA,MACpB,MAAM;AAAA,MACN,QAAQ,WAAW;AAAA,MACnB,GAAG;AAAA,IACL,CAAC;AAED,iBAAa,SAAS;AAEtB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO;AAAA,EACT,CAAC;AAED,MAAI,cAAc,OAAO;AACvB,WAAO,aAAa,cAAc,KAAK;AAAA,EACzC;AAEA,QAAM,SAAS,cAAc;AAE7B,MAAI,CAAC,OAAO,QAAQ;AAClB,WAAO,EAAE,OAAO,OAAO,SAAS,MAAM,OAAO,KAAK;AAAA,EACpD;AAEA,SAAO,EAAE,OAAO,MAAM,MAAM,OAAO,KAAK;AAC1C;AAMO,SAAS,cACd,SACA,QACA,OACA,QACU;AACV,QAAM,WAAW,IAAI,SAAS;AAG9B,WAAS,OAAO,UAAU,SAAS;AACnC,WAAS,OAAO,WAAW,OAAO;AAClC,WAAS,OAAO,UAAU,MAAM;AAGhC,MAAI,QAAQ;AACV,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,eAAS,OAAO,KAAK,KAAK;AAAA,IAC5B;AAAA,EACF;AAGA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,iBAAW,QAAQ,OAAO;AACxB,iBAAS,OAAO,KAAK,IAAI;AAAA,MAC3B;AAAA,IACF,OAAO;AACL,eAAS,OAAO,KAAK,KAAK;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO;AACT;AAGA,SAAS,aAAgB,OAAiC;AACxD,MAAI,WAAW;AAEf,MAAI,iBAAiB,OAAO;AAC1B,eACE,MAAM,SAAS,eAAe,sBAAsB,MAAM;AAAA,EAC9D,OAAO;AACL,eAAW,OAAO,KAAK;AAAA,EACzB;AAEA,SAAO,EAAE,OAAO,UAAU,MAAM,KAAK;AACvC;;;ACvJO,SAAS,iBAEd,QAAiD;AACjD,SAAO;AAAA;AAAA,IAEL,QAAQ,OAIN,WAMG;AACH,YAAM,EAAE,SAAS,QAAQ,SAAS,aAAa,IAAI;AACnD,YAAM,SAAS,MAAM;AAAA,QACnB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA;AAAA,IAGA,SAAS,OAAO,WAAW;AACzB,YAAM,EAAE,SAAS,QAAQ,aAAa,IAAI;AAC1C,aAAO,MAAM;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,CAAC;AAAA,QACD;AAAA,MACF;AAAA,IACF;AAAA;AAAA,IAGA,QAAQ,OAAO,WAAW;AACxB,YAAM,EAAE,SAAS,QAAQ,aAAa,IAAI;AAC1C,aAAO,MAAM;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,CAAC;AAAA,QACD;AAAA,MACF;AAAA,IACF;AAAA;AAAA,IAGA,QAAQ,OAAO,WAAyB;AACtC,YAAM,EAAE,SAAS,QAAQ,OAAO,QAAQ,aAAa,IAAI;AACzD,aAAO,MAAM;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
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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"}