@rhwp/editor 0.7.18 → 0.7.19
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/README.md +26 -3
- package/index.d.ts +67 -1
- package/index.js +52 -41
- package/package.json +6 -2
- package/transport.js +238 -0
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
웹 페이지에 HWP 에디터를 통째로 임베드합니다.
|
|
9
9
|
메뉴, 툴바, 서식, 표 편집 — rhwp-studio의 모든 기능을 그대로 사용할 수 있습니다.
|
|
10
10
|
|
|
11
|
+
SDK는 지원되는 Studio와 `MessageChannel` v1을 협상해 binary를 transferable로 전송합니다.
|
|
12
|
+
구버전 Studio에는 기존 `postMessage` protocol로 자동 전환되며 기존 공개 API는 유지됩니다.
|
|
13
|
+
`getRendererDiagnostics()`는 `renderer-diagnostics-v1` capability를 협상한 Studio에서만 사용할 수 있습니다.
|
|
14
|
+
호스트와 `studioUrl`은 HTTP(S) origin만 지원합니다. `file:`, `data:`, 브라우저 확장처럼
|
|
15
|
+
origin이 `null`이거나 불투명한 환경의 연결은 SDK와 Studio 양쪽에서 거부합니다.
|
|
16
|
+
|
|
11
17
|
> **[온라인 데모](https://edwardkim.github.io/rhwp/)** 에서 먼저 체험해보세요.
|
|
12
18
|
|
|
13
19
|
## 설치
|
|
@@ -72,9 +78,11 @@ const editor = await createEditor(document.getElementById('editor'));
|
|
|
72
78
|
|
|
73
79
|
| 옵션 | 기본값 | 설명 |
|
|
74
80
|
|------|--------|------|
|
|
75
|
-
| `studioUrl` | `https://edwardkim.github.io/rhwp/` | rhwp-studio URL |
|
|
81
|
+
| `studioUrl` | `https://edwardkim.github.io/rhwp/` | rhwp-studio HTTP(S) URL. opaque origin은 지원하지 않음 |
|
|
76
82
|
| `width` | `'100%'` | iframe 너비 |
|
|
77
83
|
| `height` | `'100%'` | iframe 높이 |
|
|
84
|
+
| `requestTimeoutMs` | method별 기본값 | 모든 method 제한 시간 override(ms). 일반 10초, load/export 60초 |
|
|
85
|
+
| `handshakeTimeoutMs` | `1000` | v1 협상 후 legacy 전환까지의 제한 시간(ms) |
|
|
78
86
|
|
|
79
87
|
### editor.loadFile(data, fileName?)
|
|
80
88
|
|
|
@@ -101,6 +109,16 @@ const count = await editor.pageCount();
|
|
|
101
109
|
const svg = await editor.getPageSvg(0); // 첫 페이지
|
|
102
110
|
```
|
|
103
111
|
|
|
112
|
+
### editor.getRendererDiagnostics(page?)
|
|
113
|
+
|
|
114
|
+
선택된 renderer와 0부터 시작하는 페이지별 readiness 진단을 반환합니다.
|
|
115
|
+
Studio가 `renderer-diagnostics-v1` capability를 제공하지 않으면 명시적으로 실패합니다.
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
const diagnostics = await editor.getRendererDiagnostics(0);
|
|
119
|
+
console.log(diagnostics.schemaVersion, diagnostics.effectiveBackend);
|
|
120
|
+
```
|
|
121
|
+
|
|
104
122
|
### editor.exportHwp()
|
|
105
123
|
|
|
106
124
|
현재 편집 중인 문서를 HWP 바이너리로 내보냅니다.
|
|
@@ -266,7 +284,9 @@ setInterval(() => {
|
|
|
266
284
|
|
|
267
285
|
### 기본 동작 — 별도 설정 없이 사용 가능
|
|
268
286
|
|
|
269
|
-
`@rhwp/editor
|
|
287
|
+
`@rhwp/editor`가 기본으로 연결하는 rhwp-studio 배포본은 오픈소스 폰트를 포함하므로
|
|
288
|
+
**별도 폰트 설정 없이 바로 사용**할 수 있습니다. `@rhwp/editor` 패키지 자체에는 폰트 파일이나
|
|
289
|
+
UI runtime dependency가 포함되지 않습니다.
|
|
270
290
|
|
|
271
291
|
HWP 문서에서 사용된 한컴 전용 폰트(한컴바탕, HY명조 등)는 자동으로 오픈소스 폰트로 폴백됩니다.
|
|
272
292
|
|
|
@@ -290,7 +310,10 @@ HWP 문서에서 사용된 한컴 전용 폰트(한컴바탕, HY명조 등)는
|
|
|
290
310
|
|
|
291
311
|
### 셀프 호스팅 시 폰트
|
|
292
312
|
|
|
293
|
-
|
|
313
|
+
저장소의 canonical source는 `assets/fonts/`이고, rhwp-studio build는 이를 배포본의 runtime `fonts/`
|
|
314
|
+
경로로 노출합니다. 셀프 호스팅 서버에서는 이 runtime 경로가 함께 배포되므로 별도 설정 없이
|
|
315
|
+
오픈소스 폰트를 사용합니다. 추가 폰트는 `assets/fonts/`에 WOFF2를 추가하고
|
|
316
|
+
`rhwp-studio/src/core/font-loader.ts`에 `@font-face`와 로딩 정책을 등록해야 합니다.
|
|
294
317
|
|
|
295
318
|
## 셀프 호스팅
|
|
296
319
|
|
package/index.d.ts
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export interface EditorOptions {
|
|
6
|
-
/** rhwp-studio
|
|
6
|
+
/** rhwp-studio HTTP(S) URL. file:, data:, browser extension 등 opaque origin은 지원하지 않음 */
|
|
7
7
|
studioUrl?: string;
|
|
8
8
|
/** iframe 너비 (기본: '100%') */
|
|
9
9
|
width?: string;
|
|
10
10
|
/** iframe 높이 (기본: '100%') */
|
|
11
11
|
height?: string;
|
|
12
|
+
/** 모든 method 요청 제한 시간 override(ms, 기본: 일반 10000, load/export 60000) */
|
|
13
|
+
requestTimeoutMs?: number;
|
|
14
|
+
/** v1 협상 제한 시간(ms, 기본: 1000) */
|
|
15
|
+
handshakeTimeoutMs?: number;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export interface LoadResult {
|
|
@@ -26,17 +30,79 @@ export interface HwpVerifyResult {
|
|
|
26
30
|
recovered: boolean;
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
export interface HmlSaveBlocker {
|
|
34
|
+
code: string;
|
|
35
|
+
xmlPath: string;
|
|
36
|
+
message: string;
|
|
37
|
+
preserved: false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HmlSaveState {
|
|
41
|
+
sourceFormat: string;
|
|
42
|
+
hmlSavable: boolean;
|
|
43
|
+
blockers: HmlSaveBlocker[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CanvasKitRendererDiagnostics {
|
|
47
|
+
mode: 'default' | 'compat';
|
|
48
|
+
surfacePreference: 'auto' | 'webgpu' | 'webgl' | 'software';
|
|
49
|
+
surfaceBackend: 'default' | 'software' | null;
|
|
50
|
+
surfaceFallbackReason: string | null;
|
|
51
|
+
lastRenderCompleted: boolean;
|
|
52
|
+
lastUnsupportedOps: string[];
|
|
53
|
+
lastExpectedUnsupportedOps: string[];
|
|
54
|
+
lastUnexpectedUnsupportedOps: string[];
|
|
55
|
+
lastRenderError: string | null;
|
|
56
|
+
passesRuntimeReadinessGate: boolean;
|
|
57
|
+
readinessBlockers: Array<'renderNotCompleted' | 'renderError' | 'unexpectedUnsupportedOps' | 'localFontsPending'>;
|
|
58
|
+
hiddenCanvas2dOverlayUsed: false;
|
|
59
|
+
lastRenderDurationMs: number | null;
|
|
60
|
+
renderCount: number;
|
|
61
|
+
imageCacheEntries: number;
|
|
62
|
+
imageCacheLimit: number;
|
|
63
|
+
imageCachePixels: number;
|
|
64
|
+
imageCachePixelLimit: number;
|
|
65
|
+
imageCacheHits: number;
|
|
66
|
+
imageCacheMisses: number;
|
|
67
|
+
imageCacheEvictions: number;
|
|
68
|
+
localTypefaceCount: number;
|
|
69
|
+
localTypefaceLoadFailureCount: number;
|
|
70
|
+
localTypefacePendingCount: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RendererDiagnosticsV1 {
|
|
74
|
+
schemaVersion: 1;
|
|
75
|
+
request: {
|
|
76
|
+
backend: { backend: 'canvas2d' | 'canvaskit'; source: 'default' | 'url'; requested?: string; unsupportedReason?: string };
|
|
77
|
+
canvaskitMode: { mode: 'default' | 'compat'; source: 'default' | 'storage' | 'url'; requested?: string; unsupportedReason?: string };
|
|
78
|
+
canvaskitSurface: { preference: 'auto' | 'webgpu' | 'webgl' | 'software'; requested: string; unsupportedReason?: string };
|
|
79
|
+
renderProfile: 'fastPreview' | 'screen' | 'print' | 'highQuality';
|
|
80
|
+
} | null;
|
|
81
|
+
initialized: boolean;
|
|
82
|
+
initializationError: string | null;
|
|
83
|
+
effectiveBackend: 'canvas2d' | 'canvaskit' | null;
|
|
84
|
+
backendFallbackReason: string | null;
|
|
85
|
+
page: { index: number; canvaskit: CanvasKitRendererDiagnostics | null };
|
|
86
|
+
}
|
|
87
|
+
|
|
29
88
|
export declare class RhwpEditor {
|
|
89
|
+
private constructor();
|
|
30
90
|
/** HWP 파일을 로드합니다 */
|
|
31
91
|
loadFile(data: ArrayBuffer | Uint8Array, fileName?: string): Promise<LoadResult>;
|
|
32
92
|
/** 현재 문서의 페이지 수를 반환합니다 */
|
|
33
93
|
pageCount(): Promise<number>;
|
|
34
94
|
/** 특정 페이지를 SVG 문자열로 렌더링합니다 */
|
|
35
95
|
getPageSvg(page?: number): Promise<string>;
|
|
96
|
+
/** 선택된 renderer와 페이지별 readiness 진단을 반환합니다 */
|
|
97
|
+
getRendererDiagnostics(page?: number): Promise<RendererDiagnosticsV1>;
|
|
36
98
|
/** 현재 문서를 HWP 바이너리로 내보냅니다 */
|
|
37
99
|
exportHwp(): Promise<Uint8Array>;
|
|
38
100
|
/** 현재 문서를 HWPX(ZIP+XML) 바이너리로 내보냅니다 */
|
|
39
101
|
exportHwpx(): Promise<Uint8Array>;
|
|
102
|
+
/** 현재 문서를 HML(XML) 바이너리로 내보냅니다 */
|
|
103
|
+
exportHml(): Promise<Uint8Array>;
|
|
104
|
+
/** 현재 문서의 HML 저장 가능 여부와 blocker를 반환합니다 */
|
|
105
|
+
getHmlSaveState(): Promise<HmlSaveState>;
|
|
40
106
|
/** HWP 직렬화 + 자기 재로드 검증 메타데이터 (#178) */
|
|
41
107
|
exportHwpVerify(): Promise<HwpVerifyResult>;
|
|
42
108
|
/** iframe 엘리먼트를 반환합니다 */
|
package/index.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* 본 제품은 한글과컴퓨터의 한글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import { EditorTransport } from './transport.js';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const DEFAULT_STUDIO_URL = 'https://edwardkim.github.io/rhwp/';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* HWP 에디터를 생성하여 지정된 컨테이너에 마운트합니다.
|
|
@@ -53,9 +53,21 @@ export async function createEditor(container, options = {}) {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// WASM 초기화 대기 (ready 메서드로 확인)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
let transport;
|
|
57
|
+
try {
|
|
58
|
+
transport = new EditorTransport(iframe, studioUrl, {
|
|
59
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
60
|
+
handshakeTimeoutMs: options.handshakeTimeoutMs,
|
|
61
|
+
});
|
|
62
|
+
await transport.connect();
|
|
63
|
+
const editor = new RhwpEditor(iframe, transport);
|
|
64
|
+
await editor._waitReady();
|
|
65
|
+
return editor;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
transport?.destroy();
|
|
68
|
+
iframe.remove();
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
/**
|
|
@@ -63,25 +75,10 @@ export async function createEditor(container, options = {}) {
|
|
|
63
75
|
*
|
|
64
76
|
* iframe 내부의 rhwp-studio와 postMessage로 통신합니다.
|
|
65
77
|
*/
|
|
66
|
-
class RhwpEditor {
|
|
67
|
-
constructor(iframe) {
|
|
78
|
+
export class RhwpEditor {
|
|
79
|
+
constructor(iframe, transport) {
|
|
68
80
|
this._iframe = iframe;
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
// 응답 수신 리스너
|
|
72
|
-
window.addEventListener('message', (e) => {
|
|
73
|
-
if (e.data?.type === 'rhwp-response' && e.data.id != null) {
|
|
74
|
-
const resolver = this._pending.get(e.data.id);
|
|
75
|
-
if (resolver) {
|
|
76
|
-
this._pending.delete(e.data.id);
|
|
77
|
-
if (e.data.error) {
|
|
78
|
-
resolver.reject(new Error(e.data.error));
|
|
79
|
-
} else {
|
|
80
|
-
resolver.resolve(e.data.result);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
});
|
|
81
|
+
this._transport = transport;
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
/**
|
|
@@ -89,21 +86,7 @@ class RhwpEditor {
|
|
|
89
86
|
* @internal
|
|
90
87
|
*/
|
|
91
88
|
_request(method, params = {}) {
|
|
92
|
-
return
|
|
93
|
-
const id = ++requestId;
|
|
94
|
-
this._pending.set(id, { resolve, reject });
|
|
95
|
-
this._iframe.contentWindow.postMessage(
|
|
96
|
-
{ type: 'rhwp-request', id, method, params },
|
|
97
|
-
'*'
|
|
98
|
-
);
|
|
99
|
-
// 10초 타임아웃
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
if (this._pending.has(id)) {
|
|
102
|
-
this._pending.delete(id);
|
|
103
|
-
reject(new Error(`Request timeout: ${method}`));
|
|
104
|
-
}
|
|
105
|
-
}, 10000);
|
|
106
|
-
});
|
|
89
|
+
return this._transport.request(method, params);
|
|
107
90
|
}
|
|
108
91
|
|
|
109
92
|
/** WASM 초기화 완료 대기 @internal */
|
|
@@ -136,8 +119,7 @@ class RhwpEditor {
|
|
|
136
119
|
* ```
|
|
137
120
|
*/
|
|
138
121
|
async loadFile(data, fileName = 'document.hwp') {
|
|
139
|
-
|
|
140
|
-
return this._request('loadFile', { data: bytes, fileName });
|
|
122
|
+
return this._request('loadFile', { data, fileName });
|
|
141
123
|
}
|
|
142
124
|
|
|
143
125
|
/**
|
|
@@ -157,6 +139,24 @@ class RhwpEditor {
|
|
|
157
139
|
return this._request('getPageSvg', { page });
|
|
158
140
|
}
|
|
159
141
|
|
|
142
|
+
/**
|
|
143
|
+
* 선택된 renderer와 페이지별 CanvasKit readiness 진단을 반환합니다.
|
|
144
|
+
* @param page - 0부터 시작하는 페이지 번호
|
|
145
|
+
*/
|
|
146
|
+
async getRendererDiagnostics(page = 0) {
|
|
147
|
+
if (!Number.isSafeInteger(page) || page < 0) {
|
|
148
|
+
throw new TypeError('page must be a non-negative safe integer');
|
|
149
|
+
}
|
|
150
|
+
if (!this._transport.supports('renderer-diagnostics-v1')) {
|
|
151
|
+
throw new Error('Renderer diagnostics v1 is not supported by this Studio');
|
|
152
|
+
}
|
|
153
|
+
const result = await this._request('getRendererDiagnostics', { page });
|
|
154
|
+
if (result?.schemaVersion !== 1 || result?.page?.index !== page) {
|
|
155
|
+
throw new Error('Studio returned invalid renderer diagnostics v1');
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
160
|
/**
|
|
161
161
|
* 현재 문서를 HWP 바이너리로 내보냅니다.
|
|
162
162
|
* @returns {Promise<Uint8Array>} HWP 파일 bytes
|
|
@@ -175,6 +175,17 @@ class RhwpEditor {
|
|
|
175
175
|
return result instanceof Uint8Array ? result : new Uint8Array(result || []);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/** 현재 문서를 HML(XML) 바이너리로 내보냅니다. */
|
|
179
|
+
async exportHml() {
|
|
180
|
+
const result = await this._request('exportHml');
|
|
181
|
+
return result instanceof Uint8Array ? result : new Uint8Array(result || []);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 현재 문서의 HML 저장 가능 여부와 blocker를 반환합니다. */
|
|
185
|
+
async getHmlSaveState() {
|
|
186
|
+
return this._request('getHmlSaveState');
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
/**
|
|
179
190
|
* HWP 직렬화 + 자기 재로드 검증 메타데이터를 반환합니다 (#178).
|
|
180
191
|
*
|
|
@@ -197,7 +208,7 @@ class RhwpEditor {
|
|
|
197
208
|
* 에디터를 제거합니다.
|
|
198
209
|
*/
|
|
199
210
|
destroy() {
|
|
211
|
+
this._transport.destroy();
|
|
200
212
|
this._iframe.remove();
|
|
201
|
-
this._pending.clear();
|
|
202
213
|
}
|
|
203
214
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhwp/editor",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.19",
|
|
4
4
|
"description": "HWP 에디터 웹 컴포넌트 — iframe 기반 임베드",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test tests/*.test.mjs"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
12
|
"index.js",
|
|
10
|
-
"index.d.ts"
|
|
13
|
+
"index.d.ts",
|
|
14
|
+
"transport.js"
|
|
11
15
|
],
|
|
12
16
|
"keywords": [
|
|
13
17
|
"hwp",
|
package/transport.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const PROTOCOL_VERSION = 1;
|
|
2
|
+
const CAPABILITIES = [
|
|
3
|
+
'transferable-array-buffer',
|
|
4
|
+
'hml-export',
|
|
5
|
+
'renderer-diagnostics-v1',
|
|
6
|
+
];
|
|
7
|
+
const LONG_RUNNING_METHODS = new Set([
|
|
8
|
+
'loadFile', 'exportHwp', 'exportHwpVerify', 'exportHwpx', 'exportHml',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function requestTimeoutFor(method, configuredTimeout) {
|
|
12
|
+
if (configuredTimeout != null) return configuredTimeout;
|
|
13
|
+
return LONG_RUNNING_METHODS.has(method) ? 60000 : 10000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sessionId() {
|
|
17
|
+
const secureRandom = globalThis.crypto;
|
|
18
|
+
if (typeof secureRandom?.randomUUID === 'function') return secureRandom.randomUUID();
|
|
19
|
+
if (typeof secureRandom?.getRandomValues !== 'function') {
|
|
20
|
+
throw new Error('Secure random generation is unavailable');
|
|
21
|
+
}
|
|
22
|
+
const bytes = secureRandom.getRandomValues(new Uint8Array(16));
|
|
23
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function copiedBinary(value) {
|
|
27
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value).slice();
|
|
28
|
+
if (ArrayBuffer.isView(value)) {
|
|
29
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function prepareParams(params) {
|
|
35
|
+
const data = copiedBinary(params?.data);
|
|
36
|
+
if (!data) return { params, transfer: [] };
|
|
37
|
+
return { params: { ...params, data }, transfer: [data.buffer] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isResponseEnvelope(message, legacy, sessionId) {
|
|
41
|
+
if (message?.type !== 'rhwp-response' || !Number.isSafeInteger(message.id)) return false;
|
|
42
|
+
if (legacy) return true;
|
|
43
|
+
if (message.version !== PROTOCOL_VERSION || message.sessionId !== sessionId) return false;
|
|
44
|
+
const hasResult = Object.prototype.hasOwnProperty.call(message, 'result');
|
|
45
|
+
const hasError = Object.prototype.hasOwnProperty.call(message, 'error');
|
|
46
|
+
if (hasResult === hasError) return false;
|
|
47
|
+
return !hasError || (typeof message.error?.code === 'string'
|
|
48
|
+
&& typeof message.error?.message === 'string');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class EditorTransport {
|
|
52
|
+
constructor(iframe, studioUrl, options = {}) {
|
|
53
|
+
this._iframe = iframe;
|
|
54
|
+
const targetUrl = new URL(studioUrl, globalThis.location?.href);
|
|
55
|
+
if (targetUrl.protocol !== 'http:' && targetUrl.protocol !== 'https:') {
|
|
56
|
+
throw new Error('studioUrl must use HTTP(S)');
|
|
57
|
+
}
|
|
58
|
+
this._targetOrigin = targetUrl.origin;
|
|
59
|
+
this._window = options.window || window;
|
|
60
|
+
this._requestTimeoutMs = options.requestTimeoutMs;
|
|
61
|
+
this._handshakeTimeoutMs = options.handshakeTimeoutMs ?? 1000;
|
|
62
|
+
this._sessionId = sessionId();
|
|
63
|
+
this._nextId = 0;
|
|
64
|
+
this._pending = new Map();
|
|
65
|
+
this._port = null;
|
|
66
|
+
this._peerCapabilities = new Set();
|
|
67
|
+
this._legacy = false;
|
|
68
|
+
this._destroyed = false;
|
|
69
|
+
this._onLegacyMessage = (event) => this._handleLegacyMessage(event);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
connect() {
|
|
73
|
+
if (this._destroyed) return Promise.reject(new Error('Editor destroyed'));
|
|
74
|
+
const channel = new MessageChannel();
|
|
75
|
+
this._port = channel.port1;
|
|
76
|
+
this._port.onmessage = (event) => this._handlePortMessage(event.data);
|
|
77
|
+
this._port.start();
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
this._connectResolve = resolve;
|
|
80
|
+
this._connectReject = reject;
|
|
81
|
+
this._connectTimer = setTimeout(() => this._useLegacy(), this._handshakeTimeoutMs);
|
|
82
|
+
this._iframe.contentWindow.postMessage({
|
|
83
|
+
type: 'rhwp-connect', version: PROTOCOL_VERSION, sessionId: this._sessionId,
|
|
84
|
+
capabilities: CAPABILITIES,
|
|
85
|
+
}, this._targetOrigin, [channel.port2]);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
request(method, params = {}) {
|
|
90
|
+
if (this._destroyed) return Promise.reject(new Error('Editor destroyed'));
|
|
91
|
+
const id = ++this._nextId;
|
|
92
|
+
const prepared = prepareParams(params);
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timeout = setTimeout(() => {
|
|
95
|
+
this._pending.delete(id);
|
|
96
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
97
|
+
}, requestTimeoutFor(method, this._requestTimeoutMs));
|
|
98
|
+
this._pending.set(id, { resolve, reject, timeout });
|
|
99
|
+
try {
|
|
100
|
+
this._send({
|
|
101
|
+
type: 'rhwp-request', version: PROTOCOL_VERSION,
|
|
102
|
+
sessionId: this._sessionId, id, method, params: prepared.params,
|
|
103
|
+
}, prepared.transfer);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
this._pending.delete(id);
|
|
107
|
+
reject(error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
supports(capability) {
|
|
113
|
+
return this._peerCapabilities.has(capability);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
destroy() {
|
|
117
|
+
if (this._destroyed) return;
|
|
118
|
+
this._destroyed = true;
|
|
119
|
+
clearTimeout(this._connectTimer);
|
|
120
|
+
this._port && (this._port.onmessage = null);
|
|
121
|
+
this._port?.close();
|
|
122
|
+
this._rejectConnect(new Error('Editor destroyed'));
|
|
123
|
+
this._window.removeEventListener('message', this._onLegacyMessage);
|
|
124
|
+
for (const pending of this._pending.values()) {
|
|
125
|
+
clearTimeout(pending.timeout);
|
|
126
|
+
pending.reject(new Error('Editor destroyed'));
|
|
127
|
+
}
|
|
128
|
+
this._pending.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_send(message, transfer) {
|
|
132
|
+
if (this._legacy) {
|
|
133
|
+
const { version, sessionId: ignored, ...legacyMessage } = message;
|
|
134
|
+
this._iframe.contentWindow.postMessage(legacyMessage, this._targetOrigin, transfer);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this._port.postMessage(message, transfer);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_handlePortMessage(message) {
|
|
141
|
+
if (message?.type === 'rhwp-connected'
|
|
142
|
+
&& message.version === PROTOCOL_VERSION
|
|
143
|
+
&& message.sessionId === this._sessionId
|
|
144
|
+
&& message.capabilities?.includes('transferable-array-buffer')) {
|
|
145
|
+
clearTimeout(this._connectTimer);
|
|
146
|
+
this._peerCapabilities = new Set(message.capabilities);
|
|
147
|
+
const resolve = this._connectResolve;
|
|
148
|
+
this._connectResolve = null;
|
|
149
|
+
this._connectReject = null;
|
|
150
|
+
resolve?.();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (message?.type === 'rhwp-connect-error'
|
|
154
|
+
&& message.version === PROTOCOL_VERSION
|
|
155
|
+
&& message.sessionId === this._sessionId
|
|
156
|
+
&& typeof message.error?.message === 'string') {
|
|
157
|
+
const error = new Error(message.error.message);
|
|
158
|
+
error.code = message.error.code;
|
|
159
|
+
error.supportedVersions = message.error.supportedVersions;
|
|
160
|
+
this._rejectConnect(error);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this._handleResponse(message);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_handleLegacyMessage(event) {
|
|
167
|
+
if (event.source !== this._iframe.contentWindow || event.origin !== this._targetOrigin) return;
|
|
168
|
+
this._handleResponse(event.data, true);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_handleResponse(message, legacy = false) {
|
|
172
|
+
if (legacy) {
|
|
173
|
+
if (!isResponseEnvelope(message, true, this._sessionId)) return;
|
|
174
|
+
} else if (message?.type !== 'rhwp-response'
|
|
175
|
+
|| !Number.isSafeInteger(message.id)
|
|
176
|
+
|| message.sessionId !== this._sessionId) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const pending = this._pending.get(message.id);
|
|
180
|
+
if (!pending) return;
|
|
181
|
+
if (!legacy && Number.isSafeInteger(message.version)
|
|
182
|
+
&& message.version !== PROTOCOL_VERSION) {
|
|
183
|
+
this._rejectPendingResponse(message.id, pending, 'UNSUPPORTED_VERSION',
|
|
184
|
+
`Unsupported embed protocol version: ${message.version}`, [PROTOCOL_VERSION]);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (!isResponseEnvelope(message, legacy, this._sessionId)) {
|
|
188
|
+
setTimeout(() => this._rejectPendingResponse(
|
|
189
|
+
message.id, pending, 'INVALID_RESPONSE', 'Invalid response envelope',
|
|
190
|
+
), 0);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this._pending.delete(message.id);
|
|
194
|
+
clearTimeout(pending.timeout);
|
|
195
|
+
if (message.error) {
|
|
196
|
+
const error = new Error(message.error.message || message.error);
|
|
197
|
+
if (message.error.code) error.code = message.error.code;
|
|
198
|
+
pending.reject(error);
|
|
199
|
+
}
|
|
200
|
+
else pending.resolve(message.result);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_rejectPendingResponse(id, pending, code, message, supportedVersions) {
|
|
204
|
+
if (this._pending.get(id) !== pending) return;
|
|
205
|
+
this._pending.delete(id);
|
|
206
|
+
clearTimeout(pending.timeout);
|
|
207
|
+
const error = new Error(message);
|
|
208
|
+
error.code = code;
|
|
209
|
+
if (supportedVersions) error.supportedVersions = supportedVersions;
|
|
210
|
+
pending.reject(error);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_rejectConnect(error) {
|
|
214
|
+
if (!this._connectReject) return;
|
|
215
|
+
clearTimeout(this._connectTimer);
|
|
216
|
+
this._port && (this._port.onmessage = null);
|
|
217
|
+
this._port?.close();
|
|
218
|
+
this._port = null;
|
|
219
|
+
const reject = this._connectReject;
|
|
220
|
+
this._connectResolve = null;
|
|
221
|
+
this._connectReject = null;
|
|
222
|
+
reject(error);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_useLegacy() {
|
|
226
|
+
if (this._destroyed || !this._connectResolve) return;
|
|
227
|
+
this._port && (this._port.onmessage = null);
|
|
228
|
+
this._port?.close();
|
|
229
|
+
this._port = null;
|
|
230
|
+
this._legacy = true;
|
|
231
|
+
this._peerCapabilities.clear();
|
|
232
|
+
this._window.addEventListener('message', this._onLegacyMessage);
|
|
233
|
+
const resolve = this._connectResolve;
|
|
234
|
+
this._connectResolve = null;
|
|
235
|
+
this._connectReject = null;
|
|
236
|
+
resolve();
|
|
237
|
+
}
|
|
238
|
+
}
|