@resultdev/sdk 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ssr.cjs CHANGED
@@ -690,7 +690,15 @@ var HttpClient = class {
690
690
  */
691
691
  constructor(config, tokenManager, logger) {
692
692
  this.config = config;
693
- this.baseUrl = config.baseUrl || "http://localhost:7130";
693
+ if (!config.baseUrl) {
694
+ throw new ResultError(
695
+ "createClient needs a baseUrl, but it was undefined at runtime.",
696
+ 0,
697
+ "MISSING_BASE_URL",
698
+ "Pass the NEXT_PUBLIC_BACKEND_URL value from .env.local. In browser code, write process.env.NEXT_PUBLIC_BACKEND_URL as a literal expression (never through an imported process module) so the bundler inlines it, and set it as a deployments build variable before deploying."
699
+ );
700
+ }
701
+ this.baseUrl = config.baseUrl;
694
702
  this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
695
703
  this.anonKey = config.anonKey;
696
704
  this.defaultHeaders = {
@@ -1208,6 +1216,70 @@ var HttpClient = class {
1208
1216
  };
1209
1217
 
1210
1218
  // src/modules/ai.ts
1219
+ var SUPPORTED_AUDIO_FORMATS = [
1220
+ "wav",
1221
+ "mp3",
1222
+ "aiff",
1223
+ "aac",
1224
+ "ogg",
1225
+ "flac",
1226
+ "m4a"
1227
+ ];
1228
+ var MULTIMODAL_MODEL = "google/gemini-2.5-flash";
1229
+ function summarizeContent(messages) {
1230
+ const summary = {
1231
+ hasImage: false,
1232
+ hasAudio: false,
1233
+ badAudioFormats: []
1234
+ };
1235
+ for (const message of messages) {
1236
+ if (message.images?.length) {
1237
+ summary.hasImage = true;
1238
+ }
1239
+ if (!Array.isArray(message.content)) {
1240
+ continue;
1241
+ }
1242
+ for (const part of message.content) {
1243
+ if (part?.type === "image_url") {
1244
+ summary.hasImage = true;
1245
+ } else if (part?.type === "input_audio") {
1246
+ summary.hasAudio = true;
1247
+ const format2 = part.input_audio?.format;
1248
+ if (format2 && !SUPPORTED_AUDIO_FORMATS.includes(format2)) {
1249
+ summary.badAudioFormats.push(format2);
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ return summary;
1255
+ }
1256
+ function assertSupportedContent(model, summary) {
1257
+ if (summary.badAudioFormats.length) {
1258
+ throw new ResultError(
1259
+ `Audio format "${summary.badAudioFormats[0]}" is not supported. Supported formats: ${SUPPORTED_AUDIO_FORMATS.join(", ")}. Browser MediaRecorder produces webm/opus, which the gateway rejects - capture PCM with the Web Audio API and encode a 16 kHz mono WAV instead.`,
1260
+ 400,
1261
+ "AI_INVALID_INPUT"
1262
+ );
1263
+ }
1264
+ if (summary.hasAudio && model.startsWith("openai/")) {
1265
+ throw new ResultError(
1266
+ `Model "${model}" does not accept audio input on this gateway. Use ${MULTIMODAL_MODEL} (verified for audio).`,
1267
+ 400,
1268
+ "AI_INVALID_INPUT"
1269
+ );
1270
+ }
1271
+ }
1272
+ function enrichUpstreamError(error, summary) {
1273
+ if (!(error instanceof ResultError) || error.error !== "AI_UPSTREAM_UNAVAILABLE" || !summary.hasImage && !summary.hasAudio) {
1274
+ return error;
1275
+ }
1276
+ const kind = summary.hasImage && summary.hasAudio ? "image and audio" : summary.hasImage ? "image" : "audio";
1277
+ return new ResultError(
1278
+ `${error.message} This request included ${kind} input; if the error persists, use ${MULTIMODAL_MODEL} (verified for vision and audio).`,
1279
+ error.statusCode,
1280
+ error.error
1281
+ );
1282
+ }
1211
1283
  var AI = class {
1212
1284
  chat;
1213
1285
  images;
@@ -1230,6 +1302,8 @@ var ChatCompletions = class {
1230
1302
  }
1231
1303
  http;
1232
1304
  async create(params) {
1305
+ const contentSummary = summarizeContent(params.messages ?? []);
1306
+ assertSupportedContent(params.model, contentSummary);
1233
1307
  const backendParams = {
1234
1308
  model: params.model,
1235
1309
  messages: params.messages,
@@ -1258,15 +1332,28 @@ var ChatCompletions = class {
1258
1332
  }
1259
1333
  );
1260
1334
  if (!response2.ok) {
1261
- const error = await response2.json();
1262
- throw new Error(error.error || "Stream request failed");
1335
+ let body = null;
1336
+ try {
1337
+ body = await response2.json();
1338
+ } catch {
1339
+ }
1340
+ throw enrichUpstreamError(
1341
+ new ResultError(
1342
+ body?.message || body?.error || "Stream request failed",
1343
+ body?.statusCode ?? response2.status,
1344
+ body?.error || "AI_STREAM_ERROR"
1345
+ ),
1346
+ contentSummary
1347
+ );
1263
1348
  }
1264
1349
  return this.parseSSEStream(response2, params.model);
1265
1350
  }
1266
- const response = await this.http.post(
1267
- "/api/ai/chat/completion",
1268
- backendParams
1269
- );
1351
+ let response;
1352
+ try {
1353
+ response = await this.http.post("/api/ai/chat/completion", backendParams);
1354
+ } catch (error) {
1355
+ throw enrichUpstreamError(error, contentSummary);
1356
+ }
1270
1357
  const content = response.text || "";
1271
1358
  return {
1272
1359
  id: `chatcmpl-${Date.now()}`,
@@ -1361,6 +1448,37 @@ var ChatCompletions = class {
1361
1448
  ]
1362
1449
  };
1363
1450
  }
1451
+ if (data.annotations?.length) {
1452
+ yield {
1453
+ id: `chatcmpl-${Date.now()}`,
1454
+ object: "chat.completion.chunk",
1455
+ created: Math.floor(Date.now() / 1e3),
1456
+ model,
1457
+ choices: [
1458
+ {
1459
+ index: 0,
1460
+ delta: {
1461
+ annotations: data.annotations
1462
+ },
1463
+ finish_reason: null
1464
+ }
1465
+ ]
1466
+ };
1467
+ }
1468
+ if (data.tokenUsage) {
1469
+ yield {
1470
+ id: `chatcmpl-${Date.now()}`,
1471
+ object: "chat.completion.chunk",
1472
+ created: Math.floor(Date.now() / 1e3),
1473
+ model,
1474
+ choices: [],
1475
+ usage: {
1476
+ prompt_tokens: data.tokenUsage.promptTokens || 0,
1477
+ completion_tokens: data.tokenUsage.completionTokens || 0,
1478
+ total_tokens: data.tokenUsage.totalTokens || 0
1479
+ }
1480
+ };
1481
+ }
1364
1482
  if (data.done) {
1365
1483
  reader.releaseLock();
1366
1484
  return;
@@ -2597,13 +2715,13 @@ var Functions = class _Functions {
2597
2715
  functionsUrl;
2598
2716
  constructor(http, functionsUrl) {
2599
2717
  this.http = http;
2600
- this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
2718
+ this.functionsUrl = functionsUrl;
2601
2719
  }
2602
2720
  /**
2603
- * Derive the subhosting URL from the base URL.
2604
- * Rewrites the backend base URL to its functions subhosting URL
2721
+ * Derive the legacy subhosting URL from the base URL
2605
2722
  * ({appKey}.{region}.<domain> -> {appKey}.functions.<domain>).
2606
- * Only applies to platform-hosted backends.
2723
+ * Only used to recognize when a configured functionsUrl still points at
2724
+ * the local deployment, so in-process dispatch can short-circuit it.
2607
2725
  */
2608
2726
  static deriveSubhostingUrl(baseUrl) {
2609
2727
  try {
@@ -2636,11 +2754,13 @@ var Functions = class _Functions {
2636
2754
  * Invoke an Edge Function.
2637
2755
  *
2638
2756
  * Dispatch order:
2639
- * 1. If the platform's in-process dispatch hook is present, call it directly.
2640
- * This avoids Deno Subhosting's 508 Loop Detected when one bundled
2641
- * function invokes another inside the same deployment.
2642
- * 2. Otherwise, try the configured subhosting URL.
2643
- * 3. On 404 from subhosting, fall back to the proxy path.
2757
+ * 1. If the platform's in-process dispatch hook is present and no foreign
2758
+ * functionsUrl is configured, call it directly (function-to-function
2759
+ * calls inside the same deployment).
2760
+ * 2. If a custom functionsUrl is configured, try it; fall back to the
2761
+ * proxy path on 404 or network failure.
2762
+ * 3. Otherwise, invoke through the main host's /functions/{slug} proxy.
2763
+ * This route works from browsers too (correct CORS).
2644
2764
  *
2645
2765
  * @param slug The function slug to invoke
2646
2766
  * @param options Request options
@@ -2649,7 +2769,7 @@ var Functions = class _Functions {
2649
2769
  const { method = "POST", body, headers = {} } = options;
2650
2770
  const dispatch = globalThis.__insforge_dispatch__;
2651
2771
  const localFunctionsUrl = _Functions.deriveSubhostingUrl(this.http.baseUrl);
2652
- if (typeof dispatch === "function" && !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl) {
2772
+ if (typeof dispatch === "function" && (this.functionsUrl === void 0 || !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl)) {
2653
2773
  try {
2654
2774
  const req = this.buildInProcessRequest(slug, method, body, headers);
2655
2775
  const res = await dispatch(req);
@@ -2684,7 +2804,8 @@ var Functions = class _Functions {
2684
2804
  if (error instanceof Error && error.name === "AbortError") {
2685
2805
  throw error;
2686
2806
  }
2687
- if (error instanceof ResultError && error.statusCode === 404) {
2807
+ const unreachable = error instanceof ResultError && (error.statusCode === 404 || error.statusCode === 0);
2808
+ if (unreachable) {
2688
2809
  } else {
2689
2810
  return {
2690
2811
  data: null,
@@ -2720,6 +2841,17 @@ var Functions = class _Functions {
2720
2841
  // src/modules/realtime.ts
2721
2842
  var CONNECT_TIMEOUT = 1e4;
2722
2843
  var SUBSCRIBE_TIMEOUT = 1e4;
2844
+ var CHANNEL_PREFIX = "realtime:";
2845
+ function normalizeChannelMeta(message) {
2846
+ const channel = message?.meta?.channel;
2847
+ if (typeof channel !== "string" || !channel.startsWith(CHANNEL_PREFIX)) {
2848
+ return message;
2849
+ }
2850
+ return {
2851
+ ...message,
2852
+ meta: { ...message.meta, channel: channel.slice(CHANNEL_PREFIX.length) }
2853
+ };
2854
+ }
2723
2855
  var Realtime = class {
2724
2856
  constructor(baseUrl, tokenManager, anonKey, getValidAccessToken = async () => tokenManager.getAccessToken()) {
2725
2857
  this.baseUrl = baseUrl;
@@ -2841,8 +2973,9 @@ var Realtime = class {
2841
2973
  if (event === "realtime:error") {
2842
2974
  return;
2843
2975
  }
2844
- this.applyPresenceEvent(event, message);
2845
- this.notifyListeners(event, message);
2976
+ const normalized = normalizeChannelMeta(message);
2977
+ this.applyPresenceEvent(event, normalized);
2978
+ this.notifyListeners(event, normalized);
2846
2979
  };
2847
2980
  this.connectionAttempt = { id: attemptId, socket, cancel: fail };
2848
2981
  socket.on("connect", onConnect);
@@ -3090,6 +3223,17 @@ var Realtime = class {
3090
3223
  this.socket.emit("realtime:unsubscribe", { channel });
3091
3224
  }
3092
3225
  }
3226
+ /**
3227
+ * Publish an event to a channel.
3228
+ *
3229
+ * Delivery notes (verified against a live backend):
3230
+ * - Payload fields arrive spread onto the received message itself, next to
3231
+ * `meta` - not nested under a `payload` key. Publishing `{ text: "hi" }`
3232
+ * means receivers read `msg.text`.
3233
+ * - The sender receives its own message too. Deduplicate with
3234
+ * `msg.meta.messageId` (or `msg.meta.senderId`) if you also update local
3235
+ * state optimistically.
3236
+ */
3093
3237
  async publish(channel, event, payload) {
3094
3238
  if (!this.socket?.connected) {
3095
3239
  throw new Error(
@@ -3098,6 +3242,12 @@ var Realtime = class {
3098
3242
  }
3099
3243
  this.socket.emit("realtime:publish", { channel, event, payload });
3100
3244
  }
3245
+ /**
3246
+ * Listen for an event by name, across all subscribed channels.
3247
+ * Filter by channel inside the callback: `msg.meta.channel` always matches
3248
+ * the exact name passed to subscribe() (the SDK strips the server's
3249
+ * transport prefix before delivery).
3250
+ */
3101
3251
  on(event, callback) {
3102
3252
  const listeners = this.eventListeners.get(event) ?? /* @__PURE__ */ new Set();
3103
3253
  listeners.add(callback);
package/dist/ssr.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { R as ResultClient } from './client-DeE086La.cjs';
2
- import { o as ResultError, a as ResultConfig, G as AuthRefreshResponse } from './types-CbsaMttt.cjs';
3
- import { A as AuthCookieSettings, C as CookieStore, a as CookieWriter } from './middleware-BBY_6hyx.cjs';
4
- export { b as AuthCookieNames, c as AuthCookieOptions, d as CookieOptions, e as CookieReader, D as DEFAULT_ACCESS_TOKEN_COOKIE, f as DEFAULT_REFRESH_TOKEN_COOKIE, U as UpdateSessionOptions, g as UpdateSessionResult, h as accessTokenCookieOptions, i as clearAuthCookies, j as getAccessTokenCookieName, k as getRefreshTokenCookieName, r as refreshTokenCookieOptions, s as setAuthCookies, u as updateSession } from './middleware-BBY_6hyx.cjs';
1
+ import { R as ResultClient } from './client-BrOD2EO5.cjs';
2
+ import { o as ResultError, a as ResultConfig, G as AuthRefreshResponse } from './types-BmU3deRO.cjs';
3
+ import { A as AuthCookieSettings, C as CookieStore, a as CookieWriter } from './middleware-VCWoai4o.cjs';
4
+ export { b as AuthCookieNames, c as AuthCookieOptions, d as CookieOptions, e as CookieReader, D as DEFAULT_ACCESS_TOKEN_COOKIE, f as DEFAULT_REFRESH_TOKEN_COOKIE, U as UpdateSessionOptions, g as UpdateSessionResult, h as accessTokenCookieOptions, i as clearAuthCookies, j as getAccessTokenCookieName, k as getRefreshTokenCookieName, r as refreshTokenCookieOptions, s as setAuthCookies, u as updateSession } from './middleware-VCWoai4o.cjs';
5
5
  import '@supabase/postgrest-js';
6
6
 
7
7
  interface CreateAuthActionsOptions extends Omit<ResultConfig, "accessToken" | "edgeFunctionToken" | "isServerMode" | "auth">, AuthCookieSettings {
package/dist/ssr.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { R as ResultClient } from './client-Z0zUHGE3.js';
2
- import { o as ResultError, a as ResultConfig, G as AuthRefreshResponse } from './types-CbsaMttt.js';
3
- import { A as AuthCookieSettings, C as CookieStore, a as CookieWriter } from './middleware-TGaDOhE9.js';
4
- export { b as AuthCookieNames, c as AuthCookieOptions, d as CookieOptions, e as CookieReader, D as DEFAULT_ACCESS_TOKEN_COOKIE, f as DEFAULT_REFRESH_TOKEN_COOKIE, U as UpdateSessionOptions, g as UpdateSessionResult, h as accessTokenCookieOptions, i as clearAuthCookies, j as getAccessTokenCookieName, k as getRefreshTokenCookieName, r as refreshTokenCookieOptions, s as setAuthCookies, u as updateSession } from './middleware-TGaDOhE9.js';
1
+ import { R as ResultClient } from './client--Xjaaw1l.js';
2
+ import { o as ResultError, a as ResultConfig, G as AuthRefreshResponse } from './types-BmU3deRO.js';
3
+ import { A as AuthCookieSettings, C as CookieStore, a as CookieWriter } from './middleware-Uh09_lFn.js';
4
+ export { b as AuthCookieNames, c as AuthCookieOptions, d as CookieOptions, e as CookieReader, D as DEFAULT_ACCESS_TOKEN_COOKIE, f as DEFAULT_REFRESH_TOKEN_COOKIE, U as UpdateSessionOptions, g as UpdateSessionResult, h as accessTokenCookieOptions, i as clearAuthCookies, j as getAccessTokenCookieName, k as getRefreshTokenCookieName, r as refreshTokenCookieOptions, s as setAuthCookies, u as updateSession } from './middleware-Uh09_lFn.js';
5
5
  import '@supabase/postgrest-js';
6
6
 
7
7
  interface CreateAuthActionsOptions extends Omit<ResultConfig, "accessToken" | "edgeFunctionToken" | "isServerMode" | "auth">, AuthCookieSettings {
package/dist/ssr.js CHANGED
@@ -641,7 +641,15 @@ var HttpClient = class {
641
641
  */
642
642
  constructor(config, tokenManager, logger) {
643
643
  this.config = config;
644
- this.baseUrl = config.baseUrl || "http://localhost:7130";
644
+ if (!config.baseUrl) {
645
+ throw new ResultError(
646
+ "createClient needs a baseUrl, but it was undefined at runtime.",
647
+ 0,
648
+ "MISSING_BASE_URL",
649
+ "Pass the NEXT_PUBLIC_BACKEND_URL value from .env.local. In browser code, write process.env.NEXT_PUBLIC_BACKEND_URL as a literal expression (never through an imported process module) so the bundler inlines it, and set it as a deployments build variable before deploying."
650
+ );
651
+ }
652
+ this.baseUrl = config.baseUrl;
645
653
  this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
646
654
  this.anonKey = config.anonKey;
647
655
  this.defaultHeaders = {
@@ -1159,6 +1167,70 @@ var HttpClient = class {
1159
1167
  };
1160
1168
 
1161
1169
  // src/modules/ai.ts
1170
+ var SUPPORTED_AUDIO_FORMATS = [
1171
+ "wav",
1172
+ "mp3",
1173
+ "aiff",
1174
+ "aac",
1175
+ "ogg",
1176
+ "flac",
1177
+ "m4a"
1178
+ ];
1179
+ var MULTIMODAL_MODEL = "google/gemini-2.5-flash";
1180
+ function summarizeContent(messages) {
1181
+ const summary = {
1182
+ hasImage: false,
1183
+ hasAudio: false,
1184
+ badAudioFormats: []
1185
+ };
1186
+ for (const message of messages) {
1187
+ if (message.images?.length) {
1188
+ summary.hasImage = true;
1189
+ }
1190
+ if (!Array.isArray(message.content)) {
1191
+ continue;
1192
+ }
1193
+ for (const part of message.content) {
1194
+ if (part?.type === "image_url") {
1195
+ summary.hasImage = true;
1196
+ } else if (part?.type === "input_audio") {
1197
+ summary.hasAudio = true;
1198
+ const format2 = part.input_audio?.format;
1199
+ if (format2 && !SUPPORTED_AUDIO_FORMATS.includes(format2)) {
1200
+ summary.badAudioFormats.push(format2);
1201
+ }
1202
+ }
1203
+ }
1204
+ }
1205
+ return summary;
1206
+ }
1207
+ function assertSupportedContent(model, summary) {
1208
+ if (summary.badAudioFormats.length) {
1209
+ throw new ResultError(
1210
+ `Audio format "${summary.badAudioFormats[0]}" is not supported. Supported formats: ${SUPPORTED_AUDIO_FORMATS.join(", ")}. Browser MediaRecorder produces webm/opus, which the gateway rejects - capture PCM with the Web Audio API and encode a 16 kHz mono WAV instead.`,
1211
+ 400,
1212
+ "AI_INVALID_INPUT"
1213
+ );
1214
+ }
1215
+ if (summary.hasAudio && model.startsWith("openai/")) {
1216
+ throw new ResultError(
1217
+ `Model "${model}" does not accept audio input on this gateway. Use ${MULTIMODAL_MODEL} (verified for audio).`,
1218
+ 400,
1219
+ "AI_INVALID_INPUT"
1220
+ );
1221
+ }
1222
+ }
1223
+ function enrichUpstreamError(error, summary) {
1224
+ if (!(error instanceof ResultError) || error.error !== "AI_UPSTREAM_UNAVAILABLE" || !summary.hasImage && !summary.hasAudio) {
1225
+ return error;
1226
+ }
1227
+ const kind = summary.hasImage && summary.hasAudio ? "image and audio" : summary.hasImage ? "image" : "audio";
1228
+ return new ResultError(
1229
+ `${error.message} This request included ${kind} input; if the error persists, use ${MULTIMODAL_MODEL} (verified for vision and audio).`,
1230
+ error.statusCode,
1231
+ error.error
1232
+ );
1233
+ }
1162
1234
  var AI = class {
1163
1235
  chat;
1164
1236
  images;
@@ -1181,6 +1253,8 @@ var ChatCompletions = class {
1181
1253
  }
1182
1254
  http;
1183
1255
  async create(params) {
1256
+ const contentSummary = summarizeContent(params.messages ?? []);
1257
+ assertSupportedContent(params.model, contentSummary);
1184
1258
  const backendParams = {
1185
1259
  model: params.model,
1186
1260
  messages: params.messages,
@@ -1209,15 +1283,28 @@ var ChatCompletions = class {
1209
1283
  }
1210
1284
  );
1211
1285
  if (!response2.ok) {
1212
- const error = await response2.json();
1213
- throw new Error(error.error || "Stream request failed");
1286
+ let body = null;
1287
+ try {
1288
+ body = await response2.json();
1289
+ } catch {
1290
+ }
1291
+ throw enrichUpstreamError(
1292
+ new ResultError(
1293
+ body?.message || body?.error || "Stream request failed",
1294
+ body?.statusCode ?? response2.status,
1295
+ body?.error || "AI_STREAM_ERROR"
1296
+ ),
1297
+ contentSummary
1298
+ );
1214
1299
  }
1215
1300
  return this.parseSSEStream(response2, params.model);
1216
1301
  }
1217
- const response = await this.http.post(
1218
- "/api/ai/chat/completion",
1219
- backendParams
1220
- );
1302
+ let response;
1303
+ try {
1304
+ response = await this.http.post("/api/ai/chat/completion", backendParams);
1305
+ } catch (error) {
1306
+ throw enrichUpstreamError(error, contentSummary);
1307
+ }
1221
1308
  const content = response.text || "";
1222
1309
  return {
1223
1310
  id: `chatcmpl-${Date.now()}`,
@@ -1312,6 +1399,37 @@ var ChatCompletions = class {
1312
1399
  ]
1313
1400
  };
1314
1401
  }
1402
+ if (data.annotations?.length) {
1403
+ yield {
1404
+ id: `chatcmpl-${Date.now()}`,
1405
+ object: "chat.completion.chunk",
1406
+ created: Math.floor(Date.now() / 1e3),
1407
+ model,
1408
+ choices: [
1409
+ {
1410
+ index: 0,
1411
+ delta: {
1412
+ annotations: data.annotations
1413
+ },
1414
+ finish_reason: null
1415
+ }
1416
+ ]
1417
+ };
1418
+ }
1419
+ if (data.tokenUsage) {
1420
+ yield {
1421
+ id: `chatcmpl-${Date.now()}`,
1422
+ object: "chat.completion.chunk",
1423
+ created: Math.floor(Date.now() / 1e3),
1424
+ model,
1425
+ choices: [],
1426
+ usage: {
1427
+ prompt_tokens: data.tokenUsage.promptTokens || 0,
1428
+ completion_tokens: data.tokenUsage.completionTokens || 0,
1429
+ total_tokens: data.tokenUsage.totalTokens || 0
1430
+ }
1431
+ };
1432
+ }
1315
1433
  if (data.done) {
1316
1434
  reader.releaseLock();
1317
1435
  return;
@@ -2548,13 +2666,13 @@ var Functions = class _Functions {
2548
2666
  functionsUrl;
2549
2667
  constructor(http, functionsUrl) {
2550
2668
  this.http = http;
2551
- this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
2669
+ this.functionsUrl = functionsUrl;
2552
2670
  }
2553
2671
  /**
2554
- * Derive the subhosting URL from the base URL.
2555
- * Rewrites the backend base URL to its functions subhosting URL
2672
+ * Derive the legacy subhosting URL from the base URL
2556
2673
  * ({appKey}.{region}.<domain> -> {appKey}.functions.<domain>).
2557
- * Only applies to platform-hosted backends.
2674
+ * Only used to recognize when a configured functionsUrl still points at
2675
+ * the local deployment, so in-process dispatch can short-circuit it.
2558
2676
  */
2559
2677
  static deriveSubhostingUrl(baseUrl) {
2560
2678
  try {
@@ -2587,11 +2705,13 @@ var Functions = class _Functions {
2587
2705
  * Invoke an Edge Function.
2588
2706
  *
2589
2707
  * Dispatch order:
2590
- * 1. If the platform's in-process dispatch hook is present, call it directly.
2591
- * This avoids Deno Subhosting's 508 Loop Detected when one bundled
2592
- * function invokes another inside the same deployment.
2593
- * 2. Otherwise, try the configured subhosting URL.
2594
- * 3. On 404 from subhosting, fall back to the proxy path.
2708
+ * 1. If the platform's in-process dispatch hook is present and no foreign
2709
+ * functionsUrl is configured, call it directly (function-to-function
2710
+ * calls inside the same deployment).
2711
+ * 2. If a custom functionsUrl is configured, try it; fall back to the
2712
+ * proxy path on 404 or network failure.
2713
+ * 3. Otherwise, invoke through the main host's /functions/{slug} proxy.
2714
+ * This route works from browsers too (correct CORS).
2595
2715
  *
2596
2716
  * @param slug The function slug to invoke
2597
2717
  * @param options Request options
@@ -2600,7 +2720,7 @@ var Functions = class _Functions {
2600
2720
  const { method = "POST", body, headers = {} } = options;
2601
2721
  const dispatch = globalThis.__insforge_dispatch__;
2602
2722
  const localFunctionsUrl = _Functions.deriveSubhostingUrl(this.http.baseUrl);
2603
- if (typeof dispatch === "function" && !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl) {
2723
+ if (typeof dispatch === "function" && (this.functionsUrl === void 0 || !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl)) {
2604
2724
  try {
2605
2725
  const req = this.buildInProcessRequest(slug, method, body, headers);
2606
2726
  const res = await dispatch(req);
@@ -2635,7 +2755,8 @@ var Functions = class _Functions {
2635
2755
  if (error instanceof Error && error.name === "AbortError") {
2636
2756
  throw error;
2637
2757
  }
2638
- if (error instanceof ResultError && error.statusCode === 404) {
2758
+ const unreachable = error instanceof ResultError && (error.statusCode === 404 || error.statusCode === 0);
2759
+ if (unreachable) {
2639
2760
  } else {
2640
2761
  return {
2641
2762
  data: null,
@@ -2671,6 +2792,17 @@ var Functions = class _Functions {
2671
2792
  // src/modules/realtime.ts
2672
2793
  var CONNECT_TIMEOUT = 1e4;
2673
2794
  var SUBSCRIBE_TIMEOUT = 1e4;
2795
+ var CHANNEL_PREFIX = "realtime:";
2796
+ function normalizeChannelMeta(message) {
2797
+ const channel = message?.meta?.channel;
2798
+ if (typeof channel !== "string" || !channel.startsWith(CHANNEL_PREFIX)) {
2799
+ return message;
2800
+ }
2801
+ return {
2802
+ ...message,
2803
+ meta: { ...message.meta, channel: channel.slice(CHANNEL_PREFIX.length) }
2804
+ };
2805
+ }
2674
2806
  var Realtime = class {
2675
2807
  constructor(baseUrl, tokenManager, anonKey, getValidAccessToken = async () => tokenManager.getAccessToken()) {
2676
2808
  this.baseUrl = baseUrl;
@@ -2792,8 +2924,9 @@ var Realtime = class {
2792
2924
  if (event === "realtime:error") {
2793
2925
  return;
2794
2926
  }
2795
- this.applyPresenceEvent(event, message);
2796
- this.notifyListeners(event, message);
2927
+ const normalized = normalizeChannelMeta(message);
2928
+ this.applyPresenceEvent(event, normalized);
2929
+ this.notifyListeners(event, normalized);
2797
2930
  };
2798
2931
  this.connectionAttempt = { id: attemptId, socket, cancel: fail };
2799
2932
  socket.on("connect", onConnect);
@@ -3041,6 +3174,17 @@ var Realtime = class {
3041
3174
  this.socket.emit("realtime:unsubscribe", { channel });
3042
3175
  }
3043
3176
  }
3177
+ /**
3178
+ * Publish an event to a channel.
3179
+ *
3180
+ * Delivery notes (verified against a live backend):
3181
+ * - Payload fields arrive spread onto the received message itself, next to
3182
+ * `meta` - not nested under a `payload` key. Publishing `{ text: "hi" }`
3183
+ * means receivers read `msg.text`.
3184
+ * - The sender receives its own message too. Deduplicate with
3185
+ * `msg.meta.messageId` (or `msg.meta.senderId`) if you also update local
3186
+ * state optimistically.
3187
+ */
3044
3188
  async publish(channel, event, payload) {
3045
3189
  if (!this.socket?.connected) {
3046
3190
  throw new Error(
@@ -3049,6 +3193,12 @@ var Realtime = class {
3049
3193
  }
3050
3194
  this.socket.emit("realtime:publish", { channel, event, payload });
3051
3195
  }
3196
+ /**
3197
+ * Listen for an event by name, across all subscribed channels.
3198
+ * Filter by channel inside the callback: `msg.meta.channel` always matches
3199
+ * the exact name passed to subscribe() (the SDK strips the server's
3200
+ * transport prefix before delivery).
3201
+ */
3052
3202
  on(event, callback) {
3053
3203
  const listeners = this.eventListeners.get(event) ?? /* @__PURE__ */ new Set();
3054
3204
  listeners.add(callback);
@@ -498,13 +498,21 @@ interface RealtimeErrorPayload {
498
498
  message: string;
499
499
  }
500
500
  interface SocketMessageMeta {
501
- /** Present for room broadcasts. */
501
+ /**
502
+ * Present for room broadcasts. Matches the exact channel name passed to
503
+ * subscribe() - the SDK strips the server's transport prefix on delivery.
504
+ */
502
505
  channel?: string;
503
506
  messageId: string;
504
507
  senderType: SenderType;
505
508
  senderId?: string;
506
509
  timestamp: string;
507
510
  }
511
+ /**
512
+ * A received realtime message. The published payload's fields arrive spread
513
+ * onto this object next to `meta` (publish `{ text: "hi" }`, read
514
+ * `msg.text`) - there is no `payload` wrapper key.
515
+ */
508
516
  interface SocketMessage {
509
517
  meta: SocketMessageMeta;
510
518
  [key: string]: unknown;
@@ -518,8 +526,9 @@ interface SocketMessage {
518
526
  type ResultErrorCode = ErrorCode | (string & {});
519
527
  interface ResultConfig {
520
528
  /**
521
- * The base URL of the Result backend API
522
- * @default "http://localhost:7130"
529
+ * The base URL of the Result backend API (the NEXT_PUBLIC_BACKEND_URL
530
+ * value from .env.local). Required at runtime: constructing a client
531
+ * without it throws MISSING_BASE_URL.
523
532
  */
524
533
  baseUrl?: string;
525
534
  /**
@@ -498,13 +498,21 @@ interface RealtimeErrorPayload {
498
498
  message: string;
499
499
  }
500
500
  interface SocketMessageMeta {
501
- /** Present for room broadcasts. */
501
+ /**
502
+ * Present for room broadcasts. Matches the exact channel name passed to
503
+ * subscribe() - the SDK strips the server's transport prefix on delivery.
504
+ */
502
505
  channel?: string;
503
506
  messageId: string;
504
507
  senderType: SenderType;
505
508
  senderId?: string;
506
509
  timestamp: string;
507
510
  }
511
+ /**
512
+ * A received realtime message. The published payload's fields arrive spread
513
+ * onto this object next to `meta` (publish `{ text: "hi" }`, read
514
+ * `msg.text`) - there is no `payload` wrapper key.
515
+ */
508
516
  interface SocketMessage {
509
517
  meta: SocketMessageMeta;
510
518
  [key: string]: unknown;
@@ -518,8 +526,9 @@ interface SocketMessage {
518
526
  type ResultErrorCode = ErrorCode | (string & {});
519
527
  interface ResultConfig {
520
528
  /**
521
- * The base URL of the Result backend API
522
- * @default "http://localhost:7130"
529
+ * The base URL of the Result backend API (the NEXT_PUBLIC_BACKEND_URL
530
+ * value from .env.local). Required at runtime: constructing a client
531
+ * without it throws MISSING_BASE_URL.
523
532
  */
524
533
  baseUrl?: string;
525
534
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resultdev/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Result Backend SDK - authentication, database, storage, functions, realtime, AI, email, web analytics and customer support for apps built on Result.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://docs.result.dev",