@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.
@@ -1,3 +1,3 @@
1
1
  import type { HostBridge, HostBridgeConfig } from './types.js';
2
- /** Create a bridge between an iframe app and its host OS via `postMessage`. */
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;
@@ -13,29 +13,49 @@ function assertStoreName(storeName) {
13
13
  }
14
14
  return normalized;
15
15
  }
16
- /** Create a bridge between an iframe app and its host OS via `postMessage`. */
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
- log('createHostBridge', { appId: hintAppId, targetOrigin, isInsideIframe });
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 onMessage(event) {
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
- hostWindow.addEventListener('message', onMessage);
146
+ const unsubscribe = channel.subscribe(receive);
135
147
  function postToHost(message) {
136
- hostWindow.parent.postMessage(message, targetOrigin);
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
- hostWindow.removeEventListener('message', onMessage);
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
- setReadyConfigKey(null);
58
- setErrorState(null);
59
- const bridge = createHostBridge(bridgeConfig);
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
- .connect()
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
- bridge.destroy();
79
- if (bridgeRef.current === bridge) {
80
- bridgeRef.current = null;
81
- }
82
- if (bridgeConfigKeyRef.current === configKey) {
83
- bridgeConfigKeyRef.current = null;
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 (readyConfigKey !== configKey ||
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`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bardioc/app-sdk",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "description": "SDK for building apps for the Bardioc OS",
5
5
  "license": "MIT",
6
6
  "private": false,