@driggsby/sdk 0.2.2 → 0.2.3

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.
@@ -11,6 +11,10 @@ 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 APP_ROUTE_PATTERN = /^#[A-Za-z0-9/\-._]{1,256}$/;
15
+ function sanitizeAppRoute(value) {
16
+ return typeof value === "string" && APP_ROUTE_PATTERN.test(value) ? value : "";
17
+ }
14
18
  function parseProtocolMessage(data) {
15
19
  if (typeof data !== "object" || data === null) return null;
16
20
  const record = data;
@@ -27,15 +31,17 @@ function parseProtocolMessage(data) {
27
31
  type,
28
32
  id: typeof record.id === "string" ? record.id : null,
29
33
  result: record.result,
30
- errorMessage
34
+ errorMessage,
35
+ route: sanitizeAppRoute(record.route)
31
36
  };
32
37
  }
33
38
  var SdkCore = class {
34
39
  // Wired by the page entry: remove the standalone note, log tool errors,
35
- // reload on a new deployed version.
40
+ // reload on a new deployed version, restore the host-remembered route.
36
41
  onHostReady = null;
37
42
  onToolError = null;
38
43
  onNewVersion = null;
44
+ onRestoreRoute = null;
39
45
  post;
40
46
  allowLocalHostOrigins;
41
47
  hostOrigin = null;
@@ -73,7 +79,7 @@ var SdkCore = class {
73
79
  const message = parseProtocolMessage(data);
74
80
  if (message === null) return;
75
81
  if (message.type === "hello") {
76
- this.handleHello(origin);
82
+ this.handleHello(origin, message.route);
77
83
  return;
78
84
  }
79
85
  if (this.hostOrigin === null || origin !== this.hostOrigin) return;
@@ -85,13 +91,27 @@ var SdkCore = class {
85
91
  this.onNewVersion?.();
86
92
  }
87
93
  }
88
- handleHello(origin) {
94
+ handleHello(origin, route) {
89
95
  if (this.hostOrigin !== null) return;
90
96
  if (!isAllowedHostOrigin(origin, this.allowLocalHostOrigins)) return;
91
97
  this.hostOrigin = origin;
92
98
  this.onHostReady?.();
99
+ if (route !== "") this.onRestoreRoute?.(route);
93
100
  this.requestAllWatches();
94
101
  }
102
+ // The app moved to a new in-page location; tell the pinned host so it
103
+ // can mirror the fragment into its own page URL. Anything unmirrorable —
104
+ // an empty hash (the app is back at its no-hash location) or a hash
105
+ // outside the strict shape (a query-bearing route, say) — is reported
106
+ // as the bare-"#" cleared sentinel: the host must forget rather than
107
+ // keep claiming a location the app already left, or a refresh or
108
+ // redeploy reload would yank the user to a stale page. Before the
109
+ // handshake there is nobody to tell.
110
+ reportRoute(hash) {
111
+ if (this.hostOrigin === null) return;
112
+ const route = sanitizeAppRoute(hash) || "#";
113
+ this.post({ protocol: PROTOCOL, type: "route", hash: route }, this.hostOrigin);
114
+ }
95
115
  handleResult(message) {
96
116
  const entry = message.id === null ? void 0 : this.watches.get(message.id);
97
117
  if (entry === void 0) return;
@@ -150,13 +170,23 @@ var core = new SdkCore((message, targetOrigin) => {
150
170
  // exactly the Driggsby console origin.
151
171
  allowLocalHostOrigins: isLocalAppHostname(window.location.hostname)
152
172
  });
153
- core.onHostReady = removeStandaloneNote;
154
173
  core.onNewVersion = () => {
155
174
  window.location.reload();
156
175
  };
157
176
  core.onToolError = (tool, message) => {
158
177
  console.error(`Driggsby: the ${tool} call didn't work \u2014 ${message}`);
159
178
  };
179
+ core.onHostReady = () => {
180
+ removeStandaloneNote();
181
+ if (window.location.hash !== "") core.reportRoute(window.location.hash);
182
+ };
183
+ core.onRestoreRoute = (route) => {
184
+ if (window.location.hash === route) return;
185
+ window.location.replace((window.location.href.split("#")[0] ?? "") + route);
186
+ };
187
+ window.addEventListener("hashchange", () => {
188
+ core.reportRoute(window.location.hash);
189
+ });
160
190
  window.addEventListener("message", (event) => {
161
191
  core.handleMessage(event.origin, event.source === window.parent, event.data);
162
192
  });
@@ -4,10 +4,12 @@ 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
6
  export type PostFunction = (message: Record<string, unknown>, targetOrigin: string) => void;
7
+ export declare function sanitizeAppRoute(value: unknown): string;
7
8
  export declare class SdkCore {
8
9
  onHostReady: (() => void) | null;
9
10
  onToolError: ((tool: string, message: string) => void) | null;
10
11
  onNewVersion: (() => void) | null;
12
+ onRestoreRoute: ((route: string) => void) | null;
11
13
  private readonly post;
12
14
  private readonly allowLocalHostOrigins;
13
15
  private hostOrigin;
@@ -20,6 +22,7 @@ export declare class SdkCore {
20
22
  watch(tool: string, params: Record<string, unknown> | null | undefined, callback: WatchCallback): () => void;
21
23
  handleMessage(origin: string, fromParent: boolean, data: unknown): void;
22
24
  private handleHello;
25
+ reportRoute(hash: string): void;
23
26
  private handleResult;
24
27
  private requestAllWatches;
25
28
  private requestWatch;
package/dist/sdk-core.js CHANGED
@@ -38,6 +38,15 @@ export function isAllowedHostOrigin(origin, allowLocalHostOrigins) {
38
38
  return true;
39
39
  return allowLocalHostOrigins && LOCAL_HOST_ORIGIN_PATTERN.test(origin);
40
40
  }
41
+ // The app's in-page location, as a URL fragment. The host mirrors it into
42
+ // its own page URL (so refresh and shared links restore it) and hands it
43
+ // back in the hello after a reload. The shape is deliberately narrow — a
44
+ // short, boring hash — and must stay identical to the host's sanitizer;
45
+ // anything else normalizes to ''.
46
+ const APP_ROUTE_PATTERN = /^#[A-Za-z0-9/\-._]{1,256}$/;
47
+ export function sanitizeAppRoute(value) {
48
+ return typeof value === "string" && APP_ROUTE_PATTERN.test(value) ? value : "";
49
+ }
41
50
  // Untrusted wire data -> a normalized message, or null when it is not ours.
42
51
  function parseProtocolMessage(data) {
43
52
  if (typeof data !== "object" || data === null)
@@ -59,14 +68,16 @@ function parseProtocolMessage(data) {
59
68
  id: typeof record.id === "string" ? record.id : null,
60
69
  result: record.result,
61
70
  errorMessage,
71
+ route: sanitizeAppRoute(record.route),
62
72
  };
63
73
  }
64
74
  export class SdkCore {
65
75
  // Wired by the page entry: remove the standalone note, log tool errors,
66
- // reload on a new deployed version.
76
+ // reload on a new deployed version, restore the host-remembered route.
67
77
  onHostReady = null;
68
78
  onToolError = null;
69
79
  onNewVersion = null;
80
+ onRestoreRoute = null;
70
81
  post;
71
82
  allowLocalHostOrigins;
72
83
  hostOrigin = null;
@@ -112,7 +123,7 @@ export class SdkCore {
112
123
  if (message === null)
113
124
  return;
114
125
  if (message.type === "hello") {
115
- this.handleHello(origin);
126
+ this.handleHello(origin, message.route);
116
127
  return;
117
128
  }
118
129
  // Everything after the handshake must come from the pinned host origin.
@@ -128,15 +139,34 @@ export class SdkCore {
128
139
  this.onNewVersion?.();
129
140
  }
130
141
  }
131
- handleHello(origin) {
142
+ handleHello(origin, route) {
132
143
  if (this.hostOrigin !== null)
133
144
  return; // pinned; later hellos are ignored
134
145
  if (!isAllowedHostOrigin(origin, this.allowLocalHostOrigins))
135
146
  return;
136
147
  this.hostOrigin = origin;
137
148
  this.onHostReady?.();
149
+ // Restore before the watches fire so the app renders the remembered
150
+ // page (the host page's fragment, or the pre-redeploy location) with
151
+ // its first data, not after it.
152
+ if (route !== "")
153
+ this.onRestoreRoute?.(route);
138
154
  this.requestAllWatches();
139
155
  }
156
+ // The app moved to a new in-page location; tell the pinned host so it
157
+ // can mirror the fragment into its own page URL. Anything unmirrorable —
158
+ // an empty hash (the app is back at its no-hash location) or a hash
159
+ // outside the strict shape (a query-bearing route, say) — is reported
160
+ // as the bare-"#" cleared sentinel: the host must forget rather than
161
+ // keep claiming a location the app already left, or a refresh or
162
+ // redeploy reload would yank the user to a stale page. Before the
163
+ // handshake there is nobody to tell.
164
+ reportRoute(hash) {
165
+ if (this.hostOrigin === null)
166
+ return;
167
+ const route = sanitizeAppRoute(hash) || "#";
168
+ this.post({ protocol: PROTOCOL, type: "route", hash: route }, this.hostOrigin);
169
+ }
140
170
  handleResult(message) {
141
171
  const entry = message.id === null ? undefined : this.watches.get(message.id);
142
172
  if (entry === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driggsby/sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "The browser SDK for Driggsby apps: live financial data through driggsby.watch().",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {