@kehto/services 0.15.0 → 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 -115
- package/dist/index.js +711 -317
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
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
|
-
|
|
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
|
+
});
|
|
867
984
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const unsubscribe = bridge.subscribe(m.action.defaultKey, (ev) => {
|
|
873
|
-
const e = ev;
|
|
874
|
-
if ("repeat" in e && e.repeat) return;
|
|
875
|
-
options.onForward?.({
|
|
876
|
-
key: e.key,
|
|
877
|
-
code: e.code,
|
|
878
|
-
ctrlKey: e.ctrlKey,
|
|
879
|
-
altKey: e.altKey,
|
|
880
|
-
shiftKey: e.shiftKey,
|
|
881
|
-
metaKey: e.metaKey
|
|
882
|
-
});
|
|
883
|
-
const payload = {
|
|
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
|
-
}
|
|
985
|
+
if (existing) {
|
|
986
|
+
try {
|
|
987
|
+
existing();
|
|
988
|
+
} catch {
|
|
901
989
|
}
|
|
902
|
-
|
|
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;
|
|
990
|
+
unsubscribeHandles.delete(m.action.id);
|
|
910
991
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
}
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
default: {
|
|
929
|
-
const id = message.id ?? "";
|
|
930
|
-
send({
|
|
931
|
-
type: `${message.type}.error`,
|
|
932
|
-
id,
|
|
933
|
-
error: `Unknown keys method: ${message.type}`
|
|
934
|
-
});
|
|
935
|
-
return;
|
|
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;
|
|
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);
|
|
1057
1145
|
}
|
|
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;
|
|
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) {
|
|
@@ -2030,6 +2127,60 @@ var OUTBOX_DESCRIPTOR = {
|
|
|
2030
2127
|
version: OUTBOX_SERVICE_VERSION,
|
|
2031
2128
|
description: "NAP-OUTBOX outbox-aware relay routing \u2014 getEvent/query/subscribe/publish/resolveRelays"
|
|
2032
2129
|
};
|
|
2130
|
+
function isRecord(value) {
|
|
2131
|
+
return typeof value === "object" && value !== null;
|
|
2132
|
+
}
|
|
2133
|
+
function stringArray(value) {
|
|
2134
|
+
if (!Array.isArray(value)) return void 0;
|
|
2135
|
+
const strings = value.filter((item) => typeof item === "string");
|
|
2136
|
+
return strings.length > 0 ? strings : void 0;
|
|
2137
|
+
}
|
|
2138
|
+
function uint(value) {
|
|
2139
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
2140
|
+
}
|
|
2141
|
+
function sanitizeEventOptions(raw) {
|
|
2142
|
+
if (!isRecord(raw)) return void 0;
|
|
2143
|
+
const out = {};
|
|
2144
|
+
if (typeof raw.author === "string") out.author = raw.author;
|
|
2145
|
+
const relays = stringArray(raw.relays);
|
|
2146
|
+
if (relays) out.relays = relays;
|
|
2147
|
+
const timeoutMs = uint(raw.timeoutMs);
|
|
2148
|
+
if (timeoutMs !== void 0) out.timeoutMs = timeoutMs;
|
|
2149
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2150
|
+
}
|
|
2151
|
+
function sanitizeQueryOptions(raw) {
|
|
2152
|
+
if (!isRecord(raw)) return void 0;
|
|
2153
|
+
const out = {};
|
|
2154
|
+
const authors = stringArray(raw.authors);
|
|
2155
|
+
if (authors) out.authors = authors;
|
|
2156
|
+
const relays = stringArray(raw.relays);
|
|
2157
|
+
if (relays) out.relays = relays;
|
|
2158
|
+
const limit = uint(raw.limit);
|
|
2159
|
+
if (limit !== void 0) out.limit = limit;
|
|
2160
|
+
const timeoutMs = uint(raw.timeoutMs);
|
|
2161
|
+
if (timeoutMs !== void 0) out.timeoutMs = timeoutMs;
|
|
2162
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2163
|
+
}
|
|
2164
|
+
var sanitizeSubscribeOptions = sanitizeQueryOptions;
|
|
2165
|
+
function sanitizePublishOptions(raw) {
|
|
2166
|
+
if (!isRecord(raw)) return void 0;
|
|
2167
|
+
const out = {};
|
|
2168
|
+
const relays = stringArray(raw.relays);
|
|
2169
|
+
if (relays) out.relays = relays;
|
|
2170
|
+
if (typeof raw.toOutbox === "boolean") out.toOutbox = raw.toOutbox;
|
|
2171
|
+
const toInboxes = stringArray(raw.toInboxes);
|
|
2172
|
+
if (toInboxes) out.toInboxes = toInboxes;
|
|
2173
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2174
|
+
}
|
|
2175
|
+
function sanitizeTarget(raw) {
|
|
2176
|
+
if (!isRecord(raw)) return void 0;
|
|
2177
|
+
const out = {};
|
|
2178
|
+
const authors = stringArray(raw.authors);
|
|
2179
|
+
if (authors) out.authors = authors;
|
|
2180
|
+
if (typeof raw.pubkey === "string") out.pubkey = raw.pubkey;
|
|
2181
|
+
if (raw.direction === "read" || raw.direction === "write") out.direction = raw.direction;
|
|
2182
|
+
return out;
|
|
2183
|
+
}
|
|
2033
2184
|
function normalizeFilters(raw) {
|
|
2034
2185
|
if (Array.isArray(raw)) {
|
|
2035
2186
|
const filters = raw.filter((f) => typeof f === "object" && f !== null);
|
|
@@ -2046,7 +2197,6 @@ function buildEventQuery(eventId, eventOptions) {
|
|
|
2046
2197
|
queryOptions.authors = [eventOptions.author];
|
|
2047
2198
|
}
|
|
2048
2199
|
if (Array.isArray(eventOptions?.relays)) queryOptions.relays = eventOptions.relays;
|
|
2049
|
-
if (eventOptions?.strategy !== void 0) queryOptions.strategy = eventOptions.strategy;
|
|
2050
2200
|
if (eventOptions?.timeoutMs !== void 0) queryOptions.timeoutMs = eventOptions.timeoutMs;
|
|
2051
2201
|
return { filter, queryOptions };
|
|
2052
2202
|
}
|
|
@@ -2084,7 +2234,7 @@ function handleGetEvent(router, msg, send) {
|
|
|
2084
2234
|
return;
|
|
2085
2235
|
}
|
|
2086
2236
|
const getEvent = router.getEvent ?? ((eventId, eventOptions) => fallbackGetEvent(router, eventId, eventOptions));
|
|
2087
|
-
void getEvent(m.eventId, m.options).then((result2) => sendEventResult(id, m.eventId, result2, send)).catch(
|
|
2237
|
+
void getEvent(m.eventId, sanitizeEventOptions(m.options)).then((result2) => sendEventResult(id, m.eventId, result2, send)).catch(
|
|
2088
2238
|
(err2) => send({ type: "outbox.getEvent.result", id, error: toErrorMessage(err2) })
|
|
2089
2239
|
);
|
|
2090
2240
|
}
|
|
@@ -2102,7 +2252,7 @@ function createOutboxService(options) {
|
|
|
2102
2252
|
send({ type: "outbox.query.result", id, events: [], error: "invalid filter" });
|
|
2103
2253
|
return;
|
|
2104
2254
|
}
|
|
2105
|
-
void router.query(filters, m.options).then(
|
|
2255
|
+
void router.query(filters, sanitizeQueryOptions(m.options)).then(
|
|
2106
2256
|
(result2) => send({
|
|
2107
2257
|
type: "outbox.query.result",
|
|
2108
2258
|
id,
|
|
@@ -2133,7 +2283,7 @@ function createOutboxService(options) {
|
|
|
2133
2283
|
send({ type: "outbox.closed", subId, ...reason === void 0 ? {} : { reason } });
|
|
2134
2284
|
}
|
|
2135
2285
|
};
|
|
2136
|
-
subscriptions.set(subKey, router.subscribe(filters, m.options, sink));
|
|
2286
|
+
subscriptions.set(subKey, router.subscribe(filters, sanitizeSubscribeOptions(m.options), sink));
|
|
2137
2287
|
}
|
|
2138
2288
|
function handleClose(windowId, msg, send) {
|
|
2139
2289
|
const m = msg;
|
|
@@ -2151,7 +2301,7 @@ function createOutboxService(options) {
|
|
|
2151
2301
|
send({ type: "outbox.publish.result", id, ok: false, error: "invalid filter" });
|
|
2152
2302
|
return;
|
|
2153
2303
|
}
|
|
2154
|
-
void router.publish(m.event, m.options).then(
|
|
2304
|
+
void router.publish(m.event, sanitizePublishOptions(m.options)).then(
|
|
2155
2305
|
(result2) => send({
|
|
2156
2306
|
type: "outbox.publish.result",
|
|
2157
2307
|
id,
|
|
@@ -2168,11 +2318,12 @@ function createOutboxService(options) {
|
|
|
2168
2318
|
function handleResolveRelays(msg, send) {
|
|
2169
2319
|
const m = msg;
|
|
2170
2320
|
const id = m.id ?? "";
|
|
2171
|
-
|
|
2321
|
+
const target = sanitizeTarget(m.target);
|
|
2322
|
+
if (!target) {
|
|
2172
2323
|
send({ type: "outbox.resolveRelays.result", id, error: "invalid filter" });
|
|
2173
2324
|
return;
|
|
2174
2325
|
}
|
|
2175
|
-
void router.resolveRelays(
|
|
2326
|
+
void router.resolveRelays(target).then((plan) => send({ type: "outbox.resolveRelays.result", id, plan })).catch(
|
|
2176
2327
|
(err2) => send({ type: "outbox.resolveRelays.result", id, error: toErrorMessage(err2) })
|
|
2177
2328
|
);
|
|
2178
2329
|
}
|
|
@@ -2233,11 +2384,6 @@ function deriveAuthors(filters, optionAuthors) {
|
|
|
2233
2384
|
for (const author of optionAuthors ?? []) authors.add(author);
|
|
2234
2385
|
return [...authors];
|
|
2235
2386
|
}
|
|
2236
|
-
function wantsWriteRelays(direction, strategy) {
|
|
2237
|
-
if (strategy === "outbox") return true;
|
|
2238
|
-
if (strategy === "inbox") return false;
|
|
2239
|
-
return direction === "read";
|
|
2240
|
-
}
|
|
2241
2387
|
function allowed(ctx, urls) {
|
|
2242
2388
|
const out = /* @__PURE__ */ new Set();
|
|
2243
2389
|
for (const url of urls) {
|
|
@@ -2245,9 +2391,33 @@ function allowed(ctx, urls) {
|
|
|
2245
2391
|
}
|
|
2246
2392
|
return [...out];
|
|
2247
2393
|
}
|
|
2248
|
-
async function
|
|
2249
|
-
const useWrite =
|
|
2250
|
-
const
|
|
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
|
+
}
|
|
2418
|
+
async function resolvePlan(ctx, pubkeys, direction, relayHints) {
|
|
2419
|
+
const useWrite = direction === "read";
|
|
2420
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
2251
2421
|
const missingAuthors = [];
|
|
2252
2422
|
let sawNip65 = false;
|
|
2253
2423
|
if (pubkeys.length > 0) {
|
|
@@ -2257,25 +2427,46 @@ async function resolvePlan(ctx, pubkeys, direction, strategy, relayHints) {
|
|
|
2257
2427
|
const relays2 = entry ? useWrite ? entry.write : entry.read : void 0;
|
|
2258
2428
|
if (relays2 && relays2.length > 0) {
|
|
2259
2429
|
sawNip65 = true;
|
|
2260
|
-
for (const url of relays2)
|
|
2430
|
+
for (const url of relays2) discovered.add(url);
|
|
2261
2431
|
} else {
|
|
2262
2432
|
missingAuthors.push(pubkey);
|
|
2263
2433
|
}
|
|
2264
2434
|
}
|
|
2265
2435
|
}
|
|
2266
|
-
|
|
2267
|
-
let relays = allowed(ctx,
|
|
2436
|
+
const hintRelays = allowed(ctx, relayHints ?? []);
|
|
2437
|
+
let relays = allowed(ctx, discovered);
|
|
2268
2438
|
let source;
|
|
2269
|
-
if (relays.length
|
|
2270
|
-
relays = allowed(ctx, ctx.fallbackRelays);
|
|
2271
|
-
source = "fallback";
|
|
2272
|
-
} else {
|
|
2439
|
+
if (relays.length > 0) {
|
|
2273
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);
|
|
2274
2447
|
}
|
|
2448
|
+
if (relays.length === 0) source = "fallback";
|
|
2275
2449
|
const plan = { relays, source };
|
|
2276
2450
|
if (missingAuthors.length > 0) plan.missingAuthors = missingAuthors;
|
|
2277
2451
|
return plan;
|
|
2278
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
|
+
}
|
|
2279
2470
|
function recordRelay(collector, id, relayUrl) {
|
|
2280
2471
|
let set = collector.relayMap.get(id);
|
|
2281
2472
|
if (!set) {
|
|
@@ -2284,82 +2475,166 @@ function recordRelay(collector, id, relayUrl) {
|
|
|
2284
2475
|
}
|
|
2285
2476
|
set.add(relayUrl);
|
|
2286
2477
|
}
|
|
2287
|
-
function
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
)
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
);
|
|
2300
|
-
return { events, incomplete: timedOut };
|
|
2301
|
-
}
|
|
2302
|
-
function collectFromRelays(ctx, filters, relayUrls2, timeoutMs) {
|
|
2303
|
-
return new Promise((resolve) => {
|
|
2304
|
-
const collector = { seen: /* @__PURE__ */ new Map(), relayMap: /* @__PURE__ */ new Map(), verifications: [] };
|
|
2305
|
-
const handles = [];
|
|
2306
|
-
let eoseCount = 0;
|
|
2307
|
-
let finished = false;
|
|
2308
|
-
let timedOut = false;
|
|
2309
|
-
function finalize() {
|
|
2310
|
-
if (finished) return;
|
|
2311
|
-
finished = true;
|
|
2312
|
-
clearTimeout(timer);
|
|
2313
|
-
for (const handle of handles) {
|
|
2314
|
-
try {
|
|
2315
|
-
handle.unsubscribe();
|
|
2316
|
-
} catch {
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
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;
|
|
2320
2490
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
}
|
|
2325
|
-
for (const relayUrl of relayUrls2) {
|
|
2326
|
-
handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
|
|
2327
|
-
if (finished) return;
|
|
2328
|
-
if (item === "EOSE") {
|
|
2329
|
-
eoseCount += 1;
|
|
2330
|
-
if (eoseCount >= relayUrls2.length) finalize();
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
recordRelay(collector, item.id, relayUrl);
|
|
2334
|
-
admitEvent(ctx, collector, item);
|
|
2335
|
-
}));
|
|
2491
|
+
collector.seen.set(event.id, event);
|
|
2492
|
+
try {
|
|
2493
|
+
sink.event(collectedEventResult(collector, event));
|
|
2494
|
+
} catch {
|
|
2336
2495
|
}
|
|
2337
|
-
|
|
2496
|
+
}).finally(() => {
|
|
2497
|
+
collector.pendingVerifications -= 1;
|
|
2498
|
+
onSettled();
|
|
2338
2499
|
});
|
|
2339
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
|
+
}
|
|
2340
2511
|
function relayPoolSubscribe(ctx, filters, relayUrl, cb) {
|
|
2341
2512
|
return ctx.relayPool.subscribe(filters, [relayUrl], cb);
|
|
2342
2513
|
}
|
|
2343
|
-
|
|
2514
|
+
function startQueryImpl(ctx, filters, options, sink) {
|
|
2344
2515
|
if (!ctx.relayPool.isAvailable()) {
|
|
2345
|
-
return {
|
|
2516
|
+
return {
|
|
2517
|
+
result: Promise.resolve({ events: [], incomplete: true, error: "relay list unavailable" }),
|
|
2518
|
+
close() {
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2346
2521
|
}
|
|
2347
|
-
const strategy = options?.strategy ?? "auto";
|
|
2348
2522
|
const authors = deriveAuthors(filters, options?.authors);
|
|
2349
|
-
const
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
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
|
+
}
|
|
2359
2589
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
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;
|
|
2363
2638
|
}
|
|
2364
2639
|
async function getEventImpl(ctx, eventId, options) {
|
|
2365
2640
|
if (typeof eventId !== "string" || eventId.length === 0) {
|
|
@@ -2372,7 +2647,6 @@ async function getEventImpl(ctx, eventId, options) {
|
|
|
2372
2647
|
queryOptions.authors = [options.author];
|
|
2373
2648
|
}
|
|
2374
2649
|
if (Array.isArray(options?.relays)) queryOptions.relays = options.relays;
|
|
2375
|
-
if (options?.strategy !== void 0) queryOptions.strategy = options.strategy;
|
|
2376
2650
|
if (options?.timeoutMs !== void 0) queryOptions.timeoutMs = options.timeoutMs;
|
|
2377
2651
|
const queryResult = await queryImpl(ctx, [filter], queryOptions);
|
|
2378
2652
|
const eventResult = queryResult.events.find((candidate) => candidate.event.id === eventId);
|
|
@@ -2384,72 +2658,138 @@ async function getEventImpl(ctx, eventId, options) {
|
|
|
2384
2658
|
return result2;
|
|
2385
2659
|
}
|
|
2386
2660
|
function closeLiveSub(sub) {
|
|
2387
|
-
for (const handle of sub.handles) {
|
|
2661
|
+
for (const handle of sub.handles.values()) {
|
|
2388
2662
|
try {
|
|
2389
2663
|
handle.unsubscribe();
|
|
2390
2664
|
} catch {
|
|
2391
2665
|
}
|
|
2392
2666
|
}
|
|
2393
|
-
sub.handles.
|
|
2394
|
-
}
|
|
2395
|
-
function attachLiveRelay(ctx, sub, filters, relayUrl,
|
|
2396
|
-
sub.
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
if (
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2667
|
+
sub.handles.clear();
|
|
2668
|
+
}
|
|
2669
|
+
function attachLiveRelay(ctx, sub, filters, relayUrl, sink) {
|
|
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
|
+
});
|
|
2681
|
+
});
|
|
2682
|
+
if (sub.closed) {
|
|
2683
|
+
try {
|
|
2684
|
+
handle.unsubscribe();
|
|
2685
|
+
} catch {
|
|
2407
2686
|
}
|
|
2408
|
-
|
|
2687
|
+
} else {
|
|
2688
|
+
sub.handles.set(relayUrl, handle);
|
|
2409
2689
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
});
|
|
2416
|
-
}));
|
|
2690
|
+
return true;
|
|
2691
|
+
} catch {
|
|
2692
|
+
sub.relays.delete(relayUrl);
|
|
2693
|
+
return false;
|
|
2694
|
+
}
|
|
2417
2695
|
}
|
|
2418
2696
|
function startSubscription(ctx, filters, options, sink) {
|
|
2419
|
-
const live = options?.live ?? true;
|
|
2420
|
-
const strategy = options?.strategy ?? "auto";
|
|
2421
2697
|
const authors = deriveAuthors(filters, options?.authors);
|
|
2422
|
-
const sub = {
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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();
|
|
2428
2730
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
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
|
+
}
|
|
2434
2757
|
return {
|
|
2435
2758
|
close() {
|
|
2436
2759
|
if (sub.closed) return;
|
|
2437
2760
|
sub.closed = true;
|
|
2761
|
+
discoveryOpen = false;
|
|
2762
|
+
clearTimeout(discoveryTimer);
|
|
2438
2763
|
closeLiveSub(sub);
|
|
2439
2764
|
}
|
|
2440
2765
|
};
|
|
2441
2766
|
}
|
|
2442
2767
|
async function resolvePublishTargets(ctx, signed, options) {
|
|
2443
|
-
const strategy = options?.strategy ?? "auto";
|
|
2444
2768
|
const targets = /* @__PURE__ */ new Set();
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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
|
+
}
|
|
2450
2787
|
}
|
|
2451
2788
|
for (const url of options?.relays ?? []) targets.add(url);
|
|
2452
|
-
return
|
|
2789
|
+
return {
|
|
2790
|
+
relayUrls: allowed(ctx, targets),
|
|
2791
|
+
requiredRelayUrls: allowed(ctx, requiredTargets)
|
|
2792
|
+
};
|
|
2453
2793
|
}
|
|
2454
2794
|
async function publishImpl(ctx, template, options) {
|
|
2455
2795
|
if (!ctx.signEvent) return { ok: false, error: "publish denied" };
|
|
@@ -2460,7 +2800,11 @@ async function publishImpl(ctx, template, options) {
|
|
|
2460
2800
|
} catch (err2) {
|
|
2461
2801
|
return { ok: false, error: err2 instanceof Error ? err2.message : "sign failed" };
|
|
2462
2802
|
}
|
|
2463
|
-
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;
|
|
2464
2808
|
if (relayUrls2.length === 0) return { ok: false, event: signed, eventId: signed.id, error: "relay list unavailable" };
|
|
2465
2809
|
let relays;
|
|
2466
2810
|
try {
|
|
@@ -2468,7 +2812,7 @@ async function publishImpl(ctx, template, options) {
|
|
|
2468
2812
|
} catch (err2) {
|
|
2469
2813
|
return { ok: false, event: signed, eventId: signed.id, error: err2 instanceof Error ? err2.message : "publish failed" };
|
|
2470
2814
|
}
|
|
2471
|
-
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);
|
|
2472
2816
|
const result2 = { ok, event: signed, eventId: signed.id, relays };
|
|
2473
2817
|
if (!ok) result2.error = "publish denied";
|
|
2474
2818
|
return result2;
|
|
@@ -2503,11 +2847,12 @@ function createRelayPoolOutboxRouter(options) {
|
|
|
2503
2847
|
return {
|
|
2504
2848
|
getEvent: (eventId, eventOptions) => getEventImpl(ctx, eventId, eventOptions),
|
|
2505
2849
|
query: (filters, queryOptions) => queryImpl(ctx, filters, queryOptions),
|
|
2850
|
+
queryStream: (filters, queryOptions, sink) => startQueryImpl(ctx, filters, queryOptions, sink),
|
|
2506
2851
|
subscribe: (filters, subscribeOptions, sink) => startSubscription(ctx, filters, subscribeOptions, sink),
|
|
2507
2852
|
publish: (template, publishOptions) => publishImpl(ctx, template, publishOptions),
|
|
2508
2853
|
resolveRelays: (target) => {
|
|
2509
2854
|
const pubkeys = target.authors ?? (target.pubkey ? [target.pubkey] : []);
|
|
2510
|
-
return resolvePlan(ctx, pubkeys, target.direction ?? "read"
|
|
2855
|
+
return resolvePlan(ctx, pubkeys, target.direction ?? "read");
|
|
2511
2856
|
}
|
|
2512
2857
|
};
|
|
2513
2858
|
}
|
|
@@ -2664,6 +3009,7 @@ function createHttpUploader(options) {
|
|
|
2664
3009
|
const digest = options.digestSha256 ?? defaultDigestSha256;
|
|
2665
3010
|
const nowS = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
2666
3011
|
const defaultRail = options.defaultRail ?? firstConfiguredRail(rails);
|
|
3012
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
2667
3013
|
async function upload(request, ctx) {
|
|
2668
3014
|
const rail = request.rail ?? defaultRail;
|
|
2669
3015
|
const config = rail === "nip96" ? rails.nip96 : rail === "blossom" ? rails.blossom : void 0;
|
|
@@ -2674,18 +3020,32 @@ function createHttpUploader(options) {
|
|
|
2674
3020
|
if (!server) {
|
|
2675
3021
|
return failed(ctx.uploadId, rail, "no server configured");
|
|
2676
3022
|
}
|
|
2677
|
-
const
|
|
2678
|
-
|
|
3023
|
+
const controller = new AbortController();
|
|
3024
|
+
inFlight.set(ctx.uploadId, controller);
|
|
3025
|
+
let sha256;
|
|
2679
3026
|
try {
|
|
2680
|
-
|
|
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 });
|
|
2681
3033
|
} catch (err2) {
|
|
3034
|
+
if (controller.signal.aborted) {
|
|
3035
|
+
return cancelled(ctx.uploadId, rail, sha256);
|
|
3036
|
+
}
|
|
2682
3037
|
return failed(ctx.uploadId, rail, toErrorMessage3(err2), sha256);
|
|
3038
|
+
} finally {
|
|
3039
|
+
inFlight.delete(ctx.uploadId);
|
|
2683
3040
|
}
|
|
2684
3041
|
}
|
|
2685
|
-
|
|
3042
|
+
function cancel(uploadId) {
|
|
3043
|
+
inFlight.get(uploadId)?.abort();
|
|
3044
|
+
}
|
|
3045
|
+
return { upload, cancel };
|
|
2686
3046
|
}
|
|
2687
3047
|
async function uploadNip96(args) {
|
|
2688
|
-
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
|
|
3048
|
+
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller } = args;
|
|
2689
3049
|
const auth = await signEvent({
|
|
2690
3050
|
kind: KIND_NIP98,
|
|
2691
3051
|
created_at: nowS(),
|
|
@@ -2701,10 +3061,12 @@ async function uploadNip96(args) {
|
|
|
2701
3061
|
if (request.caption !== void 0) form.append("caption", request.caption);
|
|
2702
3062
|
if (request.mimeType !== void 0) form.append("content_type", request.mimeType);
|
|
2703
3063
|
if (request.noTransform) form.append("no_transform", "true");
|
|
3064
|
+
emitUploading(ctx, "nip96", bytes.byteLength, nowS);
|
|
2704
3065
|
const res = await fetchFn(server, {
|
|
2705
3066
|
method: "POST",
|
|
2706
3067
|
headers: { Authorization: nostrAuthHeader(auth) },
|
|
2707
|
-
body: form
|
|
3068
|
+
body: form,
|
|
3069
|
+
signal: controller.signal
|
|
2708
3070
|
});
|
|
2709
3071
|
if (!res.ok) {
|
|
2710
3072
|
return failed(ctx.uploadId, "nip96", `server rejected (HTTP ${res.status})`, sha256);
|
|
@@ -2742,7 +3104,7 @@ function fromNip94Tags(uploadId, rail, tags, fallbackSize, fallbackSha) {
|
|
|
2742
3104
|
return result2;
|
|
2743
3105
|
}
|
|
2744
3106
|
async function uploadBlossom(args) {
|
|
2745
|
-
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
|
|
3107
|
+
const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS, controller } = args;
|
|
2746
3108
|
const auth = await signEvent({
|
|
2747
3109
|
kind: KIND_BLOSSOM_AUTH,
|
|
2748
3110
|
created_at: nowS(),
|
|
@@ -2756,33 +3118,65 @@ async function uploadBlossom(args) {
|
|
|
2756
3118
|
const endpoint = `${trimTrailingSlash(server)}/upload`;
|
|
2757
3119
|
const headers = { Authorization: nostrAuthHeader(auth) };
|
|
2758
3120
|
if (request.mimeType) headers["Content-Type"] = request.mimeType;
|
|
3121
|
+
emitUploading(ctx, "blossom", bytes.byteLength, nowS);
|
|
2759
3122
|
const res = await fetchFn(endpoint, {
|
|
2760
3123
|
method: "PUT",
|
|
2761
3124
|
headers,
|
|
2762
|
-
body: bytesToArrayBuffer(bytes)
|
|
3125
|
+
body: bytesToArrayBuffer(bytes),
|
|
3126
|
+
signal: controller.signal
|
|
2763
3127
|
});
|
|
2764
3128
|
if (!res.ok) {
|
|
2765
3129
|
return failed(ctx.uploadId, "blossom", `server rejected (HTTP ${res.status})`, sha256);
|
|
2766
3130
|
}
|
|
2767
3131
|
const blob = await res.json();
|
|
2768
|
-
if (
|
|
3132
|
+
if (typeof blob.url !== "string" || blob.url.length === 0) {
|
|
2769
3133
|
return failed(ctx.uploadId, "blossom", "server returned no url", sha256);
|
|
2770
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)]);
|
|
2771
3151
|
const result2 = {
|
|
2772
3152
|
ok: true,
|
|
2773
3153
|
uploadId: ctx.uploadId,
|
|
2774
3154
|
status: "complete",
|
|
2775
3155
|
rail: "blossom",
|
|
2776
3156
|
url: blob.url,
|
|
2777
|
-
sha256
|
|
2778
|
-
size:
|
|
3157
|
+
sha256,
|
|
3158
|
+
size: bytes.byteLength,
|
|
3159
|
+
nip94
|
|
2779
3160
|
};
|
|
2780
|
-
if (
|
|
3161
|
+
if (mimeType) result2.mimeType = mimeType;
|
|
2781
3162
|
return result2;
|
|
2782
3163
|
}
|
|
2783
3164
|
function failed(uploadId, rail, error, sha256) {
|
|
2784
3165
|
return { ok: false, uploadId, status: "failed", rail, error, ...sha256 ? { sha256 } : {} };
|
|
2785
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
|
+
}
|
|
2786
3180
|
function firstConfiguredRail(rails) {
|
|
2787
3181
|
if (rails.nip96?.servers?.length) return "nip96";
|
|
2788
3182
|
if (rails.blossom?.servers?.length) return "blossom";
|