@driggsby/sdk 0.4.0 → 0.5.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/README.md CHANGED
@@ -22,6 +22,22 @@ the callback runs again with fresh data. It returns an unsubscribe
22
22
  function. Identical results are deduped, so unchanged data never
23
23
  re-renders.
24
24
 
25
+ To show a section's own failure, pass `onError`. It runs when a watch
26
+ fails, after any retries the host makes, with `{ message, kind }`. A
27
+ repeat of the same failure is skipped. `kind` is `"timeout"`,
28
+ `"unavailable"`, `"busy"`, `"rate_limited"` or `null`. Under
29
+ `driggsby dev`, which doesn't retry, `kind` is always `null`. The next
30
+ successful result always reaches your callback, even when it matches
31
+ the data from before the failure, so clear the failure there. Show
32
+ `error.message` as text (for example with `textContent`), never as
33
+ HTML:
34
+
35
+ ```js
36
+ driggsby.watch("list_accounts", {}, render, {
37
+ onError: (error) => showFailure(error.message),
38
+ });
39
+ ```
40
+
25
41
  This package exists for two narrower uses:
26
42
 
27
43
  - `driggsby dev` serves the same runtime locally from this package's
@@ -11,6 +11,7 @@ function isAllowedHostOrigin(origin, allowLocalHostOrigins) {
11
11
  if (EXACT_ALLOWED_HOST_ORIGINS.has(origin)) return true;
12
12
  return allowLocalHostOrigins && LOCAL_HOST_ORIGIN_PATTERN.test(origin);
13
13
  }
14
+ var WATCH_ERROR_KINDS = /* @__PURE__ */ new Set(["timeout", "unavailable", "busy", "rate_limited"]);
14
15
  var APP_ROUTE_PATTERN = /^#[A-Za-z0-9/\-._]{1,256}$/;
15
16
  function sanitizeAppRoute(value) {
16
17
  return typeof value === "string" && APP_ROUTE_PATTERN.test(value) ? value : "";
@@ -22,16 +23,20 @@ function parseProtocolMessage(data) {
22
23
  const type = record.type;
23
24
  if (typeof type !== "string") return null;
24
25
  let errorMessage = null;
26
+ let errorKind = null;
25
27
  const error = record.error;
26
28
  if (typeof error === "object" && error !== null) {
27
29
  const message = error.message;
28
30
  errorMessage = typeof message === "string" ? message : "Something went wrong running this tool.";
31
+ const kind = error.kind;
32
+ errorKind = typeof kind === "string" && WATCH_ERROR_KINDS.has(kind) ? kind : null;
29
33
  }
30
34
  return {
31
35
  type,
32
36
  id: typeof record.id === "string" ? record.id : null,
33
37
  result: record.result,
34
38
  errorMessage,
39
+ errorKind,
35
40
  route: sanitizeAppRoute(record.route)
36
41
  };
37
42
  }
@@ -57,14 +62,15 @@ var SdkCore = class {
57
62
  // params is typed loose on purpose: app authors call this from untyped
58
63
  // JavaScript, and a null/omitted params should mean "no params", not a
59
64
  // crash.
60
- watch(tool, params, callback) {
65
+ watch(tool, params, callback, options) {
61
66
  if (this.watches.size >= MAX_WATCHES) {
62
67
  this.onToolError?.(tool, `Watch limit reached (${String(MAX_WATCHES)}); this watch will not receive data. Unsubscribe watches you no longer need.`);
63
68
  return () => void 0;
64
69
  }
65
70
  this.sequence += 1;
66
71
  const id = `w${this.sequence}`;
67
- this.watches.set(id, { tool, params: params ?? {}, callback, lastDeliveredJson: null });
72
+ const onError = typeof options?.onError === "function" ? options.onError : null;
73
+ this.watches.set(id, { tool, params: params ?? {}, callback, onError, lastDeliveredJson: null });
68
74
  this.requestWatch(id);
69
75
  return () => {
70
76
  this.watches.delete(id);
@@ -116,10 +122,11 @@ var SdkCore = class {
116
122
  const entry = message.id === null ? void 0 : this.watches.get(message.id);
117
123
  if (entry === void 0) return;
118
124
  if (message.errorMessage !== null) {
119
- const json2 = JSON.stringify({ error: message.errorMessage });
125
+ const json2 = JSON.stringify({ error: message.errorMessage, kind: message.errorKind });
120
126
  if (json2 === entry.lastDeliveredJson) return;
121
127
  entry.lastDeliveredJson = json2;
122
128
  this.onToolError?.(entry.tool, message.errorMessage);
129
+ entry.onError?.({ message: message.errorMessage, kind: message.errorKind });
123
130
  return;
124
131
  }
125
132
  const json = JSON.stringify({ result: message.result });
@@ -191,7 +198,7 @@ window.addEventListener("message", (event) => {
191
198
  core.handleMessage(event.origin, event.source === window.parent, event.data);
192
199
  });
193
200
  window.driggsby = {
194
- watch: (tool, params, callback) => core.watch(tool, params, callback)
201
+ watch: (tool, params, callback, options) => core.watch(tool, params, callback, options)
195
202
  };
196
203
  if (window.parent === window) {
197
204
  whenBodyReady(showStandaloneNote);
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { MAX_WATCHES, PROTOCOL, SdkCore, isAllowedHostOrigin, isLocalAppHostname, type PostFunction, type WatchCallback, } from "./sdk-core.ts";
1
+ export { MAX_WATCHES, PROTOCOL, SdkCore, isAllowedHostOrigin, isLocalAppHostname, type PostFunction, type WatchCallback, type WatchError, type WatchOptions, } from "./sdk-core.ts";
@@ -3,6 +3,13 @@ export declare const MAX_WATCHES = 32;
3
3
  export declare function isLocalAppHostname(hostname: string): boolean;
4
4
  export declare function isAllowedHostOrigin(origin: string, allowLocalHostOrigins: boolean): boolean;
5
5
  export type WatchCallback = (result: unknown) => void;
6
+ export interface WatchError {
7
+ message: string;
8
+ kind: string | null;
9
+ }
10
+ export interface WatchOptions {
11
+ onError?: (error: WatchError) => void;
12
+ }
6
13
  export type PostFunction = (message: Record<string, unknown>, targetOrigin: string) => void;
7
14
  export declare function sanitizeAppRoute(value: unknown): string;
8
15
  export declare class SdkCore {
@@ -19,7 +26,7 @@ export declare class SdkCore {
19
26
  allowLocalHostOrigins?: boolean;
20
27
  });
21
28
  get hostReady(): boolean;
22
- watch(tool: string, params: Record<string, unknown> | null | undefined, callback: WatchCallback): () => void;
29
+ watch(tool: string, params: Record<string, unknown> | null | undefined, callback: WatchCallback, options?: WatchOptions | null): () => void;
23
30
  handleMessage(origin: string, fromParent: boolean, data: unknown): void;
24
31
  private handleHello;
25
32
  reportRoute(hash: string): void;
package/dist/sdk-core.js CHANGED
@@ -6,7 +6,10 @@
6
6
  // first-valid-hello pin, and the parent-source requirement.
7
7
  //
8
8
  // The whole public surface an app sees is:
9
- // driggsby.watch(tool, params, callback) -> unsubscribe()
9
+ // driggsby.watch(tool, params, callback[, { onError }]) -> unsubscribe()
10
+ // onError(error) receives { message, kind } when a watch's call fails
11
+ // after the host's retries; kind is "timeout", "unavailable", "busy",
12
+ // "rate_limited" or null.
10
13
  // Every read is a live subscription: the host announces "data-changed" and
11
14
  // each watch re-runs; results are deduped by JSON identity so unchanged
12
15
  // data never re-renders.
@@ -38,6 +41,9 @@ export function isAllowedHostOrigin(origin, allowLocalHostOrigins) {
38
41
  return true;
39
42
  return allowLocalHostOrigins && LOCAL_HOST_ORIGIN_PATTERN.test(origin);
40
43
  }
44
+ // The failure kinds a watch may be told about; the host forwards only
45
+ // these, and anything else reads as null.
46
+ const WATCH_ERROR_KINDS = new Set(["timeout", "unavailable", "busy", "rate_limited"]);
41
47
  // The app's in-page location, as a URL fragment. The host mirrors it into
42
48
  // its own page URL (so refresh and shared links restore it) and hands it
43
49
  // back in the hello after a reload. The shape is deliberately narrow — a
@@ -58,16 +64,20 @@ function parseProtocolMessage(data) {
58
64
  if (typeof type !== "string")
59
65
  return null;
60
66
  let errorMessage = null;
67
+ let errorKind = null;
61
68
  const error = record.error;
62
69
  if (typeof error === "object" && error !== null) {
63
70
  const message = error.message;
64
71
  errorMessage = typeof message === "string" ? message : "Something went wrong running this tool.";
72
+ const kind = error.kind;
73
+ errorKind = typeof kind === "string" && WATCH_ERROR_KINDS.has(kind) ? kind : null;
65
74
  }
66
75
  return {
67
76
  type,
68
77
  id: typeof record.id === "string" ? record.id : null,
69
78
  result: record.result,
70
79
  errorMessage,
80
+ errorKind,
71
81
  route: sanitizeAppRoute(record.route),
72
82
  };
73
83
  }
@@ -93,7 +103,7 @@ export class SdkCore {
93
103
  // params is typed loose on purpose: app authors call this from untyped
94
104
  // JavaScript, and a null/omitted params should mean "no params", not a
95
105
  // crash.
96
- watch(tool, params, callback) {
106
+ watch(tool, params, callback, options) {
97
107
  // Watches are registered by arbitrary app code; a watch() call that
98
108
  // leaked into a render loop would otherwise grow forever and turn
99
109
  // every data-changed into an unbounded fan-out of real tool calls.
@@ -104,7 +114,10 @@ export class SdkCore {
104
114
  }
105
115
  this.sequence += 1;
106
116
  const id = `w${this.sequence}`;
107
- this.watches.set(id, { tool, params: params ?? {}, callback, lastDeliveredJson: null });
117
+ // Options come from untyped app code: a non-function onError is
118
+ // ignored rather than thrown at on the first failure.
119
+ const onError = typeof options?.onError === "function" ? options.onError : null;
120
+ this.watches.set(id, { tool, params: params ?? {}, callback, onError, lastDeliveredJson: null });
108
121
  // Before the host pins, requestWatch posts nothing; the pinning hello
109
122
  // fires every registered watch, so pre-hello watches are simply held.
110
123
  this.requestWatch(id);
@@ -172,11 +185,12 @@ export class SdkCore {
172
185
  if (entry === undefined)
173
186
  return;
174
187
  if (message.errorMessage !== null) {
175
- const json = JSON.stringify({ error: message.errorMessage });
188
+ const json = JSON.stringify({ error: message.errorMessage, kind: message.errorKind });
176
189
  if (json === entry.lastDeliveredJson)
177
190
  return;
178
191
  entry.lastDeliveredJson = json;
179
192
  this.onToolError?.(entry.tool, message.errorMessage);
193
+ entry.onError?.({ message: message.errorMessage, kind: message.errorKind });
180
194
  return;
181
195
  }
182
196
  // Wrapping in an object keeps stringify total: even `result: undefined`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driggsby/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The browser SDK for Driggsby apps: live financial data through driggsby.watch().",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {