rapier-embed 1.1.2 → 1.1.4

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.
Files changed (2) hide show
  1. package/embed.mjs +79 -16
  2. 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)) answered.set(requestId, (async () => {
36
- let reply;
37
- try {
38
- const outcome = await store.write(requestId, message.payload?.content ?? '', baseRevision, message.payload);
39
- reply = outcome && outcome.conflict
40
- ? {type: 'save-nack', payload: {code: 'conflict', ...(outcome.currentRevision !== undefined ? {currentRevision: outcome.currentRevision} : {})}}
41
- : {type: 'save-ack', payload: {revision: outcome.revision}};
42
- } catch (error) {
43
- reply = {type: 'save-nack', payload: {code: 'failed', reason: String(error?.message || error).slice(0, 500)}};
44
- }
45
- return {...ids, requestId, baseRevision, ...reply};
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: () => send('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: () => send('close'),
105
- theme: value => send('theme', {theme: value}),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-embed",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Put the Rapier editor in your site: one call frames rapier.html?embed=1, connects, loads your document and answers its saves.",
5
5
  "keywords": [
6
6
  "markdown",