@bardioc/app-sdk 0.4.11 → 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/dist/host-bridge.d.ts +1 -1
- package/dist/host-bridge.js +41 -29
- package/dist/index.d.ts +1 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +49 -18
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
package/dist/host-bridge.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { HostBridge, HostBridgeConfig } from './types.js';
|
|
2
|
-
/** Create a bridge between an
|
|
2
|
+
/** Create a bridge between an app and its host OS, over `postMessage` unless `config.channel` says otherwise. */
|
|
3
3
|
export declare function createHostBridge(config: HostBridgeConfig): HostBridge;
|
package/dist/host-bridge.js
CHANGED
|
@@ -13,29 +13,49 @@ function assertStoreName(storeName) {
|
|
|
13
13
|
}
|
|
14
14
|
return normalized;
|
|
15
15
|
}
|
|
16
|
-
/** Create a bridge between an
|
|
16
|
+
/** Create a bridge between an app and its host OS, over `postMessage` unless `config.channel` says otherwise. */
|
|
17
17
|
export function createHostBridge(config) {
|
|
18
18
|
const { appId: hintAppId, connectTimeout = 5000, debug = false } = config;
|
|
19
|
-
const hostWindow = globalThis.window;
|
|
20
|
-
// When inside an iframe, document.referrer gives us the parent's origin.
|
|
21
|
-
const inferredHostOrigin = (() => {
|
|
22
|
-
try {
|
|
23
|
-
if (hostWindow.parent !== hostWindow && document.referrer) {
|
|
24
|
-
return new URL(document.referrer).origin;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// ignore parse errors
|
|
29
|
-
}
|
|
30
|
-
return hostWindow.location.origin;
|
|
31
|
-
})();
|
|
32
|
-
const targetOrigin = config.targetOrigin ?? inferredHostOrigin;
|
|
33
|
-
const isInsideIframe = hostWindow.parent !== hostWindow;
|
|
34
19
|
function log(...args) {
|
|
35
20
|
if (debug)
|
|
36
21
|
console.log('[app-sdk]', ...args);
|
|
37
22
|
}
|
|
38
|
-
|
|
23
|
+
function windowChannel() {
|
|
24
|
+
const hostWindow = globalThis.window;
|
|
25
|
+
// When inside an iframe, document.referrer gives us the parent's origin.
|
|
26
|
+
const inferredHostOrigin = (() => {
|
|
27
|
+
try {
|
|
28
|
+
if (hostWindow.parent !== hostWindow && document.referrer) {
|
|
29
|
+
return new URL(document.referrer).origin;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// ignore parse errors
|
|
34
|
+
}
|
|
35
|
+
return hostWindow.location.origin;
|
|
36
|
+
})();
|
|
37
|
+
const targetOrigin = config.targetOrigin ?? inferredHostOrigin;
|
|
38
|
+
const isInsideIframe = hostWindow.parent !== hostWindow;
|
|
39
|
+
log('createHostBridge', { appId: hintAppId, targetOrigin, isInsideIframe });
|
|
40
|
+
function isAllowedMessageOrigin(origin) {
|
|
41
|
+
return origin === targetOrigin || origin === hostWindow.location.origin;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
post: message => hostWindow.parent.postMessage(message, targetOrigin),
|
|
45
|
+
subscribe(receive) {
|
|
46
|
+
const onMessage = (event) => {
|
|
47
|
+
if (!isAllowedMessageOrigin(event.origin)) {
|
|
48
|
+
log('ignoring message from', event.origin, '(expected', targetOrigin, ')');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
receive(event.data);
|
|
52
|
+
};
|
|
53
|
+
hostWindow.addEventListener('message', onMessage);
|
|
54
|
+
return () => hostWindow.removeEventListener('message', onMessage);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const channel = config.channel ?? windowChannel();
|
|
39
59
|
let _connected = false;
|
|
40
60
|
let _context = null;
|
|
41
61
|
let _destroyed = false;
|
|
@@ -43,9 +63,6 @@ export function createHostBridge(config) {
|
|
|
43
63
|
const osConfigUpdateCallbacks = new Set();
|
|
44
64
|
const eventHandlers = new Map();
|
|
45
65
|
const pending = new Map();
|
|
46
|
-
function isAllowedMessageOrigin(origin) {
|
|
47
|
-
return origin === targetOrigin || origin === hostWindow.location.origin;
|
|
48
|
-
}
|
|
49
66
|
function handleCommandInvoke(data) {
|
|
50
67
|
const { commandId } = data.payload;
|
|
51
68
|
const handler = commandHandlers.get(commandId);
|
|
@@ -102,12 +119,7 @@ export function createHostBridge(config) {
|
|
|
102
119
|
}
|
|
103
120
|
request.resolve(data.payload);
|
|
104
121
|
}
|
|
105
|
-
function
|
|
106
|
-
if (!isAllowedMessageOrigin(event.origin)) {
|
|
107
|
-
log('ignoring message from', event.origin, '(expected', targetOrigin, ')');
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const data = event.data;
|
|
122
|
+
function receive(data) {
|
|
111
123
|
if (!data || typeof data.type !== 'string')
|
|
112
124
|
return;
|
|
113
125
|
if (!data.id)
|
|
@@ -131,9 +143,9 @@ export function createHostBridge(config) {
|
|
|
131
143
|
return;
|
|
132
144
|
}
|
|
133
145
|
}
|
|
134
|
-
|
|
146
|
+
const unsubscribe = channel.subscribe(receive);
|
|
135
147
|
function postToHost(message) {
|
|
136
|
-
|
|
148
|
+
channel.post(message);
|
|
137
149
|
}
|
|
138
150
|
function respondToHost(id, payload, error) {
|
|
139
151
|
postToHost({
|
|
@@ -293,7 +305,7 @@ export function createHostBridge(config) {
|
|
|
293
305
|
}
|
|
294
306
|
eventHandlers.clear();
|
|
295
307
|
_destroyed = true;
|
|
296
|
-
|
|
308
|
+
unsubscribe();
|
|
297
309
|
for (const [, p] of pending)
|
|
298
310
|
p.reject(new Error('Bridge destroyed'));
|
|
299
311
|
pending.clear();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { SDK_CONTRACT_MATRIX, getRequiredPermissionForSdkMessage } from './contr
|
|
|
5
5
|
export { ALLOWED_KERNEL_MESSAGES, SDK_MSG } from './protocol.js';
|
|
6
6
|
export type { SdkMessageType } from './protocol.js';
|
|
7
7
|
export { EntityNotFoundError, NetworkError, PermissionError, SdkError, TimeoutError, ValidationError, } from './errors.js';
|
|
8
|
-
export type { Application, BackendRequestOptions, BackendTransport, BatchResult, CombinedQueryOptions, Configuration, CreateMemberInput, CreateUnitInput, DeleteOptions, EventSubscribeRequest, EventSubscribeResponse, EventUnsubscribeRequest, EventsApi, FileEntry, GetNodeOptions, GraphEdgeRaw, GraphEvent, GraphEventPushPayload, GraphEventType, GraphNodeRaw, GraphTransport, GremlinOptions, HistoryEntry, HistoryOptions, HostBridge, HostBridgeConfig, InstanceList, MutationOptions, NotifyLevel, OrgMember, OrgStructure, OrgUnit, OsConfig, OsRequestOptions, OsTransport, QueryOptions, SdkCommandDefinition, SdkCommandHandler, SdkCommandInvokePayload, SdkIdbDeletePayload, SdkIdbEntry, SdkIdbGetPayload, SdkIdbPutPayload, SdkIdbQueryPayload, SdkIdbStore, SdkInitResponse, SdkKernelProxyPayload, SdkMenuItem, SdkMenuRoot, SdkMessage, SdkNotifyPayload, SdkResponse, SdkSetActiveTabPayload, SdkSetCommandsPayload, SdkSetMenuPayload, SdkShortcutEventPayload, SdkStorage, SdkStorageDeletePayload, SdkStorageGetPayload, SdkStorageSetPayload, SdkTransport, SdkTransportPayload, TimeseriesAggregate, TimeseriesHistory, TimeseriesHistoryOptions, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions, UpdateMemberInput, UpdateUnitInput, UserProfile, } from './types.js';
|
|
8
|
+
export type { Application, BackendRequestOptions, BackendTransport, BatchResult, CombinedQueryOptions, Configuration, CreateMemberInput, CreateUnitInput, DeleteOptions, EventSubscribeRequest, EventSubscribeResponse, EventUnsubscribeRequest, EventsApi, FileEntry, GetNodeOptions, GraphEdgeRaw, GraphEvent, GraphEventPushPayload, GraphEventType, GraphNodeRaw, GraphTransport, GremlinOptions, HistoryEntry, HistoryOptions, HostBridge, HostBridgeConfig, InstanceList, MutationOptions, NotifyLevel, OrgMember, OrgStructure, OrgUnit, OsConfig, OsRequestOptions, OsTransport, QueryOptions, SdkChannel, SdkCommandDefinition, SdkCommandHandler, SdkCommandInvokePayload, SdkIdbDeletePayload, SdkIdbEntry, SdkIdbGetPayload, SdkIdbPutPayload, SdkIdbQueryPayload, SdkIdbStore, SdkInitResponse, SdkKernelProxyPayload, SdkMenuItem, SdkMenuRoot, SdkMessage, SdkNotifyPayload, SdkResponse, SdkSetActiveTabPayload, SdkSetCommandsPayload, SdkSetMenuPayload, SdkShortcutEventPayload, SdkStorage, SdkStorageDeletePayload, SdkStorageGetPayload, SdkStorageSetPayload, SdkTransport, SdkTransportPayload, TimeseriesAggregate, TimeseriesHistory, TimeseriesHistoryOptions, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions, UpdateMemberInput, UpdateUnitInput, UserProfile, } from './types.js';
|
package/dist/react.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import type { GraphEvent, HostBridge, HostBridgeConfig, NotifyLevel, OsConfig, SdkCommandDefinition, SdkCommandHandler, SdkMenuRoot } from './types.js';
|
|
3
3
|
export interface AppSdkProviderProps extends HostBridgeConfig {
|
|
4
4
|
children: ReactNode;
|
|
5
|
+
/** Apply theme, language and size to this document. Default: `true`; `false` when the app shares the host document. */
|
|
6
|
+
applyOsConfig?: boolean;
|
|
5
7
|
}
|
|
6
8
|
export interface AppCommand extends SdkCommandDefinition {
|
|
7
9
|
handler: SdkCommandHandler;
|
|
8
10
|
}
|
|
9
11
|
/** Mounts the SDK bridge and makes it available to descendant hooks. */
|
|
10
|
-
export declare function AppSdkProvider({ children, ...config }: Readonly<AppSdkProviderProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
export declare function AppSdkProvider({ children, applyOsConfig, ...config }: Readonly<AppSdkProviderProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
11
13
|
/** Returns the raw {@link HostBridge}. Must be used inside `<AppSdkProvider>`. */
|
|
12
14
|
export declare function useSdk(): HostBridge;
|
|
13
15
|
/** Returns a stable callback that fires a host notification toast. */
|
package/dist/react.js
CHANGED
|
@@ -2,6 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
3
3
|
import { createHostBridge } from './host-bridge.js';
|
|
4
4
|
const SdkContext = createContext(null);
|
|
5
|
+
// One handshake per bridge, however often the effect that owns it reconnects.
|
|
6
|
+
const connections = new WeakMap();
|
|
5
7
|
const SYSTEM_SIZE_CLASS_NAMES = ['text-sm', 'text-base', 'text-lg', 'text-xl'];
|
|
6
8
|
function applyOsConfigToDocument(osConfig) {
|
|
7
9
|
if (!osConfig) {
|
|
@@ -39,28 +41,46 @@ function hasShortcutRoots(menu) {
|
|
|
39
41
|
return menu.some(root => hasShortcutItems(root.items));
|
|
40
42
|
}
|
|
41
43
|
/** Mounts the SDK bridge and makes it available to descendant hooks. */
|
|
42
|
-
export function AppSdkProvider({ children, ...config }) {
|
|
44
|
+
export function AppSdkProvider({ children, applyOsConfig = true, ...config }) {
|
|
43
45
|
const bridgeRef = useRef(null);
|
|
44
|
-
const { appId, connectTimeout, debug, targetOrigin } = config;
|
|
46
|
+
const { appId, channel, connectTimeout, debug, targetOrigin } = config;
|
|
45
47
|
const configKey = JSON.stringify([
|
|
46
48
|
appId ?? null,
|
|
47
49
|
connectTimeout ?? null,
|
|
48
50
|
debug ?? false,
|
|
49
51
|
targetOrigin ?? null,
|
|
50
52
|
]);
|
|
51
|
-
const bridgeConfig = { appId, connectTimeout, debug, targetOrigin };
|
|
53
|
+
const bridgeConfig = { appId, channel, connectTimeout, debug, targetOrigin };
|
|
52
54
|
const bridgeConfigKeyRef = useRef(null);
|
|
55
|
+
const pendingDestroyRef = useRef(null);
|
|
53
56
|
const [readyConfigKey, setReadyConfigKey] = useState(null);
|
|
54
57
|
const [errorState, setErrorState] = useState(null);
|
|
55
58
|
useEffect(() => {
|
|
56
59
|
let cancelled = false;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
+
// A disconnect followed at once by a reconnect (StrictMode, Activity) keeps the live bridge:
|
|
61
|
+
// the children holding it reconnect their own effects over it before this one runs.
|
|
62
|
+
const pending = pendingDestroyRef.current;
|
|
63
|
+
pendingDestroyRef.current = null;
|
|
64
|
+
if (pending)
|
|
65
|
+
clearTimeout(pending.timer);
|
|
66
|
+
let bridge;
|
|
67
|
+
if (pending && pending.configKey === configKey && pending.channel === channel) {
|
|
68
|
+
bridge = pending.bridge;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
pending?.destroy();
|
|
72
|
+
setReadyConfigKey(null);
|
|
73
|
+
setErrorState(null);
|
|
74
|
+
bridge = createHostBridge(bridgeConfig);
|
|
75
|
+
}
|
|
60
76
|
bridgeRef.current = bridge;
|
|
61
77
|
bridgeConfigKeyRef.current = configKey;
|
|
62
|
-
bridge
|
|
63
|
-
|
|
78
|
+
let connecting = connections.get(bridge);
|
|
79
|
+
if (!connecting) {
|
|
80
|
+
connecting = bridge.connect();
|
|
81
|
+
connections.set(bridge, connecting);
|
|
82
|
+
}
|
|
83
|
+
connecting
|
|
64
84
|
.then(() => {
|
|
65
85
|
if (!cancelled) {
|
|
66
86
|
setReadyConfigKey(configKey);
|
|
@@ -75,17 +95,28 @@ export function AppSdkProvider({ children, ...config }) {
|
|
|
75
95
|
});
|
|
76
96
|
return () => {
|
|
77
97
|
cancelled = true;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
bridgeRef.current
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
98
|
+
const destroy = () => {
|
|
99
|
+
bridge.destroy();
|
|
100
|
+
if (bridgeRef.current === bridge) {
|
|
101
|
+
bridgeRef.current = null;
|
|
102
|
+
bridgeConfigKeyRef.current = null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
pendingDestroyRef.current = {
|
|
106
|
+
bridge,
|
|
107
|
+
configKey,
|
|
108
|
+
channel,
|
|
109
|
+
destroy,
|
|
110
|
+
timer: setTimeout(() => {
|
|
111
|
+
pendingDestroyRef.current = null;
|
|
112
|
+
destroy();
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
85
115
|
};
|
|
86
|
-
}, [appId, configKey, connectTimeout, debug, targetOrigin]);
|
|
116
|
+
}, [appId, channel, configKey, connectTimeout, debug, targetOrigin]);
|
|
87
117
|
useEffect(() => {
|
|
88
|
-
if (
|
|
118
|
+
if (!applyOsConfig ||
|
|
119
|
+
readyConfigKey !== configKey ||
|
|
89
120
|
bridgeConfigKeyRef.current !== configKey ||
|
|
90
121
|
!bridgeRef.current) {
|
|
91
122
|
return;
|
|
@@ -95,7 +126,7 @@ export function AppSdkProvider({ children, ...config }) {
|
|
|
95
126
|
applyOsConfigToDocument(newConfig);
|
|
96
127
|
});
|
|
97
128
|
return unsubscribe;
|
|
98
|
-
}, [configKey, readyConfigKey]);
|
|
129
|
+
}, [applyOsConfig, configKey, readyConfigKey]);
|
|
99
130
|
if (errorState?.configKey === configKey) {
|
|
100
131
|
return _jsx("div", { "data-sdk-error": true, children: errorState.message });
|
|
101
132
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -168,9 +168,18 @@ export interface SdkIdbQueryPayload {
|
|
|
168
168
|
prefix?: string;
|
|
169
169
|
limit?: number;
|
|
170
170
|
}
|
|
171
|
+
/** How bridge messages travel between an app and its host. */
|
|
172
|
+
export interface SdkChannel {
|
|
173
|
+
/** Delivers one message to the host. */
|
|
174
|
+
post(message: SdkMessage | SdkResponse): void;
|
|
175
|
+
/** Hands every message from the host to `receive`; returns the unsubscribe. */
|
|
176
|
+
subscribe(receive: (message: SdkResponse) => void): () => void;
|
|
177
|
+
}
|
|
171
178
|
/** Configuration passed to {@link createHostBridge}. */
|
|
172
179
|
export interface HostBridgeConfig {
|
|
173
180
|
appId?: string;
|
|
181
|
+
/** Message transport. Default: `postMessage` with the parent window. */
|
|
182
|
+
channel?: SdkChannel;
|
|
174
183
|
/** Override the host origin for `postMessage`. Inferred from `document.referrer` by default. */
|
|
175
184
|
targetOrigin?: string;
|
|
176
185
|
/** Handshake timeout in ms. Default: `5000`. */
|