@kehto/services 0.16.1 → 0.16.5
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 +31 -15
- package/dist/cvm-nostr-transport.d.ts +2 -0
- package/dist/cvm-nostr-transport.js.map +1 -1
- package/dist/index.d.ts +103 -95
- package/dist/index.js +649 -286
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -785,8 +785,7 @@ function createCoordinatedRelay(options) {
|
|
|
785
785
|
};
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
-
// src/keys-service.ts
|
|
789
|
-
var KEYS_SERVICE_VERSION = "1.2.0";
|
|
788
|
+
// src/keys-service-internals.ts
|
|
790
789
|
var MODIFIER_ALIASES = {
|
|
791
790
|
ctrl: "ctrl",
|
|
792
791
|
control: "ctrl",
|
|
@@ -815,127 +814,242 @@ function parseChord(chord) {
|
|
|
815
814
|
out.key = keyTok.length === 1 ? keyTok.toUpperCase() : keyTok;
|
|
816
815
|
return out;
|
|
817
816
|
}
|
|
817
|
+
function formatChord(spec) {
|
|
818
|
+
const parts = [];
|
|
819
|
+
if (spec.alt) parts.push("Alt");
|
|
820
|
+
if (spec.ctrl) parts.push("Ctrl");
|
|
821
|
+
if (spec.shift) parts.push("Shift");
|
|
822
|
+
if (spec.meta) parts.push("Meta");
|
|
823
|
+
parts.push(spec.key);
|
|
824
|
+
return parts.join("+");
|
|
825
|
+
}
|
|
826
|
+
var BROWSER_RESERVED_CTRL_KEYS = /* @__PURE__ */ new Set(["L", "N", "R", "T", "W"]);
|
|
827
|
+
function isReservedKeyChord(spec) {
|
|
828
|
+
if (spec.key === "Tab" || spec.key === "Escape") return true;
|
|
829
|
+
if ((spec.ctrl || spec.meta) && !spec.alt && BROWSER_RESERVED_CTRL_KEYS.has(spec.key)) return true;
|
|
830
|
+
if (spec.alt && !spec.ctrl && !spec.meta && (spec.key === "ArrowLeft" || spec.key === "ArrowRight")) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
function chordSpecKey(spec) {
|
|
836
|
+
return `${spec.ctrl}|${spec.alt}|${spec.shift}|${spec.meta}|${spec.key}`;
|
|
837
|
+
}
|
|
838
|
+
function forwardPayload(m) {
|
|
839
|
+
return {
|
|
840
|
+
key: m.key,
|
|
841
|
+
code: m.code,
|
|
842
|
+
ctrlKey: m.ctrl,
|
|
843
|
+
altKey: m.alt,
|
|
844
|
+
shiftKey: m.shift,
|
|
845
|
+
metaKey: m.meta
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function eventKey(ev) {
|
|
849
|
+
const k = ev.key.length === 1 ? ev.key.toUpperCase() : ev.key;
|
|
850
|
+
return `${ev.ctrlKey}|${ev.altKey}|${ev.shiftKey}|${ev.metaKey}|${k}`;
|
|
851
|
+
}
|
|
852
|
+
function bindingsForWindow(windowId, registry, windowIndex) {
|
|
853
|
+
const actionIds = windowIndex.get(windowId);
|
|
854
|
+
if (!actionIds) return [];
|
|
855
|
+
const bindings = [];
|
|
856
|
+
for (const actionId of actionIds) {
|
|
857
|
+
const entry = registry.get(actionId);
|
|
858
|
+
if (!entry || entry.windowId !== windowId) continue;
|
|
859
|
+
bindings.push({ actionId, key: entry.chordString });
|
|
860
|
+
}
|
|
861
|
+
return bindings;
|
|
862
|
+
}
|
|
863
|
+
function pushBindings(windowId, registry, windowIndex, send) {
|
|
864
|
+
const payload = {
|
|
865
|
+
type: "keys.bindings",
|
|
866
|
+
bindings: bindingsForWindow(windowId, registry, windowIndex)
|
|
867
|
+
};
|
|
868
|
+
send(payload);
|
|
869
|
+
}
|
|
870
|
+
function removeActionFromWindowIndex(actionId, windowIndex) {
|
|
871
|
+
for (const [wid, set] of windowIndex.entries()) {
|
|
872
|
+
if (set.delete(actionId) && set.size === 0) windowIndex.delete(wid);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/keys-service.ts
|
|
877
|
+
var KEYS_SERVICE_VERSION = "1.3.0";
|
|
878
|
+
function rememberActionForWindow(windowIndex, windowId, actionId) {
|
|
879
|
+
if (!windowIndex.has(windowId)) windowIndex.set(windowId, /* @__PURE__ */ new Set());
|
|
880
|
+
windowIndex.get(windowId).add(actionId);
|
|
881
|
+
}
|
|
882
|
+
function bindActionEntry(registry, windowIndex, actionId, windowId, chord, chordString) {
|
|
883
|
+
registry.set(actionId, { chord, chordString, windowId });
|
|
884
|
+
rememberActionForWindow(windowIndex, windowId, actionId);
|
|
885
|
+
}
|
|
886
|
+
function bindActionAndMarkChanged(registry, windowIndex, changedWindowIds, actionId, windowId, chord, chordString) {
|
|
887
|
+
bindActionEntry(registry, windowIndex, actionId, windowId, chord, chordString);
|
|
888
|
+
changedWindowIds.add(windowId);
|
|
889
|
+
return chordString;
|
|
890
|
+
}
|
|
891
|
+
function handleForwardMessage(message, onForward) {
|
|
892
|
+
onForward?.(forwardPayload(message));
|
|
893
|
+
}
|
|
894
|
+
function sendUnknownKeysMethod(send, message) {
|
|
895
|
+
const id = message.id ?? "";
|
|
896
|
+
send({
|
|
897
|
+
type: `${message.type}.error`,
|
|
898
|
+
id,
|
|
899
|
+
error: `Unknown keys method: ${message.type}`
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
function createKeysMessageHandler(options) {
|
|
903
|
+
return (windowId, message, send) => {
|
|
904
|
+
switch (message.type) {
|
|
905
|
+
case "keys.forward":
|
|
906
|
+
handleForwardMessage(message, options.onForward);
|
|
907
|
+
return;
|
|
908
|
+
case "keys.registerAction":
|
|
909
|
+
options.onRegisterAction(windowId, message, send);
|
|
910
|
+
return;
|
|
911
|
+
case "keys.unregisterAction":
|
|
912
|
+
options.onUnregisterAction(windowId, message, send);
|
|
913
|
+
return;
|
|
914
|
+
default:
|
|
915
|
+
sendUnknownKeysMethod(send, message);
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function sendRegisterActionResult(send, message, binding, error) {
|
|
920
|
+
const result2 = {
|
|
921
|
+
type: "keys.registerAction.result",
|
|
922
|
+
id: message.id,
|
|
923
|
+
actionId: message.action.id,
|
|
924
|
+
...binding ? { binding } : {},
|
|
925
|
+
...error ? { error } : {}
|
|
926
|
+
};
|
|
927
|
+
send(result2);
|
|
928
|
+
}
|
|
929
|
+
function pushChangedBindings(changedWindowIds, registry, windowIndex, sendHandles, fallbackSend) {
|
|
930
|
+
for (const changedWindowId of changedWindowIds) {
|
|
931
|
+
const ownerSend = sendHandles.get(changedWindowId) ?? fallbackSend;
|
|
932
|
+
pushBindings(changedWindowId, registry, windowIndex, ownerSend);
|
|
933
|
+
if (!windowIndex.has(changedWindowId)) sendHandles.delete(changedWindowId);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
818
936
|
function createKeysService(options = {}) {
|
|
819
937
|
const descriptor = {
|
|
820
938
|
name: "keys",
|
|
821
939
|
version: KEYS_SERVICE_VERSION,
|
|
822
940
|
description: options.hostBridge ? "NIP-5D keys NAP reference handler (host-bridge delegated)" : "NIP-5D keys NAP reference handler (document-level chord listener)"
|
|
823
941
|
};
|
|
824
|
-
function chordSpecKey(spec) {
|
|
825
|
-
return `${spec.ctrl}|${spec.alt}|${spec.shift}|${spec.meta}|${spec.key}`;
|
|
826
|
-
}
|
|
827
942
|
const reservedChordKeys = /* @__PURE__ */ new Set();
|
|
828
943
|
if (options.reservedChords) {
|
|
829
944
|
for (const chordStr of options.reservedChords) {
|
|
830
945
|
reservedChordKeys.add(chordSpecKey(parseChord(chordStr)));
|
|
831
946
|
}
|
|
832
947
|
}
|
|
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
|
-
}
|
|
948
|
+
const isUnavailableBinding = (chord) => reservedChordKeys.has(chordSpecKey(chord)) || isReservedKeyChord(chord);
|
|
851
949
|
if (options.hostBridge) {
|
|
852
950
|
const bridge = options.hostBridge;
|
|
853
951
|
const bridgeWindowActions = /* @__PURE__ */ new Map();
|
|
952
|
+
const bridgeActionRegistry = /* @__PURE__ */ new Map();
|
|
953
|
+
const bridgeSendHandles = /* @__PURE__ */ new Map();
|
|
854
954
|
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;
|
|
955
|
+
const handleBridgeRegisterAction = (windowId, m, send) => {
|
|
956
|
+
bridgeSendHandles.set(windowId, send);
|
|
957
|
+
let binding;
|
|
958
|
+
const changedWindowIds = /* @__PURE__ */ new Set();
|
|
959
|
+
if (m.action.defaultKey) {
|
|
960
|
+
try {
|
|
961
|
+
const chord = parseChord(m.action.defaultKey);
|
|
962
|
+
const normalizedChord = formatChord(chord);
|
|
963
|
+
const existingEntry = bridgeActionRegistry.get(m.action.id);
|
|
964
|
+
const existing = unsubscribeHandles.get(m.action.id);
|
|
965
|
+
let nextUnsubscribe;
|
|
966
|
+
if (!isUnavailableBinding(chord)) {
|
|
967
|
+
nextUnsubscribe = bridge.subscribe(normalizedChord, (ev) => {
|
|
968
|
+
const e = ev;
|
|
969
|
+
if ("repeat" in e && e.repeat) return;
|
|
970
|
+
options.onForward?.({
|
|
971
|
+
key: e.key,
|
|
972
|
+
code: e.code,
|
|
973
|
+
ctrlKey: e.ctrlKey,
|
|
974
|
+
altKey: e.altKey,
|
|
975
|
+
shiftKey: e.shiftKey,
|
|
976
|
+
metaKey: e.metaKey
|
|
977
|
+
});
|
|
978
|
+
const payload = {
|
|
979
|
+
type: "keys.action",
|
|
980
|
+
actionId: m.action.id
|
|
981
|
+
};
|
|
982
|
+
send(payload);
|
|
983
|
+
});
|
|
910
984
|
}
|
|
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
|
-
}
|
|
985
|
+
if (existing) {
|
|
986
|
+
try {
|
|
987
|
+
existing();
|
|
988
|
+
} catch {
|
|
925
989
|
}
|
|
926
|
-
|
|
990
|
+
unsubscribeHandles.delete(m.action.id);
|
|
927
991
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
992
|
+
if (existingEntry) changedWindowIds.add(existingEntry.windowId);
|
|
993
|
+
removeActionFromWindowIndex(m.action.id, bridgeWindowActions);
|
|
994
|
+
bridgeActionRegistry.delete(m.action.id);
|
|
995
|
+
if (nextUnsubscribe) {
|
|
996
|
+
unsubscribeHandles.set(m.action.id, nextUnsubscribe);
|
|
997
|
+
binding = bindActionAndMarkChanged(
|
|
998
|
+
bridgeActionRegistry,
|
|
999
|
+
bridgeWindowActions,
|
|
1000
|
+
changedWindowIds,
|
|
1001
|
+
m.action.id,
|
|
1002
|
+
windowId,
|
|
1003
|
+
chord,
|
|
1004
|
+
normalizedChord
|
|
1005
|
+
);
|
|
936
1006
|
}
|
|
1007
|
+
} catch (err2) {
|
|
1008
|
+
sendRegisterActionResult(
|
|
1009
|
+
send,
|
|
1010
|
+
m,
|
|
1011
|
+
void 0,
|
|
1012
|
+
`bridge subscribe failed: ${err2.message}`
|
|
1013
|
+
);
|
|
1014
|
+
return;
|
|
937
1015
|
}
|
|
938
|
-
}
|
|
1016
|
+
}
|
|
1017
|
+
sendRegisterActionResult(send, m, binding);
|
|
1018
|
+
pushChangedBindings(
|
|
1019
|
+
changedWindowIds,
|
|
1020
|
+
bridgeActionRegistry,
|
|
1021
|
+
bridgeWindowActions,
|
|
1022
|
+
bridgeSendHandles,
|
|
1023
|
+
send
|
|
1024
|
+
);
|
|
1025
|
+
};
|
|
1026
|
+
const handleBridgeUnregisterAction = (_windowId, m, send) => {
|
|
1027
|
+
if (!m.actionId) return;
|
|
1028
|
+
const unsubscribe = unsubscribeHandles.get(m.actionId);
|
|
1029
|
+
const entry = bridgeActionRegistry.get(m.actionId);
|
|
1030
|
+
if (!unsubscribe) return;
|
|
1031
|
+
try {
|
|
1032
|
+
unsubscribe();
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
unsubscribeHandles.delete(m.actionId);
|
|
1036
|
+
bridgeActionRegistry.delete(m.actionId);
|
|
1037
|
+
removeActionFromWindowIndex(m.actionId, bridgeWindowActions);
|
|
1038
|
+
if (entry) {
|
|
1039
|
+
const ownerSend = bridgeSendHandles.get(entry.windowId) ?? send;
|
|
1040
|
+
pushBindings(entry.windowId, bridgeActionRegistry, bridgeWindowActions, ownerSend);
|
|
1041
|
+
if (!bridgeWindowActions.has(entry.windowId)) {
|
|
1042
|
+
bridgeSendHandles.delete(entry.windowId);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
return {
|
|
1047
|
+
descriptor,
|
|
1048
|
+
handleMessage: createKeysMessageHandler({
|
|
1049
|
+
onForward: options.onForward,
|
|
1050
|
+
onRegisterAction: handleBridgeRegisterAction,
|
|
1051
|
+
onUnregisterAction: handleBridgeUnregisterAction
|
|
1052
|
+
}),
|
|
939
1053
|
onWindowDestroyed(windowId) {
|
|
940
1054
|
const actions = bridgeWindowActions.get(windowId);
|
|
941
1055
|
if (!actions) return;
|
|
@@ -948,8 +1062,10 @@ function createKeysService(options = {}) {
|
|
|
948
1062
|
}
|
|
949
1063
|
}
|
|
950
1064
|
unsubscribeHandles.delete(actionId);
|
|
1065
|
+
bridgeActionRegistry.delete(actionId);
|
|
951
1066
|
}
|
|
952
1067
|
bridgeWindowActions.delete(windowId);
|
|
1068
|
+
bridgeSendHandles.delete(windowId);
|
|
953
1069
|
},
|
|
954
1070
|
destroy() {
|
|
955
1071
|
for (const unsubscribe of unsubscribeHandles.values()) {
|
|
@@ -959,7 +1075,9 @@ function createKeysService(options = {}) {
|
|
|
959
1075
|
}
|
|
960
1076
|
}
|
|
961
1077
|
unsubscribeHandles.clear();
|
|
1078
|
+
bridgeActionRegistry.clear();
|
|
962
1079
|
bridgeWindowActions.clear();
|
|
1080
|
+
bridgeSendHandles.clear();
|
|
963
1081
|
}
|
|
964
1082
|
};
|
|
965
1083
|
}
|
|
@@ -1003,8 +1121,7 @@ function createKeysService(options = {}) {
|
|
|
1003
1121
|
if (send) {
|
|
1004
1122
|
const payload = {
|
|
1005
1123
|
type: "keys.action",
|
|
1006
|
-
actionId
|
|
1007
|
-
chord: entry.chord
|
|
1124
|
+
actionId
|
|
1008
1125
|
};
|
|
1009
1126
|
send(payload);
|
|
1010
1127
|
}
|
|
@@ -1012,76 +1129,56 @@ function createKeysService(options = {}) {
|
|
|
1012
1129
|
}
|
|
1013
1130
|
};
|
|
1014
1131
|
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;
|
|
1057
|
-
}
|
|
1058
|
-
case "keys.unregisterAction": {
|
|
1059
|
-
const m = message;
|
|
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;
|
|
1132
|
+
const handleDocumentRegisterAction = (windowId, m, send) => {
|
|
1133
|
+
sendHandles.set(windowId, send);
|
|
1134
|
+
let binding;
|
|
1135
|
+
const changedWindowIds = /* @__PURE__ */ new Set();
|
|
1136
|
+
if (m.action.defaultKey) {
|
|
1137
|
+
try {
|
|
1138
|
+
const chord = parseChord(m.action.defaultKey);
|
|
1139
|
+
const normalizedChord = formatChord(chord);
|
|
1140
|
+
const existing = actionRegistry.get(m.action.id);
|
|
1141
|
+
if (existing) {
|
|
1142
|
+
changedWindowIds.add(existing.windowId);
|
|
1143
|
+
removeActionFromWindowIndex(m.action.id, windowActions);
|
|
1144
|
+
actionRegistry.delete(m.action.id);
|
|
1073
1145
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
send({
|
|
1077
|
-
type: `${message.type}.error`,
|
|
1078
|
-
id,
|
|
1079
|
-
error: `Unknown keys method: ${message.type}`
|
|
1080
|
-
});
|
|
1081
|
-
return;
|
|
1146
|
+
if (!isUnavailableBinding(chord)) {
|
|
1147
|
+
binding = bindActionAndMarkChanged(actionRegistry, windowActions, changedWindowIds, m.action.id, windowId, chord, normalizedChord);
|
|
1082
1148
|
}
|
|
1149
|
+
} catch (err2) {
|
|
1150
|
+
sendRegisterActionResult(
|
|
1151
|
+
send,
|
|
1152
|
+
m,
|
|
1153
|
+
void 0,
|
|
1154
|
+
`invalid chord: ${err2.message}`
|
|
1155
|
+
);
|
|
1156
|
+
return;
|
|
1083
1157
|
}
|
|
1084
|
-
}
|
|
1158
|
+
}
|
|
1159
|
+
sendRegisterActionResult(send, m, binding);
|
|
1160
|
+
pushChangedBindings(changedWindowIds, actionRegistry, windowActions, sendHandles, send);
|
|
1161
|
+
};
|
|
1162
|
+
const handleDocumentUnregisterAction = (_windowId, m, send) => {
|
|
1163
|
+
if (!m.actionId || !actionRegistry.has(m.actionId)) return;
|
|
1164
|
+
const entry = actionRegistry.get(m.actionId);
|
|
1165
|
+
actionRegistry.delete(m.actionId);
|
|
1166
|
+
const set = windowActions.get(entry.windowId);
|
|
1167
|
+
if (set) {
|
|
1168
|
+
set.delete(m.actionId);
|
|
1169
|
+
if (set.size === 0) windowActions.delete(entry.windowId);
|
|
1170
|
+
}
|
|
1171
|
+
const ownerSend = sendHandles.get(entry.windowId) ?? send;
|
|
1172
|
+
pushBindings(entry.windowId, actionRegistry, windowActions, ownerSend);
|
|
1173
|
+
if (!windowActions.has(entry.windowId)) sendHandles.delete(entry.windowId);
|
|
1174
|
+
};
|
|
1175
|
+
return {
|
|
1176
|
+
descriptor,
|
|
1177
|
+
handleMessage: createKeysMessageHandler({
|
|
1178
|
+
onForward: options.onForward,
|
|
1179
|
+
onRegisterAction: handleDocumentRegisterAction,
|
|
1180
|
+
onUnregisterAction: handleDocumentUnregisterAction
|
|
1181
|
+
}),
|
|
1085
1182
|
onWindowDestroyed(windowId) {
|
|
1086
1183
|
const actions = windowActions.get(windowId);
|
|
1087
1184
|
if (actions) {
|
|
@@ -2070,8 +2167,9 @@ function sanitizePublishOptions(raw) {
|
|
|
2070
2167
|
const out = {};
|
|
2071
2168
|
const relays = stringArray(raw.relays);
|
|
2072
2169
|
if (relays) out.relays = relays;
|
|
2073
|
-
|
|
2074
|
-
|
|
2170
|
+
if (typeof raw.toOutbox === "boolean") out.toOutbox = raw.toOutbox;
|
|
2171
|
+
const toInboxes = stringArray(raw.toInboxes);
|
|
2172
|
+
if (toInboxes) out.toInboxes = toInboxes;
|
|
2075
2173
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
2076
2174
|
}
|
|
2077
2175
|
function sanitizeTarget(raw) {
|
|
@@ -2293,9 +2391,33 @@ function allowed(ctx, urls) {
|
|
|
2293
2391
|
}
|
|
2294
2392
|
return [...out];
|
|
2295
2393
|
}
|
|
2394
|
+
async function resolveRequiredRelays(ctx, pubkeys, direction) {
|
|
2395
|
+
const useWrite = direction === "read";
|
|
2396
|
+
const relays = /* @__PURE__ */ new Set();
|
|
2397
|
+
const missingAuthors = [];
|
|
2398
|
+
const deniedAuthors = [];
|
|
2399
|
+
const uniquePubkeys = [...new Set(pubkeys)];
|
|
2400
|
+
if (uniquePubkeys.length === 0) return { relays: [], missingAuthors, deniedAuthors };
|
|
2401
|
+
const lists = await ctx.loadRelayLists(uniquePubkeys);
|
|
2402
|
+
for (const pubkey of uniquePubkeys) {
|
|
2403
|
+
const entry = lists.get(pubkey);
|
|
2404
|
+
const candidateRelays = entry ? useWrite ? entry.write : entry.read : void 0;
|
|
2405
|
+
if (!candidateRelays || candidateRelays.length === 0) {
|
|
2406
|
+
missingAuthors.push(pubkey);
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
const allowedRelays = allowed(ctx, candidateRelays);
|
|
2410
|
+
if (allowedRelays.length === 0) {
|
|
2411
|
+
deniedAuthors.push(pubkey);
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
for (const url of allowedRelays) relays.add(url);
|
|
2415
|
+
}
|
|
2416
|
+
return { relays: [...relays], missingAuthors, deniedAuthors };
|
|
2417
|
+
}
|
|
2296
2418
|
async function resolvePlan(ctx, pubkeys, direction, relayHints) {
|
|
2297
2419
|
const useWrite = direction === "read";
|
|
2298
|
-
const
|
|
2420
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
2299
2421
|
const missingAuthors = [];
|
|
2300
2422
|
let sawNip65 = false;
|
|
2301
2423
|
if (pubkeys.length > 0) {
|
|
@@ -2305,25 +2427,46 @@ async function resolvePlan(ctx, pubkeys, direction, relayHints) {
|
|
|
2305
2427
|
const relays2 = entry ? useWrite ? entry.write : entry.read : void 0;
|
|
2306
2428
|
if (relays2 && relays2.length > 0) {
|
|
2307
2429
|
sawNip65 = true;
|
|
2308
|
-
for (const url of relays2)
|
|
2430
|
+
for (const url of relays2) discovered.add(url);
|
|
2309
2431
|
} else {
|
|
2310
2432
|
missingAuthors.push(pubkey);
|
|
2311
2433
|
}
|
|
2312
2434
|
}
|
|
2313
2435
|
}
|
|
2314
|
-
|
|
2315
|
-
let relays = allowed(ctx,
|
|
2436
|
+
const hintRelays = allowed(ctx, relayHints ?? []);
|
|
2437
|
+
let relays = allowed(ctx, discovered);
|
|
2316
2438
|
let source;
|
|
2317
|
-
if (relays.length
|
|
2318
|
-
relays = allowed(ctx, ctx.fallbackRelays);
|
|
2319
|
-
source = "fallback";
|
|
2320
|
-
} else {
|
|
2439
|
+
if (relays.length > 0) {
|
|
2321
2440
|
source = sawNip65 ? "nip65" : "policy";
|
|
2441
|
+
} else {
|
|
2442
|
+
relays = allowed(ctx, ctx.fallbackRelays);
|
|
2443
|
+
source = relays.length > 0 ? "fallback" : "policy";
|
|
2444
|
+
}
|
|
2445
|
+
for (const url of hintRelays) {
|
|
2446
|
+
if (!relays.includes(url)) relays.push(url);
|
|
2322
2447
|
}
|
|
2448
|
+
if (relays.length === 0) source = "fallback";
|
|
2323
2449
|
const plan = { relays, source };
|
|
2324
2450
|
if (missingAuthors.length > 0) plan.missingAuthors = missingAuthors;
|
|
2325
2451
|
return plan;
|
|
2326
2452
|
}
|
|
2453
|
+
function discoveredReadRelays(ctx, authors, lists) {
|
|
2454
|
+
const relays = /* @__PURE__ */ new Set();
|
|
2455
|
+
const missingAuthors = [];
|
|
2456
|
+
for (const author of new Set(authors)) {
|
|
2457
|
+
const writeRelays = lists.get(author)?.write;
|
|
2458
|
+
if (!writeRelays || writeRelays.length === 0) {
|
|
2459
|
+
missingAuthors.push(author);
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2462
|
+
for (const url of allowed(ctx, writeRelays)) relays.add(url);
|
|
2463
|
+
}
|
|
2464
|
+
return { relays: [...relays], missingAuthors };
|
|
2465
|
+
}
|
|
2466
|
+
function immediateReadRelays(ctx, relayHints) {
|
|
2467
|
+
const hints = allowed(ctx, relayHints ?? []);
|
|
2468
|
+
return hints.length > 0 ? hints : allowed(ctx, ctx.fallbackRelays);
|
|
2469
|
+
}
|
|
2327
2470
|
function recordRelay(collector, id, relayUrl) {
|
|
2328
2471
|
let set = collector.relayMap.get(id);
|
|
2329
2472
|
if (!set) {
|
|
@@ -2332,81 +2475,166 @@ function recordRelay(collector, id, relayUrl) {
|
|
|
2332
2475
|
}
|
|
2333
2476
|
set.add(relayUrl);
|
|
2334
2477
|
}
|
|
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)));
|
|
2478
|
+
function collectedEventResult(collector, event) {
|
|
2479
|
+
return createRelayEventResultWithHints3(event, [...collector.relayMap.get(event.id) ?? []]);
|
|
2480
|
+
}
|
|
2481
|
+
function admitEvent(ctx, collector, event, sink, isOpen, onSettled) {
|
|
2482
|
+
if (collector.admitted.has(event.id)) return;
|
|
2483
|
+
collector.admitted.add(event.id);
|
|
2484
|
+
collector.pendingVerifications += 1;
|
|
2485
|
+
void ctx.verify(event).then((ok) => {
|
|
2486
|
+
if (!isOpen()) return;
|
|
2487
|
+
if (!ok) {
|
|
2488
|
+
collector.relayMap.delete(event.id);
|
|
2489
|
+
return;
|
|
2368
2490
|
}
|
|
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
|
-
}));
|
|
2491
|
+
collector.seen.set(event.id, event);
|
|
2492
|
+
try {
|
|
2493
|
+
sink.event(collectedEventResult(collector, event));
|
|
2494
|
+
} catch {
|
|
2384
2495
|
}
|
|
2385
|
-
|
|
2496
|
+
}).finally(() => {
|
|
2497
|
+
collector.pendingVerifications -= 1;
|
|
2498
|
+
onSettled();
|
|
2386
2499
|
});
|
|
2387
2500
|
}
|
|
2501
|
+
function buildQueryResult(collector, options, incomplete, subscribedAny) {
|
|
2502
|
+
let events = [...collector.seen.values()].map((event) => collectedEventResult(collector, event));
|
|
2503
|
+
if (options?.limit !== void 0 && events.length > options.limit) {
|
|
2504
|
+
events = [...events].sort((a, b) => b.event.created_at - a.event.created_at).slice(0, options.limit);
|
|
2505
|
+
}
|
|
2506
|
+
const result2 = { events };
|
|
2507
|
+
if (incomplete || !subscribedAny) result2.incomplete = true;
|
|
2508
|
+
if (!subscribedAny) result2.error = "relay list unavailable";
|
|
2509
|
+
return result2;
|
|
2510
|
+
}
|
|
2388
2511
|
function relayPoolSubscribe(ctx, filters, relayUrl, cb) {
|
|
2389
2512
|
return ctx.relayPool.subscribe(filters, [relayUrl], cb);
|
|
2390
2513
|
}
|
|
2391
|
-
|
|
2514
|
+
function startQueryImpl(ctx, filters, options, sink) {
|
|
2392
2515
|
if (!ctx.relayPool.isAvailable()) {
|
|
2393
|
-
return {
|
|
2516
|
+
return {
|
|
2517
|
+
result: Promise.resolve({ events: [], incomplete: true, error: "relay list unavailable" }),
|
|
2518
|
+
close() {
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2394
2521
|
}
|
|
2395
2522
|
const authors = deriveAuthors(filters, options?.authors);
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2523
|
+
const collector = {
|
|
2524
|
+
seen: /* @__PURE__ */ new Map(),
|
|
2525
|
+
admitted: /* @__PURE__ */ new Set(),
|
|
2526
|
+
relayMap: /* @__PURE__ */ new Map(),
|
|
2527
|
+
pendingVerifications: 0
|
|
2528
|
+
};
|
|
2529
|
+
const handles = /* @__PURE__ */ new Map();
|
|
2530
|
+
const activeRelays = /* @__PURE__ */ new Set();
|
|
2531
|
+
const eoseRelays = /* @__PURE__ */ new Set();
|
|
2532
|
+
let discoveryPending = authors.length > 0;
|
|
2533
|
+
let incomplete = false;
|
|
2534
|
+
let subscribedAny = false;
|
|
2535
|
+
let finished = false;
|
|
2536
|
+
let resolveResult;
|
|
2537
|
+
const result2 = new Promise((resolve) => {
|
|
2538
|
+
resolveResult = resolve;
|
|
2539
|
+
});
|
|
2540
|
+
let timer;
|
|
2541
|
+
function closeHandles() {
|
|
2542
|
+
for (const handle of handles.values()) {
|
|
2543
|
+
try {
|
|
2544
|
+
handle.unsubscribe();
|
|
2545
|
+
} catch {
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
handles.clear();
|
|
2549
|
+
}
|
|
2550
|
+
function finalize(forceIncomplete = false) {
|
|
2551
|
+
if (finished) return;
|
|
2552
|
+
finished = true;
|
|
2553
|
+
incomplete ||= forceIncomplete;
|
|
2554
|
+
clearTimeout(timer);
|
|
2555
|
+
closeHandles();
|
|
2556
|
+
resolveResult(buildQueryResult(collector, options, incomplete, subscribedAny));
|
|
2557
|
+
}
|
|
2558
|
+
function maybeFinalize() {
|
|
2559
|
+
if (finished || discoveryPending || collector.pendingVerifications > 0) return;
|
|
2560
|
+
if (activeRelays.size === 0 || eoseRelays.size >= activeRelays.size) finalize();
|
|
2561
|
+
}
|
|
2562
|
+
function attachRelay(relayUrl) {
|
|
2563
|
+
if (finished || activeRelays.has(relayUrl)) return;
|
|
2564
|
+
activeRelays.add(relayUrl);
|
|
2565
|
+
try {
|
|
2566
|
+
const handle = relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2567
|
+
if (finished) return;
|
|
2568
|
+
if (item === "EOSE") {
|
|
2569
|
+
eoseRelays.add(relayUrl);
|
|
2570
|
+
maybeFinalize();
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
recordRelay(collector, item.id, relayUrl);
|
|
2574
|
+
admitEvent(ctx, collector, item, sink, () => !finished, maybeFinalize);
|
|
2575
|
+
});
|
|
2576
|
+
subscribedAny = true;
|
|
2577
|
+
if (finished) {
|
|
2578
|
+
try {
|
|
2579
|
+
handle.unsubscribe();
|
|
2580
|
+
} catch {
|
|
2581
|
+
}
|
|
2582
|
+
} else {
|
|
2583
|
+
handles.set(relayUrl, handle);
|
|
2584
|
+
}
|
|
2585
|
+
} catch {
|
|
2586
|
+
activeRelays.delete(relayUrl);
|
|
2587
|
+
incomplete = true;
|
|
2588
|
+
}
|
|
2406
2589
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2590
|
+
function attachMany(relayUrls2) {
|
|
2591
|
+
for (const relayUrl of relayUrls2) attachRelay(relayUrl);
|
|
2592
|
+
}
|
|
2593
|
+
timer = setTimeout(() => finalize(true), options?.timeoutMs ?? ctx.defaultTimeoutMs);
|
|
2594
|
+
if (authors.length === 0) {
|
|
2595
|
+
discoveryPending = false;
|
|
2596
|
+
attachMany(allowed(ctx, [...ctx.fallbackRelays, ...options?.relays ?? []]));
|
|
2597
|
+
maybeFinalize();
|
|
2598
|
+
} else {
|
|
2599
|
+
let loaded;
|
|
2600
|
+
try {
|
|
2601
|
+
loaded = ctx.loadRelayLists(authors);
|
|
2602
|
+
} catch {
|
|
2603
|
+
discoveryPending = false;
|
|
2604
|
+
incomplete = true;
|
|
2605
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2606
|
+
maybeFinalize();
|
|
2607
|
+
return { result: result2, close: () => finalize(true) };
|
|
2608
|
+
}
|
|
2609
|
+
if (loaded instanceof Map) {
|
|
2610
|
+
const discovery = discoveredReadRelays(ctx, authors, loaded);
|
|
2611
|
+
discoveryPending = false;
|
|
2612
|
+
incomplete ||= discovery.missingAuthors.length > 0;
|
|
2613
|
+
const initial = allowed(ctx, [...options?.relays ?? [], ...discovery.relays]);
|
|
2614
|
+
attachMany(initial.length > 0 ? initial : allowed(ctx, ctx.fallbackRelays));
|
|
2615
|
+
maybeFinalize();
|
|
2616
|
+
} else {
|
|
2617
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2618
|
+
void Promise.resolve(loaded).then((lists) => {
|
|
2619
|
+
if (finished) return;
|
|
2620
|
+
const discovery = discoveredReadRelays(ctx, authors, lists);
|
|
2621
|
+
discoveryPending = false;
|
|
2622
|
+
incomplete ||= discovery.missingAuthors.length > 0;
|
|
2623
|
+
attachMany(discovery.relays);
|
|
2624
|
+
maybeFinalize();
|
|
2625
|
+
}).catch(() => {
|
|
2626
|
+
if (finished) return;
|
|
2627
|
+
discoveryPending = false;
|
|
2628
|
+
incomplete = true;
|
|
2629
|
+
maybeFinalize();
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return { result: result2, close: () => finalize(true) };
|
|
2634
|
+
}
|
|
2635
|
+
function queryImpl(ctx, filters, options) {
|
|
2636
|
+
return startQueryImpl(ctx, filters, options, { event: () => {
|
|
2637
|
+
} }).result;
|
|
2410
2638
|
}
|
|
2411
2639
|
async function getEventImpl(ctx, eventId, options) {
|
|
2412
2640
|
if (typeof eventId !== "string" || eventId.length === 0) {
|
|
@@ -2430,57 +2658,138 @@ async function getEventImpl(ctx, eventId, options) {
|
|
|
2430
2658
|
return result2;
|
|
2431
2659
|
}
|
|
2432
2660
|
function closeLiveSub(sub) {
|
|
2433
|
-
for (const handle of sub.handles) {
|
|
2661
|
+
for (const handle of sub.handles.values()) {
|
|
2434
2662
|
try {
|
|
2435
2663
|
handle.unsubscribe();
|
|
2436
2664
|
} catch {
|
|
2437
2665
|
}
|
|
2438
2666
|
}
|
|
2439
|
-
sub.handles.
|
|
2667
|
+
sub.handles.clear();
|
|
2440
2668
|
}
|
|
2441
2669
|
function attachLiveRelay(ctx, sub, filters, relayUrl, sink) {
|
|
2442
|
-
sub.
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2670
|
+
if (sub.closed || sub.relays.has(relayUrl)) return false;
|
|
2671
|
+
sub.relays.add(relayUrl);
|
|
2672
|
+
try {
|
|
2673
|
+
const handle = relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2674
|
+
if (sub.closed || item === "EOSE" || sub.admitted.has(item.id)) return;
|
|
2675
|
+
sub.admitted.add(item.id);
|
|
2676
|
+
void ctx.verify(item).then((ok) => {
|
|
2677
|
+
if (!ok || sub.closed || sub.seen.has(item.id)) return;
|
|
2678
|
+
sub.seen.add(item.id);
|
|
2679
|
+
sink.event(createRelayEventResultWithHints3(item, [relayUrl]));
|
|
2680
|
+
});
|
|
2450
2681
|
});
|
|
2451
|
-
|
|
2682
|
+
if (sub.closed) {
|
|
2683
|
+
try {
|
|
2684
|
+
handle.unsubscribe();
|
|
2685
|
+
} catch {
|
|
2686
|
+
}
|
|
2687
|
+
} else {
|
|
2688
|
+
sub.handles.set(relayUrl, handle);
|
|
2689
|
+
}
|
|
2690
|
+
return true;
|
|
2691
|
+
} catch {
|
|
2692
|
+
sub.relays.delete(relayUrl);
|
|
2693
|
+
return false;
|
|
2694
|
+
}
|
|
2452
2695
|
}
|
|
2453
2696
|
function startSubscription(ctx, filters, options, sink) {
|
|
2454
2697
|
const authors = deriveAuthors(filters, options?.authors);
|
|
2455
|
-
const sub = {
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2698
|
+
const sub = {
|
|
2699
|
+
handles: /* @__PURE__ */ new Map(),
|
|
2700
|
+
relays: /* @__PURE__ */ new Set(),
|
|
2701
|
+
admitted: /* @__PURE__ */ new Set(),
|
|
2702
|
+
seen: /* @__PURE__ */ new Set(),
|
|
2703
|
+
closed: false
|
|
2704
|
+
};
|
|
2705
|
+
let discoveryOpen = authors.length > 0;
|
|
2706
|
+
let discoveryTimer;
|
|
2707
|
+
function attachMany(relayUrls2) {
|
|
2708
|
+
for (const relayUrl of relayUrls2) attachLiveRelay(ctx, sub, filters, relayUrl, sink);
|
|
2709
|
+
}
|
|
2710
|
+
function closeUnavailable() {
|
|
2711
|
+
if (sub.closed || sub.handles.size > 0) return;
|
|
2712
|
+
sub.closed = true;
|
|
2713
|
+
sink.closed("relay list unavailable");
|
|
2714
|
+
}
|
|
2715
|
+
if (!ctx.relayPool.isAvailable()) {
|
|
2716
|
+
closeUnavailable();
|
|
2717
|
+
} else if (authors.length === 0) {
|
|
2718
|
+
discoveryOpen = false;
|
|
2719
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2720
|
+
closeUnavailable();
|
|
2721
|
+
} else {
|
|
2722
|
+
let loaded;
|
|
2723
|
+
try {
|
|
2724
|
+
loaded = ctx.loadRelayLists(authors);
|
|
2725
|
+
} catch {
|
|
2726
|
+
discoveryOpen = false;
|
|
2727
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2728
|
+
closeUnavailable();
|
|
2729
|
+
loaded = /* @__PURE__ */ new Map();
|
|
2461
2730
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2731
|
+
if (discoveryOpen && loaded instanceof Map) {
|
|
2732
|
+
const discovery = discoveredReadRelays(ctx, authors, loaded);
|
|
2733
|
+
discoveryOpen = false;
|
|
2734
|
+
const initial = allowed(ctx, [...options?.relays ?? [], ...discovery.relays]);
|
|
2735
|
+
attachMany(initial.length > 0 ? initial : allowed(ctx, ctx.fallbackRelays));
|
|
2736
|
+
closeUnavailable();
|
|
2737
|
+
} else if (discoveryOpen) {
|
|
2738
|
+
attachMany(immediateReadRelays(ctx, options?.relays));
|
|
2739
|
+
discoveryTimer = setTimeout(() => {
|
|
2740
|
+
discoveryOpen = false;
|
|
2741
|
+
closeUnavailable();
|
|
2742
|
+
}, options?.timeoutMs ?? ctx.defaultTimeoutMs);
|
|
2743
|
+
void Promise.resolve(loaded).then((lists) => {
|
|
2744
|
+
if (!discoveryOpen || sub.closed) return;
|
|
2745
|
+
discoveryOpen = false;
|
|
2746
|
+
clearTimeout(discoveryTimer);
|
|
2747
|
+
attachMany(discoveredReadRelays(ctx, authors, lists).relays);
|
|
2748
|
+
closeUnavailable();
|
|
2749
|
+
}).catch(() => {
|
|
2750
|
+
if (!discoveryOpen || sub.closed) return;
|
|
2751
|
+
discoveryOpen = false;
|
|
2752
|
+
clearTimeout(discoveryTimer);
|
|
2753
|
+
closeUnavailable();
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2466
2757
|
return {
|
|
2467
2758
|
close() {
|
|
2468
2759
|
if (sub.closed) return;
|
|
2469
2760
|
sub.closed = true;
|
|
2761
|
+
discoveryOpen = false;
|
|
2762
|
+
clearTimeout(discoveryTimer);
|
|
2470
2763
|
closeLiveSub(sub);
|
|
2471
2764
|
}
|
|
2472
2765
|
};
|
|
2473
2766
|
}
|
|
2474
2767
|
async function resolvePublishTargets(ctx, signed, options) {
|
|
2475
2768
|
const targets = /* @__PURE__ */ new Set();
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2769
|
+
const requiredTargets = /* @__PURE__ */ new Set();
|
|
2770
|
+
if (options?.toOutbox !== false) {
|
|
2771
|
+
const outboxPlan = await resolveRequiredRelays(ctx, [signed.pubkey], "read");
|
|
2772
|
+
if (outboxPlan.missingAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "relay list unavailable" };
|
|
2773
|
+
if (outboxPlan.deniedAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "policy denied" };
|
|
2774
|
+
for (const url of outboxPlan.relays) {
|
|
2775
|
+
targets.add(url);
|
|
2776
|
+
requiredTargets.add(url);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (options?.toInboxes && options.toInboxes.length > 0) {
|
|
2780
|
+
const inboxPlan = await resolveRequiredRelays(ctx, options.toInboxes, "write");
|
|
2781
|
+
if (inboxPlan.missingAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "relay list unavailable" };
|
|
2782
|
+
if (inboxPlan.deniedAuthors.length > 0) return { relayUrls: [], requiredRelayUrls: [], error: "policy denied" };
|
|
2783
|
+
for (const url of inboxPlan.relays) {
|
|
2784
|
+
targets.add(url);
|
|
2785
|
+
requiredTargets.add(url);
|
|
2786
|
+
}
|
|
2481
2787
|
}
|
|
2482
2788
|
for (const url of options?.relays ?? []) targets.add(url);
|
|
2483
|
-
return
|
|
2789
|
+
return {
|
|
2790
|
+
relayUrls: allowed(ctx, targets),
|
|
2791
|
+
requiredRelayUrls: allowed(ctx, requiredTargets)
|
|
2792
|
+
};
|
|
2484
2793
|
}
|
|
2485
2794
|
async function publishImpl(ctx, template, options) {
|
|
2486
2795
|
if (!ctx.signEvent) return { ok: false, error: "publish denied" };
|
|
@@ -2491,7 +2800,11 @@ async function publishImpl(ctx, template, options) {
|
|
|
2491
2800
|
} catch (err2) {
|
|
2492
2801
|
return { ok: false, error: err2 instanceof Error ? err2.message : "sign failed" };
|
|
2493
2802
|
}
|
|
2494
|
-
const
|
|
2803
|
+
const publishTargets = await resolvePublishTargets(ctx, signed, options);
|
|
2804
|
+
if (publishTargets.error) {
|
|
2805
|
+
return { ok: false, event: signed, eventId: signed.id, error: publishTargets.error };
|
|
2806
|
+
}
|
|
2807
|
+
const relayUrls2 = publishTargets.relayUrls;
|
|
2495
2808
|
if (relayUrls2.length === 0) return { ok: false, event: signed, eventId: signed.id, error: "relay list unavailable" };
|
|
2496
2809
|
let relays;
|
|
2497
2810
|
try {
|
|
@@ -2499,7 +2812,7 @@ async function publishImpl(ctx, template, options) {
|
|
|
2499
2812
|
} catch (err2) {
|
|
2500
2813
|
return { ok: false, event: signed, eventId: signed.id, error: err2 instanceof Error ? err2.message : "publish failed" };
|
|
2501
2814
|
}
|
|
2502
|
-
const ok = Object.values(relays).some(Boolean);
|
|
2815
|
+
const ok = publishTargets.requiredRelayUrls.length > 0 ? publishTargets.requiredRelayUrls.every((url) => relays[url] === true) : Object.values(relays).some(Boolean);
|
|
2503
2816
|
const result2 = { ok, event: signed, eventId: signed.id, relays };
|
|
2504
2817
|
if (!ok) result2.error = "publish denied";
|
|
2505
2818
|
return result2;
|
|
@@ -2534,6 +2847,7 @@ function createRelayPoolOutboxRouter(options) {
|
|
|
2534
2847
|
return {
|
|
2535
2848
|
getEvent: (eventId, eventOptions) => getEventImpl(ctx, eventId, eventOptions),
|
|
2536
2849
|
query: (filters, queryOptions) => queryImpl(ctx, filters, queryOptions),
|
|
2850
|
+
queryStream: (filters, queryOptions, sink) => startQueryImpl(ctx, filters, queryOptions, sink),
|
|
2537
2851
|
subscribe: (filters, subscribeOptions, sink) => startSubscription(ctx, filters, subscribeOptions, sink),
|
|
2538
2852
|
publish: (template, publishOptions) => publishImpl(ctx, template, publishOptions),
|
|
2539
2853
|
resolveRelays: (target) => {
|
|
@@ -2695,6 +3009,7 @@ function createHttpUploader(options) {
|
|
|
2695
3009
|
const digest = options.digestSha256 ?? defaultDigestSha256;
|
|
2696
3010
|
const nowS = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
2697
3011
|
const defaultRail = options.defaultRail ?? firstConfiguredRail(rails);
|
|
3012
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
2698
3013
|
async function upload(request, ctx) {
|
|
2699
3014
|
const rail = request.rail ?? defaultRail;
|
|
2700
3015
|
const config = rail === "nip96" ? rails.nip96 : rail === "blossom" ? rails.blossom : void 0;
|
|
@@ -2705,18 +3020,32 @@ function createHttpUploader(options) {
|
|
|
2705
3020
|
if (!server) {
|
|
2706
3021
|
return failed(ctx.uploadId, rail, "no server configured");
|
|
2707
3022
|
}
|
|
2708
|
-
const
|
|
2709
|
-
|
|
3023
|
+
const controller = new AbortController();
|
|
3024
|
+
inFlight.set(ctx.uploadId, controller);
|
|
3025
|
+
let sha256;
|
|
2710
3026
|
try {
|
|
2711
|
-
|
|
3027
|
+
const bytes = await toBytes(request.data);
|
|
3028
|
+
sha256 = await digest(bytes);
|
|
3029
|
+
if (controller.signal.aborted) {
|
|
3030
|
+
return cancelled(ctx.uploadId, rail, sha256);
|
|
3031
|
+
}
|
|
3032
|
+
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
3033
|
} catch (err2) {
|
|
3034
|
+
if (controller.signal.aborted) {
|
|
3035
|
+
return cancelled(ctx.uploadId, rail, sha256);
|
|
3036
|
+
}
|
|
2713
3037
|
return failed(ctx.uploadId, rail, toErrorMessage3(err2), sha256);
|
|
3038
|
+
} finally {
|
|
3039
|
+
inFlight.delete(ctx.uploadId);
|
|
2714
3040
|
}
|
|
2715
3041
|
}
|
|
2716
|
-
|
|
3042
|
+
function cancel(uploadId) {
|
|
3043
|
+
inFlight.get(uploadId)?.abort();
|
|
3044
|
+
}
|
|
3045
|
+
return { upload, cancel };
|
|
2717
3046
|
}
|
|
2718
3047
|
async function uploadNip96(args) {
|
|
2719
|
-
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
|
|
3048
|
+
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller } = args;
|
|
2720
3049
|
const auth = await signEvent({
|
|
2721
3050
|
kind: KIND_NIP98,
|
|
2722
3051
|
created_at: nowS(),
|
|
@@ -2732,10 +3061,12 @@ async function uploadNip96(args) {
|
|
|
2732
3061
|
if (request.caption !== void 0) form.append("caption", request.caption);
|
|
2733
3062
|
if (request.mimeType !== void 0) form.append("content_type", request.mimeType);
|
|
2734
3063
|
if (request.noTransform) form.append("no_transform", "true");
|
|
3064
|
+
emitUploading(ctx, "nip96", bytes.byteLength, nowS);
|
|
2735
3065
|
const res = await fetchFn(server, {
|
|
2736
3066
|
method: "POST",
|
|
2737
3067
|
headers: { Authorization: nostrAuthHeader(auth) },
|
|
2738
|
-
body: form
|
|
3068
|
+
body: form,
|
|
3069
|
+
signal: controller.signal
|
|
2739
3070
|
});
|
|
2740
3071
|
if (!res.ok) {
|
|
2741
3072
|
return failed(ctx.uploadId, "nip96", `server rejected (HTTP ${res.status})`, sha256);
|
|
@@ -2773,7 +3104,7 @@ function fromNip94Tags(uploadId, rail, tags, fallbackSize, fallbackSha) {
|
|
|
2773
3104
|
return result2;
|
|
2774
3105
|
}
|
|
2775
3106
|
async function uploadBlossom(args) {
|
|
2776
|
-
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
|
|
3107
|
+
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller } = args;
|
|
2777
3108
|
const auth = await signEvent({
|
|
2778
3109
|
kind: KIND_BLOSSOM_AUTH,
|
|
2779
3110
|
created_at: nowS(),
|
|
@@ -2787,33 +3118,65 @@ async function uploadBlossom(args) {
|
|
|
2787
3118
|
const endpoint = `${trimTrailingSlash(server)}/upload`;
|
|
2788
3119
|
const headers = { Authorization: nostrAuthHeader(auth) };
|
|
2789
3120
|
if (request.mimeType) headers["Content-Type"] = request.mimeType;
|
|
3121
|
+
emitUploading(ctx, "blossom", bytes.byteLength, nowS);
|
|
2790
3122
|
const res = await fetchFn(endpoint, {
|
|
2791
3123
|
method: "PUT",
|
|
2792
3124
|
headers,
|
|
2793
|
-
body: bytesToArrayBuffer(bytes)
|
|
3125
|
+
body: bytesToArrayBuffer(bytes),
|
|
3126
|
+
signal: controller.signal
|
|
2794
3127
|
});
|
|
2795
3128
|
if (!res.ok) {
|
|
2796
3129
|
return failed(ctx.uploadId, "blossom", `server rejected (HTTP ${res.status})`, sha256);
|
|
2797
3130
|
}
|
|
2798
3131
|
const blob = await res.json();
|
|
2799
|
-
if (
|
|
3132
|
+
if (typeof blob.url !== "string" || blob.url.length === 0) {
|
|
2800
3133
|
return failed(ctx.uploadId, "blossom", "server returned no url", sha256);
|
|
2801
3134
|
}
|
|
3135
|
+
if (typeof blob.sha256 !== "string" || blob.sha256.length === 0) {
|
|
3136
|
+
return failed(ctx.uploadId, "blossom", "server returned no sha256", sha256);
|
|
3137
|
+
}
|
|
3138
|
+
if (blob.sha256 !== sha256) {
|
|
3139
|
+
return failed(ctx.uploadId, "blossom", "server returned mismatched sha256", sha256);
|
|
3140
|
+
}
|
|
3141
|
+
if (!Number.isSafeInteger(blob.size) || (blob.size ?? -1) < 0) {
|
|
3142
|
+
return failed(ctx.uploadId, "blossom", "server returned invalid size", sha256);
|
|
3143
|
+
}
|
|
3144
|
+
if (blob.size !== bytes.byteLength) {
|
|
3145
|
+
return failed(ctx.uploadId, "blossom", "server returned mismatched size", sha256);
|
|
3146
|
+
}
|
|
3147
|
+
const mimeType = blob.type ?? request.mimeType;
|
|
3148
|
+
const nip94 = [["url", blob.url]];
|
|
3149
|
+
if (mimeType) nip94.push(["m", mimeType]);
|
|
3150
|
+
nip94.push(["x", sha256], ["size", String(bytes.byteLength)]);
|
|
2802
3151
|
const result2 = {
|
|
2803
3152
|
ok: true,
|
|
2804
3153
|
uploadId: ctx.uploadId,
|
|
2805
3154
|
status: "complete",
|
|
2806
3155
|
rail: "blossom",
|
|
2807
3156
|
url: blob.url,
|
|
2808
|
-
sha256
|
|
2809
|
-
size:
|
|
3157
|
+
sha256,
|
|
3158
|
+
size: bytes.byteLength,
|
|
3159
|
+
nip94
|
|
2810
3160
|
};
|
|
2811
|
-
if (
|
|
3161
|
+
if (mimeType) result2.mimeType = mimeType;
|
|
2812
3162
|
return result2;
|
|
2813
3163
|
}
|
|
2814
3164
|
function failed(uploadId, rail, error, sha256) {
|
|
2815
3165
|
return { ok: false, uploadId, status: "failed", rail, error, ...sha256 ? { sha256 } : {} };
|
|
2816
3166
|
}
|
|
3167
|
+
function cancelled(uploadId, rail, sha256) {
|
|
3168
|
+
return { ok: false, uploadId, status: "cancelled", rail, error: "user cancelled", ...sha256 ? { sha256 } : {} };
|
|
3169
|
+
}
|
|
3170
|
+
function emitUploading(ctx, rail, bytesTotal, nowS) {
|
|
3171
|
+
ctx.onStatus({
|
|
3172
|
+
ok: true,
|
|
3173
|
+
uploadId: ctx.uploadId,
|
|
3174
|
+
status: "uploading",
|
|
3175
|
+
rail,
|
|
3176
|
+
bytesTotal,
|
|
3177
|
+
updatedAt: nowS() * 1e3
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
2817
3180
|
function firstConfiguredRail(rails) {
|
|
2818
3181
|
if (rails.nip96?.servers?.length) return "nip96";
|
|
2819
3182
|
if (rails.blossom?.servers?.length) return "blossom";
|