@dp-bohrium/widget-sdk 0.0.1
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 +21 -0
- package/README.md +201 -0
- package/dist/compatibility/testing.d.ts +3 -0
- package/dist/compatibility/testing.js +129 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/protocol-endpoint-CHWoXvKN.js +214 -0
- package/dist/runtime/bootstrap.d.ts +95 -0
- package/dist/runtime/client.d.ts +82 -0
- package/dist/runtime/event-validation.d.ts +3 -0
- package/dist/runtime/events.d.ts +45 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/mock-host.d.ts +45 -0
- package/dist/runtime/network.d.ts +18 -0
- package/dist/runtime/presentation-keyboard.d.ts +29 -0
- package/dist/runtime/protocol-endpoint.d.ts +30 -0
- package/dist/runtime/transport.d.ts +43 -0
- package/dist/runtime-D-xKLIEj.js +1121 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DP Technology
|
|
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,201 @@
|
|
|
1
|
+
# @dp-bohrium/widget-sdk
|
|
2
|
+
|
|
3
|
+
Widget developer SDK. Use `createWidgetClient` inside a production Widget
|
|
4
|
+
Bundle. It announces the bundle to the Host, accepts the transferred
|
|
5
|
+
`MessagePort`, and exposes the shared lifecycle and capability protocol
|
|
6
|
+
without requiring Widget code to handle `window.postMessage` directly.
|
|
7
|
+
|
|
8
|
+
Install the exact prerelease from the internal registry:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @dp-bohrium/widget-sdk@0.0.1 \
|
|
12
|
+
--registry=https://registry.npmjs.org
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Host Runtime and Shrimp should keep consuming `@dp-bohrium/widget-contracts`.
|
|
16
|
+
They must not install this package.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createWidgetClient } from "@dp-bohrium/widget-sdk";
|
|
20
|
+
|
|
21
|
+
const widget = createWidgetClient({
|
|
22
|
+
widgetSdkVersion: "0.0.1",
|
|
23
|
+
async onInit(payload) {
|
|
24
|
+
// Initialize using the Host-provided locale, theme and capabilities.
|
|
25
|
+
console.log(payload.locale);
|
|
26
|
+
await Promise.resolve(); // Load application state here.
|
|
27
|
+
},
|
|
28
|
+
onRender(request) {
|
|
29
|
+
// Render the complete snapshot for the selected slot.
|
|
30
|
+
console.log(request.slot);
|
|
31
|
+
},
|
|
32
|
+
onDispose({ reason, deadlineMs }) {
|
|
33
|
+
// Release local timers, subscriptions and workers.
|
|
34
|
+
console.log(reason, deadlineMs);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await widget.ready();
|
|
39
|
+
widget.resize({ width: 600, height: 400 });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Host-managed network access
|
|
43
|
+
|
|
44
|
+
Declare `requestedCapabilities.network.fetch` in the Widget Manifest, then use the typed facade after
|
|
45
|
+
`ready()` resolves. The Widget never receives Host credentials and does not call browser `fetch` directly:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const result = await widget.network.fetch(
|
|
50
|
+
{
|
|
51
|
+
url: "https://api.example.org/papers",
|
|
52
|
+
auth: "optional",
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers: { accept: "application/json" },
|
|
55
|
+
},
|
|
56
|
+
{ signal: controller.signal, timeoutMs: 15_000 },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (result.ok) {
|
|
60
|
+
consume(result.data.status, result.data.headers, result.data.body);
|
|
61
|
+
} else {
|
|
62
|
+
showNetworkFailure(result.reason, result.retryable);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The Host decides whether to prompt, remember an exact Origin grant, select an opaque credential binding,
|
|
67
|
+
follow a redirect, or deny the request. Calls before readiness, after disposal, or without a current
|
|
68
|
+
`network.fetch` grant reject locally. Abort and timeout reuse the existing MessageChannel capability lifecycle.
|
|
69
|
+
`requestCapability` remains available for protocol-level integrations, but `widget.network.fetch` is the
|
|
70
|
+
supported network API for Widget application code.
|
|
71
|
+
|
|
72
|
+
## Host-governed panel presentation
|
|
73
|
+
|
|
74
|
+
Panel Widgets can ask the Host to present their current panel in a workspace,
|
|
75
|
+
floating, or overlay surface. The same methods are available on the ordinary
|
|
76
|
+
`createWidgetClient()` result and the low-level `WidgetClient`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { PRESENTATION_CONFIRMATION_TIMEOUT_MS } from "@dp-bohrium/widget-sdk";
|
|
80
|
+
|
|
81
|
+
const presentationAbort = new AbortController();
|
|
82
|
+
const result = await widget.present("floating", {
|
|
83
|
+
timeoutMs: PRESENTATION_CONFIRMATION_TIMEOUT_MS,
|
|
84
|
+
signal: presentationAbort.signal,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
showPresentationDenial(result.reason, result.message);
|
|
89
|
+
} else if ("surface" in result.data) {
|
|
90
|
+
// The Host can report a different actual surface, such as a compact-mode
|
|
91
|
+
// downgrade from floating to overlay or workspace.
|
|
92
|
+
showCurrentSurface(result.data.surface);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await widget.changePresentationSurface("overlay");
|
|
96
|
+
await widget.focusPresentationBoundary("forward");
|
|
97
|
+
await widget.closePresentation();
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
When the Host grants `ui.presentation` with the `overlay` surface, the ordinary
|
|
101
|
+
client also installs keyboard cooperation for cross-origin Panel overlays. An
|
|
102
|
+
unhandled, trusted, non-composing Escape requests `closePresentation()`. A Tab
|
|
103
|
+
that is followed by focus leaving the iframe requests
|
|
104
|
+
`focusPresentationBoundary("forward" | "backward")`; internal focus movement
|
|
105
|
+
does not send anything. Frameworks with a custom focus manager can call the
|
|
106
|
+
typed helper directly.
|
|
107
|
+
|
|
108
|
+
The default confirmation timeout is 60 seconds. `timeoutMs` overrides it and
|
|
109
|
+
`signal` cancels local waiting. A Host policy or user denial resolves normally
|
|
110
|
+
as `{ ok: false, reason, message, retryable }`; client timeout, abort, dispose,
|
|
111
|
+
or a malformed successful response rejects the Promise. Browser resize,
|
|
112
|
+
visibility, focus, and pointer events are never treated as confirmation.
|
|
113
|
+
|
|
114
|
+
These methods only request a presentation. The Host remains the final authority
|
|
115
|
+
for grants, supported surfaces, confirmation, compact-mode downgrade, and the
|
|
116
|
+
actual panel state. `isTrusted` and `defaultPrevented` are Widget-realm noise
|
|
117
|
+
filters, not authorization: the MessageChannel identifies the current iframe
|
|
118
|
+
instance but cannot prove a physical key or human activation. The Widget cannot
|
|
119
|
+
name a target Panel, Host selector, identity, runtime generation, coordinate, or
|
|
120
|
+
layer. The Host must recheck the current instance, top Overlay, focus epoch, and
|
|
121
|
+
higher security layers before accepting either request.
|
|
122
|
+
|
|
123
|
+
`ready()` is not sent to the Host until `onInit` returns or its Promise
|
|
124
|
+
settles. If initialization fails, the SDK reports a structured `onError`
|
|
125
|
+
event and then invokes `onDispose` so local resources can be released.
|
|
126
|
+
`handshakeTimeoutMs` also bounds this initialization step after the Host
|
|
127
|
+
connection has been established.
|
|
128
|
+
|
|
129
|
+
When only `onUpdate` is supplied, the first complete `render` snapshot is also
|
|
130
|
+
delivered to `onUpdate`.
|
|
131
|
+
|
|
132
|
+
`WidgetClient` provides the framework-neutral, low-level runtime interface.
|
|
133
|
+
Capability requests accept `{ timeoutMs, signal }`, so application lifecycle
|
|
134
|
+
hooks can cancel pending reads without receiving late results. It is available
|
|
135
|
+
from `@dp-bohrium/widget-sdk/runtime` for advanced integrations and protocol-level
|
|
136
|
+
tests.
|
|
137
|
+
|
|
138
|
+
The main entry exposes the production `createWidgetClient` factory. Low-level
|
|
139
|
+
protocol classes and bootstrap constants are available from
|
|
140
|
+
`@dp-bohrium/widget-sdk/runtime`; `MessagePortTransport` and the bootstrap
|
|
141
|
+
messages are not part of the ordinary Widget API.
|
|
142
|
+
|
|
143
|
+
`@dp-bohrium/widget-sdk/testing` provides `MemoryTransport` and
|
|
144
|
+
`MockWidgetHost`. The Mock Host owns init/ready sequencing, first-render versus
|
|
145
|
+
update selection, resize events, capability adapter responses, and terminal
|
|
146
|
+
dispose behavior. It uses the same public contracts as production Hosts but
|
|
147
|
+
does not claim production iframe, CSP, authorization, or resource-isolation
|
|
148
|
+
guarantees. Contract validators stay in the unpublished
|
|
149
|
+
`@dp-bohrium/widget-contract-testing` workspace package.
|
|
150
|
+
|
|
151
|
+
Set `capabilityResult` for one deterministic default allow or denial. A supplied
|
|
152
|
+
`handleCapability` remains the authoritative advanced handler when both are
|
|
153
|
+
present. `host.capabilityRequests` returns an ordered defensive-copy snapshot,
|
|
154
|
+
so tests can inspect requests without mutating Mock Host state.
|
|
155
|
+
|
|
156
|
+
or fallback UI. If the Host iframe uses `sandbox="allow-scripts"` without
|
|
157
|
+
`allow-same-origin`, load the bundled Widget as a classic script or IIFE
|
|
158
|
+
unless the isolated origin serves CORS headers for `type="module"`.
|
|
159
|
+
|
|
160
|
+
If the Host sends a malformed Bridge envelope or payload, the transport reports
|
|
161
|
+
a structured `MESSAGE_INVALID` error and closes the connection. The Widget is
|
|
162
|
+
not left waiting on a connection that the Host still considers active.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## 受授权的事件订阅与工具 Hook(本地候选)
|
|
166
|
+
|
|
167
|
+
新增 `subscribeEvents`、`onTool` 和 `invokeAction`。Manifest 声明与 Host 实际 grant 必须同时包含 topic/action,旧 Host 不会自动获得这些能力;发布时需一起升级 contracts 和消费者。订阅类型从包根与 `/runtime` 导出。
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const events = client.subscribeEvents({
|
|
171
|
+
topic: "jobs.progress",
|
|
172
|
+
onBatch(batch, { signal }) {
|
|
173
|
+
if (signal.aborted) return;
|
|
174
|
+
// 只应用本批真实事件;batch.reset 时替换旧状态。
|
|
175
|
+
renderProgress(batch.events);
|
|
176
|
+
},
|
|
177
|
+
onError: showSubscriptionError,
|
|
178
|
+
});
|
|
179
|
+
const hooks = client.onTool({
|
|
180
|
+
onToolCall: showStarted,
|
|
181
|
+
onToolResult: showCompleted,
|
|
182
|
+
onToolError: showFailed,
|
|
183
|
+
onToolCancelled: showCancelled,
|
|
184
|
+
}, { onError: showSubscriptionError });
|
|
185
|
+
const result = await client.invokeAction({
|
|
186
|
+
action: "tools.inspect",
|
|
187
|
+
idempotencyKey: crypto.randomUUID(),
|
|
188
|
+
arguments: { toolCallId },
|
|
189
|
+
});
|
|
190
|
+
const resumeCursor = events.cursor;
|
|
191
|
+
events.unsubscribe();
|
|
192
|
+
hooks.unsubscribe();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Hook 观察已经发生的服务端工具事实,不是执行前拦截器;不允许通过 iframe 回调改写参数、审批或控制 durable 执行。`tools.inspect` 是当前 Shrimp 的只读动作 provider,返回工具身份和最新可读取状态;其他 action 由 Host/provider 显式实现并授权。SDK 不自动重试动作,也不把回调完成解释为工具执行完成。
|
|
196
|
+
|
|
197
|
+
所有订阅共享实例的 events 请求预算,并各自遵守 topic 最小间隔。游标仅在 `onBatch` 成功后提交;消费者异常、权限撤销和不可恢复的历史错误会关闭订阅并调用 `onError`。`unsubscribe` 和 Host dispose 释放订阅、排队请求和本地在途等待,忽略迟到结果;异步回调需检查传入 signal 后再更新自己的状态。`closed` 表示订阅已关闭,并不等待用户未完成的回调结束。重新订阅时显式传入已保存 cursor。
|
|
198
|
+
|
|
199
|
+
维护者可复验包在仓库 `examples/forge/tool-observer`,不包含在开发者 npm 包中。
|
|
200
|
+
|
|
201
|
+
每次 events.read 是有界、非阻塞读取;当前 V1 没有单请求取消 wire 消息,unsubscribe 会停止后续轮询并取消本地等待,已经发出的服务端读取可能完成但其结果被忽略。Host 实例卸载另通过既有 lease/AbortSignal 释放在途宿主请求。
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { n as MemoryTransport, t as ProtocolEndpoint } from "../protocol-endpoint-CHWoXvKN.js";
|
|
2
|
+
//#region src/runtime/mock-host.ts
|
|
3
|
+
const widgetMessageTypes = /* @__PURE__ */ new Set([
|
|
4
|
+
"ready",
|
|
5
|
+
"resize",
|
|
6
|
+
"capability-request",
|
|
7
|
+
"error"
|
|
8
|
+
]);
|
|
9
|
+
function isWidgetToHostMessage(message) {
|
|
10
|
+
return widgetMessageTypes.has(message.type);
|
|
11
|
+
}
|
|
12
|
+
var MockWidgetHost = class {
|
|
13
|
+
endpoint;
|
|
14
|
+
init;
|
|
15
|
+
events;
|
|
16
|
+
handleCapability;
|
|
17
|
+
capabilityResult;
|
|
18
|
+
receivedCapabilityRequests = [];
|
|
19
|
+
state = "created";
|
|
20
|
+
rendered = false;
|
|
21
|
+
readyPromise;
|
|
22
|
+
resolveReady;
|
|
23
|
+
rejectReady;
|
|
24
|
+
constructor(options, events = {}) {
|
|
25
|
+
this.init = options.init;
|
|
26
|
+
this.events = events;
|
|
27
|
+
this.handleCapability = options.handleCapability;
|
|
28
|
+
this.capabilityResult = options.capabilityResult ?? {
|
|
29
|
+
ok: false,
|
|
30
|
+
reason: "unsupported",
|
|
31
|
+
message: "mock host capability is not configured",
|
|
32
|
+
retryable: false
|
|
33
|
+
};
|
|
34
|
+
this.endpoint = new ProtocolEndpoint({
|
|
35
|
+
transport: options.transport,
|
|
36
|
+
channelId: options.channelId,
|
|
37
|
+
createMessageId: options.createMessageId ?? (() => crypto.randomUUID()),
|
|
38
|
+
acceptsInbound: isWidgetToHostMessage,
|
|
39
|
+
onMessage: (message) => this.receive(message)
|
|
40
|
+
});
|
|
41
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
42
|
+
this.resolveReady = resolve;
|
|
43
|
+
this.rejectReady = reject;
|
|
44
|
+
});
|
|
45
|
+
this.readyPromise.catch(() => void 0);
|
|
46
|
+
}
|
|
47
|
+
get currentState() {
|
|
48
|
+
return this.state;
|
|
49
|
+
}
|
|
50
|
+
get capabilityRequests() {
|
|
51
|
+
return structuredClone(this.receivedCapabilityRequests);
|
|
52
|
+
}
|
|
53
|
+
ready() {
|
|
54
|
+
return this.readyPromise;
|
|
55
|
+
}
|
|
56
|
+
start() {
|
|
57
|
+
if (this.state !== "created") return;
|
|
58
|
+
this.state = "waiting-ready";
|
|
59
|
+
this.endpoint.send({
|
|
60
|
+
type: "init",
|
|
61
|
+
payload: this.init
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
render(request) {
|
|
65
|
+
this.assertReady();
|
|
66
|
+
this.endpoint.send({
|
|
67
|
+
type: this.rendered ? "update" : "render",
|
|
68
|
+
payload: request
|
|
69
|
+
});
|
|
70
|
+
this.rendered = true;
|
|
71
|
+
}
|
|
72
|
+
dispose(reason = "mock-host-disposed") {
|
|
73
|
+
if (this.state === "disposed") return;
|
|
74
|
+
const disposedBeforeReady = this.state !== "ready";
|
|
75
|
+
this.endpoint.send({
|
|
76
|
+
type: "dispose",
|
|
77
|
+
payload: {
|
|
78
|
+
reason,
|
|
79
|
+
deadlineMs: this.init.resourceBudget.timeouts.disposeGraceMs
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
this.state = "disposed";
|
|
83
|
+
if (disposedBeforeReady) this.rejectReady(/* @__PURE__ */ new Error(`mock host disposed before ready: ${reason}`));
|
|
84
|
+
this.endpoint.close();
|
|
85
|
+
}
|
|
86
|
+
receive(message) {
|
|
87
|
+
if (this.state === "disposed") return;
|
|
88
|
+
if (message.type === "ready") {
|
|
89
|
+
if (this.state !== "waiting-ready") return;
|
|
90
|
+
this.state = "ready";
|
|
91
|
+
this.resolveReady();
|
|
92
|
+
this.events.onReady?.(message.payload);
|
|
93
|
+
} else if (message.type === "resize") {
|
|
94
|
+
if (this.state !== "ready") return;
|
|
95
|
+
this.events.onResize?.(message.payload);
|
|
96
|
+
} else if (message.type === "capability-request") {
|
|
97
|
+
if (this.state !== "ready") return;
|
|
98
|
+
this.resolveCapability(message.payload.requestId, message.payload.request);
|
|
99
|
+
} else if (message.type === "error") this.events.onError?.(message.payload);
|
|
100
|
+
}
|
|
101
|
+
async resolveCapability(requestId, request) {
|
|
102
|
+
this.receivedCapabilityRequests.push(structuredClone(request));
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = this.handleCapability ? await this.handleCapability(request) : structuredClone(this.capabilityResult);
|
|
106
|
+
} catch {
|
|
107
|
+
result = {
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: "internal-error",
|
|
110
|
+
message: "mock capability handler failed",
|
|
111
|
+
retryable: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (this.state !== "ready") return;
|
|
115
|
+
this.endpoint.send({
|
|
116
|
+
type: "capability-result",
|
|
117
|
+
payload: {
|
|
118
|
+
requestId,
|
|
119
|
+
result
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
assertReady() {
|
|
124
|
+
if (this.state === "disposed") throw new Error("mock host is disposed");
|
|
125
|
+
if (this.state !== "ready") throw new Error("mock host is not ready");
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
export { MemoryTransport, MockWidgetHost };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export type { CreateWidgetClientOptions, PresentationFocusDirection, PresentationSurface, SubscribeWidgetEventsOptions, WidgetCapabilityRequestOptions, WidgetClientEvents, WidgetClientFactoryOptions, WidgetClientOptions, WidgetClientState, WidgetDisposeEvent, WidgetEventBatchContext, WidgetEventSubscription, WidgetHostConnectMessage, WidgetMessagePort, WidgetNetworkClient, WidgetNetworkFetchResult, WidgetRuntimeWindow, WidgetSize, WidgetToolHooks, WidgetTransport, } from "./runtime/index.js";
|
|
2
|
+
export { createWidgetClient, PRESENTATION_CONFIRMATION_TIMEOUT_MS, } from "./runtime/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
//#region src/runtime/transport.ts
|
|
2
|
+
const protocolMessageTypes = /* @__PURE__ */ new Set([
|
|
3
|
+
"init",
|
|
4
|
+
"render",
|
|
5
|
+
"update",
|
|
6
|
+
"capability-result",
|
|
7
|
+
"error",
|
|
8
|
+
"dispose",
|
|
9
|
+
"ready",
|
|
10
|
+
"resize",
|
|
11
|
+
"capability-request"
|
|
12
|
+
]);
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function property(value, key) {
|
|
17
|
+
return value[key];
|
|
18
|
+
}
|
|
19
|
+
function hasRequiredKeys(value, keys) {
|
|
20
|
+
return keys.every((key) => Object.hasOwn(value, key));
|
|
21
|
+
}
|
|
22
|
+
function isFiniteNonNegative(value) {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
24
|
+
}
|
|
25
|
+
function isProtocolPayload(type, payload) {
|
|
26
|
+
switch (type) {
|
|
27
|
+
case "init": return hasRequiredKeys(payload, [
|
|
28
|
+
"instanceId",
|
|
29
|
+
"locale",
|
|
30
|
+
"theme",
|
|
31
|
+
"capabilities",
|
|
32
|
+
"resourceBudget"
|
|
33
|
+
]) && typeof property(payload, "instanceId") === "string" && typeof property(payload, "locale") === "string" && isRecord(property(payload, "theme")) && Array.isArray(property(payload, "capabilities")) && isRecord(property(payload, "resourceBudget"));
|
|
34
|
+
case "render":
|
|
35
|
+
case "update": {
|
|
36
|
+
const revision = property(payload, "revision");
|
|
37
|
+
const context = property(payload, "context");
|
|
38
|
+
const content = property(payload, "content");
|
|
39
|
+
return property(payload, "schemaVersion") === 1 && typeof property(payload, "requestId") === "string" && typeof property(payload, "instanceId") === "string" && typeof revision === "number" && Number.isSafeInteger(revision) && revision >= 1 && typeof property(payload, "slot") === "string" && typeof property(payload, "state") === "string" && isRecord(context) && isRecord(content);
|
|
40
|
+
}
|
|
41
|
+
case "capability-result": return typeof property(payload, "requestId") === "string" && isRecord(property(payload, "result"));
|
|
42
|
+
case "error": return typeof property(payload, "code") === "string" && typeof property(payload, "phase") === "string" && typeof property(payload, "message") === "string" && typeof property(payload, "retryable") === "boolean" && typeof property(payload, "correlationId") === "string";
|
|
43
|
+
case "dispose": return typeof property(payload, "reason") === "string" && isFiniteNonNegative(property(payload, "deadlineMs"));
|
|
44
|
+
case "ready": return typeof property(payload, "widgetSdkVersion") === "string" && property(payload, "hostApiVersion") === 1;
|
|
45
|
+
case "resize": return isFiniteNonNegative(property(payload, "width")) && isFiniteNonNegative(property(payload, "height"));
|
|
46
|
+
case "capability-request": return typeof property(payload, "requestId") === "string" && isRecord(property(payload, "request"));
|
|
47
|
+
default: return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Performs the inexpensive checks that are possible at the transport edge.
|
|
52
|
+
* Payload-specific validation stays with the contract/conformance layer.
|
|
53
|
+
*/
|
|
54
|
+
function isWidgetProtocolMessage(value) {
|
|
55
|
+
if (!isRecord(value)) return false;
|
|
56
|
+
const keys = Object.keys(value);
|
|
57
|
+
const expectedKeys = [
|
|
58
|
+
"protocolVersion",
|
|
59
|
+
"channelId",
|
|
60
|
+
"messageId",
|
|
61
|
+
"sequence",
|
|
62
|
+
"type",
|
|
63
|
+
"payload"
|
|
64
|
+
];
|
|
65
|
+
if (keys.length !== expectedKeys.length || expectedKeys.some((key) => !keys.includes(key))) return false;
|
|
66
|
+
const protocolVersion = property(value, "protocolVersion");
|
|
67
|
+
const channelId = property(value, "channelId");
|
|
68
|
+
const messageId = property(value, "messageId");
|
|
69
|
+
const sequence = property(value, "sequence");
|
|
70
|
+
const type = property(value, "type");
|
|
71
|
+
const payload = property(value, "payload");
|
|
72
|
+
return protocolVersion === 1 && typeof channelId === "string" && channelId.length >= 16 && typeof messageId === "string" && messageId.length > 0 && typeof sequence === "number" && Number.isSafeInteger(sequence) && sequence > 0 && typeof type === "string" && protocolMessageTypes.has(type) && isRecord(payload) && isProtocolPayload(type, payload);
|
|
73
|
+
}
|
|
74
|
+
var MemoryTransport = class MemoryTransport {
|
|
75
|
+
peer;
|
|
76
|
+
listeners = /* @__PURE__ */ new Set();
|
|
77
|
+
closed = false;
|
|
78
|
+
static pair() {
|
|
79
|
+
const first = new MemoryTransport();
|
|
80
|
+
const second = new MemoryTransport();
|
|
81
|
+
first.peer = second;
|
|
82
|
+
second.peer = first;
|
|
83
|
+
return [first, second];
|
|
84
|
+
}
|
|
85
|
+
send(message) {
|
|
86
|
+
if (this.closed) throw new Error("transport is closed");
|
|
87
|
+
if (this.peer?.closed) return;
|
|
88
|
+
for (const listener of this.peer?.listeners ?? []) listener(message);
|
|
89
|
+
}
|
|
90
|
+
onMessage(listener) {
|
|
91
|
+
if (this.closed) return () => void 0;
|
|
92
|
+
this.listeners.add(listener);
|
|
93
|
+
return () => this.listeners.delete(listener);
|
|
94
|
+
}
|
|
95
|
+
onError() {
|
|
96
|
+
return () => void 0;
|
|
97
|
+
}
|
|
98
|
+
close() {
|
|
99
|
+
this.closed = true;
|
|
100
|
+
this.listeners.clear();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
/** Adapts a browser MessagePort to the SDK's contract-message transport. */
|
|
104
|
+
var MessagePortTransport = class {
|
|
105
|
+
port;
|
|
106
|
+
listeners = /* @__PURE__ */ new Set();
|
|
107
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
108
|
+
closed = false;
|
|
109
|
+
constructor(port) {
|
|
110
|
+
this.port = port;
|
|
111
|
+
this.port.onmessage = (event) => {
|
|
112
|
+
if (this.closed) return;
|
|
113
|
+
const message = event.data;
|
|
114
|
+
if (!isWidgetProtocolMessage(message)) {
|
|
115
|
+
this.reportError(/* @__PURE__ */ new Error("invalid widget protocol message"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const listener of this.listeners) listener(message);
|
|
119
|
+
};
|
|
120
|
+
this.port.onmessageerror = () => {
|
|
121
|
+
this.reportError(/* @__PURE__ */ new Error("MessagePort reported a message error"));
|
|
122
|
+
};
|
|
123
|
+
this.port.start?.();
|
|
124
|
+
}
|
|
125
|
+
send(message) {
|
|
126
|
+
if (this.closed) throw new Error("transport is closed");
|
|
127
|
+
this.port.postMessage(message);
|
|
128
|
+
}
|
|
129
|
+
onMessage(listener) {
|
|
130
|
+
if (this.closed) return () => void 0;
|
|
131
|
+
this.listeners.add(listener);
|
|
132
|
+
return () => this.listeners.delete(listener);
|
|
133
|
+
}
|
|
134
|
+
onError(listener) {
|
|
135
|
+
if (this.closed) return () => void 0;
|
|
136
|
+
this.errorListeners.add(listener);
|
|
137
|
+
return () => this.errorListeners.delete(listener);
|
|
138
|
+
}
|
|
139
|
+
reportError(error) {
|
|
140
|
+
if (this.closed) return;
|
|
141
|
+
for (const listener of [...this.errorListeners]) try {
|
|
142
|
+
listener(error);
|
|
143
|
+
} catch {}
|
|
144
|
+
this.close();
|
|
145
|
+
}
|
|
146
|
+
close() {
|
|
147
|
+
if (this.closed) return;
|
|
148
|
+
this.closed = true;
|
|
149
|
+
this.listeners.clear();
|
|
150
|
+
this.errorListeners.clear();
|
|
151
|
+
this.port.onmessage = null;
|
|
152
|
+
if (this.port.onmessageerror !== void 0) this.port.onmessageerror = null;
|
|
153
|
+
this.port.close();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/runtime/protocol-endpoint.ts
|
|
158
|
+
const inboundMessageWindowSize = 256;
|
|
159
|
+
var ProtocolEndpoint = class {
|
|
160
|
+
transport;
|
|
161
|
+
channelId;
|
|
162
|
+
createMessageId;
|
|
163
|
+
acceptsInbound;
|
|
164
|
+
unsubscribe;
|
|
165
|
+
unsubscribeError;
|
|
166
|
+
sequence = 0;
|
|
167
|
+
inboundSequence = 0;
|
|
168
|
+
inboundMessageIds = /* @__PURE__ */ new Set();
|
|
169
|
+
closed = false;
|
|
170
|
+
constructor(options) {
|
|
171
|
+
this.transport = options.transport;
|
|
172
|
+
this.channelId = options.channelId;
|
|
173
|
+
this.createMessageId = options.createMessageId;
|
|
174
|
+
this.acceptsInbound = options.acceptsInbound;
|
|
175
|
+
this.unsubscribe = this.transport.onMessage((message) => {
|
|
176
|
+
if (!isWidgetProtocolMessage(message)) {
|
|
177
|
+
options.onError?.(/* @__PURE__ */ new Error("invalid widget protocol message"));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!this.accepts(message)) return;
|
|
181
|
+
options.onMessage(message);
|
|
182
|
+
});
|
|
183
|
+
this.unsubscribeError = this.transport.onError?.((error) => options.onError?.(error)) ?? (() => void 0);
|
|
184
|
+
}
|
|
185
|
+
send(message) {
|
|
186
|
+
if (this.closed) throw new Error("protocol endpoint is closed");
|
|
187
|
+
this.transport.send({
|
|
188
|
+
...message,
|
|
189
|
+
protocolVersion: 1,
|
|
190
|
+
channelId: this.channelId,
|
|
191
|
+
messageId: this.createMessageId(),
|
|
192
|
+
sequence: ++this.sequence
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
close() {
|
|
196
|
+
if (this.closed) return;
|
|
197
|
+
this.closed = true;
|
|
198
|
+
this.unsubscribe();
|
|
199
|
+
this.unsubscribeError();
|
|
200
|
+
this.transport.close();
|
|
201
|
+
}
|
|
202
|
+
accepts(message) {
|
|
203
|
+
if (this.closed || message.protocolVersion !== 1 || message.channelId !== this.channelId || !this.acceptsInbound(message) || this.inboundMessageIds.has(message.messageId) || message.sequence <= this.inboundSequence) return false;
|
|
204
|
+
this.inboundSequence = message.sequence;
|
|
205
|
+
this.inboundMessageIds.add(message.messageId);
|
|
206
|
+
if (this.inboundMessageIds.size > inboundMessageWindowSize) {
|
|
207
|
+
const oldestMessageId = this.inboundMessageIds.values().next().value;
|
|
208
|
+
if (oldestMessageId) this.inboundMessageIds.delete(oldestMessageId);
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
//#endregion
|
|
214
|
+
export { isWidgetProtocolMessage as i, MemoryTransport as n, MessagePortTransport as r, ProtocolEndpoint as t };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CapabilityRequest, CapabilityResult, InitPayload, PresentationCapabilityResult, RenderRequest, WidgetError } from "@dp-bohrium/widget-contracts";
|
|
2
|
+
import { type PresentationFocusDirection, type PresentationSurface, type WidgetCapabilityRequestOptions, type WidgetClientEvents, type WidgetClientState } from "./client.js";
|
|
3
|
+
import { type SubscribeWidgetEventsOptions, type WidgetEventSubscription, type WidgetToolHooks } from "./events.js";
|
|
4
|
+
import type { WidgetNetworkClient } from "./network.js";
|
|
5
|
+
export declare const WIDGET_BOOTSTRAP_READY: "widget-bootstrap-ready";
|
|
6
|
+
export declare const WIDGET_HOST_CONNECT: "widget-host-connect";
|
|
7
|
+
export interface WidgetHostConnectMessage {
|
|
8
|
+
type: typeof WIDGET_HOST_CONNECT;
|
|
9
|
+
protocolVersion: 1;
|
|
10
|
+
channelId: string;
|
|
11
|
+
initPayload: InitPayload;
|
|
12
|
+
}
|
|
13
|
+
export interface WidgetRuntimeWindow {
|
|
14
|
+
readonly parent: {
|
|
15
|
+
postMessage(message: unknown, targetOrigin: string): void;
|
|
16
|
+
};
|
|
17
|
+
addEventListener(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
|
|
18
|
+
removeEventListener(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
|
|
19
|
+
}
|
|
20
|
+
export interface WidgetDisposeEvent {
|
|
21
|
+
reason: string;
|
|
22
|
+
deadlineMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface CreateWidgetClientOptions {
|
|
25
|
+
widgetSdkVersion: string;
|
|
26
|
+
hostApiVersion?: 1;
|
|
27
|
+
handshakeTimeoutMs?: number;
|
|
28
|
+
capabilityTimeoutMs?: number;
|
|
29
|
+
connectionTimeoutMs?: number;
|
|
30
|
+
createMessageId?: () => string;
|
|
31
|
+
expectedHostOrigin?: string;
|
|
32
|
+
window?: WidgetRuntimeWindow;
|
|
33
|
+
events?: WidgetClientEvents;
|
|
34
|
+
onInit?: (payload: InitPayload) => void | PromiseLike<void>;
|
|
35
|
+
onRender?: (request: RenderRequest) => void;
|
|
36
|
+
onUpdate?: (request: RenderRequest) => void;
|
|
37
|
+
onError?: (error: WidgetError) => void;
|
|
38
|
+
onDispose?: (event: WidgetDisposeEvent) => void;
|
|
39
|
+
}
|
|
40
|
+
export type WidgetClientFactoryOptions = CreateWidgetClientOptions;
|
|
41
|
+
export interface WidgetSize {
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function isWidgetHostConnectMessage(value: unknown): value is WidgetHostConnectMessage;
|
|
46
|
+
/**
|
|
47
|
+
* High-level Widget-side runtime. It hides bootstrap postMessage and the
|
|
48
|
+
* transferred MessagePort from Widget application code.
|
|
49
|
+
*/
|
|
50
|
+
export declare class WidgetRuntimeClient {
|
|
51
|
+
readonly network: WidgetNetworkClient;
|
|
52
|
+
private readonly runtimeWindow;
|
|
53
|
+
private readonly options;
|
|
54
|
+
private readonly events;
|
|
55
|
+
private readonly messageListener;
|
|
56
|
+
private readonly readyPromise;
|
|
57
|
+
private resolveReady;
|
|
58
|
+
private rejectReady;
|
|
59
|
+
private readySettled;
|
|
60
|
+
private connectionTimer?;
|
|
61
|
+
private client?;
|
|
62
|
+
private state;
|
|
63
|
+
private disposed;
|
|
64
|
+
private capabilities;
|
|
65
|
+
private overlayPresentationGranted;
|
|
66
|
+
private readonly subscriptions;
|
|
67
|
+
private readonly eventReads;
|
|
68
|
+
private readonly eventReadsLifetime;
|
|
69
|
+
private presentationKeyboard;
|
|
70
|
+
constructor(options: CreateWidgetClientOptions);
|
|
71
|
+
get currentState(): WidgetClientState;
|
|
72
|
+
ready(): Promise<void>;
|
|
73
|
+
resize(width: number, height: number): void;
|
|
74
|
+
resize(size: WidgetSize): void;
|
|
75
|
+
requestCapability(request: CapabilityRequest, timeoutMs?: number | WidgetCapabilityRequestOptions): Promise<CapabilityResult>;
|
|
76
|
+
present(surface: PresentationSurface, options?: WidgetCapabilityRequestOptions): Promise<PresentationCapabilityResult>;
|
|
77
|
+
closePresentation(options?: WidgetCapabilityRequestOptions): Promise<PresentationCapabilityResult>;
|
|
78
|
+
changePresentationSurface(surface: PresentationSurface, options?: WidgetCapabilityRequestOptions): Promise<PresentationCapabilityResult>;
|
|
79
|
+
focusPresentationBoundary(direction: PresentationFocusDirection, options?: WidgetCapabilityRequestOptions): Promise<PresentationCapabilityResult>;
|
|
80
|
+
subscribeEvents(options: SubscribeWidgetEventsOptions): WidgetEventSubscription;
|
|
81
|
+
onTool(hooks: WidgetToolHooks, options: Pick<SubscribeWidgetEventsOptions, "cursor" | "onError">): WidgetEventSubscription;
|
|
82
|
+
invokeAction(input: Extract<CapabilityRequest, {
|
|
83
|
+
operation: "actions.invoke";
|
|
84
|
+
}>["input"], options?: WidgetCapabilityRequestOptions): Promise<CapabilityResult>;
|
|
85
|
+
private stopSubscriptions;
|
|
86
|
+
private startPresentationKeyboard;
|
|
87
|
+
private stopPresentationKeyboard;
|
|
88
|
+
dispose(): void;
|
|
89
|
+
private receiveConnection;
|
|
90
|
+
private requireClient;
|
|
91
|
+
private removeConnectionListener;
|
|
92
|
+
private failConnection;
|
|
93
|
+
private rejectBeforeReady;
|
|
94
|
+
}
|
|
95
|
+
export declare function createWidgetClient(options: CreateWidgetClientOptions): WidgetRuntimeClient;
|