rapier-embed 1.1.2 → 1.1.3
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/embed.mjs +79 -16
- package/package.json +1 -1
package/embed.mjs
CHANGED
|
@@ -12,6 +12,8 @@ export function connectRapier(iframe, options) {
|
|
|
12
12
|
if (capabilities.includes('read') && typeof store?.write !== 'function') throw new TypeError('read needs store.write(requestId, content, baseRevision) -> {revision} or {conflict: true, currentRevision}');
|
|
13
13
|
const ids = {sessionId, documentId};
|
|
14
14
|
const answered = new Map(), waiting = new Map();
|
|
15
|
+
// save() waits on the frame's own save-request, not the command id the helper just minted.
|
|
16
|
+
const commandWaiters = {save: []};
|
|
15
17
|
let port = null, posted = false, connected = false, seq = 0;
|
|
16
18
|
const listeners = new Map();
|
|
17
19
|
|
|
@@ -19,31 +21,57 @@ export function connectRapier(iframe, options) {
|
|
|
19
21
|
const fail = error => { emit('error', error); if (onError) onError(error); };
|
|
20
22
|
const sameFrame = event => event.source === iframe.contentWindow && event.origin === origin;
|
|
21
23
|
|
|
22
|
-
function send(type, payload, baseRevision) {
|
|
24
|
+
function send(type, payload, baseRevision, quiet) {
|
|
23
25
|
if (!port || !connected) return Promise.reject(new Error('not connected'));
|
|
24
26
|
const requestId = 'h' + (++seq) + '-' + Math.random().toString(36).slice(2, 10);
|
|
25
27
|
const message = {type, ...ids, requestId, ...(baseRevision !== undefined ? {baseRevision} : {}), ...(payload ? {payload} : {})};
|
|
28
|
+
// Theme is painted and not answered. A refusal has this requestId and reaches onError below.
|
|
29
|
+
if (quiet) {
|
|
30
|
+
try { port.postMessage(message); } catch (error) { return Promise.reject(error); }
|
|
31
|
+
return Promise.resolve();
|
|
32
|
+
}
|
|
26
33
|
return new Promise((resolve, reject) => {
|
|
27
34
|
waiting.set(requestId, {resolve, reject, type});
|
|
28
35
|
try { port.postMessage(message); } catch (error) { waiting.delete(requestId); reject(error); }
|
|
29
36
|
});
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
// Drop the command's own waiter once the frame has answered it by another id, so it cannot hang.
|
|
40
|
+
function releaseCommand(type) {
|
|
41
|
+
for (const [requestId, pending] of waiting) {
|
|
42
|
+
if (pending.type !== type) continue;
|
|
43
|
+
waiting.delete(requestId);
|
|
44
|
+
pending.resolve();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
async function answerSave(message) {
|
|
33
50
|
const {requestId, baseRevision} = message;
|
|
34
51
|
// The answer is held from the moment storage starts: a Retry while it is pending shares the one write.
|
|
35
|
-
if (!answered.has(requestId))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
if (!answered.has(requestId)) {
|
|
53
|
+
const job = (async () => {
|
|
54
|
+
let reply;
|
|
55
|
+
try {
|
|
56
|
+
const outcome = await store.write(requestId, message.payload?.content ?? '', baseRevision, message.payload);
|
|
57
|
+
reply = outcome && outcome.conflict
|
|
58
|
+
? {type: 'save-nack', payload: {code: 'conflict', ...(outcome.currentRevision !== undefined ? {currentRevision: outcome.currentRevision} : {})}}
|
|
59
|
+
: {type: 'save-ack', payload: {revision: outcome.revision}};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reply = {type: 'save-nack', payload: {code: 'failed', reason: String(error?.message || error).slice(0, 500)}};
|
|
62
|
+
}
|
|
63
|
+
return {...ids, requestId, baseRevision, ...reply};
|
|
64
|
+
})();
|
|
65
|
+
answered.set(requestId, job);
|
|
66
|
+
// The host's save() is settled by this save-request, which carries the frame's id, not the command's.
|
|
67
|
+
job.then(reply => {
|
|
68
|
+
const slot = commandWaiters.save.shift();
|
|
69
|
+
if (!slot) return;
|
|
70
|
+
releaseCommand('save');
|
|
71
|
+
if (reply.type === 'save-ack') slot.resolve(reply.payload);
|
|
72
|
+
else slot.reject(Object.assign(new Error(reply.payload?.reason || reply.payload?.code || 'refused'), {code: reply.payload?.code, payload: reply.payload}));
|
|
73
|
+
}, () => {});
|
|
74
|
+
}
|
|
47
75
|
port.postMessage(await answered.get(requestId));
|
|
48
76
|
}
|
|
49
77
|
|
|
@@ -99,10 +127,45 @@ export function connectRapier(iframe, options) {
|
|
|
99
127
|
return Object.freeze({
|
|
100
128
|
get connected() { return connected; },
|
|
101
129
|
load: (content, {filename, revision = 1, readOnly, title} = {}) => send('load', {content, ...(filename ? {filename} : {}), revision, ...(readOnly !== undefined ? {readOnly} : {}), ...(title ? {title} : {})}, null),
|
|
102
|
-
save
|
|
130
|
+
save() {
|
|
131
|
+
const posted = send('save');
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
let settled = false;
|
|
134
|
+
const slot = {
|
|
135
|
+
resolve: value => { if (settled) return; settled = true; resolve(value); },
|
|
136
|
+
reject: error => { if (settled) return; settled = true; reject(error); },
|
|
137
|
+
};
|
|
138
|
+
commandWaiters.save.push(slot);
|
|
139
|
+
posted.catch(error => {
|
|
140
|
+
const index = commandWaiters.save.indexOf(slot);
|
|
141
|
+
if (index >= 0) commandWaiters.save.splice(index, 1);
|
|
142
|
+
slot.reject(error);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
},
|
|
103
146
|
compare: (content, {filename} = {}) => send('compare', {content, ...(filename ? {filename} : {})}),
|
|
104
|
-
close
|
|
105
|
-
|
|
147
|
+
close() {
|
|
148
|
+
const posted = send('close');
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
let settled = false;
|
|
151
|
+
const finish = payload => {
|
|
152
|
+
if (settled) return;
|
|
153
|
+
settled = true;
|
|
154
|
+
listeners.get('closed')?.delete(finish);
|
|
155
|
+
releaseCommand('close');
|
|
156
|
+
resolve(payload);
|
|
157
|
+
};
|
|
158
|
+
if (!listeners.has('closed')) listeners.set('closed', new Set());
|
|
159
|
+
listeners.get('closed').add(finish);
|
|
160
|
+
posted.catch(error => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
listeners.get('closed')?.delete(finish);
|
|
164
|
+
reject(error);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
theme: value => send('theme', {theme: value}, undefined, true),
|
|
106
169
|
disconnect() {
|
|
107
170
|
send('disconnect').catch(() => {});
|
|
108
171
|
globalThis.removeEventListener?.('message', onWindowMessage);
|
package/package.json
CHANGED