socket-function 1.2.26 → 1.2.28

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.
@@ -5,7 +5,8 @@
5
5
  "Bash(cp -r 'C:/Users/quent/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo' C:/Users/quent/AppData/Local/Temp/claude/D--repos-socket-function/117d10bd-31c6-4c68-938d-1fff47788fa6/scratchpad/tm-recover/LocalExtSettings)",
6
6
  "Bash(yarn test-dns-claude *)",
7
7
  "Bash(yarn type *)",
8
- "Bash(yarn test *)"
8
+ "Bash(yarn test *)",
9
+ "PowerShell(yarn type *)"
9
10
  ]
10
11
  }
11
12
  }
@@ -5,12 +5,15 @@ import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, S
5
5
  import { SocketServerConfig } from "./src/webSocketServer";
6
6
  import { Args, MaybePromise } from "./src/types";
7
7
  import "./SetProcessVariables";
8
- type ExtractShape<ClassType, Shape> = {
9
- [key in keyof ClassType]: (key extends keyof Shape ? ClassType[key] extends SocketExposedInterface[""] ? ClassType[key] : ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never : "Function has implementation but is not exposed in the SocketFunction.register call");
10
- };
11
- export declare class SocketFunction {
12
- static logMessages: boolean;
13
- static trackMessageSizes: {
8
+ /** The values behind SocketFunction's configuration statics. They are exposed on the class as
9
+ * accessors onto a singleton (see the defineSingletonConfig call at the bottom of this file), so
10
+ * that configuration set through one copy of this package applies to all of them. This interface
11
+ * is the single source of truth for their types - the class declares each one as
12
+ * `declare static X: SocketFunctionConfig["X"]`.
13
+ */
14
+ export interface SocketFunctionConfig {
15
+ logMessages: boolean;
16
+ trackMessageSizes: {
14
17
  upload: ((size: number, nodeId: string) => void)[];
15
18
  download: ((size: number, nodeId: string) => void)[];
16
19
  callTimes: ((obj: {
@@ -19,25 +22,49 @@ export declare class SocketFunction {
19
22
  nodeId: string;
20
23
  }) => void)[];
21
24
  };
22
- static MAX_MESSAGE_SIZE: number;
23
- static HTTP_ETAG_CACHE: boolean;
24
- static silent: boolean;
25
- static HTTP_COMPRESS: boolean;
26
- static COEP: string;
27
- static COOP: string;
28
- static TOTAL_CALLS: number;
29
- static ENABLE_CLIENT_MODE: boolean;
30
- static isClient(): boolean;
31
- static readonly WIRE_SERIALIZER: {
25
+ MAX_MESSAGE_SIZE: number;
26
+ HTTP_ETAG_CACHE: boolean;
27
+ silent: boolean;
28
+ HTTP_COMPRESS: boolean;
29
+ /** If you have HTTP resources that require cookies you might to set `SocketFunction.COEP = "require-corp"`
30
+ * - Cross-origin-resource-policy.
31
+ * NOTE: This COOP and COEP defaults are required so window.crossOriginIsolated will be true.
32
+ */
33
+ COEP: string;
34
+ COOP: string;
35
+ TOTAL_CALLS: number;
36
+ ENABLE_CLIENT_MODE: boolean;
37
+ WIRE_SERIALIZER: {
32
38
  serialize: (obj: unknown) => MaybePromise<Buffer[]>;
33
39
  deserialize: (buffers: Buffer[]) => MaybePromise<unknown>;
34
40
  };
35
41
  /** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
36
42
  * VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
37
43
  */
38
- static GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
39
- static WIRE_WARN_TIME: number;
40
- static DISABLE_COMPRESSION: boolean;
44
+ GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
45
+ WIRE_WARN_TIME: number;
46
+ /** Process-wide compression kill switch. When set before connections are established, LZ4 is left out of the protocol negotiation entirely (both for connections we initiate and ones we accept), so NEITHER side compresses — the wire format stays the plain backwards-compatible one. Overrides per-function `compress` flags. */
47
+ DISABLE_COMPRESSION: boolean;
48
+ }
49
+ type ExtractShape<ClassType, Shape> = {
50
+ [key in keyof ClassType]: (key extends keyof Shape ? ClassType[key] extends SocketExposedInterface[""] ? ClassType[key] : ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never : "Function has implementation but is not exposed in the SocketFunction.register call");
51
+ };
52
+ export declare class SocketFunction {
53
+ static logMessages: SocketFunctionConfig["logMessages"];
54
+ static trackMessageSizes: SocketFunctionConfig["trackMessageSizes"];
55
+ static MAX_MESSAGE_SIZE: SocketFunctionConfig["MAX_MESSAGE_SIZE"];
56
+ static HTTP_ETAG_CACHE: SocketFunctionConfig["HTTP_ETAG_CACHE"];
57
+ static silent: SocketFunctionConfig["silent"];
58
+ static HTTP_COMPRESS: SocketFunctionConfig["HTTP_COMPRESS"];
59
+ static COEP: SocketFunctionConfig["COEP"];
60
+ static COOP: SocketFunctionConfig["COOP"];
61
+ static TOTAL_CALLS: SocketFunctionConfig["TOTAL_CALLS"];
62
+ static ENABLE_CLIENT_MODE: SocketFunctionConfig["ENABLE_CLIENT_MODE"];
63
+ static readonly WIRE_SERIALIZER: SocketFunctionConfig["WIRE_SERIALIZER"];
64
+ static GET_ALTERNATE_NODE_IDS: SocketFunctionConfig["GET_ALTERNATE_NODE_IDS"];
65
+ static WIRE_WARN_TIME: SocketFunctionConfig["WIRE_WARN_TIME"];
66
+ static DISABLE_COMPRESSION: SocketFunctionConfig["DISABLE_COMPRESSION"];
67
+ static isClient(): boolean;
41
68
  private static onMountCallbacks;
42
69
  private static exposedClassesSingleton;
43
70
  static get exposedClasses(): Set<string>;
package/SocketFunction.ts CHANGED
@@ -9,7 +9,7 @@ import { Args, MaybePromise } from "./src/types";
9
9
  import { setDefaultHTTPCall } from "./src/callHTTPHandler";
10
10
  import debugbreak from "debugbreak";
11
11
  import { lazy } from "./src/caching";
12
- import { createSingleton } from "./src/createSingleton";
12
+ import { createSingleton, defineSingletonConfig } from "./src/createSingleton";
13
13
  import { delay } from "./src/batching";
14
14
  import { blue, magenta } from "./src/formatting/logColors";
15
15
  import { JSONLACKS } from "./src/JSONLACKS/JSONLACKS";
@@ -45,61 +45,89 @@ const socketContext = createSingleton("SocketFunction.socketContext", 1, () => (
45
45
  caller: undefined as CallerContext | undefined,
46
46
  }));
47
47
 
48
- type ExtractShape<ClassType, Shape> = {
49
- [key in keyof ClassType]: (
50
- key extends keyof Shape
51
- ? ClassType[key] extends SocketExposedInterface[""]
52
- ? ClassType[key]
53
- : ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never
54
- : "Function has implementation but is not exposed in the SocketFunction.register call"
55
- );
56
- };
57
-
58
- export class SocketFunction {
59
- public static logMessages = false;
60
- public static trackMessageSizes = {
61
- upload: [] as ((size: number, nodeId: string) => void)[],
62
- download: [] as ((size: number, nodeId: string) => void)[],
63
- callTimes: [] as ((obj: { start: number; end: number; nodeId: string; }) => void)[],
48
+ /** The values behind SocketFunction's configuration statics. They are exposed on the class as
49
+ * accessors onto a singleton (see the defineSingletonConfig call at the bottom of this file), so
50
+ * that configuration set through one copy of this package applies to all of them. This interface
51
+ * is the single source of truth for their types - the class declares each one as
52
+ * `declare static X: SocketFunctionConfig["X"]`.
53
+ */
54
+ export interface SocketFunctionConfig {
55
+ logMessages: boolean;
56
+ trackMessageSizes: {
57
+ upload: ((size: number, nodeId: string) => void)[];
58
+ download: ((size: number, nodeId: string) => void)[];
59
+ callTimes: ((obj: { start: number; end: number; nodeId: string; }) => void)[];
64
60
  };
65
61
 
66
- public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
62
+ MAX_MESSAGE_SIZE: number;
67
63
 
68
- public static HTTP_ETAG_CACHE = false;
69
- public static silent = true;
64
+ HTTP_ETAG_CACHE: boolean;
65
+ silent: boolean;
70
66
 
71
- public static HTTP_COMPRESS = false;
67
+ HTTP_COMPRESS: boolean;
72
68
 
73
- // If you have HTTP resources that require cookies you might to set `SocketFunction.COEP = "require-corp"`
74
- // - Cross-origin-resource-policy.
75
- public static COEP = "credentialless";
76
- // NOTE: This COOP and COEP defaults are required so window.crossOriginIsolated will be true.
77
- public static COOP = "same-origin";
69
+ /** If you have HTTP resources that require cookies you might to set `SocketFunction.COEP = "require-corp"`
70
+ * - Cross-origin-resource-policy.
71
+ * NOTE: This COOP and COEP defaults are required so window.crossOriginIsolated will be true.
72
+ */
73
+ COEP: string;
74
+ COOP: string;
78
75
 
79
- public static TOTAL_CALLS = 0;
76
+ TOTAL_CALLS: number;
80
77
 
81
- public static ENABLE_CLIENT_MODE = false;
82
- // Places where we decide if we want to act as a client. Most places we check for is node, but some places it's not, depending on if we're in Node.js or not, it's depending on if we're a client or not.
83
- public static isClient() { return !isNode() || SocketFunction.ENABLE_CLIENT_MODE; }
78
+ ENABLE_CLIENT_MODE: boolean;
84
79
 
85
80
  // In retrospect... dynamically changing the wire serializer is a BAD idea. If any calls happen
86
81
  // before it is changed, things just break. Also, it needs to be changed on both sides,
87
82
  // or else things break. Also, it is very hard to detect when the issue is different serializers
88
83
  // NOTE: The only reason this is still exposed is in case in the future we want to intercept our traffic, and we want convenient functions to know how to decode it (although there are a still few other layers under this, for compression and Buffer[] sending efficiency).
89
- public static readonly WIRE_SERIALIZER = {
90
- serialize: measureWrap((obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)], "WIRE_SERIALIZER|serialize"),
91
- deserialize: measureWrap((buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]), "WIRE_SERIALIZER|deserialize"),
84
+ WIRE_SERIALIZER: {
85
+ serialize: (obj: unknown) => MaybePromise<Buffer[]>;
86
+ deserialize: (buffers: Buffer[]) => MaybePromise<unknown>;
92
87
  };
93
88
 
94
- /** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
89
+ /** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
95
90
  * VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
96
91
  */
97
- public static GET_ALTERNATE_NODE_IDS = (nodeId: string): MaybePromise<string[] | undefined> => undefined;
92
+ GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
93
+
94
+ WIRE_WARN_TIME: number;
95
+
96
+ /** Process-wide compression kill switch. When set before connections are established, LZ4 is left out of the protocol negotiation entirely (both for connections we initiate and ones we accept), so NEITHER side compresses — the wire format stays the plain backwards-compatible one. Overrides per-function `compress` flags. */
97
+ DISABLE_COMPRESSION: boolean;
98
+ }
99
+
100
+ type ExtractShape<ClassType, Shape> = {
101
+ [key in keyof ClassType]: (
102
+ key extends keyof Shape
103
+ ? ClassType[key] extends SocketExposedInterface[""]
104
+ ? ClassType[key]
105
+ : ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never
106
+ : "Function has implementation but is not exposed in the SocketFunction.register call"
107
+ );
108
+ };
98
109
 
99
- public static WIRE_WARN_TIME = 100;
110
+ export class SocketFunction {
111
+ // All of these are accessors onto a shared singleton, installed after the class declaration.
112
+ // See SocketFunctionConfig for their documentation, and defineSingletonConfig for why they
113
+ // cannot be plain static fields.
114
+ public declare static logMessages: SocketFunctionConfig["logMessages"];
115
+ public declare static trackMessageSizes: SocketFunctionConfig["trackMessageSizes"];
116
+ public declare static MAX_MESSAGE_SIZE: SocketFunctionConfig["MAX_MESSAGE_SIZE"];
117
+ public declare static HTTP_ETAG_CACHE: SocketFunctionConfig["HTTP_ETAG_CACHE"];
118
+ public declare static silent: SocketFunctionConfig["silent"];
119
+ public declare static HTTP_COMPRESS: SocketFunctionConfig["HTTP_COMPRESS"];
120
+ public declare static COEP: SocketFunctionConfig["COEP"];
121
+ public declare static COOP: SocketFunctionConfig["COOP"];
122
+ public declare static TOTAL_CALLS: SocketFunctionConfig["TOTAL_CALLS"];
123
+ public declare static ENABLE_CLIENT_MODE: SocketFunctionConfig["ENABLE_CLIENT_MODE"];
124
+ public declare static readonly WIRE_SERIALIZER: SocketFunctionConfig["WIRE_SERIALIZER"];
125
+ public declare static GET_ALTERNATE_NODE_IDS: SocketFunctionConfig["GET_ALTERNATE_NODE_IDS"];
126
+ public declare static WIRE_WARN_TIME: SocketFunctionConfig["WIRE_WARN_TIME"];
127
+ public declare static DISABLE_COMPRESSION: SocketFunctionConfig["DISABLE_COMPRESSION"];
100
128
 
101
- // Process-wide compression kill switch. When set before connections are established, LZ4 is left out of the protocol negotiation entirely (both for connections we initiate and ones we accept), so NEITHER side compresses the wire format stays the plain backwards-compatible one. Overrides per-function `compress` flags.
102
- public static DISABLE_COMPRESSION = false;
129
+ // Places where we decide if we want to act as a client. Most places we check for is node, but some places it's not, depending on if we're in Node.js or not, it's depending on if we're a client or not.
130
+ public static isClient() { return !isNode() || SocketFunction.ENABLE_CLIENT_MODE; }
103
131
 
104
132
  // Shared across copies of this package, so onMount callbacks registered through one copy still
105
133
  // fire when another copy mounts. See createSingleton.
@@ -513,6 +541,30 @@ export class SocketFunction {
513
541
  }
514
542
  }
515
543
 
544
+ defineSingletonConfig<SocketFunctionConfig>(SocketFunction, "SocketFunction.config", 1, () => ({
545
+ logMessages: false,
546
+ trackMessageSizes: {
547
+ upload: [],
548
+ download: [],
549
+ callTimes: [],
550
+ },
551
+ MAX_MESSAGE_SIZE: 1024 * 1024 * 32,
552
+ HTTP_ETAG_CACHE: false,
553
+ silent: true,
554
+ HTTP_COMPRESS: false,
555
+ COEP: "credentialless",
556
+ COOP: "same-origin",
557
+ TOTAL_CALLS: 0,
558
+ ENABLE_CLIENT_MODE: false,
559
+ WIRE_SERIALIZER: {
560
+ serialize: measureWrap((obj: unknown): MaybePromise<Buffer[]> => [cborxInstance.encode(obj)], "WIRE_SERIALIZER|serialize"),
561
+ deserialize: measureWrap((buffers: Buffer[]): MaybePromise<unknown> => cborxInstance.decode(buffers[0]), "WIRE_SERIALIZER|deserialize"),
562
+ },
563
+ GET_ALTERNATE_NODE_IDS: (nodeId: string): MaybePromise<string[] | undefined> => undefined,
564
+ WIRE_WARN_TIME: 100,
565
+ DISABLE_COMPRESSION: false,
566
+ }));
567
+
516
568
  declare global {
517
569
  var BOOTED_EDGE_NODE: { host: string } | undefined;
518
570
  }
package/index.d.ts CHANGED
@@ -14,12 +14,15 @@ declare module "socket-function/SocketFunction" {
14
14
  import { SocketServerConfig } from "socket-function/src/webSocketServer";
15
15
  import { Args, MaybePromise } from "socket-function/src/types";
16
16
  import "./SetProcessVariables";
17
- type ExtractShape<ClassType, Shape> = {
18
- [key in keyof ClassType]: (key extends keyof Shape ? ClassType[key] extends SocketExposedInterface[""] ? ClassType[key] : ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never : "Function has implementation but is not exposed in the SocketFunction.register call");
19
- };
20
- export declare class SocketFunction {
21
- static logMessages: boolean;
22
- static trackMessageSizes: {
17
+ /** The values behind SocketFunction's configuration statics. They are exposed on the class as
18
+ * accessors onto a singleton (see the defineSingletonConfig call at the bottom of this file), so
19
+ * that configuration set through one copy of this package applies to all of them. This interface
20
+ * is the single source of truth for their types - the class declares each one as
21
+ * `declare static X: SocketFunctionConfig["X"]`.
22
+ */
23
+ export interface SocketFunctionConfig {
24
+ logMessages: boolean;
25
+ trackMessageSizes: {
23
26
  upload: ((size: number, nodeId: string) => void)[];
24
27
  download: ((size: number, nodeId: string) => void)[];
25
28
  callTimes: ((obj: {
@@ -28,25 +31,49 @@ declare module "socket-function/SocketFunction" {
28
31
  nodeId: string;
29
32
  }) => void)[];
30
33
  };
31
- static MAX_MESSAGE_SIZE: number;
32
- static HTTP_ETAG_CACHE: boolean;
33
- static silent: boolean;
34
- static HTTP_COMPRESS: boolean;
35
- static COEP: string;
36
- static COOP: string;
37
- static TOTAL_CALLS: number;
38
- static ENABLE_CLIENT_MODE: boolean;
39
- static isClient(): boolean;
40
- static readonly WIRE_SERIALIZER: {
34
+ MAX_MESSAGE_SIZE: number;
35
+ HTTP_ETAG_CACHE: boolean;
36
+ silent: boolean;
37
+ HTTP_COMPRESS: boolean;
38
+ /** If you have HTTP resources that require cookies you might to set `SocketFunction.COEP = "require-corp"`
39
+ * - Cross-origin-resource-policy.
40
+ * NOTE: This COOP and COEP defaults are required so window.crossOriginIsolated will be true.
41
+ */
42
+ COEP: string;
43
+ COOP: string;
44
+ TOTAL_CALLS: number;
45
+ ENABLE_CLIENT_MODE: boolean;
46
+ WIRE_SERIALIZER: {
41
47
  serialize: (obj: unknown) => MaybePromise<Buffer[]>;
42
48
  deserialize: (buffers: Buffer[]) => MaybePromise<unknown>;
43
49
  };
44
50
  /** We will try the alternate node IDs first, however, if they fail, we will go through all of them and then eventually try the original node ID.
45
51
  * VERY useful, allowing us to change global ips to local ones, which short-circuits the router, massively increasing bandwidth and decreasing latency.
46
52
  */
47
- static GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
48
- static WIRE_WARN_TIME: number;
49
- static DISABLE_COMPRESSION: boolean;
53
+ GET_ALTERNATE_NODE_IDS: (nodeId: string) => MaybePromise<string[] | undefined>;
54
+ WIRE_WARN_TIME: number;
55
+ /** Process-wide compression kill switch. When set before connections are established, LZ4 is left out of the protocol negotiation entirely (both for connections we initiate and ones we accept), so NEITHER side compresses — the wire format stays the plain backwards-compatible one. Overrides per-function `compress` flags. */
56
+ DISABLE_COMPRESSION: boolean;
57
+ }
58
+ type ExtractShape<ClassType, Shape> = {
59
+ [key in keyof ClassType]: (key extends keyof Shape ? ClassType[key] extends SocketExposedInterface[""] ? ClassType[key] : ClassType[key] extends Function ? "All exposed function must be async (or return a Promise)" : never : "Function has implementation but is not exposed in the SocketFunction.register call");
60
+ };
61
+ export declare class SocketFunction {
62
+ static logMessages: SocketFunctionConfig["logMessages"];
63
+ static trackMessageSizes: SocketFunctionConfig["trackMessageSizes"];
64
+ static MAX_MESSAGE_SIZE: SocketFunctionConfig["MAX_MESSAGE_SIZE"];
65
+ static HTTP_ETAG_CACHE: SocketFunctionConfig["HTTP_ETAG_CACHE"];
66
+ static silent: SocketFunctionConfig["silent"];
67
+ static HTTP_COMPRESS: SocketFunctionConfig["HTTP_COMPRESS"];
68
+ static COEP: SocketFunctionConfig["COEP"];
69
+ static COOP: SocketFunctionConfig["COOP"];
70
+ static TOTAL_CALLS: SocketFunctionConfig["TOTAL_CALLS"];
71
+ static ENABLE_CLIENT_MODE: SocketFunctionConfig["ENABLE_CLIENT_MODE"];
72
+ static readonly WIRE_SERIALIZER: SocketFunctionConfig["WIRE_SERIALIZER"];
73
+ static GET_ALTERNATE_NODE_IDS: SocketFunctionConfig["GET_ALTERNATE_NODE_IDS"];
74
+ static WIRE_WARN_TIME: SocketFunctionConfig["WIRE_WARN_TIME"];
75
+ static DISABLE_COMPRESSION: SocketFunctionConfig["DISABLE_COMPRESSION"];
76
+ static isClient(): boolean;
50
77
  private static onMountCallbacks;
51
78
  private static exposedClassesSingleton;
52
79
  static get exposedClasses(): Set<string>;
@@ -849,6 +876,17 @@ declare module "socket-function/src/createSingleton" {
849
876
  * get()/set() at each access so every copy sees the latest.
850
877
  */
851
878
  export declare function createSingleton<T>(name: string, version: string | number, getDefault: () => T): Singleton<T>;
879
+ /** Installs accessors on target for every key of the config object, with the values living in a
880
+ * singleton, so `Target.SOME_SETTING = x` configures every copy of the package instead of only
881
+ * the copy the caller happened to import (which is otherwise decided by module resolution, and
882
+ * so is essentially arbitrary from the caller's perspective).
883
+ * - Declare the properties on the class with `declare static X: Config["X"]`. `declare` emits no
884
+ * field, so there is no own property to shadow the accessors installed here, and taking the
885
+ * types from the config interface keeps the two from drifting apart.
886
+ * - Same versioning rules as createSingleton, except that adding a property is not a shape change:
887
+ * a copy that knows about a property older copies don't backfills it below.
888
+ */
889
+ export declare function defineSingletonConfig<T extends object>(target: object, name: string, version: string | number, getDefaults: () => T): Singleton<T>;
852
890
 
853
891
  }
854
892
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.2.26",
3
+ "version": "1.2.28",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -9,6 +9,7 @@ import parser from "./JSONLACKS.generated.js";
9
9
  import { recursiveFreeze } from "../misc";
10
10
  import { canHaveChildren } from "../types";
11
11
  import { delay } from "../batching";
12
+ import { defineSingletonConfig } from "../createSingleton";
12
13
 
13
14
  const SERIALIZE_OBJECT_BATCH_COUNT = 1000;
14
15
  const PARSE_BYTE_CHUNK_SIZE = 1024 * 1024 * 10;
@@ -45,8 +46,8 @@ export class JSONLACKS {
45
46
  public static readonly LACKS_KEY = "__JSONLACKS__98cfb4a05fa34d828661cae15b8779ce__";
46
47
 
47
48
  /** If set to true parses non-quoted field names, comments, trailing commas, etc */
48
- public static EXTENDED_PARSER = false;
49
- public static IGNORE_MISSING_REFERENCES = false;
49
+ public declare static EXTENDED_PARSER: boolean;
50
+ public declare static IGNORE_MISSING_REFERENCES: boolean;
50
51
 
51
52
  public static stringify(obj: unknown, config?: JSONLACKS_StringifyConfig): string {
52
53
  let serialized = JSONLACKS.escapeSpecialObjects(obj, config);
@@ -299,6 +300,11 @@ export class JSONLACKS {
299
300
  }
300
301
  }
301
302
 
303
+ defineSingletonConfig(JSONLACKS, "JSONLACKS.config", 1, () => ({
304
+ EXTENDED_PARSER: false,
305
+ IGNORE_MISSING_REFERENCES: false,
306
+ }));
307
+
302
308
  async function benchmark() {
303
309
  const loops = 1000 * 100;
304
310
  let inputs: string[] = [];
@@ -13,6 +13,9 @@ import { createSingleton } from "./createSingleton";
13
13
  // the HTTP handler of any other copy. See createSingleton.
14
14
  const defaultHTTPCall = createSingleton("callHTTPHandler.defaultHTTPCall", 1, () => ({ call: undefined as CallType | undefined }));
15
15
 
16
+ // Statuses where the spec forbids a message body, so we must not write resultBuffer
17
+ const BODYLESS_STATUS_CODES = new Set([204, 205, 304]);
18
+
16
19
  export function setDefaultHTTPCall(call: CallType) {
17
20
  defaultHTTPCall.get().call = call;
18
21
  }
@@ -253,12 +256,18 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
253
256
 
254
257
  if (headers) {
255
258
  for (let headerName in headers) {
259
+ // "status" is the status line, not a header, so don't emit it on the wire
260
+ if (headerName.toLowerCase() === "status") continue;
256
261
  response.setHeader(headerName, headers[headerName]);
257
262
  }
258
- let status = headers["status"];
263
+ let status = headers["status"] ?? headers["Status"];
259
264
  if (status) {
260
- response.writeHead(+status);
261
- return;
265
+ // Only set statusCode (NOT writeHead), as writeHead locks the header block, which would
266
+ // drop the Content-Type / Content-Length / compression handling below
267
+ response.statusCode = +status;
268
+ if (BODYLESS_STATUS_CODES.has(+status) || +status < 200) {
269
+ return;
270
+ }
262
271
  }
263
272
  }
264
273
  // With nosniff set, an untyped response is unusable to the browser, so results without explicit headers need their actual type (JSON) declared
@@ -17,3 +17,14 @@ export interface Singleton<T> {
17
17
  * get()/set() at each access so every copy sees the latest.
18
18
  */
19
19
  export declare function createSingleton<T>(name: string, version: string | number, getDefault: () => T): Singleton<T>;
20
+ /** Installs accessors on target for every key of the config object, with the values living in a
21
+ * singleton, so `Target.SOME_SETTING = x` configures every copy of the package instead of only
22
+ * the copy the caller happened to import (which is otherwise decided by module resolution, and
23
+ * so is essentially arbitrary from the caller's perspective).
24
+ * - Declare the properties on the class with `declare static X: Config["X"]`. `declare` emits no
25
+ * field, so there is no own property to shadow the accessors installed here, and taking the
26
+ * types from the config interface keeps the two from drifting apart.
27
+ * - Same versioning rules as createSingleton, except that adding a property is not a shape change:
28
+ * a copy that knows about a property older copies don't backfills it below.
29
+ */
30
+ export declare function defineSingletonConfig<T extends object>(target: object, name: string, version: string | number, getDefaults: () => T): Singleton<T>;
@@ -74,3 +74,40 @@ export function createSingleton<T>(
74
74
  },
75
75
  };
76
76
  }
77
+
78
+ /** Installs accessors on target for every key of the config object, with the values living in a
79
+ * singleton, so `Target.SOME_SETTING = x` configures every copy of the package instead of only
80
+ * the copy the caller happened to import (which is otherwise decided by module resolution, and
81
+ * so is essentially arbitrary from the caller's perspective).
82
+ * - Declare the properties on the class with `declare static X: Config["X"]`. `declare` emits no
83
+ * field, so there is no own property to shadow the accessors installed here, and taking the
84
+ * types from the config interface keeps the two from drifting apart.
85
+ * - Same versioning rules as createSingleton, except that adding a property is not a shape change:
86
+ * a copy that knows about a property older copies don't backfills it below.
87
+ */
88
+ export function defineSingletonConfig<T extends object>(
89
+ target: object,
90
+ name: string,
91
+ version: string | number,
92
+ getDefaults: () => T,
93
+ ): Singleton<T> {
94
+ const defaults = getDefaults();
95
+ const singleton = createSingleton<T>(name, version, () => defaults);
96
+ const values = singleton.get() as { [key: string]: unknown };
97
+ for (const key of Object.keys(defaults)) {
98
+ if (!(key in values)) {
99
+ values[key] = (defaults as { [key: string]: unknown })[key];
100
+ }
101
+ Object.defineProperty(target, key, {
102
+ get() {
103
+ return (singleton.get() as { [key: string]: unknown })[key];
104
+ },
105
+ set(value: unknown) {
106
+ (singleton.get() as { [key: string]: unknown })[key] = value;
107
+ },
108
+ enumerable: true,
109
+ configurable: true,
110
+ });
111
+ }
112
+ return singleton;
113
+ }