@kehto/services 0.16.1 → 0.17.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.
- package/README.md +129 -33
- package/dist/cvm-nostr-transport.d.ts +2 -0
- package/dist/cvm-nostr-transport.js.map +1 -1
- package/dist/index.d.ts +249 -376
- package/dist/index.js +1041 -580
- package/dist/index.js.map +1 -1
- package/package.json +7 -8
package/dist/index.js
CHANGED
|
@@ -1,71 +1,3 @@
|
|
|
1
|
-
// src/audio-service.ts
|
|
2
|
-
var AUDIO_SERVICE_VERSION = "1.0.0";
|
|
3
|
-
function createAudioService(options) {
|
|
4
|
-
const sources = /* @__PURE__ */ new Map();
|
|
5
|
-
const onChange = options?.onChange;
|
|
6
|
-
function notify2() {
|
|
7
|
-
onChange?.(new Map(sources));
|
|
8
|
-
}
|
|
9
|
-
const descriptor = {
|
|
10
|
-
name: "audio",
|
|
11
|
-
version: AUDIO_SERVICE_VERSION,
|
|
12
|
-
description: "Audio source registry \u2014 tracks active audio sources per napplet window"
|
|
13
|
-
};
|
|
14
|
-
return {
|
|
15
|
-
descriptor,
|
|
16
|
-
handleMessage(windowId, message, send) {
|
|
17
|
-
if (message.type !== "inc.emit") return;
|
|
18
|
-
const incMessage = message;
|
|
19
|
-
const topic = typeof incMessage.topic === "string" ? incMessage.topic : void 0;
|
|
20
|
-
if (!topic?.startsWith("audio:")) return;
|
|
21
|
-
const action = topic.slice(6);
|
|
22
|
-
const payload = incMessage.payload && typeof incMessage.payload === "object" ? incMessage.payload : {};
|
|
23
|
-
switch (action) {
|
|
24
|
-
case "register": {
|
|
25
|
-
const nappletClass = typeof payload.nappletClass === "string" ? payload.nappletClass : "";
|
|
26
|
-
const title = typeof payload.title === "string" ? payload.title : "";
|
|
27
|
-
sources.set(windowId, { windowId, nappletClass, title, muted: false });
|
|
28
|
-
notify2();
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
case "unregister": {
|
|
32
|
-
if (sources.delete(windowId)) {
|
|
33
|
-
notify2();
|
|
34
|
-
}
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
case "state-changed": {
|
|
38
|
-
const source = sources.get(windowId);
|
|
39
|
-
if (!source) return;
|
|
40
|
-
if (typeof payload.title === "string") {
|
|
41
|
-
source.title = payload.title;
|
|
42
|
-
}
|
|
43
|
-
notify2();
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
case "mute": {
|
|
47
|
-
const targetWindowId = typeof payload.windowId === "string" ? payload.windowId : windowId;
|
|
48
|
-
const muted = payload.muted === true;
|
|
49
|
-
const source = sources.get(targetWindowId);
|
|
50
|
-
if (source) {
|
|
51
|
-
source.muted = muted;
|
|
52
|
-
notify2();
|
|
53
|
-
}
|
|
54
|
-
send({ type: "inc.event", topic: "napplet:audio-muted", payload: { muted } });
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
default:
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
onWindowDestroyed(windowId) {
|
|
62
|
-
if (sources.delete(windowId)) {
|
|
63
|
-
notify2();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
1
|
// src/notification-service.ts
|
|
70
2
|
var NOTIFICATION_SERVICE_VERSION = "1.0.0";
|
|
71
3
|
var DEFAULT_MAX_PER_WINDOW = 100;
|
|
@@ -97,14 +29,11 @@ function enforceLimit(store, list) {
|
|
|
97
29
|
list.shift();
|
|
98
30
|
}
|
|
99
31
|
}
|
|
100
|
-
function findById(store, id) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return void 0;
|
|
32
|
+
function findById(store, windowId, id) {
|
|
33
|
+
const list = store.notifications.get(windowId);
|
|
34
|
+
const index = list?.findIndex((notification) => notification.id === id) ?? -1;
|
|
35
|
+
if (!list || index === -1) return void 0;
|
|
36
|
+
return [list[index], index];
|
|
108
37
|
}
|
|
109
38
|
function createNotification(store, windowId, title, body) {
|
|
110
39
|
const notification = {
|
|
@@ -121,20 +50,20 @@ function createNotification(store, windowId, title, body) {
|
|
|
121
50
|
notify(store);
|
|
122
51
|
return notification;
|
|
123
52
|
}
|
|
124
|
-
function dismissNotification(store, id) {
|
|
125
|
-
const found = findById(store, id);
|
|
53
|
+
function dismissNotification(store, windowId, id) {
|
|
54
|
+
const found = findById(store, windowId, id);
|
|
126
55
|
if (!found) return;
|
|
127
|
-
const [
|
|
128
|
-
const list = store.notifications.get(
|
|
56
|
+
const [, index] = found;
|
|
57
|
+
const list = store.notifications.get(windowId);
|
|
129
58
|
if (!list) return;
|
|
130
59
|
list.splice(index, 1);
|
|
131
|
-
if (list.length === 0) store.notifications.delete(
|
|
60
|
+
if (list.length === 0) store.notifications.delete(windowId);
|
|
132
61
|
notify(store);
|
|
133
62
|
}
|
|
134
|
-
function markNotificationRead(store, id) {
|
|
135
|
-
const found = findById(store, id);
|
|
63
|
+
function markNotificationRead(store, windowId, id) {
|
|
64
|
+
const found = findById(store, windowId, id);
|
|
136
65
|
if (!found) return;
|
|
137
|
-
const [
|
|
66
|
+
const [notification] = found;
|
|
138
67
|
if (!notification.read) {
|
|
139
68
|
notification.read = true;
|
|
140
69
|
notify(store);
|
|
@@ -151,12 +80,12 @@ function handleNotifyEnvelope(store, windowId, action, msg, send) {
|
|
|
151
80
|
}
|
|
152
81
|
case "dismiss": {
|
|
153
82
|
const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
|
|
154
|
-
if (notifId) dismissNotification(store, notifId);
|
|
83
|
+
if (notifId) dismissNotification(store, windowId, notifId);
|
|
155
84
|
break;
|
|
156
85
|
}
|
|
157
86
|
case "read": {
|
|
158
87
|
const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
|
|
159
|
-
if (notifId) markNotificationRead(store, notifId);
|
|
88
|
+
if (notifId) markNotificationRead(store, windowId, notifId);
|
|
160
89
|
break;
|
|
161
90
|
}
|
|
162
91
|
case "list": {
|
|
@@ -168,34 +97,6 @@ function handleNotifyEnvelope(store, windowId, action, msg, send) {
|
|
|
168
97
|
break;
|
|
169
98
|
}
|
|
170
99
|
}
|
|
171
|
-
function handleIncNotification(store, windowId, action, payload, send) {
|
|
172
|
-
switch (action) {
|
|
173
|
-
case "create": {
|
|
174
|
-
const title = typeof payload.title === "string" ? payload.title : "";
|
|
175
|
-
const body = typeof payload.body === "string" ? payload.body : "";
|
|
176
|
-
const notification = createNotification(store, windowId, title, body);
|
|
177
|
-
send({ type: "inc.event", topic: "notifications:created", payload: { id: notification.id } });
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
case "dismiss": {
|
|
181
|
-
const id = typeof payload.id === "string" ? payload.id : "";
|
|
182
|
-
if (id) dismissNotification(store, id);
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
case "read": {
|
|
186
|
-
const id = typeof payload.id === "string" ? payload.id : "";
|
|
187
|
-
if (id) markNotificationRead(store, id);
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
case "list": {
|
|
191
|
-
const windowNotifs = store.notifications.get(windowId) ?? [];
|
|
192
|
-
send({ type: "inc.event", topic: "notifications:listed", payload: { notifications: windowNotifs } });
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
default:
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
100
|
function createNotificationService(options) {
|
|
200
101
|
const store = {
|
|
201
102
|
notifications: /* @__PURE__ */ new Map(),
|
|
@@ -203,23 +104,21 @@ function createNotificationService(options) {
|
|
|
203
104
|
maxPerWindow: options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW
|
|
204
105
|
};
|
|
205
106
|
const descriptor = {
|
|
206
|
-
name: "
|
|
107
|
+
name: "notify",
|
|
207
108
|
version: NOTIFICATION_SERVICE_VERSION,
|
|
208
109
|
description: "Notification state registry \u2014 tracks notifications per napplet window"
|
|
209
110
|
};
|
|
210
111
|
return {
|
|
211
112
|
descriptor,
|
|
212
113
|
handleMessage(windowId, message, send) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const payload = msg.payload ?? {};
|
|
222
|
-
handleIncNotification(store, windowId, topic.slice(14), payload, send);
|
|
114
|
+
if (!message.type.startsWith("notify.")) return;
|
|
115
|
+
handleNotifyEnvelope(
|
|
116
|
+
store,
|
|
117
|
+
windowId,
|
|
118
|
+
message.type.slice(7),
|
|
119
|
+
message,
|
|
120
|
+
send
|
|
121
|
+
);
|
|
223
122
|
},
|
|
224
123
|
onWindowDestroyed(windowId) {
|
|
225
124
|
if (store.notifications.delete(windowId)) {
|
|
@@ -231,16 +130,16 @@ function createNotificationService(options) {
|
|
|
231
130
|
|
|
232
131
|
// src/identity-service.ts
|
|
233
132
|
var IDENTITY_SERVICE_VERSION = "1.0.0";
|
|
234
|
-
function sendProviderError(send, result2, fallback,
|
|
133
|
+
function sendProviderError(send, result2, fallback, _err) {
|
|
235
134
|
send({
|
|
236
135
|
...result2,
|
|
237
|
-
error:
|
|
136
|
+
error: fallback
|
|
238
137
|
});
|
|
239
138
|
}
|
|
240
139
|
async function getCurrentPubkey(options) {
|
|
241
|
-
const currentSigner = options.getSigner();
|
|
242
|
-
if (!currentSigner?.getPublicKey) return "";
|
|
243
140
|
try {
|
|
141
|
+
const currentSigner = options.getSigner();
|
|
142
|
+
if (!currentSigner?.getPublicKey) return "";
|
|
244
143
|
return await currentSigner.getPublicKey() ?? "";
|
|
245
144
|
} catch {
|
|
246
145
|
return "";
|
|
@@ -253,46 +152,29 @@ function sendOptionalProviderResult(options, send, fallbackResult, errorFallback
|
|
|
253
152
|
}
|
|
254
153
|
Promise.resolve(getCurrentPubkey(options)).then((pubkey) => buildResult(pubkey)).then((result2) => send(result2)).catch((err2) => sendProviderError(send, fallbackResult, errorFallback, err2));
|
|
255
154
|
}
|
|
256
|
-
function sendIdentityError(send, id, typeBase, error) {
|
|
257
|
-
send({ type: `${typeBase}.error`, id, error });
|
|
258
|
-
}
|
|
259
|
-
function sendSignerError(send, id, typeBase, fallback, err2) {
|
|
260
|
-
sendIdentityError(send, id, typeBase, err2?.message ?? fallback);
|
|
261
|
-
}
|
|
262
155
|
function handleGetPublicKey(options, id, send) {
|
|
263
|
-
|
|
264
|
-
if (!signer) {
|
|
265
|
-
const result2 = {
|
|
266
|
-
type: "identity.getPublicKey.result",
|
|
267
|
-
id,
|
|
268
|
-
pubkey: ""
|
|
269
|
-
};
|
|
270
|
-
send(result2);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
Promise.resolve(signer.getPublicKey?.()).then((pubkey) => {
|
|
156
|
+
Promise.resolve().then(() => options.getSigner()).then(async (signer) => signer?.getPublicKey ? signer.getPublicKey() : "").catch(() => "").then((pubkey) => {
|
|
274
157
|
const result2 = {
|
|
275
158
|
type: "identity.getPublicKey.result",
|
|
276
159
|
id,
|
|
277
160
|
pubkey: pubkey ?? ""
|
|
278
161
|
};
|
|
279
162
|
send(result2);
|
|
280
|
-
})
|
|
163
|
+
});
|
|
281
164
|
}
|
|
282
165
|
function handleGetRelays(options, id, send) {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
Promise.resolve(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
relays
|
|
166
|
+
const fallbackResult = {
|
|
167
|
+
type: "identity.getRelays.result",
|
|
168
|
+
id,
|
|
169
|
+
relays: {}
|
|
170
|
+
};
|
|
171
|
+
Promise.resolve().then(() => options.getSigner()).then(async (signer) => {
|
|
172
|
+
if (!signer?.getRelays) return fallbackResult;
|
|
173
|
+
return {
|
|
174
|
+
...fallbackResult,
|
|
175
|
+
relays: await signer.getRelays() ?? {}
|
|
293
176
|
};
|
|
294
|
-
|
|
295
|
-
}).catch((err2) => sendSignerError(send, id, "identity.getRelays", "getRelays failed", err2));
|
|
177
|
+
}).catch(() => ({ ...fallbackResult, error: "identity.getRelays failed" })).then(send);
|
|
296
178
|
}
|
|
297
179
|
function handleReadProvider(options, id, message, send) {
|
|
298
180
|
switch (message.type) {
|
|
@@ -301,11 +183,11 @@ function handleReadProvider(options, id, message, send) {
|
|
|
301
183
|
options,
|
|
302
184
|
send,
|
|
303
185
|
{ type: "identity.getProfile.result", id, profile: null },
|
|
304
|
-
"getProfile failed",
|
|
186
|
+
"identity.getProfile failed",
|
|
305
187
|
options.getProfile ? async (pubkey) => ({
|
|
306
188
|
type: "identity.getProfile.result",
|
|
307
189
|
id,
|
|
308
|
-
profile: await options.getProfile(pubkey, message)
|
|
190
|
+
profile: await options.getProfile(pubkey, message) ?? null
|
|
309
191
|
}) : void 0
|
|
310
192
|
);
|
|
311
193
|
return true;
|
|
@@ -314,11 +196,11 @@ function handleReadProvider(options, id, message, send) {
|
|
|
314
196
|
options,
|
|
315
197
|
send,
|
|
316
198
|
{ type: "identity.getFollows.result", id, pubkeys: [] },
|
|
317
|
-
"getFollows failed",
|
|
199
|
+
"identity.getFollows failed",
|
|
318
200
|
options.getFollows ? async (pubkey) => ({
|
|
319
201
|
type: "identity.getFollows.result",
|
|
320
202
|
id,
|
|
321
|
-
pubkeys: await options.getFollows(pubkey, message)
|
|
203
|
+
pubkeys: await options.getFollows(pubkey, message) ?? []
|
|
322
204
|
}) : void 0
|
|
323
205
|
);
|
|
324
206
|
return true;
|
|
@@ -330,11 +212,11 @@ function handleReadProvider(options, id, message, send) {
|
|
|
330
212
|
options,
|
|
331
213
|
send,
|
|
332
214
|
{ type: "identity.getZaps.result", id, zaps: [] },
|
|
333
|
-
"getZaps failed",
|
|
215
|
+
"identity.getZaps failed",
|
|
334
216
|
options.getZaps ? async (pubkey) => ({
|
|
335
217
|
type: "identity.getZaps.result",
|
|
336
218
|
id,
|
|
337
|
-
zaps: await options.getZaps(pubkey, message)
|
|
219
|
+
zaps: await options.getZaps(pubkey, message) ?? []
|
|
338
220
|
}) : void 0
|
|
339
221
|
);
|
|
340
222
|
return true;
|
|
@@ -343,11 +225,11 @@ function handleReadProvider(options, id, message, send) {
|
|
|
343
225
|
options,
|
|
344
226
|
send,
|
|
345
227
|
{ type: "identity.getMutes.result", id, pubkeys: [] },
|
|
346
|
-
"getMutes failed",
|
|
228
|
+
"identity.getMutes failed",
|
|
347
229
|
options.getMutes ? async (pubkey) => ({
|
|
348
230
|
type: "identity.getMutes.result",
|
|
349
231
|
id,
|
|
350
|
-
pubkeys: await options.getMutes(pubkey, message)
|
|
232
|
+
pubkeys: await options.getMutes(pubkey, message) ?? []
|
|
351
233
|
}) : void 0
|
|
352
234
|
);
|
|
353
235
|
return true;
|
|
@@ -356,11 +238,11 @@ function handleReadProvider(options, id, message, send) {
|
|
|
356
238
|
options,
|
|
357
239
|
send,
|
|
358
240
|
{ type: "identity.getBlocked.result", id, pubkeys: [] },
|
|
359
|
-
"getBlocked failed",
|
|
241
|
+
"identity.getBlocked failed",
|
|
360
242
|
options.getBlocked ? async (pubkey) => ({
|
|
361
243
|
type: "identity.getBlocked.result",
|
|
362
244
|
id,
|
|
363
|
-
pubkeys: await options.getBlocked(pubkey, message)
|
|
245
|
+
pubkeys: await options.getBlocked(pubkey, message) ?? []
|
|
364
246
|
}) : void 0
|
|
365
247
|
);
|
|
366
248
|
return true;
|
|
@@ -369,11 +251,11 @@ function handleReadProvider(options, id, message, send) {
|
|
|
369
251
|
options,
|
|
370
252
|
send,
|
|
371
253
|
{ type: "identity.getBadges.result", id, badges: [] },
|
|
372
|
-
"getBadges failed",
|
|
254
|
+
"identity.getBadges failed",
|
|
373
255
|
options.getBadges ? async (pubkey) => ({
|
|
374
256
|
type: "identity.getBadges.result",
|
|
375
257
|
id,
|
|
376
|
-
badges: await options.getBadges(pubkey, message)
|
|
258
|
+
badges: await options.getBadges(pubkey, message) ?? []
|
|
377
259
|
}) : void 0
|
|
378
260
|
);
|
|
379
261
|
return true;
|
|
@@ -386,11 +268,11 @@ function handleGetList(options, id, message, send) {
|
|
|
386
268
|
options,
|
|
387
269
|
send,
|
|
388
270
|
{ type: "identity.getList.result", id, entries: [] },
|
|
389
|
-
"getList failed",
|
|
271
|
+
"identity.getList failed",
|
|
390
272
|
options.getList ? async (pubkey) => ({
|
|
391
273
|
type: "identity.getList.result",
|
|
392
274
|
id,
|
|
393
|
-
entries: await options.getList(message.listType, pubkey, message)
|
|
275
|
+
entries: await options.getList(message.listType, pubkey, message) ?? []
|
|
394
276
|
}) : void 0
|
|
395
277
|
);
|
|
396
278
|
}
|
|
@@ -404,9 +286,7 @@ function handleIdentityServiceMessage(options, message, send) {
|
|
|
404
286
|
handleGetRelays(options, id, send);
|
|
405
287
|
return;
|
|
406
288
|
default:
|
|
407
|
-
|
|
408
|
-
sendIdentityError(send, id, message.type, `Unknown identity method: ${message.type}`);
|
|
409
|
-
}
|
|
289
|
+
handleReadProvider(options, id, message, send);
|
|
410
290
|
}
|
|
411
291
|
}
|
|
412
292
|
function createIdentityService(options) {
|
|
@@ -430,6 +310,37 @@ import { createRelayEventResultWithHints } from "@kehto/runtime";
|
|
|
430
310
|
var EOSE_FALLBACK_MS = 15e3;
|
|
431
311
|
function createRelayPoolService(options) {
|
|
432
312
|
const tracked = /* @__PURE__ */ new Map();
|
|
313
|
+
function publishEvent(relayMessage, resultType, send) {
|
|
314
|
+
const id = typeof relayMessage.id === "string" ? relayMessage.id : "";
|
|
315
|
+
const event = relayMessage.event;
|
|
316
|
+
if (!event || typeof event !== "object") {
|
|
317
|
+
send({ type: resultType, id, ok: false, error: "invalid event" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (!options.isAvailable()) {
|
|
321
|
+
send({ type: resultType, id, ok: false, error: "no relay pool available" });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
void (async () => {
|
|
325
|
+
try {
|
|
326
|
+
await options.publish(event);
|
|
327
|
+
send({
|
|
328
|
+
type: resultType,
|
|
329
|
+
id,
|
|
330
|
+
ok: true,
|
|
331
|
+
event,
|
|
332
|
+
eventId: event.id
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
send({
|
|
336
|
+
type: resultType,
|
|
337
|
+
id,
|
|
338
|
+
ok: false,
|
|
339
|
+
error: error instanceof Error && error.message ? error.message : "publish failed"
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
}
|
|
433
344
|
return {
|
|
434
345
|
descriptor: {
|
|
435
346
|
name: "relay-pool",
|
|
@@ -489,17 +400,11 @@ function createRelayPoolService(options) {
|
|
|
489
400
|
return;
|
|
490
401
|
}
|
|
491
402
|
if (message.type === "relay.publish") {
|
|
492
|
-
|
|
493
|
-
if (event && typeof event === "object" && options.isAvailable()) {
|
|
494
|
-
options.publish(event);
|
|
495
|
-
}
|
|
403
|
+
publishEvent(relayMessage, "relay.publish.result", send);
|
|
496
404
|
return;
|
|
497
405
|
}
|
|
498
406
|
if (message.type === "relay.publishEncrypted") {
|
|
499
|
-
|
|
500
|
-
if (event && typeof event === "object" && options.isAvailable()) {
|
|
501
|
-
options.publish(event);
|
|
502
|
-
}
|
|
407
|
+
publishEvent(relayMessage, "relay.publishEncrypted.result", send);
|
|
503
408
|
return;
|
|
504
409
|
}
|
|
505
410
|
},
|
|
@@ -651,6 +556,48 @@ function isPlainObject(value) {
|
|
|
651
556
|
// src/coordinated-relay.ts
|
|
652
557
|
import { createRelayEventResult as createRelayEventResult2, createRelayEventResultWithHints as createRelayEventResultWithHints2 } from "@kehto/runtime";
|
|
653
558
|
var DEFAULT_EOSE_TIMEOUT_MS = 15e3;
|
|
559
|
+
function publishCoordinatedEvent(options, relayMessage, send) {
|
|
560
|
+
const id = typeof relayMessage.id === "string" ? relayMessage.id : "";
|
|
561
|
+
const event = relayMessage.event;
|
|
562
|
+
if (!event || typeof event !== "object") {
|
|
563
|
+
send({ type: "relay.publish.result", id, ok: false, error: "invalid event" });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!options.relayPool.isAvailable()) {
|
|
567
|
+
send({
|
|
568
|
+
type: "relay.publish.result",
|
|
569
|
+
id,
|
|
570
|
+
ok: false,
|
|
571
|
+
error: "no relay pool available"
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
void (async () => {
|
|
576
|
+
try {
|
|
577
|
+
await options.relayPool.publish(event);
|
|
578
|
+
if (options.cache.isAvailable()) {
|
|
579
|
+
try {
|
|
580
|
+
options.cache.store(event);
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
send({
|
|
585
|
+
type: "relay.publish.result",
|
|
586
|
+
id,
|
|
587
|
+
ok: true,
|
|
588
|
+
event,
|
|
589
|
+
eventId: event.id
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
send({
|
|
593
|
+
type: "relay.publish.result",
|
|
594
|
+
id,
|
|
595
|
+
ok: false,
|
|
596
|
+
error: error instanceof Error && error.message ? error.message : "publish failed"
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
})();
|
|
600
|
+
}
|
|
654
601
|
function createCoordinatedRelay(options) {
|
|
655
602
|
const timeoutMs = options.eoseTimeoutMs ?? DEFAULT_EOSE_TIMEOUT_MS;
|
|
656
603
|
const subs = /* @__PURE__ */ new Map();
|
|
@@ -758,17 +705,7 @@ function createCoordinatedRelay(options) {
|
|
|
758
705
|
return;
|
|
759
706
|
}
|
|
760
707
|
if (message.type === "relay.publish") {
|
|
761
|
-
|
|
762
|
-
if (!event || typeof event !== "object") return;
|
|
763
|
-
if (options.relayPool.isAvailable()) {
|
|
764
|
-
options.relayPool.publish(event);
|
|
765
|
-
}
|
|
766
|
-
if (options.cache.isAvailable()) {
|
|
767
|
-
try {
|
|
768
|
-
options.cache.store(event);
|
|
769
|
-
} catch {
|
|
770
|
-
}
|
|
771
|
-
}
|
|
708
|
+
publishCoordinatedEvent(options, relayMessage, send);
|
|
772
709
|
return;
|
|
773
710
|
}
|
|
774
711
|
},
|
|
@@ -785,8 +722,7 @@ function createCoordinatedRelay(options) {
|
|
|
785
722
|
};
|
|
786
723
|
}
|
|
787
724
|
|
|
788
|
-
// src/keys-service.ts
|
|
789
|
-
var KEYS_SERVICE_VERSION = "1.2.0";
|
|
725
|
+
// src/keys-service-internals.ts
|
|
790
726
|
var MODIFIER_ALIASES = {
|
|
791
727
|
ctrl: "ctrl",
|
|
792
728
|
control: "ctrl",
|
|
@@ -815,127 +751,242 @@ function parseChord(chord) {
|
|
|
815
751
|
out.key = keyTok.length === 1 ? keyTok.toUpperCase() : keyTok;
|
|
816
752
|
return out;
|
|
817
753
|
}
|
|
754
|
+
function formatChord(spec) {
|
|
755
|
+
const parts = [];
|
|
756
|
+
if (spec.alt) parts.push("Alt");
|
|
757
|
+
if (spec.ctrl) parts.push("Ctrl");
|
|
758
|
+
if (spec.shift) parts.push("Shift");
|
|
759
|
+
if (spec.meta) parts.push("Meta");
|
|
760
|
+
parts.push(spec.key);
|
|
761
|
+
return parts.join("+");
|
|
762
|
+
}
|
|
763
|
+
var BROWSER_RESERVED_CTRL_KEYS = /* @__PURE__ */ new Set(["L", "N", "R", "T", "W"]);
|
|
764
|
+
function isReservedKeyChord(spec) {
|
|
765
|
+
if (spec.key === "Tab" || spec.key === "Escape") return true;
|
|
766
|
+
if ((spec.ctrl || spec.meta) && !spec.alt && BROWSER_RESERVED_CTRL_KEYS.has(spec.key)) return true;
|
|
767
|
+
if (spec.alt && !spec.ctrl && !spec.meta && (spec.key === "ArrowLeft" || spec.key === "ArrowRight")) {
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
function chordSpecKey(spec) {
|
|
773
|
+
return `${spec.ctrl}|${spec.alt}|${spec.shift}|${spec.meta}|${spec.key}`;
|
|
774
|
+
}
|
|
775
|
+
function forwardPayload(m) {
|
|
776
|
+
return {
|
|
777
|
+
key: m.key,
|
|
778
|
+
code: m.code,
|
|
779
|
+
ctrlKey: m.ctrl,
|
|
780
|
+
altKey: m.alt,
|
|
781
|
+
shiftKey: m.shift,
|
|
782
|
+
metaKey: m.meta
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function eventKey(ev) {
|
|
786
|
+
const k = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;
|
|
787
|
+
return `${ev.ctrlKey}|${ev.altKey}|${ev.shiftKey}|${ev.metaKey}|${k}`;
|
|
788
|
+
}
|
|
789
|
+
function bindingsForWindow(windowId, registry, windowIndex) {
|
|
790
|
+
const actionIds = windowIndex.get(windowId);
|
|
791
|
+
if (!actionIds) return [];
|
|
792
|
+
const bindings = [];
|
|
793
|
+
for (const actionId of actionIds) {
|
|
794
|
+
const entry = registry.get(actionId);
|
|
795
|
+
if (!entry || entry.windowId !== windowId) continue;
|
|
796
|
+
bindings.push({ actionId, key: entry.chordString });
|
|
797
|
+
}
|
|
798
|
+
return bindings;
|
|
799
|
+
}
|
|
800
|
+
function pushBindings(windowId, registry, windowIndex, send) {
|
|
801
|
+
const payload = {
|
|
802
|
+
type: "keys.bindings",
|
|
803
|
+
bindings: bindingsForWindow(windowId, registry, windowIndex)
|
|
804
|
+
};
|
|
805
|
+
send(payload);
|
|
806
|
+
}
|
|
807
|
+
function removeActionFromWindowIndex(actionId, windowIndex) {
|
|
808
|
+
for (const [wid, set] of windowIndex.entries()) {
|
|
809
|
+
if (set.delete(actionId) && set.size === 0) windowIndex.delete(wid);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/keys-service.ts
|
|
814
|
+
var KEYS_SERVICE_VERSION = "1.3.0";
|
|
815
|
+
function rememberActionForWindow(windowIndex, windowId, actionId) {
|
|
816
|
+
if (!windowIndex.has(windowId)) windowIndex.set(windowId, /* @__PURE__ */ new Set());
|
|
817
|
+
windowIndex.get(windowId).add(actionId);
|
|
818
|
+
}
|
|
819
|
+
function bindActionEntry(registry, windowIndex, actionId, windowId, chord, chordString) {
|
|
820
|
+
registry.set(actionId, { chord, chordString, windowId });
|
|
821
|
+
rememberActionForWindow(windowIndex, windowId, actionId);
|
|
822
|
+
}
|
|
823
|
+
function bindActionAndMarkChanged(registry, windowIndex, changedWindowIds, actionId, windowId, chord, chordString) {
|
|
824
|
+
bindActionEntry(registry, windowIndex, actionId, windowId, chord, chordString);
|
|
825
|
+
changedWindowIds.add(windowId);
|
|
826
|
+
return chordString;
|
|
827
|
+
}
|
|
828
|
+
function handleForwardMessage(message, onForward) {
|
|
829
|
+
onForward?.(forwardPayload(message));
|
|
830
|
+
}
|
|
831
|
+
function sendUnknownKeysMethod(send, message) {
|
|
832
|
+
const id = message.id ?? "";
|
|
833
|
+
send({
|
|
834
|
+
type: `${message.type}.error`,
|
|
835
|
+
id,
|
|
836
|
+
error: `Unknown keys method: ${message.type}`
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function createKeysMessageHandler(options) {
|
|
840
|
+
return (windowId, message, send) => {
|
|
841
|
+
switch (message.type) {
|
|
842
|
+
case "keys.forward":
|
|
843
|
+
handleForwardMessage(message, options.onForward);
|
|
844
|
+
return;
|
|
845
|
+
case "keys.registerAction":
|
|
846
|
+
options.onRegisterAction(windowId, message, send);
|
|
847
|
+
return;
|
|
848
|
+
case "keys.unregisterAction":
|
|
849
|
+
options.onUnregisterAction(windowId, message, send);
|
|
850
|
+
return;
|
|
851
|
+
default:
|
|
852
|
+
sendUnknownKeysMethod(send, message);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function sendRegisterActionResult(send, message, binding, error) {
|
|
857
|
+
const result2 = {
|
|
858
|
+
type: "keys.registerAction.result",
|
|
859
|
+
id: message.id,
|
|
860
|
+
actionId: message.action.id,
|
|
861
|
+
...binding ? { binding } : {},
|
|
862
|
+
...error ? { error } : {}
|
|
863
|
+
};
|
|
864
|
+
send(result2);
|
|
865
|
+
}
|
|
866
|
+
function pushChangedBindings(changedWindowIds, registry, windowIndex, sendHandles, fallbackSend) {
|
|
867
|
+
for (const changedWindowId of changedWindowIds) {
|
|
868
|
+
const ownerSend = sendHandles.get(changedWindowId) ?? fallbackSend;
|
|
869
|
+
pushBindings(changedWindowId, registry, windowIndex, ownerSend);
|
|
870
|
+
if (!windowIndex.has(changedWindowId)) sendHandles.delete(changedWindowId);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
818
873
|
function createKeysService(options = {}) {
|
|
819
874
|
const descriptor = {
|
|
820
875
|
name: "keys",
|
|
821
876
|
version: KEYS_SERVICE_VERSION,
|
|
822
877
|
description: options.hostBridge ? "NIP-5D keys NAP reference handler (host-bridge delegated)" : "NIP-5D keys NAP reference handler (document-level chord listener)"
|
|
823
878
|
};
|
|
824
|
-
function chordSpecKey(spec) {
|
|
825
|
-
return `${spec.ctrl}|${spec.alt}|${spec.shift}|${spec.meta}|${spec.key}`;
|
|
826
|
-
}
|
|
827
879
|
const reservedChordKeys = /* @__PURE__ */ new Set();
|
|
828
880
|
if (options.reservedChords) {
|
|
829
881
|
for (const chordStr of options.reservedChords) {
|
|
830
882
|
reservedChordKeys.add(chordSpecKey(parseChord(chordStr)));
|
|
831
883
|
}
|
|
832
884
|
}
|
|
833
|
-
|
|
834
|
-
const k = m.key.length === 1 ? m.key.toUpperCase() : m.key;
|
|
835
|
-
return `${m.ctrl}|${m.alt}|${m.shift}|${m.meta}|${k}`;
|
|
836
|
-
}
|
|
837
|
-
function forwardPayload(m) {
|
|
838
|
-
return {
|
|
839
|
-
key: m.key,
|
|
840
|
-
code: m.code,
|
|
841
|
-
ctrlKey: m.ctrl,
|
|
842
|
-
altKey: m.alt,
|
|
843
|
-
shiftKey: m.shift,
|
|
844
|
-
metaKey: m.meta
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
function eventKey(ev) {
|
|
848
|
-
const k = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;
|
|
849
|
-
return `${ev.ctrlKey}|${ev.altKey}|${ev.shiftKey}|${ev.metaKey}|${k}`;
|
|
850
|
-
}
|
|
885
|
+
const isUnavailableBinding = (chord) => reservedChordKeys.has(chordSpecKey(chord)) || isReservedKeyChord(chord);
|
|
851
886
|
if (options.hostBridge) {
|
|
852
887
|
const bridge = options.hostBridge;
|
|
853
888
|
const bridgeWindowActions = /* @__PURE__ */ new Map();
|
|
889
|
+
const bridgeActionRegistry = /* @__PURE__ */ new Map();
|
|
890
|
+
const bridgeSendHandles = /* @__PURE__ */ new Map();
|
|
854
891
|
const unsubscribeHandles = /* @__PURE__ */ new Map();
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
type: "keys.action",
|
|
885
|
-
actionId: m.action.id
|
|
886
|
-
};
|
|
887
|
-
send(payload);
|
|
888
|
-
});
|
|
889
|
-
unsubscribeHandles.set(m.action.id, unsubscribe);
|
|
890
|
-
if (!bridgeWindowActions.has(windowId)) bridgeWindowActions.set(windowId, /* @__PURE__ */ new Set());
|
|
891
|
-
bridgeWindowActions.get(windowId).add(m.action.id);
|
|
892
|
-
} catch (err2) {
|
|
893
|
-
const id = m.id ?? "";
|
|
894
|
-
send({
|
|
895
|
-
type: "keys.registerAction.error",
|
|
896
|
-
id,
|
|
897
|
-
error: `bridge subscribe failed: ${err2.message}`
|
|
898
|
-
});
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
const result2 = {
|
|
903
|
-
type: "keys.registerAction.result",
|
|
904
|
-
id: m.id,
|
|
905
|
-
actionId: m.action.id,
|
|
906
|
-
...m.action.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
907
|
-
};
|
|
908
|
-
send(result2);
|
|
909
|
-
return;
|
|
892
|
+
const handleBridgeRegisterAction = (windowId, m, send) => {
|
|
893
|
+
bridgeSendHandles.set(windowId, send);
|
|
894
|
+
let binding;
|
|
895
|
+
const changedWindowIds = /* @__PURE__ */ new Set();
|
|
896
|
+
if (m.action.defaultKey) {
|
|
897
|
+
try {
|
|
898
|
+
const chord = parseChord(m.action.defaultKey);
|
|
899
|
+
const normalizedChord = formatChord(chord);
|
|
900
|
+
const existingEntry = bridgeActionRegistry.get(m.action.id);
|
|
901
|
+
const existing = unsubscribeHandles.get(m.action.id);
|
|
902
|
+
let nextUnsubscribe;
|
|
903
|
+
if (!isUnavailableBinding(chord)) {
|
|
904
|
+
nextUnsubscribe = bridge.subscribe(normalizedChord, (ev) => {
|
|
905
|
+
const e = ev;
|
|
906
|
+
if ("repeat" in e && e.repeat) return;
|
|
907
|
+
options.onForward?.({
|
|
908
|
+
key: e.key,
|
|
909
|
+
code: e.code,
|
|
910
|
+
ctrlKey: e.ctrlKey,
|
|
911
|
+
altKey: e.altKey,
|
|
912
|
+
shiftKey: e.shiftKey,
|
|
913
|
+
metaKey: e.metaKey
|
|
914
|
+
});
|
|
915
|
+
const payload = {
|
|
916
|
+
type: "keys.action",
|
|
917
|
+
actionId: m.action.id
|
|
918
|
+
};
|
|
919
|
+
send(payload);
|
|
920
|
+
});
|
|
910
921
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (unsubscribe) {
|
|
916
|
-
try {
|
|
917
|
-
unsubscribe();
|
|
918
|
-
} catch {
|
|
919
|
-
}
|
|
920
|
-
unsubscribeHandles.delete(m.actionId);
|
|
921
|
-
for (const [wid, set] of bridgeWindowActions.entries()) {
|
|
922
|
-
if (set.delete(m.actionId) && set.size === 0) bridgeWindowActions.delete(wid);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
922
|
+
if (existing) {
|
|
923
|
+
try {
|
|
924
|
+
existing();
|
|
925
|
+
} catch {
|
|
925
926
|
}
|
|
926
|
-
|
|
927
|
+
unsubscribeHandles.delete(m.action.id);
|
|
927
928
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
929
|
+
if (existingEntry) changedWindowIds.add(existingEntry.windowId);
|
|
930
|
+
removeActionFromWindowIndex(m.action.id, bridgeWindowActions);
|
|
931
|
+
bridgeActionRegistry.delete(m.action.id);
|
|
932
|
+
if (nextUnsubscribe) {
|
|
933
|
+
unsubscribeHandles.set(m.action.id, nextUnsubscribe);
|
|
934
|
+
binding = bindActionAndMarkChanged(
|
|
935
|
+
bridgeActionRegistry,
|
|
936
|
+
bridgeWindowActions,
|
|
937
|
+
changedWindowIds,
|
|
938
|
+
m.action.id,
|
|
939
|
+
windowId,
|
|
940
|
+
chord,
|
|
941
|
+
normalizedChord
|
|
942
|
+
);
|
|
936
943
|
}
|
|
944
|
+
} catch (err2) {
|
|
945
|
+
sendRegisterActionResult(
|
|
946
|
+
send,
|
|
947
|
+
m,
|
|
948
|
+
void 0,
|
|
949
|
+
`bridge subscribe failed: ${err2.message}`
|
|
950
|
+
);
|
|
951
|
+
return;
|
|
937
952
|
}
|
|
938
|
-
}
|
|
953
|
+
}
|
|
954
|
+
sendRegisterActionResult(send, m, binding);
|
|
955
|
+
pushChangedBindings(
|
|
956
|
+
changedWindowIds,
|
|
957
|
+
bridgeActionRegistry,
|
|
958
|
+
bridgeWindowActions,
|
|
959
|
+
bridgeSendHandles,
|
|
960
|
+
send
|
|
961
|
+
);
|
|
962
|
+
};
|
|
963
|
+
const handleBridgeUnregisterAction = (_windowId, m, send) => {
|
|
964
|
+
if (!m.actionId) return;
|
|
965
|
+
const unsubscribe = unsubscribeHandles.get(m.actionId);
|
|
966
|
+
const entry = bridgeActionRegistry.get(m.actionId);
|
|
967
|
+
if (!unsubscribe) return;
|
|
968
|
+
try {
|
|
969
|
+
unsubscribe();
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
972
|
+
unsubscribeHandles.delete(m.actionId);
|
|
973
|
+
bridgeActionRegistry.delete(m.actionId);
|
|
974
|
+
removeActionFromWindowIndex(m.actionId, bridgeWindowActions);
|
|
975
|
+
if (entry) {
|
|
976
|
+
const ownerSend = bridgeSendHandles.get(entry.windowId) ?? send;
|
|
977
|
+
pushBindings(entry.windowId, bridgeActionRegistry, bridgeWindowActions, ownerSend);
|
|
978
|
+
if (!bridgeWindowActions.has(entry.windowId)) {
|
|
979
|
+
bridgeSendHandles.delete(entry.windowId);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
return {
|
|
984
|
+
descriptor,
|
|
985
|
+
handleMessage: createKeysMessageHandler({
|
|
986
|
+
onForward: options.onForward,
|
|
987
|
+
onRegisterAction: handleBridgeRegisterAction,
|
|
988
|
+
onUnregisterAction: handleBridgeUnregisterAction
|
|
989
|
+
}),
|
|
939
990
|
onWindowDestroyed(windowId) {
|
|
940
991
|
const actions = bridgeWindowActions.get(windowId);
|
|
941
992
|
if (!actions) return;
|
|
@@ -948,8 +999,10 @@ function createKeysService(options = {}) {
|
|
|
948
999
|
}
|
|
949
1000
|
}
|
|
950
1001
|
unsubscribeHandles.delete(actionId);
|
|
1002
|
+
bridgeActionRegistry.delete(actionId);
|
|
951
1003
|
}
|
|
952
1004
|
bridgeWindowActions.delete(windowId);
|
|
1005
|
+
bridgeSendHandles.delete(windowId);
|
|
953
1006
|
},
|
|
954
1007
|
destroy() {
|
|
955
1008
|
for (const unsubscribe of unsubscribeHandles.values()) {
|
|
@@ -959,7 +1012,9 @@ function createKeysService(options = {}) {
|
|
|
959
1012
|
}
|
|
960
1013
|
}
|
|
961
1014
|
unsubscribeHandles.clear();
|
|
1015
|
+
bridgeActionRegistry.clear();
|
|
962
1016
|
bridgeWindowActions.clear();
|
|
1017
|
+
bridgeSendHandles.clear();
|
|
963
1018
|
}
|
|
964
1019
|
};
|
|
965
1020
|
}
|
|
@@ -1003,8 +1058,7 @@ function createKeysService(options = {}) {
|
|
|
1003
1058
|
if (send) {
|
|
1004
1059
|
const payload = {
|
|
1005
1060
|
type: "keys.action",
|
|
1006
|
-
actionId
|
|
1007
|
-
chord: entry.chord
|
|
1061
|
+
actionId
|
|
1008
1062
|
};
|
|
1009
1063
|
send(payload);
|
|
1010
1064
|
}
|
|
@@ -1012,76 +1066,56 @@ function createKeysService(options = {}) {
|
|
|
1012
1066
|
}
|
|
1013
1067
|
};
|
|
1014
1068
|
target.addEventListener("keydown", listener);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
sendHandles.set(windowId, send);
|
|
1029
|
-
if (m.action.defaultKey) {
|
|
1030
|
-
try {
|
|
1031
|
-
const chord = parseChord(m.action.defaultKey);
|
|
1032
|
-
actionRegistry.set(m.action.id, {
|
|
1033
|
-
chord,
|
|
1034
|
-
chordString: m.action.defaultKey,
|
|
1035
|
-
windowId
|
|
1036
|
-
});
|
|
1037
|
-
if (!windowActions.has(windowId)) windowActions.set(windowId, /* @__PURE__ */ new Set());
|
|
1038
|
-
windowActions.get(windowId).add(m.action.id);
|
|
1039
|
-
} catch (err2) {
|
|
1040
|
-
const id = m.id ?? "";
|
|
1041
|
-
send({
|
|
1042
|
-
type: "keys.registerAction.error",
|
|
1043
|
-
id,
|
|
1044
|
-
error: `invalid chord: ${err2.message}`
|
|
1045
|
-
});
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
const result2 = {
|
|
1050
|
-
type: "keys.registerAction.result",
|
|
1051
|
-
id: m.id,
|
|
1052
|
-
actionId: m.action.id,
|
|
1053
|
-
...m.action.defaultKey ? { binding: m.action.defaultKey } : {}
|
|
1054
|
-
};
|
|
1055
|
-
send(result2);
|
|
1056
|
-
return;
|
|
1069
|
+
const handleDocumentRegisterAction = (windowId, m, send) => {
|
|
1070
|
+
sendHandles.set(windowId, send);
|
|
1071
|
+
let binding;
|
|
1072
|
+
const changedWindowIds = /* @__PURE__ */ new Set();
|
|
1073
|
+
if (m.action.defaultKey) {
|
|
1074
|
+
try {
|
|
1075
|
+
const chord = parseChord(m.action.defaultKey);
|
|
1076
|
+
const normalizedChord = formatChord(chord);
|
|
1077
|
+
const existing = actionRegistry.get(m.action.id);
|
|
1078
|
+
if (existing) {
|
|
1079
|
+
changedWindowIds.add(existing.windowId);
|
|
1080
|
+
removeActionFromWindowIndex(m.action.id, windowActions);
|
|
1081
|
+
actionRegistry.delete(m.action.id);
|
|
1057
1082
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
if (m.actionId && actionRegistry.has(m.actionId)) {
|
|
1061
|
-
const entry = actionRegistry.get(m.actionId);
|
|
1062
|
-
actionRegistry.delete(m.actionId);
|
|
1063
|
-
const set = windowActions.get(entry.windowId);
|
|
1064
|
-
if (set) {
|
|
1065
|
-
set.delete(m.actionId);
|
|
1066
|
-
if (set.size === 0) {
|
|
1067
|
-
windowActions.delete(entry.windowId);
|
|
1068
|
-
sendHandles.delete(entry.windowId);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
default: {
|
|
1075
|
-
const id = message.id ?? "";
|
|
1076
|
-
send({
|
|
1077
|
-
type: `${message.type}.error`,
|
|
1078
|
-
id,
|
|
1079
|
-
error: `Unknown keys method: ${message.type}`
|
|
1080
|
-
});
|
|
1081
|
-
return;
|
|
1083
|
+
if (!isUnavailableBinding(chord)) {
|
|
1084
|
+
binding = bindActionAndMarkChanged(actionRegistry, windowActions, changedWindowIds, m.action.id, windowId, chord, normalizedChord);
|
|
1082
1085
|
}
|
|
1086
|
+
} catch (err2) {
|
|
1087
|
+
sendRegisterActionResult(
|
|
1088
|
+
send,
|
|
1089
|
+
m,
|
|
1090
|
+
void 0,
|
|
1091
|
+
`invalid chord: ${err2.message}`
|
|
1092
|
+
);
|
|
1093
|
+
return;
|
|
1083
1094
|
}
|
|
1084
|
-
}
|
|
1095
|
+
}
|
|
1096
|
+
sendRegisterActionResult(send, m, binding);
|
|
1097
|
+
pushChangedBindings(changedWindowIds, actionRegistry, windowActions, sendHandles, send);
|
|
1098
|
+
};
|
|
1099
|
+
const handleDocumentUnregisterAction = (_windowId, m, send) => {
|
|
1100
|
+
if (!m.actionId || !actionRegistry.has(m.actionId)) return;
|
|
1101
|
+
const entry = actionRegistry.get(m.actionId);
|
|
1102
|
+
actionRegistry.delete(m.actionId);
|
|
1103
|
+
const set = windowActions.get(entry.windowId);
|
|
1104
|
+
if (set) {
|
|
1105
|
+
set.delete(m.actionId);
|
|
1106
|
+
if (set.size === 0) windowActions.delete(entry.windowId);
|
|
1107
|
+
}
|
|
1108
|
+
const ownerSend = sendHandles.get(entry.windowId) ?? send;
|
|
1109
|
+
pushBindings(entry.windowId, actionRegistry, windowActions, ownerSend);
|
|
1110
|
+
if (!windowActions.has(entry.windowId)) sendHandles.delete(entry.windowId);
|
|
1111
|
+
};
|
|
1112
|
+
return {
|
|
1113
|
+
descriptor,
|
|
1114
|
+
handleMessage: createKeysMessageHandler({
|
|
1115
|
+
onForward: options.onForward,
|
|
1116
|
+
onRegisterAction: handleDocumentRegisterAction,
|
|
1117
|
+
onUnregisterAction: handleDocumentUnregisterAction
|
|
1118
|
+
}),
|
|
1085
1119
|
onWindowDestroyed(windowId) {
|
|
1086
1120
|
const actions = windowActions.get(windowId);
|
|
1087
1121
|
if (actions) {
|
|
@@ -1531,8 +1565,15 @@ var DEFAULT_THEME = {
|
|
|
1531
1565
|
// fonts, background, title intentionally undefined — all optional per
|
|
1532
1566
|
// @napplet/nap/theme Theme interface.
|
|
1533
1567
|
};
|
|
1568
|
+
function isCompleteTheme(theme) {
|
|
1569
|
+
const colors = theme?.colors;
|
|
1570
|
+
return typeof colors?.background === "string" && typeof colors.text === "string" && typeof colors.primary === "string";
|
|
1571
|
+
}
|
|
1572
|
+
function normalizeTheme(theme) {
|
|
1573
|
+
return isCompleteTheme(theme) ? theme : DEFAULT_THEME;
|
|
1574
|
+
}
|
|
1534
1575
|
function createThemeService(options = {}) {
|
|
1535
|
-
let currentTheme = options.initialTheme
|
|
1576
|
+
let currentTheme = normalizeTheme(options.initialTheme);
|
|
1536
1577
|
const descriptor = {
|
|
1537
1578
|
name: "theme",
|
|
1538
1579
|
version: THEME_SERVICE_VERSION,
|
|
@@ -1551,19 +1592,15 @@ function createThemeService(options = {}) {
|
|
|
1551
1592
|
send(result2);
|
|
1552
1593
|
return;
|
|
1553
1594
|
}
|
|
1554
|
-
|
|
1555
|
-
type: `${message.type}.error`,
|
|
1556
|
-
id,
|
|
1557
|
-
error: `Unknown theme method: ${message.type}`
|
|
1558
|
-
});
|
|
1595
|
+
return;
|
|
1559
1596
|
},
|
|
1560
1597
|
// Theme service has no per-window state to clean up.
|
|
1561
1598
|
onWindowDestroyed(_windowId) {
|
|
1562
1599
|
}
|
|
1563
1600
|
};
|
|
1564
1601
|
function publishTheme(theme) {
|
|
1565
|
-
currentTheme = theme;
|
|
1566
|
-
const envelope = { type: "theme.changed", theme };
|
|
1602
|
+
currentTheme = normalizeTheme(theme);
|
|
1603
|
+
const envelope = { type: "theme.changed", theme: currentTheme };
|
|
1567
1604
|
options.onBroadcast?.(envelope);
|
|
1568
1605
|
return envelope;
|
|
1569
1606
|
}
|
|
@@ -2070,8 +2107,9 @@ function sanitizePublishOptions(raw) {
|
|
|
2070
2107
|
const out = {};
|
|
2071
2108
|
const relays = stringArray(raw.relays);
|
|
2072
2109
|
if (relays) out.relays = relays;
|
|
2073
|
-
|
|
2074
|
-
|
|
2110
|
+
if (typeof raw.toOutbox === "boolean") out.toOutbox = raw.toOutbox;
|
|
2111
|
+
const toInboxes = stringArray(raw.toInboxes);
|
|
2112
|
+
if (toInboxes) out.toInboxes = toInboxes;
|
|
2075
2113
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
2076
2114
|
}
|
|
2077
2115
|
function sanitizeTarget(raw) {
|
|
@@ -2293,9 +2331,33 @@ function allowed(ctx, urls) {
|
|
|
2293
2331
|
}
|
|
2294
2332
|
return [...out];
|
|
2295
2333
|
}
|
|
2334
|
+
async function resolveRequiredRelays(ctx, pubkeys, direction) {
|
|
2335
|
+
const useWrite = direction === "read";
|
|
2336
|
+
const relays = /* @__PURE__ */ new Set();
|
|
2337
|
+
const missingAuthors = [];
|
|
2338
|
+
const deniedAuthors = [];
|
|
2339
|
+
const uniquePubkeys = [...new Set(pubkeys)];
|
|
2340
|
+
if (uniquePubkeys.length === 0) return { relays: [], missingAuthors, deniedAuthors };
|
|
2341
|
+
const lists = await ctx.loadRelayLists(uniquePubkeys);
|
|
2342
|
+
for (const pubkey of uniquePubkeys) {
|
|
2343
|
+
const entry = lists.get(pubkey);
|
|
2344
|
+
const candidateRelays = entry ? useWrite ? entry.write : entry.read : void 0;
|
|
2345
|
+
if (!candidateRelays || candidateRelays.length === 0) {
|
|
2346
|
+
missingAuthors.push(pubkey);
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
const allowedRelays = allowed(ctx, candidateRelays);
|
|
2350
|
+
if (allowedRelays.length === 0) {
|
|
2351
|
+
deniedAuthors.push(pubkey);
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
for (const url of allowedRelays) relays.add(url);
|
|
2355
|
+
}
|
|
2356
|
+
return { relays: [...relays], missingAuthors, deniedAuthors };
|
|
2357
|
+
}
|
|
2296
2358
|
async function resolvePlan(ctx, pubkeys, direction, relayHints) {
|
|
2297
2359
|
const useWrite = direction === "read";
|
|
2298
|
-
const
|
|
2360
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
2299
2361
|
const missingAuthors = [];
|
|
2300
2362
|
let sawNip65 = false;
|
|
2301
2363
|
if (pubkeys.length > 0) {
|
|
@@ -2305,25 +2367,46 @@ async function resolvePlan(ctx, pubkeys, direction, relayHints) {
|
|
|
2305
2367
|
const relays2 = entry ? useWrite ? entry.write : entry.read : void 0;
|
|
2306
2368
|
if (relays2 && relays2.length > 0) {
|
|
2307
2369
|
sawNip65 = true;
|
|
2308
|
-
for (const url of relays2)
|
|
2370
|
+
for (const url of relays2) discovered.add(url);
|
|
2309
2371
|
} else {
|
|
2310
2372
|
missingAuthors.push(pubkey);
|
|
2311
2373
|
}
|
|
2312
2374
|
}
|
|
2313
2375
|
}
|
|
2314
|
-
|
|
2315
|
-
let relays = allowed(ctx,
|
|
2376
|
+
const hintRelays = allowed(ctx, relayHints ?? []);
|
|
2377
|
+
let relays = allowed(ctx, discovered);
|
|
2316
2378
|
let source;
|
|
2317
|
-
if (relays.length
|
|
2318
|
-
relays = allowed(ctx, ctx.fallbackRelays);
|
|
2319
|
-
source = "fallback";
|
|
2320
|
-
} else {
|
|
2379
|
+
if (relays.length > 0) {
|
|
2321
2380
|
source = sawNip65 ? "nip65" : "policy";
|
|
2381
|
+
} else {
|
|
2382
|
+
relays = allowed(ctx, ctx.fallbackRelays);
|
|
2383
|
+
source = relays.length > 0 ? "fallback" : "policy";
|
|
2384
|
+
}
|
|
2385
|
+
for (const url of hintRelays) {
|
|
2386
|
+
if (!relays.includes(url)) relays.push(url);
|
|
2322
2387
|
}
|
|
2388
|
+
if (relays.length === 0) source = "fallback";
|
|
2323
2389
|
const plan = { relays, source };
|
|
2324
2390
|
if (missingAuthors.length > 0) plan.missingAuthors = missingAuthors;
|
|
2325
2391
|
return plan;
|
|
2326
2392
|
}
|
|
2393
|
+
function discoveredReadRelays(ctx, authors, lists) {
|
|
2394
|
+
const relays = /* @__PURE__ */ new Set();
|
|
2395
|
+
const missingAuthors = [];
|
|
2396
|
+
for (const author of new Set(authors)) {
|
|
2397
|
+
const writeRelays = lists.get(author)?.write;
|
|
2398
|
+
if (!writeRelays || writeRelays.length === 0) {
|
|
2399
|
+
missingAuthors.push(author);
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
for (const url of allowed(ctx, writeRelays)) relays.add(url);
|
|
2403
|
+
}
|
|
2404
|
+
return { relays: [...relays], missingAuthors };
|
|
2405
|
+
}
|
|
2406
|
+
function immediateReadRelays(ctx, relayHints) {
|
|
2407
|
+
const hints = allowed(ctx, relayHints ?? []);
|
|
2408
|
+
return hints.length > 0 ? hints : allowed(ctx, ctx.fallbackRelays);
|
|
2409
|
+
}
|
|
2327
2410
|
function recordRelay(collector, id, relayUrl) {
|
|
2328
2411
|
let set = collector.relayMap.get(id);
|
|
2329
2412
|
if (!set) {
|
|
@@ -2332,81 +2415,166 @@ function recordRelay(collector, id, relayUrl) {
|
|
|
2332
2415
|
}
|
|
2333
2416
|
set.add(relayUrl);
|
|
2334
2417
|
}
|
|
2335
|
-
function
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
)
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
);
|
|
2348
|
-
return { events, incomplete: timedOut };
|
|
2349
|
-
}
|
|
2350
|
-
function collectFromRelays(ctx, filters, relayUrls2, timeoutMs) {
|
|
2351
|
-
return new Promise((resolve) => {
|
|
2352
|
-
const collector = { seen: /* @__PURE__ */ new Map(), relayMap: /* @__PURE__ */ new Map(), verifications: [] };
|
|
2353
|
-
const handles = [];
|
|
2354
|
-
let eoseCount = 0;
|
|
2355
|
-
let finished = false;
|
|
2356
|
-
let timedOut = false;
|
|
2357
|
-
function finalize() {
|
|
2358
|
-
if (finished) return;
|
|
2359
|
-
finished = true;
|
|
2360
|
-
clearTimeout(timer);
|
|
2361
|
-
for (const handle of handles) {
|
|
2362
|
-
try {
|
|
2363
|
-
handle.unsubscribe();
|
|
2364
|
-
} catch {
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
void Promise.all(collector.verifications).then(() => resolve(buildCollectResult(collector, timedOut)));
|
|
2418
|
+
function collectedEventResult(collector, event) {
|
|
2419
|
+
return createRelayEventResultWithHints3(event, [...collector.relayMap.get(event.id) ?? []]);
|
|
2420
|
+
}
|
|
2421
|
+
function admitEvent(ctx, collector, event, sink, isOpen, onSettled) {
|
|
2422
|
+
if (collector.admitted.has(event.id)) return;
|
|
2423
|
+
collector.admitted.add(event.id);
|
|
2424
|
+
collector.pendingVerifications += 1;
|
|
2425
|
+
void ctx.verify(event).then((ok) => {
|
|
2426
|
+
if (!isOpen()) return;
|
|
2427
|
+
if (!ok) {
|
|
2428
|
+
collector.relayMap.delete(event.id);
|
|
2429
|
+
return;
|
|
2368
2430
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
}
|
|
2373
|
-
for (const relayUrl of relayUrls2) {
|
|
2374
|
-
handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2375
|
-
if (finished) return;
|
|
2376
|
-
if (item === "EOSE") {
|
|
2377
|
-
eoseCount += 1;
|
|
2378
|
-
if (eoseCount >= relayUrls2.length) finalize();
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
recordRelay(collector, item.id, relayUrl);
|
|
2382
|
-
admitEvent(ctx, collector, item);
|
|
2383
|
-
}));
|
|
2431
|
+
collector.seen.set(event.id, event);
|
|
2432
|
+
try {
|
|
2433
|
+
sink.event(collectedEventResult(collector, event));
|
|
2434
|
+
} catch {
|
|
2384
2435
|
}
|
|
2385
|
-
|
|
2436
|
+
}).finally(() => {
|
|
2437
|
+
collector.pendingVerifications -= 1;
|
|
2438
|
+
onSettled();
|
|
2386
2439
|
});
|
|
2387
2440
|
}
|
|
2441
|
+
function buildQueryResult(collector, options, incomplete, subscribedAny) {
|
|
2442
|
+
let events = [...collector.seen.values()].map((event) => collectedEventResult(collector, event));
|
|
2443
|
+
if (options?.limit !== void 0 && events.length > options.limit) {
|
|
2444
|
+
events = [...events].sort((a, b) => b.event.created_at - a.event.created_at).slice(0, options.limit);
|
|
2445
|
+
}
|
|
2446
|
+
const result2 = { events };
|
|
2447
|
+
if (incomplete || !subscribedAny) result2.incomplete = true;
|
|
2448
|
+
if (!subscribedAny) result2.error = "relay list unavailable";
|
|
2449
|
+
return result2;
|
|
2450
|
+
}
|
|
2388
2451
|
function relayPoolSubscribe(ctx, filters, relayUrl, cb) {
|
|
2389
2452
|
return ctx.relayPool.subscribe(filters, [relayUrl], cb);
|
|
2390
2453
|
}
|
|
2391
|
-
|
|
2454
|
+
function startQueryImpl(ctx, filters, options, sink) {
|
|
2392
2455
|
if (!ctx.relayPool.isAvailable()) {
|
|
2393
|
-
return {
|
|
2456
|
+
return {
|
|
2457
|
+
result: Promise.resolve({ events: [], incomplete: true, error: "relay list unavailable" }),
|
|
2458
|
+
close() {
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2394
2461
|
}
|
|
2395
2462
|
const authors = deriveAuthors(filters, options?.authors);
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2463
|
+
const collector = {
|
|
2464
|
+
seen: /* @__PURE__ */ new Map(),
|
|
2465
|
+
admitted: /* @__PURE__ */ new Set(),
|
|
2466
|
+
relayMap: /* @__PURE__ */ new Map(),
|
|
2467
|
+
pendingVerifications: 0
|
|
2468
|
+
};
|
|
2469
|
+
const handles = /* @__PURE__ */ new Map();
|
|
2470
|
+
const activeRelays = /* @__PURE__ */ new Set();
|
|
2471
|
+
const eoseRelays = /* @__PURE__ */ new Set();
|
|
2472
|
+
let discoveryPending = authors.length > 0;
|
|
2473
|
+
let incomplete = false;
|
|
2474
|
+
let subscribedAny = false;
|
|
2475
|
+
let finished = false;
|
|
2476
|
+
let resolveResult;
|
|
2477
|
+
const result2 = new Promise((resolve) => {
|
|
2478
|
+
resolveResult = resolve;
|
|
2479
|
+
});
|
|
2480
|
+
let timer;
|
|
2481
|
+
function closeHandles() {
|
|
2482
|
+
for (const handle of handles.values()) {
|
|
2483
|
+
try {
|
|
2484
|
+
handle.unsubscribe();
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
handles.clear();
|
|
2489
|
+
}
|
|
2490
|
+
function finalize(forceIncomplete = false) {
|
|
2491
|
+
if (finished) return;
|
|
2492
|
+
finished = true;
|
|
2493
|
+
incomplete ||= forceIncomplete;
|
|
2494
|
+
clearTimeout(timer);
|
|
2495
|
+
closeHandles();
|
|
2496
|
+
resolveResult(buildQueryResult(collector, options, incomplete, subscribedAny));
|
|
2497
|
+
}
|
|
2498
|
+
function maybeFinalize() {
|
|
2499
|
+
if (finished || discoveryPending || collector.pendingVerifications > 0) return;
|
|
2500
|
+
if (activeRelays.size === 0 || eoseRelays.size >= activeRelays.size) finalize();
|
|
2501
|
+
}
|
|
2502
|
+
function attachRelay(relayUrl) {
|
|
2503
|
+
if (finished || activeRelays.has(relayUrl)) return;
|
|
2504
|
+
activeRelays.add(relayUrl);
|
|
2505
|
+
try {
|
|
2506
|
+
const handle = relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2507
|
+
if (finished) return;
|
|
2508
|
+
if (item === "EOSE") {
|
|
2509
|
+
eoseRelays.add(relayUrl);
|
|
2510
|
+
maybeFinalize();
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
recordRelay(collector, item.id, relayUrl);
|
|
2514
|
+
admitEvent(ctx, collector, item, sink, () => !finished, maybeFinalize);
|
|
2515
|
+
});
|
|
2516
|
+
subscribedAny = true;
|
|
2517
|
+
if (finished) {
|
|
2518
|
+
try {
|
|
2519
|
+
handle.unsubscribe();
|
|
2520
|
+
} catch {
|
|
2521
|
+
}
|
|
2522
|
+
} else {
|
|
2523
|
+
handles.set(relayUrl, handle);
|
|
2524
|
+
}
|
|
2525
|
+
} catch {
|
|
2526
|
+
activeRelays.delete(relayUrl);
|
|
2527
|
+
incomplete = true;
|
|
2528
|
+
}
|
|
2406
2529
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2530
|
+
function attachMany(relayUrls2) {
|
|
2531
|
+
for (const relayUrl of relayUrls2) attachRelay(relayUrl);
|
|
2532
|
+
}
|
|
2533
|
+
timer = setTimeout(() => finalize(true), options?.timeoutMs ?? ctx.defaultTimeoutMs);
|
|
2534
|
+
if (authors.length === 0) {
|
|
2535
|
+
discoveryPending = false;
|
|
2536
|
+
attachMany(allowed(ctx, [...ctx.fallbackRelays, ...options?.relays ?? []]));
|
|
2537
|
+
maybeFinalize();
|
|
2538
|
+
} else {
|
|
2539
|
+
let loaded;
|
|
2540
|
+
try {
|
|
2541
|
+
loaded = ctx.loadRelayLists(authors);
|
|
2542
|
+
} catch {
|
|
2543
|
+
discoveryPending = false;
|
|
2544
|
+
incomplete = true;
|
|
2545
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2546
|
+
maybeFinalize();
|
|
2547
|
+
return { result: result2, close: () => finalize(true) };
|
|
2548
|
+
}
|
|
2549
|
+
if (loaded instanceof Map) {
|
|
2550
|
+
const discovery = discoveredReadRelays(ctx, authors, loaded);
|
|
2551
|
+
discoveryPending = false;
|
|
2552
|
+
incomplete ||= discovery.missingAuthors.length > 0;
|
|
2553
|
+
const initial = allowed(ctx, [...options?.relays ?? [], ...discovery.relays]);
|
|
2554
|
+
attachMany(initial.length > 0 ? initial : allowed(ctx, ctx.fallbackRelays));
|
|
2555
|
+
maybeFinalize();
|
|
2556
|
+
} else {
|
|
2557
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2558
|
+
void Promise.resolve(loaded).then((lists) => {
|
|
2559
|
+
if (finished) return;
|
|
2560
|
+
const discovery = discoveredReadRelays(ctx, authors, lists);
|
|
2561
|
+
discoveryPending = false;
|
|
2562
|
+
incomplete ||= discovery.missingAuthors.length > 0;
|
|
2563
|
+
attachMany(discovery.relays);
|
|
2564
|
+
maybeFinalize();
|
|
2565
|
+
}).catch(() => {
|
|
2566
|
+
if (finished) return;
|
|
2567
|
+
discoveryPending = false;
|
|
2568
|
+
incomplete = true;
|
|
2569
|
+
maybeFinalize();
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
return { result: result2, close: () => finalize(true) };
|
|
2574
|
+
}
|
|
2575
|
+
function queryImpl(ctx, filters, options) {
|
|
2576
|
+
return startQueryImpl(ctx, filters, options, { event: () => {
|
|
2577
|
+
} }).result;
|
|
2410
2578
|
}
|
|
2411
2579
|
async function getEventImpl(ctx, eventId, options) {
|
|
2412
2580
|
if (typeof eventId !== "string" || eventId.length === 0) {
|
|
@@ -2430,57 +2598,138 @@ async function getEventImpl(ctx, eventId, options) {
|
|
|
2430
2598
|
return result2;
|
|
2431
2599
|
}
|
|
2432
2600
|
function closeLiveSub(sub) {
|
|
2433
|
-
for (const handle of sub.handles) {
|
|
2601
|
+
for (const handle of sub.handles.values()) {
|
|
2434
2602
|
try {
|
|
2435
2603
|
handle.unsubscribe();
|
|
2436
2604
|
} catch {
|
|
2437
2605
|
}
|
|
2438
2606
|
}
|
|
2439
|
-
sub.handles.
|
|
2607
|
+
sub.handles.clear();
|
|
2440
2608
|
}
|
|
2441
2609
|
function attachLiveRelay(ctx, sub, filters, relayUrl, sink) {
|
|
2442
|
-
sub.
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2610
|
+
if (sub.closed || sub.relays.has(relayUrl)) return false;
|
|
2611
|
+
sub.relays.add(relayUrl);
|
|
2612
|
+
try {
|
|
2613
|
+
const handle = relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2614
|
+
if (sub.closed || item === "EOSE" || sub.admitted.has(item.id)) return;
|
|
2615
|
+
sub.admitted.add(item.id);
|
|
2616
|
+
void ctx.verify(item).then((ok) => {
|
|
2617
|
+
if (!ok || sub.closed || sub.seen.has(item.id)) return;
|
|
2618
|
+
sub.seen.add(item.id);
|
|
2619
|
+
sink.event(createRelayEventResultWithHints3(item, [relayUrl]));
|
|
2620
|
+
});
|
|
2450
2621
|
});
|
|
2451
|
-
|
|
2622
|
+
if (sub.closed) {
|
|
2623
|
+
try {
|
|
2624
|
+
handle.unsubscribe();
|
|
2625
|
+
} catch {
|
|
2626
|
+
}
|
|
2627
|
+
} else {
|
|
2628
|
+
sub.handles.set(relayUrl, handle);
|
|
2629
|
+
}
|
|
2630
|
+
return true;
|
|
2631
|
+
} catch {
|
|
2632
|
+
sub.relays.delete(relayUrl);
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2452
2635
|
}
|
|
2453
2636
|
function startSubscription(ctx, filters, options, sink) {
|
|
2454
2637
|
const authors = deriveAuthors(filters, options?.authors);
|
|
2455
|
-
const sub = {
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2638
|
+
const sub = {
|
|
2639
|
+
handles: /* @__PURE__ */ new Map(),
|
|
2640
|
+
relays: /* @__PURE__ */ new Set(),
|
|
2641
|
+
admitted: /* @__PURE__ */ new Set(),
|
|
2642
|
+
seen: /* @__PURE__ */ new Set(),
|
|
2643
|
+
closed: false
|
|
2644
|
+
};
|
|
2645
|
+
let discoveryOpen = authors.length > 0;
|
|
2646
|
+
let discoveryTimer;
|
|
2647
|
+
function attachMany(relayUrls2) {
|
|
2648
|
+
for (const relayUrl of relayUrls2) attachLiveRelay(ctx, sub, filters, relayUrl, sink);
|
|
2649
|
+
}
|
|
2650
|
+
function closeUnavailable() {
|
|
2651
|
+
if (sub.closed || sub.handles.size > 0) return;
|
|
2652
|
+
sub.closed = true;
|
|
2653
|
+
sink.closed("relay list unavailable");
|
|
2654
|
+
}
|
|
2655
|
+
if (!ctx.relayPool.isAvailable()) {
|
|
2656
|
+
closeUnavailable();
|
|
2657
|
+
} else if (authors.length === 0) {
|
|
2658
|
+
discoveryOpen = false;
|
|
2659
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2660
|
+
closeUnavailable();
|
|
2661
|
+
} else {
|
|
2662
|
+
let loaded;
|
|
2663
|
+
try {
|
|
2664
|
+
loaded = ctx.loadRelayLists(authors);
|
|
2665
|
+
} catch {
|
|
2666
|
+
discoveryOpen = false;
|
|
2667
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2668
|
+
closeUnavailable();
|
|
2669
|
+
loaded = /* @__PURE__ */ new Map();
|
|
2461
2670
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2671
|
+
if (discoveryOpen && loaded instanceof Map) {
|
|
2672
|
+
const discovery = discoveredReadRelays(ctx, authors, loaded);
|
|
2673
|
+
discoveryOpen = false;
|
|
2674
|
+
const initial = allowed(ctx, [...options?.relays ?? [], ...discovery.relays]);
|
|
2675
|
+
attachMany(initial.length > 0 ? initial : allowed(ctx, ctx.fallbackRelays));
|
|
2676
|
+
closeUnavailable();
|
|
2677
|
+
} else if (discoveryOpen) {
|
|
2678
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2679
|
+
discoveryTimer = setTimeout(() => {
|
|
2680
|
+
discoveryOpen = false;
|
|
2681
|
+
closeUnavailable();
|
|
2682
|
+
}, options?.timeoutMs ?? ctx.defaultTimeoutMs);
|
|
2683
|
+
void Promise.resolve(loaded).then((lists) => {
|
|
2684
|
+
if (!discoveryOpen || sub.closed) return;
|
|
2685
|
+
discoveryOpen = false;
|
|
2686
|
+
clearTimeout(discoveryTimer);
|
|
2687
|
+
attachMany(discoveredReadRelays(ctx, authors, lists).relays);
|
|
2688
|
+
closeUnavailable();
|
|
2689
|
+
}).catch(() => {
|
|
2690
|
+
if (!discoveryOpen || sub.closed) return;
|
|
2691
|
+
discoveryOpen = false;
|
|
2692
|
+
clearTimeout(discoveryTimer);
|
|
2693
|
+
closeUnavailable();
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2466
2697
|
return {
|
|
2467
2698
|
close() {
|
|
2468
2699
|
if (sub.closed) return;
|
|
2469
2700
|
sub.closed = true;
|
|
2701
|
+
discoveryOpen = false;
|
|
2702
|
+
clearTimeout(discoveryTimer);
|
|
2470
2703
|
closeLiveSub(sub);
|
|
2471
2704
|
}
|
|
2472
2705
|
};
|
|
2473
2706
|
}
|
|
2474
2707
|
async function resolvePublishTargets(ctx, signed, options) {
|
|
2475
2708
|
const targets = /* @__PURE__ */ new Set();
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2709
|
+
const requiredTargets = /* @__PURE__ */ new Set();
|
|
2710
|
+
if (options?.toOutbox !== false) {
|
|
2711
|
+
const outboxPlan = await resolveRequiredRelays(ctx, [signed.pubkey], "read");
|
|
2712
|
+
if (outboxPlan.missingAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "relay list unavailable" };
|
|
2713
|
+
if (outboxPlan.deniedAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "policy denied" };
|
|
2714
|
+
for (const url of outboxPlan.relays) {
|
|
2715
|
+
targets.add(url);
|
|
2716
|
+
requiredTargets.add(url);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
if (options?.toInboxes && options.toInboxes.length > 0) {
|
|
2720
|
+
const inboxPlan = await resolveRequiredRelays(ctx, options.toInboxes, "write");
|
|
2721
|
+
if (inboxPlan.missingAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "relay list unavailable" };
|
|
2722
|
+
if (inboxPlan.deniedAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "policy denied" };
|
|
2723
|
+
for (const url of inboxPlan.relays) {
|
|
2724
|
+
targets.add(url);
|
|
2725
|
+
requiredTargets.add(url);
|
|
2726
|
+
}
|
|
2481
2727
|
}
|
|
2482
2728
|
for (const url of options?.relays ?? []) targets.add(url);
|
|
2483
|
-
return
|
|
2729
|
+
return {
|
|
2730
|
+
relayUrls: allowed(ctx, targets),
|
|
2731
|
+
requiredRelayUrls: allowed(ctx, requiredTargets)
|
|
2732
|
+
};
|
|
2484
2733
|
}
|
|
2485
2734
|
async function publishImpl(ctx, template, options) {
|
|
2486
2735
|
if (!ctx.signEvent) return { ok: false, error: "publish denied" };
|
|
@@ -2491,7 +2740,11 @@ async function publishImpl(ctx, template, options) {
|
|
|
2491
2740
|
} catch (err2) {
|
|
2492
2741
|
return { ok: false, error: err2 instanceof Error ? err2.message : "sign failed" };
|
|
2493
2742
|
}
|
|
2494
|
-
const
|
|
2743
|
+
const publishTargets = await resolvePublishTargets(ctx, signed, options);
|
|
2744
|
+
if (publishTargets.error) {
|
|
2745
|
+
return { ok: false, event: signed, eventId: signed.id, error: publishTargets.error };
|
|
2746
|
+
}
|
|
2747
|
+
const relayUrls2 = publishTargets.relayUrls;
|
|
2495
2748
|
if (relayUrls2.length === 0) return { ok: false, event: signed, eventId: signed.id, error: "relay list unavailable" };
|
|
2496
2749
|
let relays;
|
|
2497
2750
|
try {
|
|
@@ -2499,7 +2752,7 @@ async function publishImpl(ctx, template, options) {
|
|
|
2499
2752
|
} catch (err2) {
|
|
2500
2753
|
return { ok: false, event: signed, eventId: signed.id, error: err2 instanceof Error ? err2.message : "publish failed" };
|
|
2501
2754
|
}
|
|
2502
|
-
const ok = Object.values(relays).some(Boolean);
|
|
2755
|
+
const ok = publishTargets.requiredRelayUrls.length > 0 ? publishTargets.requiredRelayUrls.every((url) => relays[url] === true) : Object.values(relays).some(Boolean);
|
|
2503
2756
|
const result2 = { ok, event: signed, eventId: signed.id, relays };
|
|
2504
2757
|
if (!ok) result2.error = "publish denied";
|
|
2505
2758
|
return result2;
|
|
@@ -2534,6 +2787,7 @@ function createRelayPoolOutboxRouter(options) {
|
|
|
2534
2787
|
return {
|
|
2535
2788
|
getEvent: (eventId, eventOptions) => getEventImpl(ctx, eventId, eventOptions),
|
|
2536
2789
|
query: (filters, queryOptions) => queryImpl(ctx, filters, queryOptions),
|
|
2790
|
+
queryStream: (filters, queryOptions, sink) => startQueryImpl(ctx, filters, queryOptions, sink),
|
|
2537
2791
|
subscribe: (filters, subscribeOptions, sink) => startSubscription(ctx, filters, subscribeOptions, sink),
|
|
2538
2792
|
publish: (template, publishOptions) => publishImpl(ctx, template, publishOptions),
|
|
2539
2793
|
resolveRelays: (target) => {
|
|
@@ -2695,6 +2949,7 @@ function createHttpUploader(options) {
|
|
|
2695
2949
|
const digest = options.digestSha256 ?? defaultDigestSha256;
|
|
2696
2950
|
const nowS = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
2697
2951
|
const defaultRail = options.defaultRail ?? firstConfiguredRail(rails);
|
|
2952
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
2698
2953
|
async function upload(request, ctx) {
|
|
2699
2954
|
const rail = request.rail ?? defaultRail;
|
|
2700
2955
|
const config = rail === "nip96" ? rails.nip96 : rail === "blossom" ? rails.blossom : void 0;
|
|
@@ -2705,18 +2960,32 @@ function createHttpUploader(options) {
|
|
|
2705
2960
|
if (!server) {
|
|
2706
2961
|
return failed(ctx.uploadId, rail, "no server configured");
|
|
2707
2962
|
}
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2963
|
+
const controller = new AbortController();
|
|
2964
|
+
inFlight.set(ctx.uploadId, controller);
|
|
2965
|
+
let sha256;
|
|
2710
2966
|
try {
|
|
2711
|
-
|
|
2967
|
+
const bytes = await toBytes(request.data);
|
|
2968
|
+
sha256 = await digest(bytes);
|
|
2969
|
+
if (controller.signal.aborted) {
|
|
2970
|
+
return cancelled(ctx.uploadId, rail, sha256);
|
|
2971
|
+
}
|
|
2972
|
+
return rail === "nip96" ? await uploadNip96({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller }) : await uploadBlossom({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller });
|
|
2712
2973
|
} catch (err2) {
|
|
2974
|
+
if (controller.signal.aborted) {
|
|
2975
|
+
return cancelled(ctx.uploadId, rail, sha256);
|
|
2976
|
+
}
|
|
2713
2977
|
return failed(ctx.uploadId, rail, toErrorMessage3(err2), sha256);
|
|
2978
|
+
} finally {
|
|
2979
|
+
inFlight.delete(ctx.uploadId);
|
|
2714
2980
|
}
|
|
2715
2981
|
}
|
|
2716
|
-
|
|
2982
|
+
function cancel(uploadId) {
|
|
2983
|
+
inFlight.get(uploadId)?.abort();
|
|
2984
|
+
}
|
|
2985
|
+
return { upload, cancel };
|
|
2717
2986
|
}
|
|
2718
2987
|
async function uploadNip96(args) {
|
|
2719
|
-
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
|
|
2988
|
+
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller } = args;
|
|
2720
2989
|
const auth = await signEvent({
|
|
2721
2990
|
kind: KIND_NIP98,
|
|
2722
2991
|
created_at: nowS(),
|
|
@@ -2732,10 +3001,12 @@ async function uploadNip96(args) {
|
|
|
2732
3001
|
if (request.caption !== void 0) form.append("caption", request.caption);
|
|
2733
3002
|
if (request.mimeType !== void 0) form.append("content_type", request.mimeType);
|
|
2734
3003
|
if (request.noTransform) form.append("no_transform", "true");
|
|
3004
|
+
emitUploading(ctx, "nip96", bytes.byteLength, nowS);
|
|
2735
3005
|
const res = await fetchFn(server, {
|
|
2736
3006
|
method: "POST",
|
|
2737
3007
|
headers: { Authorization: nostrAuthHeader(auth) },
|
|
2738
|
-
body: form
|
|
3008
|
+
body: form,
|
|
3009
|
+
signal: controller.signal
|
|
2739
3010
|
});
|
|
2740
3011
|
if (!res.ok) {
|
|
2741
3012
|
return failed(ctx.uploadId, "nip96", `server rejected (HTTP ${res.status})`, sha256);
|
|
@@ -2773,7 +3044,7 @@ function fromNip94Tags(uploadId, rail, tags, fallbackSize, fallbackSha) {
|
|
|
2773
3044
|
return result2;
|
|
2774
3045
|
}
|
|
2775
3046
|
async function uploadBlossom(args) {
|
|
2776
|
-
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
|
|
3047
|
+
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller } = args;
|
|
2777
3048
|
const auth = await signEvent({
|
|
2778
3049
|
kind: KIND_BLOSSOM_AUTH,
|
|
2779
3050
|
created_at: nowS(),
|
|
@@ -2787,33 +3058,65 @@ async function uploadBlossom(args) {
|
|
|
2787
3058
|
const endpoint = `${trimTrailingSlash(server)}/upload`;
|
|
2788
3059
|
const headers = { Authorization: nostrAuthHeader(auth) };
|
|
2789
3060
|
if (request.mimeType) headers["Content-Type"] = request.mimeType;
|
|
3061
|
+
emitUploading(ctx, "blossom", bytes.byteLength, nowS);
|
|
2790
3062
|
const res = await fetchFn(endpoint, {
|
|
2791
3063
|
method: "PUT",
|
|
2792
3064
|
headers,
|
|
2793
|
-
body: bytesToArrayBuffer(bytes)
|
|
3065
|
+
body: bytesToArrayBuffer(bytes),
|
|
3066
|
+
signal: controller.signal
|
|
2794
3067
|
});
|
|
2795
3068
|
if (!res.ok) {
|
|
2796
3069
|
return failed(ctx.uploadId, "blossom", `server rejected (HTTP ${res.status})`, sha256);
|
|
2797
3070
|
}
|
|
2798
3071
|
const blob = await res.json();
|
|
2799
|
-
if (
|
|
3072
|
+
if (typeof blob.url !== "string" || blob.url.length === 0) {
|
|
2800
3073
|
return failed(ctx.uploadId, "blossom", "server returned no url", sha256);
|
|
2801
3074
|
}
|
|
3075
|
+
if (typeof blob.sha256 !== "string" || blob.sha256.length === 0) {
|
|
3076
|
+
return failed(ctx.uploadId, "blossom", "server returned no sha256", sha256);
|
|
3077
|
+
}
|
|
3078
|
+
if (blob.sha256 !== sha256) {
|
|
3079
|
+
return failed(ctx.uploadId, "blossom", "server returned mismatched sha256", sha256);
|
|
3080
|
+
}
|
|
3081
|
+
if (!Number.isSafeInteger(blob.size) || (blob.size ?? -1) < 0) {
|
|
3082
|
+
return failed(ctx.uploadId, "blossom", "server returned invalid size", sha256);
|
|
3083
|
+
}
|
|
3084
|
+
if (blob.size !== bytes.byteLength) {
|
|
3085
|
+
return failed(ctx.uploadId, "blossom", "server returned mismatched size", sha256);
|
|
3086
|
+
}
|
|
3087
|
+
const mimeType = blob.type ?? request.mimeType;
|
|
3088
|
+
const nip94 = [["url", blob.url]];
|
|
3089
|
+
if (mimeType) nip94.push(["m", mimeType]);
|
|
3090
|
+
nip94.push(["x", sha256], ["size", String(bytes.byteLength)]);
|
|
2802
3091
|
const result2 = {
|
|
2803
3092
|
ok: true,
|
|
2804
3093
|
uploadId: ctx.uploadId,
|
|
2805
3094
|
status: "complete",
|
|
2806
3095
|
rail: "blossom",
|
|
2807
3096
|
url: blob.url,
|
|
2808
|
-
sha256
|
|
2809
|
-
size:
|
|
3097
|
+
sha256,
|
|
3098
|
+
size: bytes.byteLength,
|
|
3099
|
+
nip94
|
|
2810
3100
|
};
|
|
2811
|
-
if (
|
|
3101
|
+
if (mimeType) result2.mimeType = mimeType;
|
|
2812
3102
|
return result2;
|
|
2813
3103
|
}
|
|
2814
3104
|
function failed(uploadId, rail, error, sha256) {
|
|
2815
3105
|
return { ok: false, uploadId, status: "failed", rail, error, ...sha256 ? { sha256 } : {} };
|
|
2816
3106
|
}
|
|
3107
|
+
function cancelled(uploadId, rail, sha256) {
|
|
3108
|
+
return { ok: false, uploadId, status: "cancelled", rail, error: "user cancelled", ...sha256 ? { sha256 } : {} };
|
|
3109
|
+
}
|
|
3110
|
+
function emitUploading(ctx, rail, bytesTotal, nowS) {
|
|
3111
|
+
ctx.onStatus({
|
|
3112
|
+
ok: true,
|
|
3113
|
+
uploadId: ctx.uploadId,
|
|
3114
|
+
status: "uploading",
|
|
3115
|
+
rail,
|
|
3116
|
+
bytesTotal,
|
|
3117
|
+
updatedAt: nowS() * 1e3
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
2817
3120
|
function firstConfiguredRail(rails) {
|
|
2818
3121
|
if (rails.nip96?.servers?.length) return "nip96";
|
|
2819
3122
|
if (rails.blossom?.servers?.length) return "blossom";
|
|
@@ -2863,42 +3166,125 @@ function toErrorMessage4(err2) {
|
|
|
2863
3166
|
if (typeof err2 === "string") return err2;
|
|
2864
3167
|
return "intent request failed";
|
|
2865
3168
|
}
|
|
3169
|
+
function hasOwn(value, key) {
|
|
3170
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
3171
|
+
}
|
|
3172
|
+
function validateIntentRequest(value) {
|
|
3173
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3174
|
+
return { error: "invalid convention" };
|
|
3175
|
+
}
|
|
3176
|
+
const request = value;
|
|
3177
|
+
if (typeof request.archetype !== "string" || request.archetype.length === 0 || typeof request.action !== "string" || request.action.length === 0 || typeof request.convention !== "string" || request.convention.length === 0) {
|
|
3178
|
+
return { error: "invalid convention" };
|
|
3179
|
+
}
|
|
3180
|
+
const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(request.convention);
|
|
3181
|
+
if (!match || match[1] !== request.archetype || match[2] !== request.action) {
|
|
3182
|
+
return { error: "invalid convention" };
|
|
3183
|
+
}
|
|
3184
|
+
const allowedKeys = ["archetype", "action", "convention", "payload", "handler", "behavior"];
|
|
3185
|
+
if (Object.keys(request).some((key) => !allowedKeys.includes(key))) {
|
|
3186
|
+
return { error: "invoke rejected" };
|
|
3187
|
+
}
|
|
3188
|
+
if (hasOwn(request, "handler") && request.handler !== void 0 && (typeof request.handler !== "string" || request.handler.length === 0)) {
|
|
3189
|
+
return { error: "invoke rejected" };
|
|
3190
|
+
}
|
|
3191
|
+
let behavior;
|
|
3192
|
+
if (hasOwn(request, "behavior") && request.behavior !== void 0) {
|
|
3193
|
+
if (typeof request.behavior !== "object" || request.behavior === null || Array.isArray(request.behavior)) {
|
|
3194
|
+
return { error: "invoke rejected" };
|
|
3195
|
+
}
|
|
3196
|
+
const record = request.behavior;
|
|
3197
|
+
if (Object.keys(record).some((key) => key !== "focus" && key !== "reuse") || hasOwn(record, "focus") && typeof record.focus !== "boolean" || hasOwn(record, "reuse") && typeof record.reuse !== "boolean") {
|
|
3198
|
+
return { error: "invoke rejected" };
|
|
3199
|
+
}
|
|
3200
|
+
behavior = {
|
|
3201
|
+
...record.focus === void 0 ? {} : { focus: record.focus },
|
|
3202
|
+
...record.reuse === void 0 ? {} : { reuse: record.reuse }
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
return {
|
|
3206
|
+
request: {
|
|
3207
|
+
archetype: request.archetype,
|
|
3208
|
+
action: request.action,
|
|
3209
|
+
convention: request.convention,
|
|
3210
|
+
...hasOwn(request, "payload") ? { payload: request.payload } : {},
|
|
3211
|
+
...typeof request.handler === "string" ? { handler: request.handler } : {},
|
|
3212
|
+
...behavior === void 0 ? {} : { behavior }
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
function settleResolverCall(call, send, resultType, id, onValue) {
|
|
3217
|
+
let pending;
|
|
3218
|
+
try {
|
|
3219
|
+
pending = Promise.resolve(call());
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
send({ type: resultType, id, error: toErrorMessage4(error) });
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
pending.then((value) => send(onValue(value))).catch((error) => send({ type: resultType, id, error: toErrorMessage4(error) }));
|
|
3225
|
+
}
|
|
3226
|
+
function rejectInvoke(send, id) {
|
|
3227
|
+
send({
|
|
3228
|
+
type: "intent.invoke.result",
|
|
3229
|
+
id,
|
|
3230
|
+
result: { ok: false, error: "invoke rejected" }
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
2866
3233
|
function createIntentService(options) {
|
|
2867
3234
|
if (!options || typeof options.resolver !== "object" || options.resolver === null) {
|
|
2868
3235
|
throw new Error("createIntentService: options.resolver is required");
|
|
2869
3236
|
}
|
|
2870
3237
|
const { resolver } = options;
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
for (const send of windows.values()) {
|
|
2874
|
-
send({ type: "intent.changed", availability });
|
|
2875
|
-
}
|
|
2876
|
-
});
|
|
2877
|
-
function settle6(call, send, resultType, id, onValue) {
|
|
2878
|
-
let pending;
|
|
2879
|
-
try {
|
|
2880
|
-
pending = Promise.resolve(call());
|
|
2881
|
-
} catch (err2) {
|
|
2882
|
-
send({ type: resultType, id, error: toErrorMessage4(err2) });
|
|
2883
|
-
return;
|
|
2884
|
-
}
|
|
2885
|
-
pending.then((value) => send(onValue(value))).catch((err2) => send({ type: resultType, id, error: toErrorMessage4(err2) }));
|
|
2886
|
-
}
|
|
3238
|
+
let runtimeContext;
|
|
3239
|
+
let unsubscribeChanged;
|
|
2887
3240
|
function handleInvoke(windowId, msg, send) {
|
|
2888
3241
|
const m = msg;
|
|
2889
3242
|
const id = m.id ?? "";
|
|
2890
|
-
const
|
|
2891
|
-
if (
|
|
2892
|
-
send({
|
|
3243
|
+
const validation = validateIntentRequest(m.request);
|
|
3244
|
+
if ("error" in validation) {
|
|
3245
|
+
send({
|
|
3246
|
+
type: "intent.invoke.result",
|
|
3247
|
+
id,
|
|
3248
|
+
result: { ok: false, error: validation.error }
|
|
3249
|
+
});
|
|
2893
3250
|
return;
|
|
2894
3251
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
send,
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3252
|
+
const sender = runtimeContext?.resolveDTag(windowId);
|
|
3253
|
+
if (!sender) {
|
|
3254
|
+
rejectInvoke(send, id);
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
let pending;
|
|
3258
|
+
try {
|
|
3259
|
+
pending = Promise.resolve(resolver.invoke(validation.request, { sender }));
|
|
3260
|
+
} catch {
|
|
3261
|
+
rejectInvoke(send, id);
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
void pending.then(
|
|
3265
|
+
(outcome) => {
|
|
3266
|
+
if (outcome.result.ok && (!("retained" in outcome) || typeof outcome.retained.start !== "function")) {
|
|
3267
|
+
rejectInvoke(send, id);
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
send({
|
|
3271
|
+
type: "intent.invoke.result",
|
|
3272
|
+
id,
|
|
3273
|
+
result: outcome.result
|
|
3274
|
+
});
|
|
3275
|
+
if (!outcome.result.ok || !("retained" in outcome)) return;
|
|
3276
|
+
try {
|
|
3277
|
+
const started = outcome.retained.start();
|
|
3278
|
+
void Promise.resolve(started).catch(() => {
|
|
3279
|
+
});
|
|
3280
|
+
} catch {
|
|
3281
|
+
}
|
|
3282
|
+
},
|
|
3283
|
+
() => {
|
|
3284
|
+
rejectInvoke(send, id);
|
|
3285
|
+
}
|
|
3286
|
+
).catch(() => {
|
|
3287
|
+
});
|
|
2902
3288
|
}
|
|
2903
3289
|
function handleAvailable(msg, send) {
|
|
2904
3290
|
const m = msg;
|
|
@@ -2908,7 +3294,7 @@ function createIntentService(options) {
|
|
|
2908
3294
|
return;
|
|
2909
3295
|
}
|
|
2910
3296
|
const archetype = m.archetype;
|
|
2911
|
-
|
|
3297
|
+
settleResolverCall(
|
|
2912
3298
|
() => resolver.available(archetype),
|
|
2913
3299
|
send,
|
|
2914
3300
|
"intent.available.result",
|
|
@@ -2919,7 +3305,7 @@ function createIntentService(options) {
|
|
|
2919
3305
|
function handleHandlers(msg, send) {
|
|
2920
3306
|
const m = msg;
|
|
2921
3307
|
const id = m.id ?? "";
|
|
2922
|
-
|
|
3308
|
+
settleResolverCall(
|
|
2923
3309
|
() => resolver.handlers(),
|
|
2924
3310
|
send,
|
|
2925
3311
|
"intent.handlers.result",
|
|
@@ -2929,8 +3315,31 @@ function createIntentService(options) {
|
|
|
2929
3315
|
}
|
|
2930
3316
|
return {
|
|
2931
3317
|
descriptor: INTENT_DESCRIPTOR,
|
|
3318
|
+
onRegistered(context) {
|
|
3319
|
+
try {
|
|
3320
|
+
unsubscribeChanged?.();
|
|
3321
|
+
} catch {
|
|
3322
|
+
}
|
|
3323
|
+
runtimeContext = context;
|
|
3324
|
+
unsubscribeChanged = resolver.onChanged?.((availability) => {
|
|
3325
|
+
const current = runtimeContext;
|
|
3326
|
+
if (!current) return;
|
|
3327
|
+
const message = { type: "intent.changed", availability };
|
|
3328
|
+
for (const windowId of current.listWindowIds()) {
|
|
3329
|
+
current.sendToEligibleNapplet(windowId, message);
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
},
|
|
3333
|
+
onUnregistered() {
|
|
3334
|
+
const unsubscribe = unsubscribeChanged;
|
|
3335
|
+
unsubscribeChanged = void 0;
|
|
3336
|
+
runtimeContext = void 0;
|
|
3337
|
+
try {
|
|
3338
|
+
unsubscribe?.();
|
|
3339
|
+
} catch {
|
|
3340
|
+
}
|
|
3341
|
+
},
|
|
2932
3342
|
handleMessage(windowId, message, send) {
|
|
2933
|
-
windows.set(windowId, send);
|
|
2934
3343
|
switch (message.type) {
|
|
2935
3344
|
case "intent.invoke":
|
|
2936
3345
|
handleInvoke(windowId, message, send);
|
|
@@ -2944,9 +3353,6 @@ function createIntentService(options) {
|
|
|
2944
3353
|
default:
|
|
2945
3354
|
return;
|
|
2946
3355
|
}
|
|
2947
|
-
},
|
|
2948
|
-
onWindowDestroyed(windowId) {
|
|
2949
|
-
windows.delete(windowId);
|
|
2950
3356
|
}
|
|
2951
3357
|
};
|
|
2952
3358
|
}
|
|
@@ -2960,91 +3366,126 @@ function candidatesFor(catalog, archetype, defaultHandler) {
|
|
|
2960
3366
|
candidates.push({
|
|
2961
3367
|
dTag: entry.dTag,
|
|
2962
3368
|
...entry.title === void 0 ? {} : { title: entry.title },
|
|
2963
|
-
actions: support.actions,
|
|
2964
|
-
|
|
3369
|
+
actions: [...support.actions],
|
|
3370
|
+
conventions: [...support.conventions],
|
|
3371
|
+
contracts: support.contracts.map((contract) => ({
|
|
3372
|
+
convention: contract.convention,
|
|
3373
|
+
...contract.eventKinds === void 0 ? {} : { eventKinds: [...contract.eventKinds] }
|
|
3374
|
+
})),
|
|
2965
3375
|
...entry.dTag === defaultHandler ? { isDefault: true } : {}
|
|
2966
3376
|
});
|
|
2967
3377
|
}
|
|
2968
3378
|
return candidates;
|
|
2969
3379
|
}
|
|
2970
|
-
function
|
|
2971
|
-
return { ok: false,
|
|
3380
|
+
function reject(error) {
|
|
3381
|
+
return { ok: false, error };
|
|
2972
3382
|
}
|
|
2973
3383
|
function createCatalogIntentResolver(options) {
|
|
2974
3384
|
if (!options || typeof options.loadCatalog !== "function") {
|
|
2975
3385
|
throw new Error("createCatalogIntentResolver: options.loadCatalog is required");
|
|
2976
3386
|
}
|
|
2977
|
-
if (!options.
|
|
2978
|
-
throw new Error("createCatalogIntentResolver: options.
|
|
3387
|
+
if (!options.targets || typeof options.targets.retain !== "function") {
|
|
3388
|
+
throw new Error("createCatalogIntentResolver: options.targets is required");
|
|
2979
3389
|
}
|
|
2980
|
-
const {
|
|
3390
|
+
const {
|
|
3391
|
+
loadCatalog,
|
|
3392
|
+
targets,
|
|
3393
|
+
getDefaultHandler,
|
|
3394
|
+
chooseHandler,
|
|
3395
|
+
authorizeExplicitHandler
|
|
3396
|
+
} = options;
|
|
2981
3397
|
const listeners = /* @__PURE__ */ new Set();
|
|
2982
3398
|
async function availabilityFor(archetype) {
|
|
2983
3399
|
const catalog = await loadCatalog();
|
|
2984
|
-
const
|
|
2985
|
-
const candidates = candidatesFor(catalog, archetype,
|
|
3400
|
+
const defaultHandler = getDefaultHandler?.(archetype);
|
|
3401
|
+
const candidates = candidatesFor(catalog, archetype, defaultHandler);
|
|
2986
3402
|
return {
|
|
2987
3403
|
archetype,
|
|
2988
3404
|
available: candidates.length > 0,
|
|
2989
3405
|
candidates,
|
|
2990
|
-
hasDefault:
|
|
3406
|
+
hasDefault: defaultHandler !== void 0 && candidates.some((candidate) => candidate.dTag === defaultHandler)
|
|
2991
3407
|
};
|
|
2992
3408
|
}
|
|
2993
|
-
async function
|
|
3409
|
+
async function chooseCompatible(request, candidates, sender) {
|
|
3410
|
+
if (!chooseHandler) {
|
|
3411
|
+
return request.handler === "choose" ? { error: "user cancelled" } : { error: "invoke rejected" };
|
|
3412
|
+
}
|
|
3413
|
+
const picked = await chooseHandler(request.archetype, candidates, sender);
|
|
3414
|
+
if (picked === void 0) return { error: "user cancelled" };
|
|
3415
|
+
const candidate = candidates.find((item) => item.dTag === picked);
|
|
3416
|
+
return candidate ? { candidate } : { error: "invoke rejected" };
|
|
3417
|
+
}
|
|
3418
|
+
async function pickHandler(request, compatible, sender) {
|
|
3419
|
+
const preference = request.handler;
|
|
2994
3420
|
if (typeof preference === "string" && preference !== "default" && preference !== "choose") {
|
|
2995
|
-
|
|
3421
|
+
const candidate = compatible.find((item) => item.dTag === preference);
|
|
3422
|
+
if (!candidate || !authorizeExplicitHandler) return { error: "invoke rejected" };
|
|
3423
|
+
let authorized = false;
|
|
3424
|
+
try {
|
|
3425
|
+
authorized = await authorizeExplicitHandler(sender, preference, request, candidate);
|
|
3426
|
+
} catch {
|
|
3427
|
+
return { error: "invoke rejected" };
|
|
3428
|
+
}
|
|
3429
|
+
return authorized ? { candidate } : { error: "invoke rejected" };
|
|
2996
3430
|
}
|
|
2997
3431
|
if (preference === "choose") {
|
|
2998
|
-
|
|
2999
|
-
return picked ?? null;
|
|
3432
|
+
return chooseCompatible(request, compatible, sender);
|
|
3000
3433
|
}
|
|
3001
|
-
const
|
|
3002
|
-
if (
|
|
3003
|
-
|
|
3004
|
-
if (chooseHandler) {
|
|
3005
|
-
const picked = await chooseHandler(archetype, candidates, callerWindowId);
|
|
3006
|
-
return picked ?? null;
|
|
3434
|
+
const defaultCandidate = compatible.find((candidate) => candidate.isDefault === true);
|
|
3435
|
+
if (preference === "default") {
|
|
3436
|
+
return defaultCandidate ? { candidate: defaultCandidate } : { error: "invoke rejected" };
|
|
3007
3437
|
}
|
|
3008
|
-
return
|
|
3438
|
+
if (defaultCandidate) return { candidate: defaultCandidate };
|
|
3439
|
+
if (compatible.length === 1) return { candidate: compatible[0] };
|
|
3440
|
+
return chooseCompatible(request, compatible, sender);
|
|
3009
3441
|
}
|
|
3010
3442
|
async function invoke(request, context) {
|
|
3011
|
-
const archetype = request.archetype;
|
|
3012
|
-
const action = request.action ?? "open";
|
|
3013
3443
|
const catalog = await loadCatalog();
|
|
3014
|
-
const
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
if (
|
|
3021
|
-
|
|
3022
|
-
if (protocol !== void 0 && handler.protocols.length > 0 && !handler.protocols.includes(protocol)) {
|
|
3023
|
-
return fail(archetype, action, "unsupported protocol");
|
|
3444
|
+
const defaultHandler = getDefaultHandler?.(request.archetype);
|
|
3445
|
+
const candidates = candidatesFor(catalog, request.archetype, defaultHandler);
|
|
3446
|
+
if (candidates.length === 0) return { result: reject("no handler") };
|
|
3447
|
+
const compatible = candidates.filter((candidate) => candidate.contracts.some((contract) => contract.convention === request.convention));
|
|
3448
|
+
if (compatible.length === 0) return { result: reject("unsupported convention") };
|
|
3449
|
+
const sender = context.sender;
|
|
3450
|
+
if (typeof sender !== "string" || sender.length === 0) {
|
|
3451
|
+
return { result: reject("invoke rejected") };
|
|
3024
3452
|
}
|
|
3025
|
-
|
|
3453
|
+
const selected = await pickHandler(request, compatible, sender);
|
|
3454
|
+
if ("error" in selected) return { result: reject(selected.error) };
|
|
3455
|
+
const delivery = Object.freeze({
|
|
3456
|
+
sender,
|
|
3457
|
+
archetype: request.archetype,
|
|
3458
|
+
action: request.action,
|
|
3459
|
+
convention: request.convention,
|
|
3460
|
+
...request.payload === void 0 ? {} : { payload: request.payload }
|
|
3461
|
+
});
|
|
3462
|
+
const behavior = request.behavior === void 0 ? void 0 : Object.freeze({
|
|
3463
|
+
...request.behavior.focus === void 0 ? {} : { focus: request.behavior.focus },
|
|
3464
|
+
...request.behavior.reuse === void 0 ? {} : { reuse: request.behavior.reuse }
|
|
3465
|
+
});
|
|
3466
|
+
const params = Object.freeze({
|
|
3467
|
+
handler: selected.candidate.dTag,
|
|
3468
|
+
delivery,
|
|
3469
|
+
...behavior === void 0 ? {} : { behavior }
|
|
3470
|
+
});
|
|
3471
|
+
let retained;
|
|
3026
3472
|
try {
|
|
3027
|
-
|
|
3028
|
-
dTag: handler.dTag,
|
|
3029
|
-
archetype,
|
|
3030
|
-
action,
|
|
3031
|
-
...protocol === void 0 ? {} : { protocol },
|
|
3032
|
-
...request.payload === void 0 ? {} : { payload: request.payload },
|
|
3033
|
-
...request.behavior === void 0 ? {} : { behavior: request.behavior },
|
|
3034
|
-
callerWindowId: context.windowId
|
|
3035
|
-
});
|
|
3036
|
-
windowId = opened.windowId;
|
|
3473
|
+
retained = await targets.retain(params);
|
|
3037
3474
|
} catch {
|
|
3038
|
-
return
|
|
3475
|
+
return { result: reject("invoke rejected") };
|
|
3476
|
+
}
|
|
3477
|
+
if (!retained || typeof retained.start !== "function") {
|
|
3478
|
+
return { result: reject("invoke rejected") };
|
|
3039
3479
|
}
|
|
3040
3480
|
return {
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3481
|
+
result: {
|
|
3482
|
+
ok: true,
|
|
3483
|
+
archetype: request.archetype,
|
|
3484
|
+
action: request.action,
|
|
3485
|
+
convention: request.convention,
|
|
3486
|
+
handler: selected.candidate.dTag
|
|
3487
|
+
},
|
|
3488
|
+
retained
|
|
3048
3489
|
};
|
|
3049
3490
|
}
|
|
3050
3491
|
async function handlers() {
|
|
@@ -3053,7 +3494,7 @@ function createCatalogIntentResolver(options) {
|
|
|
3053
3494
|
for (const entry of catalog) {
|
|
3054
3495
|
for (const slug of Object.keys(entry.archetypes)) archetypes.add(slug);
|
|
3055
3496
|
}
|
|
3056
|
-
return Promise.all([...archetypes].map((
|
|
3497
|
+
return Promise.all([...archetypes].map((archetype) => availabilityFor(archetype)));
|
|
3057
3498
|
}
|
|
3058
3499
|
return {
|
|
3059
3500
|
invoke,
|
|
@@ -3073,14 +3514,35 @@ function createCatalogIntentResolver(options) {
|
|
|
3073
3514
|
}
|
|
3074
3515
|
|
|
3075
3516
|
// src/manifest-intent-catalog.ts
|
|
3076
|
-
|
|
3517
|
+
function actionFromConvention(slug, convention) {
|
|
3518
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
3519
|
+
throw new TypeError("manifest archetype slug is invalid");
|
|
3520
|
+
}
|
|
3521
|
+
const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
|
|
3522
|
+
if (!match || match[1] !== slug) {
|
|
3523
|
+
throw new TypeError("manifest archetype convention is invalid or mismatched");
|
|
3524
|
+
}
|
|
3525
|
+
return match[2];
|
|
3526
|
+
}
|
|
3077
3527
|
function manifestToIntentCatalogEntry(manifest) {
|
|
3078
3528
|
const archetypes = {};
|
|
3079
|
-
for (const { slug,
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3529
|
+
for (const { slug, convention, eventKinds } of manifest.archetypes) {
|
|
3530
|
+
const action = actionFromConvention(slug, convention);
|
|
3531
|
+
if (eventKinds?.some((kind) => !Number.isSafeInteger(kind) || kind < 0)) {
|
|
3532
|
+
throw new TypeError("manifest archetype event kinds must be unsigned safe integers");
|
|
3533
|
+
}
|
|
3534
|
+
const support = archetypes[slug] ??= {
|
|
3535
|
+
actions: [],
|
|
3536
|
+
conventions: [],
|
|
3537
|
+
contracts: []
|
|
3538
|
+
};
|
|
3539
|
+
const contract = {
|
|
3540
|
+
convention,
|
|
3541
|
+
...eventKinds === void 0 || eventKinds.length === 0 ? {} : { eventKinds: [...eventKinds] }
|
|
3083
3542
|
};
|
|
3543
|
+
support.contracts.push(contract);
|
|
3544
|
+
if (!support.actions.includes(action)) support.actions.push(action);
|
|
3545
|
+
if (!support.conventions.includes(convention)) support.conventions.push(convention);
|
|
3084
3546
|
}
|
|
3085
3547
|
return {
|
|
3086
3548
|
dTag: manifest.dTag,
|
|
@@ -4569,7 +5031,6 @@ function createCordnDmAdapter(options) {
|
|
|
4569
5031
|
}
|
|
4570
5032
|
export {
|
|
4571
5033
|
DmMemoryStore,
|
|
4572
|
-
createAudioService,
|
|
4573
5034
|
createBleService,
|
|
4574
5035
|
createBrowserMediaBridge,
|
|
4575
5036
|
createCacheService,
|