@microsoft/rayfin-app-state-fabric 1.35.0-alpha.1412

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) Microsoft Corporation.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @microsoft/rayfin-app-state-fabric
2
+
3
+ Deep-link application state for Rayfin apps embedded in the Fabric portal.
4
+
5
+ ## Getting started
6
+
7
+ ```bash
8
+ npm install @microsoft/rayfin-app-state-fabric
9
+ ```
10
+
11
+ For more details, [visit our docs](https://aka.ms/rayfin/docs).
12
+
13
+ ## Deep linking
14
+
15
+ A Fabric App runs in an iframe and cannot touch the portal address bar directly.
16
+ `createFabricAppStateClient()` lets your app read the state it was launched with and write state back, so a user can share a link that reopens the exact view they were looking at.
17
+
18
+ Read launch state *before* your first render so default state never flashes:
19
+
20
+ ```ts
21
+ import { createFabricAppStateClient } from '@microsoft/rayfin-app-state-fabric';
22
+
23
+ const appState = createFabricAppStateClient({
24
+ targetOrigin: 'https://app.fabric.microsoft.com',
25
+ });
26
+
27
+ const launch = appState.getLaunchStateSync();
28
+ renderApp(launch ?? defaultView);
29
+ ```
30
+
31
+ Write state as the user navigates.
32
+ Use `setState` for navigation the user would expect the Back button to undo, and `replaceState` for transient changes such as dragging a slider:
33
+
34
+ ```ts
35
+ await appState.setState({ view: 'sales-by-region', filter: 'AT' });
36
+ await appState.replaceState({ view: 'sales-by-region', filter: 'AT', zoom: 3 });
37
+ ```
38
+
39
+ React to the Back and Forward buttons.
40
+ The listener receives `undefined` when the user reaches a URL that carries no state, which means you should restore your defaults:
41
+
42
+ ```ts
43
+ const unsubscribe = appState.onStateChange((state) => {
44
+ restore(state ?? defaultView);
45
+ });
46
+
47
+ // On teardown
48
+ unsubscribe();
49
+ appState.dispose();
50
+ ```
51
+
52
+ Deep linking rolls out per tenant, so check support before showing a share button:
53
+
54
+ ```ts
55
+ const capabilities = await appState.isSupported();
56
+ if (capabilities) {
57
+ showShareButton();
58
+ // Some hosts can update the URL but not add history entries.
59
+ if (!capabilities.canPush) hideBackForwardHints();
60
+ }
61
+ ```
62
+
63
+ ### Running outside the portal
64
+
65
+ The same app often ships standalone as well as embedded, where it owns its own address bar and there is no host to talk to.
66
+ The client is safe to construct either way: nothing throws at construction, and it never rewrites a URL it does not own.
67
+
68
+ `isSupported()` is the single branch point.
69
+ It resolves to `undefined` when the app is not embedded, so the check that guards a share button also selects your standalone path:
70
+
71
+ ```ts
72
+ const capabilities = await appState.isSupported();
73
+
74
+ if (capabilities) {
75
+ // Embedded in Fabric: the portal owns the address bar.
76
+ await appState.setState({ view: 'sales', region: 'AT' });
77
+ } else {
78
+ // Standalone: the app owns its own URL, so use your router.
79
+ router.push({ path: '/sales', query: { region: 'AT' } });
80
+ }
81
+ ```
82
+
83
+ What each call does when the app is not embedded:
84
+
85
+ | Call | Standalone result |
86
+ | --- | --- |
87
+ | `isSupported()` | `undefined` |
88
+ | `getLaunchStateSync()` | seeded state when the URL carries it, otherwise `undefined` |
89
+ | `getLaunchState()` | the same, and never rejects |
90
+ | `setState()` / `replaceState()` | rejects with `NO_HOST_WINDOW` |
91
+ | `onStateChange()` | the listener registers but never fires |
92
+
93
+ Writes reject rather than silently doing nothing, so a missing branch shows up in development instead of quietly dropping state.
94
+ Branch on `isSupported()`, or catch `NO_HOST_WINDOW` if you would rather attempt the write.
95
+
96
+ ### Rules and limits
97
+
98
+ - State must be a plain JSON object.
99
+ `Date`, `Map`, `Set`, class instances, functions, and `undefined` are rejected rather than silently degraded.
100
+ - State is capped at 4 KiB encoded and 20 levels deep so links survive proxies, mail gateways, and chat clients.
101
+ For anything larger, store it yourself and put an identifier in the state.
102
+ - The whole object is replaced on every write.
103
+ There is no partial or namespaced update, so an app with several independent pieces of state must merge them itself before writing.
104
+ - Your app owns the shape of its state.
105
+ The encoding is versioned, but the payload is not, so a link shared before a shape change will still arrive in the old shape and your app must tolerate it.
106
+ - Errors are `FabricAppStateError` with a stable `code`.
107
+ Branch on the code, not the message.
108
+
109
+ ### Security
110
+
111
+ State travels in a URL, so treat it accordingly.
112
+
113
+ - **It is visible to the user.** It appears in the address bar, browser history, and bookmarks.
114
+ Never put secrets, tokens, or personal data in it.
115
+ - **It is untrusted input.** Anyone can edit a link before sending it, so validate launch state exactly as you would a query parameter before using it to drive queries.
116
+ Your app should also tolerate state written by a different version of itself.
117
+
118
+ ## Security
119
+
120
+ Microsoft takes the security of our software products and services seriously, which
121
+ includes all source code repositories in our GitHub organizations.
122
+
123
+ **Please do not report security vulnerabilities through public GitHub issues.**
124
+
125
+ For security reporting information, locations, contact information, and policies,
126
+ please review the latest guidance for Microsoft repositories at
127
+ [https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md).
128
+
129
+ ## Trademarks
130
+
131
+ This project may contain trademarks or logos for projects, products, or services.
132
+ Authorized use of Microsoft trademarks or logos must follow the [Microsoft Trademark and Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general).
133
+ Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
134
+ Any use of third-party trademarks or logos is subject to those third parties' policies.
135
+
136
+ ## License
137
+
138
+ Copyright (c) Microsoft Corporation.
139
+
140
+ MIT License
@@ -0,0 +1,211 @@
1
+ # Fabric deep-link app state
2
+
3
+ Shareable, bookmarkable URLs for Rayfin applications embedded in the Fabric portal.
4
+
5
+ A Fabric App runs inside an iframe and cannot touch the portal address bar directly.
6
+ `@microsoft/rayfin-app-state-fabric` bridges that gap: your app can read the state it was launched with and write state back, so a user can copy the browser URL and send someone the exact view they were looking at.
7
+
8
+ The package builds on the message bridge in `@microsoft/fabric-embedded-host`, which handles the underlying `postMessage` transport.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @microsoft/rayfin-app-state-fabric
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ Create one client for the lifetime of the app and read the launch state before your first render, so a default view never flashes before the real one appears.
19
+
20
+ ```typescript
21
+ import { createFabricAppStateClient } from '@microsoft/rayfin-app-state-fabric';
22
+
23
+ const appState = createFabricAppStateClient({
24
+ targetOrigin: 'https://app.fabric.microsoft.com',
25
+ });
26
+
27
+ const launch = appState.getLaunchStateSync();
28
+ renderApp(launch ?? defaultView);
29
+ ```
30
+
31
+ `targetOrigin` is strongly recommended.
32
+ When it is omitted, messages are posted with `"*"` and inbound events are not origin-checked.
33
+
34
+ ## Reading launch state
35
+
36
+ Two readers are available, and which one you want depends on whether your code path can be asynchronous.
37
+
38
+ `getLaunchStateSync()` returns the seeded state without awaiting, which is what you want on the first render path.
39
+ It returns `undefined` when the host did not seed the URL.
40
+
41
+ `getLaunchState()` resolves synchronously from the seeded URL when the host supports it, and falls back to a bridge round trip only on hosts that do not seed.
42
+ Awaiting it does not delay first paint on a modern host.
43
+
44
+ ```typescript
45
+ const launch = await appState.getLaunchState();
46
+ ```
47
+
48
+ Both return `undefined` when the URL carries no state, which is the normal case for a fresh navigation.
49
+
50
+ ## Writing state
51
+
52
+ Choose between the two writers by asking **who caused the change**.
53
+
54
+ Use `setState()` when the *user* caused it, such as a click, a filter change, or opening a record.
55
+ It creates a history entry, so Back returns the user to where they were.
56
+
57
+ ```typescript
58
+ // The user picked a region: Back should undo it.
59
+ await appState.setState({ view: 'sales', region: 'AT' });
60
+ ```
61
+
62
+ Use `replaceState()` when the *app* caused it, such as restoring, reconciling, or normalising state the user never asked for.
63
+ Also use it for high-frequency updates like a slider drag, where one history entry per update would make Back unusable.
64
+
65
+ ```typescript
66
+ // Continuous updates while dragging: do not grow history.
67
+ await appState.replaceState({ view: 'sales', threshold: value });
68
+ ```
69
+
70
+ The whole object is replaced on every write.
71
+ There is no partial or namespaced update, so an app with several independent pieces of state must merge them itself before writing.
72
+
73
+ ### Platform caveat
74
+
75
+ The Fabric host downgrades a replace to a push when the previous history entry belongs to a different extension, which stops one extension from overwriting another's history.
76
+ The first `replaceState()` after a user arrives from elsewhere in Fabric may therefore still create an entry.
77
+ This is platform behaviour and cannot be overridden.
78
+
79
+ ## Reacting to Back and Forward
80
+
81
+ Subscribe to observe changes the app did not initiate: browser Back or Forward, or a deep link opened in the current tab.
82
+
83
+ The listener receives `undefined` when navigation reaches a URL that carries no state, which means the app should restore its own defaults.
84
+
85
+ ```typescript
86
+ const unsubscribe = appState.onStateChange((state) => {
87
+ restore(state ?? defaultView);
88
+ });
89
+
90
+ // On teardown
91
+ unsubscribe();
92
+ appState.dispose();
93
+ ```
94
+
95
+ ## Checking host support
96
+
97
+ Deep linking rolls out per tenant, so check support before showing a share button rather than letting a user click one that cannot work.
98
+ The result is cached.
99
+
100
+ ```typescript
101
+ const capabilities = await appState.isSupported();
102
+
103
+ if (capabilities) {
104
+ showShareButton();
105
+
106
+ // Some hosts can update the URL but not add history entries.
107
+ if (!capabilities.canPush) {
108
+ hideBackForwardHints();
109
+ }
110
+ }
111
+ ```
112
+
113
+ `isSupported()` resolves to `undefined` when the host does not implement deep-link state.
114
+ Otherwise it reports the host's `version`, `maxEncodedBytes`, `maxDepth`, and `canPush`.
115
+
116
+ The host is authoritative.
117
+ The client's own limits exist only to fail fast before a round trip, so treat the reported values as the real contract.
118
+
119
+ ## Running outside the portal
120
+
121
+ Many apps ship standalone as well as embedded.
122
+ Standalone, the app owns its own address bar and there is no host to talk to, so deep-link state is unavailable by design.
123
+
124
+ The client is safe to construct either way.
125
+ Nothing throws at construction, and it never rewrites a URL it does not own — the launch parameter is only scrubbed when the app is actually embedded.
126
+
127
+ `isSupported()` is the single branch point.
128
+ It resolves to `undefined` when the app is not embedded, so the same check that guards a share button also selects your standalone path.
129
+
130
+ ```typescript
131
+ const capabilities = await appState.isSupported();
132
+
133
+ if (capabilities) {
134
+ // Embedded in Fabric: the portal owns the address bar.
135
+ await appState.setState({ view: 'sales', region: 'AT' });
136
+ } else {
137
+ // Standalone: the app owns its own URL, so use your router.
138
+ router.push({ path: '/sales', query: { region: 'AT' } });
139
+ }
140
+ ```
141
+
142
+ What each call does when the app is not embedded:
143
+
144
+ | Call | Standalone result |
145
+ | --- | --- |
146
+ | `isSupported()` | `undefined` |
147
+ | `getLaunchStateSync()` | seeded state when the URL carries it, otherwise `undefined` |
148
+ | `getLaunchState()` | the same, and never rejects |
149
+ | `setState()` / `replaceState()` | rejects with `NO_HOST_WINDOW` |
150
+ | `onStateChange()` | the listener registers but never fires |
151
+
152
+ Reads degrade quietly so startup code needs no branching, but writes reject rather than silently doing nothing.
153
+ A missing branch therefore surfaces during development instead of dropping state without a trace.
154
+ Branch on `isSupported()`, or catch `NO_HOST_WINDOW` if you would rather attempt the write and handle the failure.
155
+
156
+ ## Limits and rules
157
+
158
+ - State must be a plain JSON object.
159
+ `Date`, `Map`, `Set`, class instances, functions, and `undefined` are rejected rather than silently degraded.
160
+ - State is capped at 4 KiB encoded and 20 levels deep, so links survive proxies, mail gateways, and chat clients.
161
+ For anything larger, store it yourself and put an identifier in the state.
162
+ - Your app owns the shape of its state.
163
+ The encoding is versioned, but the payload is not, so a link shared before a shape change will still arrive in the old shape and your app must tolerate it.
164
+
165
+ ## Error handling
166
+
167
+ Failures throw `FabricAppStateError` with a stable `code`.
168
+ Branch on the code, never on the message.
169
+ State values are never included in the code or the message, so these errors are safe to log.
170
+
171
+ | Code | Meaning |
172
+ | --- | --- |
173
+ | `INVALID_STATE` | State is not JSON-serialisable |
174
+ | `STATE_TOO_LARGE` | State exceeds the encoded-size budget |
175
+ | `STATE_TOO_DEEP` | State exceeds the nesting-depth limit |
176
+ | `UNSUPPORTED_HOST_CAPABILITY` | Host does not implement deep-link state |
177
+ | `NO_HOST_WINDOW` | App is not running embedded in the Fabric portal |
178
+ | `BRIDGE_TIMEOUT` | Host did not respond in time |
179
+
180
+ ```typescript
181
+ import { FabricAppStateError } from '@microsoft/rayfin-app-state-fabric';
182
+
183
+ try {
184
+ await appState.setState(nextState);
185
+ } catch (error) {
186
+ if (error instanceof FabricAppStateError) {
187
+ if (error.code === 'STATE_TOO_LARGE') {
188
+ // Fall back to storing the state server-side.
189
+ }
190
+ }
191
+ }
192
+ ```
193
+
194
+ A host that predates this feature, or has the feature switch turned off, surfaces as `UNSUPPORTED_HOST_CAPABILITY` so the app can degrade gracefully instead of treating it as a bug.
195
+
196
+ ## Security
197
+
198
+ State travels in a URL, so treat it accordingly.
199
+
200
+ **It is visible to the user.** It appears in the address bar, browser history, bookmarks, screenshots, copied links, and corporate proxy logs.
201
+ Never put secrets, access tokens, or personal data in it.
202
+ When the state is sensitive, use an opaque identifier that maps to server-side data.
203
+
204
+ **It is untrusted input.** Anyone can edit a link before sending it, so validate launch state exactly as you would validate a query parameter before using it to drive queries.
205
+ Your app should also tolerate state written by a different version of itself.
206
+
207
+ ## Browser requirements
208
+
209
+ This package is intended for browser environments running embedded in the Fabric portal.
210
+
211
+ It depends on browser APIs such as `postMessage`, `window.parent`, and `window.location`.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Error type and transport-error mapping for the app state client.
3
+ */
4
+ import { SdkError } from '@microsoft/rayfin-lib';
5
+ /**
6
+ * Error thrown for invalid state or a host that cannot service the
7
+ * request.
8
+ *
9
+ * The `code` is stable and safe to branch on; the `message` is not.
10
+ * State values are never included in either, so the error can be logged
11
+ * without leaking user data.
12
+ *
13
+ * Codes currently emitted:
14
+ *
15
+ * | Code | Meaning |
16
+ * | --- | --- |
17
+ * | `INVALID_STATE` | State is not JSON-serialisable |
18
+ * | `STATE_TOO_LARGE` | State exceeds the encoded-size budget |
19
+ * | `STATE_TOO_DEEP` | State exceeds the nesting-depth limit |
20
+ * | `UNSUPPORTED_HOST_CAPABILITY` | Host does not implement deep-link state |
21
+ * | `NO_HOST_WINDOW` | App is not running embedded in the Fabric portal |
22
+ * | `BRIDGE_TIMEOUT` | Host did not respond in time |
23
+ */
24
+ export declare class FabricAppStateError extends SdkError {
25
+ name: string;
26
+ constructor(message: string, code: string);
27
+ }
28
+ /**
29
+ * Translate transport errors into a stable app-state vocabulary.
30
+ *
31
+ * A host that predates this feature, or has the feature switch off,
32
+ * replies `UNKNOWN_CHANNEL`. Surfacing that as
33
+ * `UNSUPPORTED_HOST_CAPABILITY` lets an app degrade gracefully instead
34
+ * of treating it as a bug.
35
+ *
36
+ * @internal
37
+ */
38
+ export declare function toAppStateError(err: unknown): FabricAppStateError;
39
+ //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Error type and transport-error mapping for the app state client.
3
+ */
4
+ import { BridgeError } from '@microsoft/fabric-embedded-host';
5
+ import { SdkError } from '@microsoft/rayfin-lib';
6
+ /**
7
+ * Error thrown for invalid state or a host that cannot service the
8
+ * request.
9
+ *
10
+ * The `code` is stable and safe to branch on; the `message` is not.
11
+ * State values are never included in either, so the error can be logged
12
+ * without leaking user data.
13
+ *
14
+ * Codes currently emitted:
15
+ *
16
+ * | Code | Meaning |
17
+ * | --- | --- |
18
+ * | `INVALID_STATE` | State is not JSON-serialisable |
19
+ * | `STATE_TOO_LARGE` | State exceeds the encoded-size budget |
20
+ * | `STATE_TOO_DEEP` | State exceeds the nesting-depth limit |
21
+ * | `UNSUPPORTED_HOST_CAPABILITY` | Host does not implement deep-link state |
22
+ * | `NO_HOST_WINDOW` | App is not running embedded in the Fabric portal |
23
+ * | `BRIDGE_TIMEOUT` | Host did not respond in time |
24
+ */
25
+ export class FabricAppStateError extends SdkError {
26
+ name = 'FabricAppStateError';
27
+ constructor(message, code) {
28
+ super(message, code);
29
+ Object.setPrototypeOf(this, FabricAppStateError.prototype);
30
+ }
31
+ }
32
+ /**
33
+ * Translate transport errors into a stable app-state vocabulary.
34
+ *
35
+ * A host that predates this feature, or has the feature switch off,
36
+ * replies `UNKNOWN_CHANNEL`. Surfacing that as
37
+ * `UNSUPPORTED_HOST_CAPABILITY` lets an app degrade gracefully instead
38
+ * of treating it as a bug.
39
+ *
40
+ * @internal
41
+ */
42
+ export function toAppStateError(err) {
43
+ if (err instanceof FabricAppStateError)
44
+ return err;
45
+ if (err instanceof BridgeError) {
46
+ if (err.code === 'UNKNOWN_CHANNEL') {
47
+ return new FabricAppStateError('This Fabric host does not support deep-link state. The app should ' +
48
+ 'continue without it.', 'UNSUPPORTED_HOST_CAPABILITY');
49
+ }
50
+ return new FabricAppStateError(err.message, err.code ?? 'BRIDGE_ERROR');
51
+ }
52
+ return new FabricAppStateError(err instanceof Error ? err.message : 'Unknown app-state error.', 'UNKNOWN_ERROR');
53
+ }
54
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * fabricAppState — deep-linking client for Rayfin apps embedded in Fabric.
3
+ *
4
+ * An embedded app cannot touch the portal address bar, so it hands the host
5
+ * an opaque JSON object and the host owns the URL. That indirection is what
6
+ * lets the encoding change without a breaking SDK release.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const appState = createFabricAppStateClient();
11
+ *
12
+ * // Restore before first render so defaults never flash.
13
+ * restoreApplicationState(appState.getLaunchStateSync());
14
+ *
15
+ * // User-initiated: Back should undo it.
16
+ * await appState.setState({ view: 'sales-by-region', filter: 'AT' });
17
+ *
18
+ * // App-initiated: do not grow history.
19
+ * await appState.replaceState({ view: 'sales-by-region', filter: 'DE' });
20
+ *
21
+ * const unsubscribe = appState.onStateChange(restoreApplicationState);
22
+ * ```
23
+ */
24
+ import type { FabricAppStateClient, FabricAppStateClientOptions } from './types.js';
25
+ /**
26
+ * Create a {@link FabricAppStateClient} bound to the Fabric host.
27
+ *
28
+ * Safe to call once per application; create a single instance and share
29
+ * it rather than constructing one per component.
30
+ */
31
+ export declare function createFabricAppStateClient(options?: FabricAppStateClientOptions): FabricAppStateClient;
32
+ //# sourceMappingURL=fabricAppState.d.ts.map