@planet-matrix/mobius-model 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +134 -21
- package/dist/index.js +45 -4
- package/dist/index.js.map +186 -11
- package/oxlint.config.ts +6 -0
- package/package.json +16 -10
- package/src/abort/README.md +92 -0
- package/src/abort/abort-manager.ts +278 -0
- package/src/abort/abort-signal-listener-manager.ts +81 -0
- package/src/abort/index.ts +2 -0
- package/src/basic/README.md +69 -117
- package/src/basic/enhance.ts +10 -0
- package/src/basic/function.ts +81 -62
- package/src/basic/index.ts +2 -0
- package/src/basic/is.ts +152 -71
- package/src/basic/object.ts +82 -0
- package/src/basic/promise.ts +29 -8
- package/src/basic/string.ts +2 -33
- package/src/color/README.md +105 -0
- package/src/color/index.ts +3 -0
- package/src/color/internal.ts +42 -0
- package/src/color/rgb/analyze.ts +236 -0
- package/src/color/rgb/construct.ts +130 -0
- package/src/color/rgb/convert.ts +227 -0
- package/src/color/rgb/derive.ts +303 -0
- package/src/color/rgb/index.ts +6 -0
- package/src/color/rgb/internal.ts +208 -0
- package/src/color/rgb/parse.ts +302 -0
- package/src/color/rgb/serialize.ts +144 -0
- package/src/color/types.ts +57 -0
- package/src/color/xyz/analyze.ts +80 -0
- package/src/color/xyz/construct.ts +19 -0
- package/src/color/xyz/convert.ts +71 -0
- package/src/color/xyz/index.ts +3 -0
- package/src/color/xyz/internal.ts +23 -0
- package/src/css/README.md +93 -0
- package/src/css/class.ts +559 -0
- package/src/css/index.ts +1 -0
- package/src/encoding/README.md +92 -0
- package/src/encoding/base64.ts +107 -0
- package/src/encoding/index.ts +1 -0
- package/src/environment/README.md +97 -0
- package/src/environment/basic.ts +26 -0
- package/src/environment/device.ts +311 -0
- package/src/environment/feature.ts +285 -0
- package/src/environment/geo.ts +337 -0
- package/src/environment/index.ts +7 -0
- package/src/environment/runtime.ts +400 -0
- package/src/environment/snapshot.ts +60 -0
- package/src/environment/variable.ts +239 -0
- package/src/event/README.md +90 -0
- package/src/event/class-event-proxy.ts +228 -0
- package/src/event/common.ts +19 -0
- package/src/event/event-manager.ts +203 -0
- package/src/event/index.ts +4 -0
- package/src/event/instance-event-proxy.ts +186 -0
- package/src/event/internal.ts +24 -0
- package/src/exception/README.md +96 -0
- package/src/exception/browser.ts +219 -0
- package/src/exception/index.ts +4 -0
- package/src/exception/nodejs.ts +169 -0
- package/src/exception/normalize.ts +106 -0
- package/src/exception/types.ts +99 -0
- package/src/identifier/README.md +92 -0
- package/src/identifier/id.ts +119 -0
- package/src/identifier/index.ts +2 -0
- package/src/identifier/uuid.ts +187 -0
- package/src/index.ts +18 -1
- package/src/log/README.md +79 -0
- package/src/log/index.ts +5 -0
- package/src/log/log-emitter.ts +72 -0
- package/src/log/log-record.ts +10 -0
- package/src/log/log-scheduler.ts +74 -0
- package/src/log/log-type.ts +8 -0
- package/src/log/logger.ts +543 -0
- package/src/orchestration/README.md +89 -0
- package/src/orchestration/coordination/barrier.ts +214 -0
- package/src/orchestration/coordination/count-down-latch.ts +215 -0
- package/src/orchestration/coordination/errors.ts +98 -0
- package/src/orchestration/coordination/index.ts +16 -0
- package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
- package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
- package/src/orchestration/coordination/keyed-lock.ts +168 -0
- package/src/orchestration/coordination/mutex.ts +257 -0
- package/src/orchestration/coordination/permit.ts +127 -0
- package/src/orchestration/coordination/read-write-lock.ts +444 -0
- package/src/orchestration/coordination/semaphore.ts +280 -0
- package/src/orchestration/index.ts +1 -0
- package/src/random/README.md +78 -0
- package/src/random/index.ts +1 -0
- package/src/random/string.ts +35 -0
- package/src/reactor/README.md +4 -0
- package/src/reactor/reactor-core/primitive.ts +9 -9
- package/src/reactor/reactor-core/reactive-system.ts +5 -5
- package/src/singleton/README.md +79 -0
- package/src/singleton/factory.ts +55 -0
- package/src/singleton/index.ts +2 -0
- package/src/singleton/manager.ts +204 -0
- package/src/storage/README.md +107 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/table.ts +449 -0
- package/src/timer/README.md +86 -0
- package/src/timer/expiration/expiration-manager.ts +594 -0
- package/src/timer/expiration/index.ts +3 -0
- package/src/timer/expiration/min-heap.ts +208 -0
- package/src/timer/expiration/remaining-manager.ts +241 -0
- package/src/timer/index.ts +1 -0
- package/src/type/README.md +54 -307
- package/src/type/class.ts +2 -2
- package/src/type/index.ts +14 -14
- package/src/type/is.ts +265 -2
- package/src/type/object.ts +37 -0
- package/src/type/string.ts +7 -2
- package/src/type/tuple.ts +6 -6
- package/src/type/union.ts +16 -0
- package/src/web/README.md +77 -0
- package/src/web/capture.ts +35 -0
- package/src/web/clipboard.ts +97 -0
- package/src/web/dom.ts +117 -0
- package/src/web/download.ts +16 -0
- package/src/web/event.ts +46 -0
- package/src/web/index.ts +10 -0
- package/src/web/local-storage.ts +113 -0
- package/src/web/location.ts +28 -0
- package/src/web/permission.ts +172 -0
- package/src/web/script-loader.ts +432 -0
- package/tests/unit/abort/abort-manager.spec.ts +225 -0
- package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
- package/tests/unit/basic/array.spec.ts +1 -1
- package/tests/unit/basic/object.spec.ts +32 -1
- package/tests/unit/basic/stream.spec.ts +1 -1
- package/tests/unit/basic/string.spec.ts +0 -9
- package/tests/unit/color/rgb/analyze.spec.ts +110 -0
- package/tests/unit/color/rgb/construct.spec.ts +56 -0
- package/tests/unit/color/rgb/convert.spec.ts +60 -0
- package/tests/unit/color/rgb/derive.spec.ts +103 -0
- package/tests/unit/color/rgb/parse.spec.ts +66 -0
- package/tests/unit/color/rgb/serialize.spec.ts +46 -0
- package/tests/unit/color/xyz/analyze.spec.ts +33 -0
- package/tests/unit/color/xyz/construct.spec.ts +10 -0
- package/tests/unit/color/xyz/convert.spec.ts +18 -0
- package/tests/unit/css/class.spec.ts +157 -0
- package/tests/unit/encoding/base64.spec.ts +40 -0
- package/tests/unit/environment/basic.spec.ts +20 -0
- package/tests/unit/environment/device.spec.ts +146 -0
- package/tests/unit/environment/feature.spec.ts +388 -0
- package/tests/unit/environment/geo.spec.ts +111 -0
- package/tests/unit/environment/runtime.spec.ts +364 -0
- package/tests/unit/environment/snapshot.spec.ts +4 -0
- package/tests/unit/environment/variable.spec.ts +190 -0
- package/tests/unit/event/class-event-proxy.spec.ts +225 -0
- package/tests/unit/event/event-manager.spec.ts +246 -0
- package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
- package/tests/unit/exception/browser.spec.ts +213 -0
- package/tests/unit/exception/nodejs.spec.ts +144 -0
- package/tests/unit/exception/normalize.spec.ts +57 -0
- package/tests/unit/identifier/id.spec.ts +71 -0
- package/tests/unit/identifier/uuid.spec.ts +85 -0
- package/tests/unit/log/log-emitter.spec.ts +33 -0
- package/tests/unit/log/log-scheduler.spec.ts +40 -0
- package/tests/unit/log/log-type.spec.ts +7 -0
- package/tests/unit/log/logger.spec.ts +222 -0
- package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
- package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
- package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
- package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
- package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
- package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
- package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
- package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
- package/tests/unit/random/string.spec.ts +11 -0
- package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
- package/tests/unit/reactor/preact-signal.spec.ts +1 -2
- package/tests/unit/singleton/singleton.spec.ts +49 -0
- package/tests/unit/storage/table.spec.ts +620 -0
- package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
- package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
- package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
- package/.oxlintrc.json +0 -5
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useFactory } from "./basic.ts"
|
|
2
|
+
|
|
3
|
+
import type { Use } from "./basic.ts"
|
|
4
|
+
|
|
5
|
+
import type { StringAutoCompletable } from "../type/string.ts"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Define built-in and custom feature keys for capability detection.
|
|
9
|
+
*/
|
|
10
|
+
export type Feature =
|
|
11
|
+
| StringAutoCompletable
|
|
12
|
+
| "process"
|
|
13
|
+
| "navigator"
|
|
14
|
+
| "permissions"
|
|
15
|
+
| "document"
|
|
16
|
+
| "css"
|
|
17
|
+
|
|
18
|
+
interface FeatureRegistryItem {
|
|
19
|
+
feature: Feature
|
|
20
|
+
detect: () => boolean
|
|
21
|
+
}
|
|
22
|
+
const internalFeatureRegistry = new Map<Feature, FeatureRegistryItem>();
|
|
23
|
+
/**
|
|
24
|
+
* Check whether a feature is registered.
|
|
25
|
+
*/
|
|
26
|
+
export const isFeatureRegistered = (feature: Feature): boolean => {
|
|
27
|
+
return internalFeatureRegistry.has(feature);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Register a feature detector.
|
|
31
|
+
*/
|
|
32
|
+
export const registerFeature = (item: FeatureRegistryItem): void => {
|
|
33
|
+
internalFeatureRegistry.set(item.feature, item);
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Unregister a feature detector.
|
|
37
|
+
*/
|
|
38
|
+
export const unregisterFeature = (feature: Feature): void => {
|
|
39
|
+
internalFeatureRegistry.delete(feature);
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* List all registered feature keys.
|
|
43
|
+
*/
|
|
44
|
+
export const listFeatures = (): Feature[] => {
|
|
45
|
+
return Array.from(internalFeatureRegistry.keys());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a feature registry item by feature key.
|
|
50
|
+
*/
|
|
51
|
+
export const getFeatureRegistryItem = (feature: Feature): FeatureRegistryItem | undefined => {
|
|
52
|
+
return internalFeatureRegistry.get(feature);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Define detection results for registered features.
|
|
57
|
+
*/
|
|
58
|
+
export type FeatureFlags = Record<Feature, boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Get the flags for all registered features.
|
|
61
|
+
*/
|
|
62
|
+
export const getFeatureFlags = (): FeatureFlags => {
|
|
63
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
64
|
+
const flags: FeatureFlags = {} as FeatureFlags;
|
|
65
|
+
|
|
66
|
+
for (const [name, item] of internalFeatureRegistry) {
|
|
67
|
+
flags[name] = item.detect();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return flags;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if the current environment satisfies the specified feature.
|
|
75
|
+
*/
|
|
76
|
+
export const isSatisfiesFeature = (feature: Feature): boolean => {
|
|
77
|
+
const featureRegistryItem = getFeatureRegistryItem(feature);
|
|
78
|
+
if (featureRegistryItem === undefined) {
|
|
79
|
+
throw new Error(`Feature "${feature}" is not registered.`);
|
|
80
|
+
}
|
|
81
|
+
return featureRegistryItem.detect() === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if the current environment satisfies the specified feature conditions.
|
|
86
|
+
*/
|
|
87
|
+
export const isSatisfiesFeatures = (condition: Partial<FeatureFlags>): boolean => {
|
|
88
|
+
let result = true;
|
|
89
|
+
Object.keys(condition).forEach((feature) => {
|
|
90
|
+
const featureRegistryItem = getFeatureRegistryItem(feature as Feature);
|
|
91
|
+
if (featureRegistryItem === undefined) {
|
|
92
|
+
throw new Error(`Feature "${feature}" is not registered.`);
|
|
93
|
+
}
|
|
94
|
+
if (featureRegistryItem.detect() !== condition[feature]) {
|
|
95
|
+
result = false;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if the `process` object is available in the environment.
|
|
103
|
+
*/
|
|
104
|
+
export const supportProcess = (): boolean => {
|
|
105
|
+
return typeof process !== "undefined" && process !== null && typeof process === "object"
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the `process` object from the current environment.
|
|
109
|
+
*/
|
|
110
|
+
export const getProcess = (): NodeJS.Process => {
|
|
111
|
+
return globalThis.process
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the `process` object if it is available, otherwise throw an error.
|
|
115
|
+
*/
|
|
116
|
+
export const ensureProcess = (): NodeJS.Process => {
|
|
117
|
+
if (supportProcess() === true) {
|
|
118
|
+
return getProcess()
|
|
119
|
+
} else {
|
|
120
|
+
throw new Error("The process object is not supported in the current environment.")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Execute a function with the `process` object if it is available.
|
|
125
|
+
*/
|
|
126
|
+
export const useProcess: Use<NodeJS.Process> = useFactory(getProcess, supportProcess)
|
|
127
|
+
registerFeature({
|
|
128
|
+
feature: "process",
|
|
129
|
+
detect: supportProcess,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if the `navigator` object is available in the environment.
|
|
134
|
+
*/
|
|
135
|
+
export const supportNavigator = (): boolean => {
|
|
136
|
+
return typeof navigator !== "undefined" && navigator !== null && typeof navigator === "object"
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get the `navigator` object from the current environment.
|
|
140
|
+
*/
|
|
141
|
+
export const getNavigator = (): Navigator => {
|
|
142
|
+
return globalThis.navigator
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the `navigator` object if it is available, otherwise throw an error.
|
|
146
|
+
*/
|
|
147
|
+
export const ensureNavigator = (): Navigator => {
|
|
148
|
+
if (supportNavigator() === true) {
|
|
149
|
+
return getNavigator()
|
|
150
|
+
} else {
|
|
151
|
+
throw new Error("The navigator object is not supported in the current environment.")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Execute a function with the `navigator` object if it is available.
|
|
156
|
+
*/
|
|
157
|
+
export const useNavigator: Use<Navigator> = useFactory(getNavigator, supportNavigator)
|
|
158
|
+
registerFeature({
|
|
159
|
+
feature: "navigator",
|
|
160
|
+
detect: supportNavigator,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if the `clipboard` API is available in the environment.
|
|
165
|
+
*/
|
|
166
|
+
export const supportClipboard = (): boolean => {
|
|
167
|
+
return supportNavigator() && "clipboard" in navigator
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get the `clipboard` API from the current environment.
|
|
171
|
+
*/
|
|
172
|
+
export const getClipboard = (): Clipboard => {
|
|
173
|
+
return navigator.clipboard
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get the `clipboard` API if it is available, otherwise throw an error.
|
|
177
|
+
*/
|
|
178
|
+
export const ensureClipboard = (): Clipboard => {
|
|
179
|
+
if (supportClipboard() === true) {
|
|
180
|
+
return getClipboard()
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error("The clipboard API is not supported in the current environment.")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Execute a function with the `clipboard` API if it is available.
|
|
187
|
+
*/
|
|
188
|
+
export const useClipboard: Use<Clipboard> = useFactory(getClipboard, supportClipboard)
|
|
189
|
+
registerFeature({
|
|
190
|
+
feature: "clipboard",
|
|
191
|
+
detect: supportClipboard,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if the `permissions` API is available in the environment.
|
|
196
|
+
*/
|
|
197
|
+
export const supportPermissions = (): boolean => {
|
|
198
|
+
return supportNavigator() && "permissions" in navigator
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get the `permissions` API from the current environment.
|
|
202
|
+
*/
|
|
203
|
+
export const getPermissions = (): Permissions => {
|
|
204
|
+
return navigator.permissions
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get the `permissions` API if it is available, otherwise throw an error.
|
|
208
|
+
*/
|
|
209
|
+
export const ensurePermissions = (): Permissions => {
|
|
210
|
+
if (supportPermissions() === true) {
|
|
211
|
+
return getPermissions()
|
|
212
|
+
} else {
|
|
213
|
+
throw new Error("The permissions API is not supported in the current environment.")
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Execute a function with the `permissions` API if it is available.
|
|
218
|
+
*/
|
|
219
|
+
export const usePermissions: Use<Permissions> = useFactory(getPermissions, supportPermissions)
|
|
220
|
+
registerFeature({
|
|
221
|
+
feature: "permissions",
|
|
222
|
+
detect: supportPermissions,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if the `document` object is available in the environment.
|
|
227
|
+
*/
|
|
228
|
+
export const supportDocument = (): boolean => {
|
|
229
|
+
return typeof document !== "undefined" && document !== null && typeof document === "object"
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get the `document` object from the current environment.
|
|
233
|
+
*/
|
|
234
|
+
export const getDocument = (): Document => {
|
|
235
|
+
return globalThis.document
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get the `document` object if it is available, otherwise throw an error.
|
|
239
|
+
*/
|
|
240
|
+
export const ensureDocument = (): Document => {
|
|
241
|
+
if (supportDocument() === true) {
|
|
242
|
+
return getDocument()
|
|
243
|
+
} else {
|
|
244
|
+
throw new Error("The document object is not supported in the current environment.")
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Execute a function with the `document` object if it is available.
|
|
249
|
+
*/
|
|
250
|
+
export const useDocument: Use<Document> = useFactory(getDocument, supportDocument)
|
|
251
|
+
registerFeature({
|
|
252
|
+
feature: "document",
|
|
253
|
+
detect: supportDocument,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check if the `CSS` object is available in the environment.
|
|
258
|
+
*/
|
|
259
|
+
export const supportCSS = (): boolean => {
|
|
260
|
+
return typeof CSS !== "undefined" && CSS !== null && typeof CSS === "object"
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get the `CSS` object from the current environment.
|
|
264
|
+
*/
|
|
265
|
+
export const getCSS = (): typeof CSS => {
|
|
266
|
+
return globalThis.CSS
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get the `CSS` object if it is available, otherwise throw an error.
|
|
270
|
+
*/
|
|
271
|
+
export const ensureCSS = (): typeof CSS => {
|
|
272
|
+
if (supportCSS() === true) {
|
|
273
|
+
return getCSS()
|
|
274
|
+
} else {
|
|
275
|
+
throw new Error("The CSS object is not supported in the current environment.")
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Execute a function with the `CSS` object if it is available.
|
|
280
|
+
*/
|
|
281
|
+
export const useCSS: Use<typeof CSS> = useFactory(getCSS, supportCSS)
|
|
282
|
+
registerFeature({
|
|
283
|
+
feature: "css",
|
|
284
|
+
detect: supportCSS,
|
|
285
|
+
})
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 描述来自 Cloudflare trace 接口的地理信息。
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```
|
|
6
|
+
* fl=1232f78
|
|
7
|
+
* h=www.cloudflare.com
|
|
8
|
+
* ip=23.132.124.129
|
|
9
|
+
* ts=1772261014.000
|
|
10
|
+
* visit_scheme=https
|
|
11
|
+
* uag=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0
|
|
12
|
+
* colo=SJC
|
|
13
|
+
* sliver=none
|
|
14
|
+
* http=http/2
|
|
15
|
+
* loc=US
|
|
16
|
+
* tls=TLSv1.3
|
|
17
|
+
* sni=plaintext
|
|
18
|
+
* warp=off
|
|
19
|
+
* gateway=off
|
|
20
|
+
* rbi=off
|
|
21
|
+
* kex=X25519MLKEM768
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export interface GeoInfoFromCloudflare {
|
|
25
|
+
/**
|
|
26
|
+
* Data center request was handled at (Cloudflare internal code)
|
|
27
|
+
*/
|
|
28
|
+
fl?: string | undefined
|
|
29
|
+
/**
|
|
30
|
+
* Hostname of the incoming HTTP request
|
|
31
|
+
*/
|
|
32
|
+
h?: string | undefined
|
|
33
|
+
/**
|
|
34
|
+
* Client IP address detected by Cloudflare
|
|
35
|
+
*/
|
|
36
|
+
ip?: string | undefined
|
|
37
|
+
/**
|
|
38
|
+
* Timestamp of request (in seconds)
|
|
39
|
+
*/
|
|
40
|
+
ts?: string | undefined
|
|
41
|
+
/**
|
|
42
|
+
* Request scheme used by the visitor (for example: http/https)
|
|
43
|
+
*/
|
|
44
|
+
visit_scheme?: string | undefined
|
|
45
|
+
/**
|
|
46
|
+
* User-Agent header value
|
|
47
|
+
*/
|
|
48
|
+
uag?: string | undefined
|
|
49
|
+
/**
|
|
50
|
+
* IATA-like code of the Cloudflare edge location
|
|
51
|
+
*/
|
|
52
|
+
colo?: string | undefined
|
|
53
|
+
/**
|
|
54
|
+
* Cloudflare edge “sliver” identifier
|
|
55
|
+
*/
|
|
56
|
+
sliver?: string | undefined
|
|
57
|
+
/**
|
|
58
|
+
* HTTP protocol version used for the request
|
|
59
|
+
*/
|
|
60
|
+
http?: string | undefined
|
|
61
|
+
/**
|
|
62
|
+
* Country code inferred from client IP (ISO 3166-1 alpha-2)
|
|
63
|
+
*/
|
|
64
|
+
loc?: string | undefined
|
|
65
|
+
/**
|
|
66
|
+
* TLS protocol version negotiated for the connection
|
|
67
|
+
*/
|
|
68
|
+
tls?: string | undefined
|
|
69
|
+
/**
|
|
70
|
+
* Server Name Indication (SNI) mode
|
|
71
|
+
*/
|
|
72
|
+
sni?: string | undefined
|
|
73
|
+
/**
|
|
74
|
+
* Whether WARP is enabled for the client
|
|
75
|
+
*/
|
|
76
|
+
warp?: string | undefined
|
|
77
|
+
/**
|
|
78
|
+
* Whether Cloudflare Gateway is enabled for the client
|
|
79
|
+
*/
|
|
80
|
+
gateway?: string | undefined
|
|
81
|
+
/**
|
|
82
|
+
* Whether browser isolation (RBI) is enabled
|
|
83
|
+
*/
|
|
84
|
+
rbi?: string | undefined
|
|
85
|
+
/**
|
|
86
|
+
* Key exchange algorithm used by TLS
|
|
87
|
+
*/
|
|
88
|
+
kex?: string | undefined
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse geolocation information from Cloudflare trace response.
|
|
92
|
+
*/
|
|
93
|
+
const parseGeoInfoFromCloudflare = (text: string): GeoInfoFromCloudflare => {
|
|
94
|
+
const pairs = text
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((pair: string) => pair.split('='))
|
|
97
|
+
.filter(([key]) => key !== '')
|
|
98
|
+
const result = pairs.reduce((geo: GeoInfoFromCloudflare, pair) => {
|
|
99
|
+
const [key, value] = pair
|
|
100
|
+
if (key !== undefined) {
|
|
101
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
102
|
+
geo[key as keyof GeoInfoFromCloudflare] = value
|
|
103
|
+
}
|
|
104
|
+
return geo
|
|
105
|
+
}, {})
|
|
106
|
+
return result
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get device geolocation information from Cloudflare
|
|
110
|
+
*
|
|
111
|
+
* @see {@link https://dev.to/brojenuel/get-ip-clients-addresses-6k6}
|
|
112
|
+
* @see {@link https://dataflowkit.com/blog/determine-location-of-users/}
|
|
113
|
+
*/
|
|
114
|
+
export const getGeoInfoFromCloudflare = async (): Promise<GeoInfoFromCloudflare> => {
|
|
115
|
+
const response = await fetch('https://www.cloudflare.com/cdn-cgi/trace', {
|
|
116
|
+
method: 'GET',
|
|
117
|
+
mode: 'cors',
|
|
118
|
+
credentials: 'omit'
|
|
119
|
+
})
|
|
120
|
+
const responseText = await response.text()
|
|
121
|
+
const geo = parseGeoInfoFromCloudflare(responseText)
|
|
122
|
+
|
|
123
|
+
return geo
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 描述来自 ipapi 响应的地理信息。
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```
|
|
131
|
+
* {
|
|
132
|
+
* "ip": "23.132.124.129",
|
|
133
|
+
* "network": "23.132.124.0/24",
|
|
134
|
+
* "version": "IPv4",
|
|
135
|
+
* "city": "Los Angeles",
|
|
136
|
+
* "region": "California",
|
|
137
|
+
* "region_code": "CA",
|
|
138
|
+
* "country": "US",
|
|
139
|
+
* "country_name": "United States",
|
|
140
|
+
* "country_code": "US",
|
|
141
|
+
* "country_code_iso3": "USA",
|
|
142
|
+
* "country_capital": "Washington",
|
|
143
|
+
* "country_tld": ".us",
|
|
144
|
+
* "continent_code": "NA",
|
|
145
|
+
* "in_eu": false,
|
|
146
|
+
* "postal": "90001",
|
|
147
|
+
* "latitude": 34.05223,
|
|
148
|
+
* "longitude": -118.24368,
|
|
149
|
+
* "timezone": "America/Los_Angeles",
|
|
150
|
+
* "utc_offset": "-0800",
|
|
151
|
+
* "country_calling_code": "+1",
|
|
152
|
+
* "currency": "USD",
|
|
153
|
+
* "currency_name": "Dollar",
|
|
154
|
+
* "languages": "en-US,es-US,haw,fr",
|
|
155
|
+
* "country_area": 9629091,
|
|
156
|
+
* "country_population": 327167434,
|
|
157
|
+
* "asn": "AS7018",
|
|
158
|
+
* "org": "AT&T Enterprises, LLC"
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export interface GeoInfoFromIpapi {
|
|
163
|
+
/**
|
|
164
|
+
* Public (external) IP address (same as URL ip).
|
|
165
|
+
*/
|
|
166
|
+
ip?: string | undefined
|
|
167
|
+
/**
|
|
168
|
+
* Network address in CIDR notation.
|
|
169
|
+
*/
|
|
170
|
+
network?: string | undefined
|
|
171
|
+
/**
|
|
172
|
+
* IP version (IPv4 or IPv6).
|
|
173
|
+
*/
|
|
174
|
+
version?: string | undefined
|
|
175
|
+
/**
|
|
176
|
+
* City name.
|
|
177
|
+
*/
|
|
178
|
+
city?: string | undefined
|
|
179
|
+
/**
|
|
180
|
+
* Region name (administrative division).
|
|
181
|
+
*/
|
|
182
|
+
region?: string | undefined
|
|
183
|
+
/**
|
|
184
|
+
* Region code.
|
|
185
|
+
*/
|
|
186
|
+
region_code?: string | undefined
|
|
187
|
+
/**
|
|
188
|
+
* Country code (2 letter, ISO 3166-1 alpha-2).
|
|
189
|
+
*/
|
|
190
|
+
country?: string | undefined
|
|
191
|
+
/**
|
|
192
|
+
* Short country name.
|
|
193
|
+
*/
|
|
194
|
+
country_name?: string | undefined
|
|
195
|
+
/**
|
|
196
|
+
* Country code (2 letter, ISO 3166-1 alpha-2).
|
|
197
|
+
*/
|
|
198
|
+
country_code?: string | undefined
|
|
199
|
+
/**
|
|
200
|
+
* Country code (3 letter, ISO 3166-1 alpha-3).
|
|
201
|
+
*/
|
|
202
|
+
country_code_iso3?: string | undefined
|
|
203
|
+
/**
|
|
204
|
+
* Capital of the country.
|
|
205
|
+
*/
|
|
206
|
+
country_capital?: string | undefined
|
|
207
|
+
/**
|
|
208
|
+
* Country specific TLD (top-level domain).
|
|
209
|
+
*/
|
|
210
|
+
country_tld?: string | undefined
|
|
211
|
+
/**
|
|
212
|
+
* Continent code.
|
|
213
|
+
*/
|
|
214
|
+
continent_code?: string | undefined
|
|
215
|
+
/**
|
|
216
|
+
* Whether IP address belongs to a country that is a member of the European Union (EU).
|
|
217
|
+
*/
|
|
218
|
+
in_eu?: boolean | undefined
|
|
219
|
+
/**
|
|
220
|
+
* Postal code / zip code.
|
|
221
|
+
*/
|
|
222
|
+
postal?: string | undefined
|
|
223
|
+
/**
|
|
224
|
+
* Latitude coordinate.
|
|
225
|
+
*/
|
|
226
|
+
latitude?: number | undefined
|
|
227
|
+
/**
|
|
228
|
+
* Longitude coordinate.
|
|
229
|
+
*/
|
|
230
|
+
longitude?: number | undefined
|
|
231
|
+
/**
|
|
232
|
+
* Timezone (IANA format i.e. “Area/Location”).
|
|
233
|
+
*/
|
|
234
|
+
timezone?: string | undefined
|
|
235
|
+
/**
|
|
236
|
+
* UTC offset (with daylight saving time) as `+HHMM` or `-HHMM` (HH is hours, MM is minutes).
|
|
237
|
+
*/
|
|
238
|
+
utc_offset?: string | undefined
|
|
239
|
+
/**
|
|
240
|
+
* Country calling code (dial in code, comma separated).
|
|
241
|
+
*/
|
|
242
|
+
country_calling_code?: string | undefined
|
|
243
|
+
/**
|
|
244
|
+
* Currency code (ISO 4217 format).
|
|
245
|
+
*/
|
|
246
|
+
currency?: string | undefined
|
|
247
|
+
/**
|
|
248
|
+
* Currency name.
|
|
249
|
+
*/
|
|
250
|
+
currency_name?: string | undefined
|
|
251
|
+
/**
|
|
252
|
+
* Languages spoken (comma separated 2 or 3 letter ISO 639 code with optional hyphen separated country suffix).
|
|
253
|
+
*/
|
|
254
|
+
languages?: string | undefined
|
|
255
|
+
/**
|
|
256
|
+
* Area of the country (in sq km).
|
|
257
|
+
*/
|
|
258
|
+
country_area?: number | undefined
|
|
259
|
+
/**
|
|
260
|
+
* Population of the country.
|
|
261
|
+
*/
|
|
262
|
+
country_population?: number | undefined
|
|
263
|
+
/**
|
|
264
|
+
* Autonomous System Number.
|
|
265
|
+
*/
|
|
266
|
+
asn?: string | undefined
|
|
267
|
+
/**
|
|
268
|
+
* Organization name.
|
|
269
|
+
*/
|
|
270
|
+
org?: string | undefined
|
|
271
|
+
/**
|
|
272
|
+
* Host or domain name associated with the IP (*optional beta add-on, please contact us for details).
|
|
273
|
+
*/
|
|
274
|
+
hostname?: string | undefined
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get geographical information from IP address using ipapi.co API.
|
|
278
|
+
*
|
|
279
|
+
* @see {@link https://ipapi.co/}
|
|
280
|
+
*/
|
|
281
|
+
export const getGeoInfoFromIpapi = async (): Promise<GeoInfoFromIpapi> => {
|
|
282
|
+
const response = await fetch(`https://ipapi.co/json/`, {
|
|
283
|
+
method: 'GET',
|
|
284
|
+
mode: 'cors',
|
|
285
|
+
credentials: 'omit'
|
|
286
|
+
})
|
|
287
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
288
|
+
const geo = await response.json() as GeoInfoFromIpapi
|
|
289
|
+
return geo
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 描述来自 ipify API 的公网 IP 信息。
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```
|
|
297
|
+
* {
|
|
298
|
+
* "ip": "23.132.124.129"
|
|
299
|
+
* }
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
export interface GeoInfoFromIpify {
|
|
303
|
+
ip: string
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get public IP address using ipify API.
|
|
307
|
+
*
|
|
308
|
+
* @see {@link https://www.ipify.org/}
|
|
309
|
+
*/
|
|
310
|
+
export const getGeoInfoFromIpify = async (): Promise<GeoInfoFromIpify> => {
|
|
311
|
+
const response = await fetch("https://api.ipify.org?format=json", {
|
|
312
|
+
method: 'GET',
|
|
313
|
+
})
|
|
314
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
315
|
+
const geo = await response.json() as GeoInfoFromIpify
|
|
316
|
+
return geo
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Describe aggregated geolocation data from all providers.
|
|
321
|
+
*/
|
|
322
|
+
export interface GeoInfo {
|
|
323
|
+
cloudflare: GeoInfoFromCloudflare
|
|
324
|
+
ipapi: GeoInfoFromIpapi
|
|
325
|
+
ipify: GeoInfoFromIpify
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get geographical information from multiple sources.
|
|
329
|
+
*/
|
|
330
|
+
export const getGeoInfo = async (): Promise<GeoInfo> => {
|
|
331
|
+
const [cloudflare, ipapi, ipify] = await Promise.all([
|
|
332
|
+
getGeoInfoFromCloudflare(),
|
|
333
|
+
getGeoInfoFromIpapi(),
|
|
334
|
+
getGeoInfoFromIpify()
|
|
335
|
+
])
|
|
336
|
+
return { cloudflare, ipapi, ipify }
|
|
337
|
+
}
|