socket-function 0.9.0 → 0.9.2
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/.eslintrc.js +50 -50
- package/SocketFunction.ts +280 -280
- package/SocketFunctionTypes.ts +90 -90
- package/hot/HotReloadController.ts +105 -105
- package/mobx/UrlParam.ts +39 -39
- package/mobx/observer.tsx +49 -49
- package/mobx/promiseToObservable.tsx +41 -41
- package/package.json +30 -28
- package/require/CSSShim.ts +19 -19
- package/require/RequireController.ts +252 -252
- package/require/buffer.js +2368 -2368
- package/require/compileFlags.ts +44 -44
- package/require/require.html +13 -13
- package/require/require.js +462 -456
- package/spec.txt +115 -115
- package/src/CallFactory.ts +389 -389
- package/src/JSONLACKS/JSONLACKS.generated.js +17 -17
- package/src/JSONLACKS/JSONLACKS.pegjs +247 -247
- package/src/JSONLACKS/JSONLACKS.ts +429 -375
- package/src/args.ts +21 -21
- package/src/batching.ts +170 -129
- package/src/caching.ts +318 -314
- package/src/callHTTPHandler.ts +203 -203
- package/src/callManager.ts +134 -134
- package/src/certStore.ts +29 -29
- package/src/fixLargeNetworkCalls.ts +8 -8
- package/src/formatting/colors.ts +78 -78
- package/src/formatting/format.ts +160 -156
- package/src/formatting/logColors.ts +17 -17
- package/src/misc.ts +302 -171
- package/src/nodeCache.ts +92 -92
- package/src/nodeProxy.ts +54 -54
- package/src/profiling/getOwnTime.ts +142 -142
- package/src/profiling/measure.ts +273 -244
- package/src/profiling/stats.ts +212 -212
- package/src/profiling/tcpLagProxy.ts +63 -63
- package/src/storagePath.ts +10 -10
- package/src/tlsParsing.ts +96 -96
- package/src/types.ts +8 -8
- package/src/webSocketServer.ts +250 -250
- package/test/client.css +2 -2
- package/test/client.ts +46 -46
- package/test/server.ts +43 -43
- package/test/shared.ts +52 -52
- package/tsconfig.json +26 -26
package/src/misc.ts
CHANGED
|
@@ -1,172 +1,303 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
|
-
import { canHaveChildren, MaybePromise } from "./types";
|
|
3
|
-
|
|
4
|
-
export type Watchable<T> = (callback: (value: T) => void) => MaybePromise<void>;
|
|
5
|
-
|
|
6
|
-
export function convertErrorStackToError(error: string): Error {
|
|
7
|
-
let errorObj = new Error();
|
|
8
|
-
errorObj.stack = String(error);
|
|
9
|
-
errorObj.message = String(error).split("\n")[0].slice("Error: ".length);
|
|
10
|
-
return errorObj;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function sha256Hash(buffer: Buffer | string): string {
|
|
14
|
-
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
15
|
-
}
|
|
16
|
-
export function sha256HashBuffer(buffer: Buffer | string): Buffer {
|
|
17
|
-
return crypto.createHash("sha256").update(buffer).digest();
|
|
18
|
-
}
|
|
19
|
-
/** Async, but works both clientside and serverside. */
|
|
20
|
-
export async function sha256HashPromise(buffer: Buffer) {
|
|
21
|
-
if (isNode()) {
|
|
22
|
-
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
23
|
-
} else {
|
|
24
|
-
let buf = await window.crypto.subtle.digest("SHA-256", buffer);
|
|
25
|
-
return Buffer.from(buf).toString("hex");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export async function sha256BufferPromise(buffer: Buffer): Promise<Buffer> {
|
|
29
|
-
if (isNode()) {
|
|
30
|
-
return crypto.createHash("sha256").update(buffer).digest();
|
|
31
|
-
} else {
|
|
32
|
-
let buf = await window.crypto.subtle.digest("SHA-256", buffer);
|
|
33
|
-
return Buffer.from(buf);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export function arrayEqual(a: unknown[], b: unknown[]) {
|
|
39
|
-
if (a.length !== b.length) return false;
|
|
40
|
-
for (let i = 0; i < a.length; i++) {
|
|
41
|
-
if (a[i] !== b[i]) return false;
|
|
42
|
-
}
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
export function isNode() {
|
|
46
|
-
return typeof document === "undefined";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function isNodeTrue() {
|
|
50
|
-
return isNode() as true;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function formatNumberSuffixed(count: number): string {
|
|
54
|
-
if (typeof count !== "number") return "0";
|
|
55
|
-
if (count < 0) {
|
|
56
|
-
return "-" + formatNumberSuffixed(-count);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let absValue = Math.abs(count);
|
|
60
|
-
|
|
61
|
-
const extraFactor = 10;
|
|
62
|
-
let divisor = 1;
|
|
63
|
-
let suffix = "";
|
|
64
|
-
if (absValue < 1000 * extraFactor) {
|
|
65
|
-
|
|
66
|
-
} else if (absValue < 1000 * 1000 * extraFactor) {
|
|
67
|
-
suffix = "K";
|
|
68
|
-
divisor = 1000;
|
|
69
|
-
} else if (absValue < 1000 * 1000 * 1000 * extraFactor) {
|
|
70
|
-
suffix = "M";
|
|
71
|
-
divisor = 1000 * 1000;
|
|
72
|
-
} else {
|
|
73
|
-
suffix = "B";
|
|
74
|
-
divisor = 1000 * 1000 * 1000;
|
|
75
|
-
}
|
|
76
|
-
count /= divisor;
|
|
77
|
-
absValue /= divisor;
|
|
78
|
-
|
|
79
|
-
return Math.round(count).toString() + suffix;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function list(count: number) {
|
|
83
|
-
let arr: number[] = [];
|
|
84
|
-
for (let i = 0; i < count; i++) {
|
|
85
|
-
arr.push(i);
|
|
86
|
-
}
|
|
87
|
-
return arr;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function recursiveFreeze<T>(obj: T): T {
|
|
91
|
-
if (!canHaveChildren(obj)) return obj;
|
|
92
|
-
let visited = new Set<unknown>();
|
|
93
|
-
function iterate(obj: unknown) {
|
|
94
|
-
if (!canHaveChildren(obj)) return;
|
|
95
|
-
if (visited.has(obj)) return;
|
|
96
|
-
visited.add(obj);
|
|
97
|
-
Object.freeze(obj);
|
|
98
|
-
let keys = getKeys(obj);
|
|
99
|
-
for (let key of keys) {
|
|
100
|
-
iterate(obj[key]);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
iterate(obj);
|
|
104
|
-
return obj;
|
|
105
|
-
}
|
|
106
|
-
export type ArrayBufferViewTypes = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigUint64Array | BigInt64Array | Float64Array | Float32Array | Uint8ClampedArray;
|
|
107
|
-
export type BufferType = ArrayBuffer | SharedArrayBuffer | ArrayBufferViewTypes;
|
|
108
|
-
export function isBufferType(obj: unknown): obj is BufferType {
|
|
109
|
-
if (typeof obj !== "object") return false;
|
|
110
|
-
if (!obj) return false;
|
|
111
|
-
if (ArrayBuffer.isView(obj)) return true;
|
|
112
|
-
if (obj instanceof ArrayBuffer) return true;
|
|
113
|
-
if (global.SharedArrayBuffer && obj instanceof global.SharedArrayBuffer) return true;
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
export function getKeys(obj: unknown) {
|
|
117
|
-
if (typeof obj !== "object" && typeof obj !== "function" || obj === null) {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
if (obj instanceof MessagePort) {
|
|
121
|
-
return [];
|
|
122
|
-
}
|
|
123
|
-
let keyArray: PropertyKey[];
|
|
124
|
-
if (isBufferType(obj)) {
|
|
125
|
-
keyArray = [];
|
|
126
|
-
} else if (Array.isArray(obj)) {
|
|
127
|
-
// NOTE: We convert the indexes to strings, because that is what javascript does,
|
|
128
|
-
// and differing from it causes regressions that we simply cannot rectify (it breaks hashing
|
|
129
|
-
// consistency).
|
|
130
|
-
keyArray = Array(obj.length).fill(0).map((x, i) => String(i));
|
|
131
|
-
} else {
|
|
132
|
-
keyArray = Object.keys(obj);
|
|
133
|
-
}
|
|
134
|
-
for (let symbol of Object.getOwnPropertySymbols(obj)) {
|
|
135
|
-
let key = Symbol.keyFor(symbol);
|
|
136
|
-
if (key) {
|
|
137
|
-
keyArray.push(symbol);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return keyArray;
|
|
141
|
-
}
|
|
142
|
-
export function getStringKeys<T extends {}>(obj: T): ((keyof T) & string)[] {
|
|
143
|
-
return Object.keys(obj) as any;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (isNode()) {
|
|
147
|
-
// TODO: Find a better place for this...
|
|
148
|
-
process.on("unhandledRejection", async (reason: any, promise) => {
|
|
149
|
-
console.error(`Uncaught promise rejection: ${String(reason.stack || reason)}`);
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function keyBy<T, K>(arr: T[], getKey: (value: T) => K): Map<K, T> {
|
|
154
|
-
let map = new Map<K, T>();
|
|
155
|
-
for (let item of arr) {
|
|
156
|
-
map.set(getKey(item), item);
|
|
157
|
-
}
|
|
158
|
-
return map;
|
|
159
|
-
}
|
|
160
|
-
export function keyByArray<T, K>(arr: T[], getKey: (value: T) => K): Map<K, T[]> {
|
|
161
|
-
let map = new Map<K, T[]>();
|
|
162
|
-
for (let item of arr) {
|
|
163
|
-
let key = getKey(item);
|
|
164
|
-
let arr = map.get(key);
|
|
165
|
-
if (!arr) {
|
|
166
|
-
arr = [];
|
|
167
|
-
map.set(key, arr);
|
|
168
|
-
}
|
|
169
|
-
arr.push(item);
|
|
170
|
-
}
|
|
171
|
-
return map;
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
import { canHaveChildren, MaybePromise } from "./types";
|
|
3
|
+
|
|
4
|
+
export type Watchable<T> = (callback: (value: T) => void) => MaybePromise<void>;
|
|
5
|
+
|
|
6
|
+
export function convertErrorStackToError(error: string): Error {
|
|
7
|
+
let errorObj = new Error();
|
|
8
|
+
errorObj.stack = String(error);
|
|
9
|
+
errorObj.message = String(error).split("\n")[0].slice("Error: ".length);
|
|
10
|
+
return errorObj;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sha256Hash(buffer: Buffer | string): string {
|
|
14
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
export function sha256HashBuffer(buffer: Buffer | string): Buffer {
|
|
17
|
+
return crypto.createHash("sha256").update(buffer).digest();
|
|
18
|
+
}
|
|
19
|
+
/** Async, but works both clientside and serverside. */
|
|
20
|
+
export async function sha256HashPromise(buffer: Buffer) {
|
|
21
|
+
if (isNode()) {
|
|
22
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
23
|
+
} else {
|
|
24
|
+
let buf = await window.crypto.subtle.digest("SHA-256", buffer);
|
|
25
|
+
return Buffer.from(buf).toString("hex");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function sha256BufferPromise(buffer: Buffer): Promise<Buffer> {
|
|
29
|
+
if (isNode()) {
|
|
30
|
+
return crypto.createHash("sha256").update(buffer).digest();
|
|
31
|
+
} else {
|
|
32
|
+
let buf = await window.crypto.subtle.digest("SHA-256", buffer);
|
|
33
|
+
return Buffer.from(buf);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export function arrayEqual(a: unknown[], b: unknown[]) {
|
|
39
|
+
if (a.length !== b.length) return false;
|
|
40
|
+
for (let i = 0; i < a.length; i++) {
|
|
41
|
+
if (a[i] !== b[i]) return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
export function isNode() {
|
|
46
|
+
return typeof document === "undefined";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isNodeTrue() {
|
|
50
|
+
return isNode() as true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatNumberSuffixed(count: number): string {
|
|
54
|
+
if (typeof count !== "number") return "0";
|
|
55
|
+
if (count < 0) {
|
|
56
|
+
return "-" + formatNumberSuffixed(-count);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let absValue = Math.abs(count);
|
|
60
|
+
|
|
61
|
+
const extraFactor = 10;
|
|
62
|
+
let divisor = 1;
|
|
63
|
+
let suffix = "";
|
|
64
|
+
if (absValue < 1000 * extraFactor) {
|
|
65
|
+
|
|
66
|
+
} else if (absValue < 1000 * 1000 * extraFactor) {
|
|
67
|
+
suffix = "K";
|
|
68
|
+
divisor = 1000;
|
|
69
|
+
} else if (absValue < 1000 * 1000 * 1000 * extraFactor) {
|
|
70
|
+
suffix = "M";
|
|
71
|
+
divisor = 1000 * 1000;
|
|
72
|
+
} else {
|
|
73
|
+
suffix = "B";
|
|
74
|
+
divisor = 1000 * 1000 * 1000;
|
|
75
|
+
}
|
|
76
|
+
count /= divisor;
|
|
77
|
+
absValue /= divisor;
|
|
78
|
+
|
|
79
|
+
return Math.round(count).toString() + suffix;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function list(count: number) {
|
|
83
|
+
let arr: number[] = [];
|
|
84
|
+
for (let i = 0; i < count; i++) {
|
|
85
|
+
arr.push(i);
|
|
86
|
+
}
|
|
87
|
+
return arr;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function recursiveFreeze<T>(obj: T): T {
|
|
91
|
+
if (!canHaveChildren(obj)) return obj;
|
|
92
|
+
let visited = new Set<unknown>();
|
|
93
|
+
function iterate(obj: unknown) {
|
|
94
|
+
if (!canHaveChildren(obj)) return;
|
|
95
|
+
if (visited.has(obj)) return;
|
|
96
|
+
visited.add(obj);
|
|
97
|
+
Object.freeze(obj);
|
|
98
|
+
let keys = getKeys(obj);
|
|
99
|
+
for (let key of keys) {
|
|
100
|
+
iterate(obj[key]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
iterate(obj);
|
|
104
|
+
return obj;
|
|
105
|
+
}
|
|
106
|
+
export type ArrayBufferViewTypes = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigUint64Array | BigInt64Array | Float64Array | Float32Array | Uint8ClampedArray;
|
|
107
|
+
export type BufferType = ArrayBuffer | SharedArrayBuffer | ArrayBufferViewTypes;
|
|
108
|
+
export function isBufferType(obj: unknown): obj is BufferType {
|
|
109
|
+
if (typeof obj !== "object") return false;
|
|
110
|
+
if (!obj) return false;
|
|
111
|
+
if (ArrayBuffer.isView(obj)) return true;
|
|
112
|
+
if (obj instanceof ArrayBuffer) return true;
|
|
113
|
+
if (global.SharedArrayBuffer && obj instanceof global.SharedArrayBuffer) return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
export function getKeys(obj: unknown) {
|
|
117
|
+
if (typeof obj !== "object" && typeof obj !== "function" || obj === null) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
if (obj instanceof MessagePort) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
let keyArray: PropertyKey[];
|
|
124
|
+
if (isBufferType(obj)) {
|
|
125
|
+
keyArray = [];
|
|
126
|
+
} else if (Array.isArray(obj)) {
|
|
127
|
+
// NOTE: We convert the indexes to strings, because that is what javascript does,
|
|
128
|
+
// and differing from it causes regressions that we simply cannot rectify (it breaks hashing
|
|
129
|
+
// consistency).
|
|
130
|
+
keyArray = Array(obj.length).fill(0).map((x, i) => String(i));
|
|
131
|
+
} else {
|
|
132
|
+
keyArray = Object.keys(obj);
|
|
133
|
+
}
|
|
134
|
+
for (let symbol of Object.getOwnPropertySymbols(obj)) {
|
|
135
|
+
let key = Symbol.keyFor(symbol);
|
|
136
|
+
if (key) {
|
|
137
|
+
keyArray.push(symbol);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return keyArray;
|
|
141
|
+
}
|
|
142
|
+
export function getStringKeys<T extends {}>(obj: T): ((keyof T) & string)[] {
|
|
143
|
+
return Object.keys(obj) as any;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isNode()) {
|
|
147
|
+
// TODO: Find a better place for this...
|
|
148
|
+
process.on("unhandledRejection", async (reason: any, promise) => {
|
|
149
|
+
console.error(`Uncaught promise rejection: ${String(reason.stack || reason)}`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function keyBy<T, K>(arr: T[], getKey: (value: T) => K): Map<K, T> {
|
|
154
|
+
let map = new Map<K, T>();
|
|
155
|
+
for (let item of arr) {
|
|
156
|
+
map.set(getKey(item), item);
|
|
157
|
+
}
|
|
158
|
+
return map;
|
|
159
|
+
}
|
|
160
|
+
export function keyByArray<T, K>(arr: T[], getKey: (value: T) => K): Map<K, T[]> {
|
|
161
|
+
let map = new Map<K, T[]>();
|
|
162
|
+
for (let item of arr) {
|
|
163
|
+
let key = getKey(item);
|
|
164
|
+
let arr = map.get(key);
|
|
165
|
+
if (!arr) {
|
|
166
|
+
arr = [];
|
|
167
|
+
map.set(key, arr);
|
|
168
|
+
}
|
|
169
|
+
arr.push(item);
|
|
170
|
+
}
|
|
171
|
+
return map;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function deepCloneJSON<T>(obj: T): T {
|
|
175
|
+
return JSON.parse(JSON.stringify(obj));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
export interface PromiseObj<T = void> {
|
|
181
|
+
resolve: (value: T | Promise<T>) => void;
|
|
182
|
+
reject: (error: any) => void;
|
|
183
|
+
promise: Promise<T>;
|
|
184
|
+
value: { value?: T; error?: string } | undefined;
|
|
185
|
+
/** Resolve called does not mean the value is ready, as it may be resolved with a promise. */
|
|
186
|
+
resolveCalled?: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function promiseObj<T = void>(): PromiseObj<T> {
|
|
190
|
+
let resolve!: (value: T | Promise<T>) => void;
|
|
191
|
+
let reject!: (error: any) => void;
|
|
192
|
+
let promise = new Promise<T>((_resolve, _reject) => {
|
|
193
|
+
resolve = _resolve;
|
|
194
|
+
reject = _reject;
|
|
195
|
+
});
|
|
196
|
+
let obj: PromiseObj<T> = {
|
|
197
|
+
resolve(value: T | Promise<T>) {
|
|
198
|
+
obj.resolveCalled = true;
|
|
199
|
+
if (typeof value === "object" && value !== null && value instanceof Promise) {
|
|
200
|
+
value.then(
|
|
201
|
+
value => obj.value = { value },
|
|
202
|
+
error => obj.value = { error },
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
obj.value = { value };
|
|
206
|
+
}
|
|
207
|
+
resolve(value);
|
|
208
|
+
},
|
|
209
|
+
reject,
|
|
210
|
+
promise,
|
|
211
|
+
value: undefined
|
|
212
|
+
};
|
|
213
|
+
promise.then(value => obj.value = { value }, error => obj.value = { error });
|
|
214
|
+
return obj;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
// Allows an immediate call, then delays the next call until the first call finishes + delay
|
|
219
|
+
// - Drops all but the latest call, but only resolves the promises return to all
|
|
220
|
+
// calls once the latest call finishes.
|
|
221
|
+
// - Esentially the same as saying "don't run this function too often, don't run it in parallel,
|
|
222
|
+
// and don't let functions runs be too close together".
|
|
223
|
+
export function throttleFunction<Args extends any[]>(
|
|
224
|
+
delay: number,
|
|
225
|
+
fnc: (...args: Args) => MaybePromise<void>
|
|
226
|
+
): (...args: Args) => Promise<void> {
|
|
227
|
+
let nextAllowedCall = 0;
|
|
228
|
+
let pendingArgs: { args: Args; promiseObj: PromiseObj<void> } | undefined = undefined;
|
|
229
|
+
function doCall(args: Args, promiseObj: PromiseObj<void>) {
|
|
230
|
+
nextAllowedCall = Number.POSITIVE_INFINITY;
|
|
231
|
+
try {
|
|
232
|
+
let result = fnc(...args);
|
|
233
|
+
promiseObj.resolve(result);
|
|
234
|
+
if (result instanceof Promise) {
|
|
235
|
+
result.finally(() => {
|
|
236
|
+
afterCall(Date.now() + delay);
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
afterCall(Date.now() + delay);
|
|
240
|
+
}
|
|
241
|
+
} catch (e: any) {
|
|
242
|
+
debugger;
|
|
243
|
+
promiseObj.reject(e);
|
|
244
|
+
afterCall(Date.now() + delay);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function afterCall(setNextAllowedCall: number | undefined) {
|
|
248
|
+
|
|
249
|
+
// NOTE: Ignore error, we really shouldn't have any here
|
|
250
|
+
if (setNextAllowedCall) {
|
|
251
|
+
nextAllowedCall = setNextAllowedCall;
|
|
252
|
+
} else {
|
|
253
|
+
if (nextAllowedCall === Number.POSITIVE_INFINITY) return;
|
|
254
|
+
}
|
|
255
|
+
if (!pendingArgs) return;
|
|
256
|
+
if (Date.now() > nextAllowedCall) {
|
|
257
|
+
let args = pendingArgs;
|
|
258
|
+
pendingArgs = undefined;
|
|
259
|
+
// Delay, so we don't turn a series of sequential calls to a series of nested calls
|
|
260
|
+
// (which will cause a stack overflow)
|
|
261
|
+
nextAllowedCall = Number.POSITIVE_INFINITY;
|
|
262
|
+
setImmediate(() => doCall(args.args, args.promiseObj));
|
|
263
|
+
} else {
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
if (pendingArgs) {
|
|
266
|
+
let args = pendingArgs;
|
|
267
|
+
pendingArgs = undefined;
|
|
268
|
+
doCall(args.args, args.promiseObj);
|
|
269
|
+
}
|
|
270
|
+
}, nextAllowedCall - Date.now());
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return function (...args: Args): Promise<void> {
|
|
274
|
+
if (pendingArgs) {
|
|
275
|
+
pendingArgs.args = args;
|
|
276
|
+
return pendingArgs.promiseObj.promise;
|
|
277
|
+
}
|
|
278
|
+
let time = Date.now();
|
|
279
|
+
if (time > nextAllowedCall) {
|
|
280
|
+
let promise = promiseObj();
|
|
281
|
+
doCall(args, promise);
|
|
282
|
+
return promise.promise;
|
|
283
|
+
} else {
|
|
284
|
+
pendingArgs = { args, promiseObj: promiseObj() };
|
|
285
|
+
afterCall(undefined);
|
|
286
|
+
return pendingArgs.promiseObj.promise;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
export function nextId() {
|
|
293
|
+
return Date.now() + "_" + Math.random();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function arrayFromOrderObject<T>(obj: { [order: number]: T }): T[] {
|
|
297
|
+
if (Array.isArray(obj)) return obj.slice();
|
|
298
|
+
return Object.entries(obj).sort((a, b) => +a[0] - +b[0]).map(x => x[1]).filter(x => x !== undefined && x !== null);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function last<T>(arr: T[]): T | undefined {
|
|
302
|
+
return arr[arr.length - 1];
|
|
172
303
|
}
|
package/src/nodeCache.ts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { CallFactory, createCallFactory } from "./CallFactory";
|
|
2
|
-
import { MaybePromise } from "./types";
|
|
3
|
-
import { lazy } from "./caching";
|
|
4
|
-
import { SocketFunction } from "../SocketFunction";
|
|
5
|
-
|
|
6
|
-
// TODO: Add CallInstanceFactory.isClosed, so nodeCache can clean up old entries.
|
|
7
|
-
// This is only needed for memory management, and not for correctness. Entries never
|
|
8
|
-
// need to be refreshed, because NetworkLocation.listeningPorts shouldn't really change.
|
|
9
|
-
// Either we will have listeningPorts and re-establish the connection, or we won't, and
|
|
10
|
-
// then it is a client, in which case we cannot re-establish the connection (and we just
|
|
11
|
-
// have to wait for the client to re-establish it). AND, if the listeningPorts change from
|
|
12
|
-
// a value to a new value... then they should be obtained using connect() anyway,
|
|
13
|
-
// and so whatever way the user got the NetworkLocation to begin with, they should use again.
|
|
14
|
-
|
|
15
|
-
export function getNodeId(domain: string, port: number): string {
|
|
16
|
-
// NOTE: As domains are never reused, this doesn't need any randomness
|
|
17
|
-
return `${domain}:${port}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** A nodeId not available for reconnecting. */
|
|
21
|
-
export function getClientNodeId(address: string): string {
|
|
22
|
-
return `client:${address}:${Date.now()}:${Math.random()}`;
|
|
23
|
-
}
|
|
24
|
-
export function isClientNodeId(nodeId: string): boolean {
|
|
25
|
-
return nodeId.startsWith("client:");
|
|
26
|
-
}
|
|
27
|
-
/** Will always be available, even if getNodeIdLocation is not (as we don't always have the port,
|
|
28
|
-
* but we should always have an address).
|
|
29
|
-
* - Rarely used, as for logging you can just log the nodeId. ALSO, it isn't sufficient to reconnect, as the port is also needed!
|
|
30
|
-
* */
|
|
31
|
-
export function getNodeIdIP(nodeId: string): string {
|
|
32
|
-
if (isClientNodeId(nodeId)) {
|
|
33
|
-
return nodeId.split(":")[1];
|
|
34
|
-
}
|
|
35
|
-
return getNodeIdLocation(nodeId)!.address;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function getNodeIdLocation(nodeId: string): { address: string, port: number; } | undefined {
|
|
39
|
-
if (isClientNodeId(nodeId)) {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
let [address, port] = nodeId.split(":");
|
|
43
|
-
return { address, port: parseInt(port) };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getNodeIdDomain(nodeId: string): string {
|
|
47
|
-
let location = getNodeIdLocation(nodeId);
|
|
48
|
-
if (!location) {
|
|
49
|
-
throw new Error(`Cannot get domain from nodeId, which is only usable as a client. NodeId: ${JSON.stringify(nodeId)}`);
|
|
50
|
-
}
|
|
51
|
-
return new URL(location.address).hostname.split(".").slice(-2).join(".");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// NOTE: CallFactory turns into an actual CallFactory when registerNodeClient is called
|
|
55
|
-
// nodeId =>
|
|
56
|
-
const nodeCache = new Map<string, MaybePromise<CallFactory>>();
|
|
57
|
-
|
|
58
|
-
// NOTE: Should be called directly inside call factory constructor whenever
|
|
59
|
-
// their nodeId changes (and on construction).
|
|
60
|
-
export function registerNodeClient(callFactory: CallFactory) {
|
|
61
|
-
nodeCache.set(callFactory.nodeId, callFactory);
|
|
62
|
-
startCleanupLoop();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function getCreateCallFactory(nodeId: string): MaybePromise<CallFactory> {
|
|
66
|
-
let callFactory = nodeCache.get(nodeId);
|
|
67
|
-
if (callFactory === undefined) {
|
|
68
|
-
callFactory = createCallFactory(undefined, nodeId);
|
|
69
|
-
nodeCache.set(nodeId, callFactory);
|
|
70
|
-
}
|
|
71
|
-
return callFactory;
|
|
72
|
-
}
|
|
73
|
-
export function getCallFactory(nodeId: string): MaybePromise<CallFactory | undefined> {
|
|
74
|
-
return nodeCache.get(nodeId);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const startCleanupLoop = lazy(() => {
|
|
78
|
-
(async () => {
|
|
79
|
-
while (true) {
|
|
80
|
-
for (let [key, value] of Array.from(nodeCache.entries())) {
|
|
81
|
-
let factory = value;
|
|
82
|
-
if (!(factory instanceof Promise)) {
|
|
83
|
-
if (factory.closedForever) {
|
|
84
|
-
nodeCache.delete(key);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
await new Promise(resolve => setTimeout(resolve, 1000 * 60 * 5));
|
|
89
|
-
}
|
|
90
|
-
})().catch(e => {
|
|
91
|
-
console.error(`nodeCache cleanup loop failed, ${e.stack}`);
|
|
92
|
-
});
|
|
1
|
+
import { CallFactory, createCallFactory } from "./CallFactory";
|
|
2
|
+
import { MaybePromise } from "./types";
|
|
3
|
+
import { lazy } from "./caching";
|
|
4
|
+
import { SocketFunction } from "../SocketFunction";
|
|
5
|
+
|
|
6
|
+
// TODO: Add CallInstanceFactory.isClosed, so nodeCache can clean up old entries.
|
|
7
|
+
// This is only needed for memory management, and not for correctness. Entries never
|
|
8
|
+
// need to be refreshed, because NetworkLocation.listeningPorts shouldn't really change.
|
|
9
|
+
// Either we will have listeningPorts and re-establish the connection, or we won't, and
|
|
10
|
+
// then it is a client, in which case we cannot re-establish the connection (and we just
|
|
11
|
+
// have to wait for the client to re-establish it). AND, if the listeningPorts change from
|
|
12
|
+
// a value to a new value... then they should be obtained using connect() anyway,
|
|
13
|
+
// and so whatever way the user got the NetworkLocation to begin with, they should use again.
|
|
14
|
+
|
|
15
|
+
export function getNodeId(domain: string, port: number): string {
|
|
16
|
+
// NOTE: As domains are never reused, this doesn't need any randomness
|
|
17
|
+
return `${domain}:${port}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A nodeId not available for reconnecting. */
|
|
21
|
+
export function getClientNodeId(address: string): string {
|
|
22
|
+
return `client:${address}:${Date.now()}:${Math.random()}`;
|
|
23
|
+
}
|
|
24
|
+
export function isClientNodeId(nodeId: string): boolean {
|
|
25
|
+
return nodeId.startsWith("client:");
|
|
26
|
+
}
|
|
27
|
+
/** Will always be available, even if getNodeIdLocation is not (as we don't always have the port,
|
|
28
|
+
* but we should always have an address).
|
|
29
|
+
* - Rarely used, as for logging you can just log the nodeId. ALSO, it isn't sufficient to reconnect, as the port is also needed!
|
|
30
|
+
* */
|
|
31
|
+
export function getNodeIdIP(nodeId: string): string {
|
|
32
|
+
if (isClientNodeId(nodeId)) {
|
|
33
|
+
return nodeId.split(":")[1];
|
|
34
|
+
}
|
|
35
|
+
return getNodeIdLocation(nodeId)!.address;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getNodeIdLocation(nodeId: string): { address: string, port: number; } | undefined {
|
|
39
|
+
if (isClientNodeId(nodeId)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
let [address, port] = nodeId.split(":");
|
|
43
|
+
return { address, port: parseInt(port) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getNodeIdDomain(nodeId: string): string {
|
|
47
|
+
let location = getNodeIdLocation(nodeId);
|
|
48
|
+
if (!location) {
|
|
49
|
+
throw new Error(`Cannot get domain from nodeId, which is only usable as a client. NodeId: ${JSON.stringify(nodeId)}`);
|
|
50
|
+
}
|
|
51
|
+
return new URL(location.address).hostname.split(".").slice(-2).join(".");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// NOTE: CallFactory turns into an actual CallFactory when registerNodeClient is called
|
|
55
|
+
// nodeId =>
|
|
56
|
+
const nodeCache = new Map<string, MaybePromise<CallFactory>>();
|
|
57
|
+
|
|
58
|
+
// NOTE: Should be called directly inside call factory constructor whenever
|
|
59
|
+
// their nodeId changes (and on construction).
|
|
60
|
+
export function registerNodeClient(callFactory: CallFactory) {
|
|
61
|
+
nodeCache.set(callFactory.nodeId, callFactory);
|
|
62
|
+
startCleanupLoop();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getCreateCallFactory(nodeId: string): MaybePromise<CallFactory> {
|
|
66
|
+
let callFactory = nodeCache.get(nodeId);
|
|
67
|
+
if (callFactory === undefined) {
|
|
68
|
+
callFactory = createCallFactory(undefined, nodeId);
|
|
69
|
+
nodeCache.set(nodeId, callFactory);
|
|
70
|
+
}
|
|
71
|
+
return callFactory;
|
|
72
|
+
}
|
|
73
|
+
export function getCallFactory(nodeId: string): MaybePromise<CallFactory | undefined> {
|
|
74
|
+
return nodeCache.get(nodeId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const startCleanupLoop = lazy(() => {
|
|
78
|
+
(async () => {
|
|
79
|
+
while (true) {
|
|
80
|
+
for (let [key, value] of Array.from(nodeCache.entries())) {
|
|
81
|
+
let factory = value;
|
|
82
|
+
if (!(factory instanceof Promise)) {
|
|
83
|
+
if (factory.closedForever) {
|
|
84
|
+
nodeCache.delete(key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * 60 * 5));
|
|
89
|
+
}
|
|
90
|
+
})().catch(e => {
|
|
91
|
+
console.error(`nodeCache cleanup loop failed, ${e.stack}`);
|
|
92
|
+
});
|
|
93
93
|
});
|