@fastls/fastls 0.0.0-stage → 0.10.1-dev.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/LICENSE +203 -0
- package/README.md +385 -2
- package/dist/cookies.d.ts +23 -0
- package/dist/cookies.js +27 -0
- package/dist/duplex.d.ts +21 -0
- package/dist/duplex.js +94 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +48 -0
- package/dist/headers.d.ts +12 -0
- package/dist/headers.js +28 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/index.mjs +2 -0
- package/dist/library-path.d.ts +2 -0
- package/dist/library-path.js +77 -0
- package/dist/native.d.ts +54 -0
- package/dist/native.js +208 -0
- package/dist/notifications.d.ts +40 -0
- package/dist/notifications.js +222 -0
- package/dist/request-options.d.ts +28 -0
- package/dist/request-options.js +172 -0
- package/dist/response.d.ts +84 -0
- package/dist/response.js +242 -0
- package/dist/scheduler.d.ts +12 -0
- package/dist/scheduler.js +75 -0
- package/dist/session.d.ts +101 -0
- package/dist/session.js +390 -0
- package/dist/sse.d.ts +52 -0
- package/dist/sse.js +197 -0
- package/dist/text.d.ts +3 -0
- package/dist/text.js +55 -0
- package/dist/timeout.d.ts +9 -0
- package/dist/timeout.js +19 -0
- package/dist/upload.d.ts +11 -0
- package/dist/upload.js +141 -0
- package/dist/websocket.d.ts +63 -0
- package/dist/websocket.js +248 -0
- package/package.json +41 -4
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Cookies = void 0;
|
|
4
|
+
class Cookies {
|
|
5
|
+
command;
|
|
6
|
+
constructor(command) {
|
|
7
|
+
this.command = command;
|
|
8
|
+
}
|
|
9
|
+
getAll(url) {
|
|
10
|
+
return this.command({ action: "get", url });
|
|
11
|
+
}
|
|
12
|
+
get(name, url) {
|
|
13
|
+
return this.getAll(url).find(cookie => cookie.name === name)?.value;
|
|
14
|
+
}
|
|
15
|
+
set(name, value, options) {
|
|
16
|
+
this.command({ action: "set", url: options.url, cookie: {
|
|
17
|
+
name, value, path: options.path, domain: options.domain,
|
|
18
|
+
secure: options.secure, http_only: options.httpOnly,
|
|
19
|
+
max_age: options.maxAge, expires: options.expires,
|
|
20
|
+
} });
|
|
21
|
+
}
|
|
22
|
+
delete(name, options) {
|
|
23
|
+
this.set(name, "", { ...options, maxAge: 0 });
|
|
24
|
+
}
|
|
25
|
+
clear() { this.command({ action: "clear" }); }
|
|
26
|
+
}
|
|
27
|
+
exports.Cookies = Cookies;
|
package/dist/duplex.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StreamResponse, Transfer } from "./response.js";
|
|
2
|
+
/** A manual HTTP/2 exchange. At most one send/finish operation may be pending. */
|
|
3
|
+
export declare class DuplexExchange {
|
|
4
|
+
private readonly cancelPending?;
|
|
5
|
+
private responsePromise?;
|
|
6
|
+
private writing;
|
|
7
|
+
private finished;
|
|
8
|
+
private uploadStopped;
|
|
9
|
+
private finish;
|
|
10
|
+
private readonly upload;
|
|
11
|
+
private readonly transferPromise;
|
|
12
|
+
private transfer?;
|
|
13
|
+
private closed;
|
|
14
|
+
constructor(transfer: Transfer | Promise<Transfer>, cancelPending?: (() => void) | undefined);
|
|
15
|
+
get response(): Promise<StreamResponse>;
|
|
16
|
+
private write;
|
|
17
|
+
send(bytes: Uint8Array): Promise<void>;
|
|
18
|
+
finishUpload(): Promise<void>;
|
|
19
|
+
close(): void;
|
|
20
|
+
closeAsync(): Promise<void>;
|
|
21
|
+
}
|
package/dist/duplex.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DuplexExchange = void 0;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
const response_js_1 = require("./response.js");
|
|
6
|
+
/** A manual HTTP/2 exchange. At most one send/finish operation may be pending. */
|
|
7
|
+
class DuplexExchange {
|
|
8
|
+
cancelPending;
|
|
9
|
+
responsePromise;
|
|
10
|
+
writing = false;
|
|
11
|
+
finished = false;
|
|
12
|
+
uploadStopped = false;
|
|
13
|
+
finish;
|
|
14
|
+
upload;
|
|
15
|
+
transferPromise;
|
|
16
|
+
transfer;
|
|
17
|
+
closed = false;
|
|
18
|
+
constructor(transfer, cancelPending) {
|
|
19
|
+
this.cancelPending = cancelPending;
|
|
20
|
+
const done = new Promise(resolve => { this.finish = resolve; });
|
|
21
|
+
this.transferPromise = Promise.resolve(transfer).then(value => {
|
|
22
|
+
this.transfer = value;
|
|
23
|
+
if (this.closed)
|
|
24
|
+
value.close();
|
|
25
|
+
const stopped = () => this.finish();
|
|
26
|
+
value.signal.addEventListener("abort", stopped, { once: true });
|
|
27
|
+
if (value.signal.aborted)
|
|
28
|
+
stopped();
|
|
29
|
+
void done.then(() => value.signal.removeEventListener("abort", stopped));
|
|
30
|
+
return value;
|
|
31
|
+
});
|
|
32
|
+
void this.transferPromise.catch(() => this.finish());
|
|
33
|
+
this.upload = { done, cleanup: Promise.resolve(), complete: () => this.finished,
|
|
34
|
+
check: () => this.transfer?.throwFailure() };
|
|
35
|
+
}
|
|
36
|
+
get response() {
|
|
37
|
+
return this.responsePromise ??= this.transferPromise.then(async (transfer) => {
|
|
38
|
+
if (this.closed)
|
|
39
|
+
throw new errors_js_1.FastlsError("exchange is closed");
|
|
40
|
+
const info = await transfer.headers();
|
|
41
|
+
return new response_js_1.StreamResponse(transfer, info, this.upload);
|
|
42
|
+
}).then(response => response, error => { this.close(); throw error; });
|
|
43
|
+
}
|
|
44
|
+
async write(bytes, end) {
|
|
45
|
+
if (this.writing)
|
|
46
|
+
throw new errors_js_1.FastlsError("another upload write is pending; await send() before sending again");
|
|
47
|
+
if (this.finished || this.uploadStopped)
|
|
48
|
+
throw new errors_js_1.FastlsError("upload is closed");
|
|
49
|
+
this.writing = true;
|
|
50
|
+
const accepted = (code) => {
|
|
51
|
+
if (code === 0)
|
|
52
|
+
return;
|
|
53
|
+
// The peer can stop receiving while its response remains readable.
|
|
54
|
+
this.uploadStopped = true;
|
|
55
|
+
this.finish();
|
|
56
|
+
throw new errors_js_1.FastlsError("server closed the upload");
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const transfer = await this.transferPromise;
|
|
60
|
+
if (this.closed)
|
|
61
|
+
throw new errors_js_1.FastlsError("exchange is closed");
|
|
62
|
+
transfer.ensureOpen();
|
|
63
|
+
if (end) {
|
|
64
|
+
accepted(await transfer.write(Buffer.alloc(0), true, transfer.signal));
|
|
65
|
+
this.finished = true;
|
|
66
|
+
this.finish();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 65536) {
|
|
70
|
+
accepted(await transfer.write(bytes.subarray(offset, offset + 65536), false, transfer.signal));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!this.uploadStopped)
|
|
76
|
+
this.transfer?.close(error instanceof Error ? error : new Error(String(error)));
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.writing = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
send(bytes) {
|
|
84
|
+
if (!(bytes instanceof Uint8Array))
|
|
85
|
+
return Promise.reject(new TypeError("send() requires Uint8Array or Buffer"));
|
|
86
|
+
return this.write(bytes, false);
|
|
87
|
+
}
|
|
88
|
+
finishUpload() {
|
|
89
|
+
return this.finished ? Promise.resolve() : this.write(Buffer.alloc(0), true);
|
|
90
|
+
}
|
|
91
|
+
close() { this.closed = true; this.transfer?.close(); this.cancelPending?.(); }
|
|
92
|
+
async closeAsync() { this.close(); await this.transferPromise.then(transfer => transfer.close(), () => { }); }
|
|
93
|
+
}
|
|
94
|
+
exports.DuplexExchange = DuplexExchange;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare class FastlsError extends Error {
|
|
2
|
+
name: string;
|
|
3
|
+
}
|
|
4
|
+
export declare class FastlsHTTPError extends FastlsError {
|
|
5
|
+
readonly response: import("./response.js").Response | import("./response.js").StreamResponse;
|
|
6
|
+
name: string;
|
|
7
|
+
readonly status: number;
|
|
8
|
+
constructor(response: import("./response.js").Response | import("./response.js").StreamResponse);
|
|
9
|
+
}
|
|
10
|
+
export type TimeoutPhase = "connect" | "write" | "headers" | "read" | "total" | "handshake";
|
|
11
|
+
export declare class FastlsTimeoutError extends FastlsError {
|
|
12
|
+
readonly phase: TimeoutPhase;
|
|
13
|
+
name: string;
|
|
14
|
+
constructor(message: string, phase: TimeoutPhase);
|
|
15
|
+
}
|
|
16
|
+
export declare class FastlsDecodeError extends FastlsError {
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function responseError(info: {
|
|
20
|
+
error?: string;
|
|
21
|
+
error_code?: string;
|
|
22
|
+
timeout_phase?: string;
|
|
23
|
+
}): FastlsError;
|
|
24
|
+
export declare class AbortError extends FastlsError {
|
|
25
|
+
name: string;
|
|
26
|
+
readonly code = "ABORT_ERR";
|
|
27
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
28
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AbortError = exports.FastlsDecodeError = exports.FastlsTimeoutError = exports.FastlsHTTPError = exports.FastlsError = void 0;
|
|
4
|
+
exports.responseError = responseError;
|
|
5
|
+
class FastlsError extends Error {
|
|
6
|
+
name = "FastlsError";
|
|
7
|
+
}
|
|
8
|
+
exports.FastlsError = FastlsError;
|
|
9
|
+
class FastlsHTTPError extends FastlsError {
|
|
10
|
+
response;
|
|
11
|
+
name = "FastlsHTTPError";
|
|
12
|
+
status;
|
|
13
|
+
constructor(response) {
|
|
14
|
+
super(`HTTP ${response.status}`);
|
|
15
|
+
this.response = response;
|
|
16
|
+
this.status = response.status;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.FastlsHTTPError = FastlsHTTPError;
|
|
20
|
+
class FastlsTimeoutError extends FastlsError {
|
|
21
|
+
phase;
|
|
22
|
+
name = "FastlsTimeoutError";
|
|
23
|
+
constructor(message, phase) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.phase = phase;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.FastlsTimeoutError = FastlsTimeoutError;
|
|
29
|
+
class FastlsDecodeError extends FastlsError {
|
|
30
|
+
name = "FastlsDecodeError";
|
|
31
|
+
}
|
|
32
|
+
exports.FastlsDecodeError = FastlsDecodeError;
|
|
33
|
+
function responseError(info) {
|
|
34
|
+
const message = info.error ?? "native request failed";
|
|
35
|
+
if (info.error_code === "timeout")
|
|
36
|
+
return new FastlsTimeoutError(message, (info.timeout_phase ?? "total"));
|
|
37
|
+
if (info.error_code === "decode_error")
|
|
38
|
+
return new FastlsDecodeError(message);
|
|
39
|
+
return new FastlsError(message);
|
|
40
|
+
}
|
|
41
|
+
class AbortError extends FastlsError {
|
|
42
|
+
name = "AbortError";
|
|
43
|
+
code = "ABORT_ERR";
|
|
44
|
+
constructor(message = "The request was aborted", options) {
|
|
45
|
+
super(message, options);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.AbortError = AbortError;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Read-only, case-insensitive response headers. Values are never comma-split. */
|
|
2
|
+
export declare class Headers implements Iterable<[string, string]> {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(values?: Record<string, string[]>, fallback?: Record<string, string>);
|
|
5
|
+
get(name: string, fallback: string): string;
|
|
6
|
+
get(name: string): string | undefined;
|
|
7
|
+
getAll(name: string): string[];
|
|
8
|
+
has(name: string): boolean;
|
|
9
|
+
entries(): IterableIterator<[string, string]>;
|
|
10
|
+
multiItems(): IterableIterator<[string, string]>;
|
|
11
|
+
[Symbol.iterator](): IterableIterator<[string, string]>;
|
|
12
|
+
}
|
package/dist/headers.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Headers = void 0;
|
|
4
|
+
/** Read-only, case-insensitive response headers. Values are never comma-split. */
|
|
5
|
+
class Headers {
|
|
6
|
+
#values = new Map();
|
|
7
|
+
constructor(values = {}, fallback = {}) {
|
|
8
|
+
for (const [name, value] of Object.entries(fallback))
|
|
9
|
+
this.#values.set(name.toLowerCase(), [value]);
|
|
10
|
+
for (const [name, list] of Object.entries(values))
|
|
11
|
+
this.#values.set(name.toLowerCase(), [...list]);
|
|
12
|
+
}
|
|
13
|
+
get(name, fallback) { return this.#values.get(name.toLowerCase())?.[0] ?? fallback; }
|
|
14
|
+
getAll(name) { return [...(this.#values.get(name.toLowerCase()) ?? [])]; }
|
|
15
|
+
has(name) { return this.#values.has(name.toLowerCase()); }
|
|
16
|
+
*entries() {
|
|
17
|
+
for (const [name, values] of this.#values)
|
|
18
|
+
if (values.length)
|
|
19
|
+
yield [name, values[0]];
|
|
20
|
+
}
|
|
21
|
+
*multiItems() {
|
|
22
|
+
for (const [name, values] of this.#values)
|
|
23
|
+
for (const value of values)
|
|
24
|
+
yield [name, value];
|
|
25
|
+
}
|
|
26
|
+
[Symbol.iterator]() { return this.entries(); }
|
|
27
|
+
}
|
|
28
|
+
exports.Headers = Headers;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { Session } from "./session.js";
|
|
2
|
+
export type { SessionOptions, RequestOptions, DuplexOptions, Stats } from "./session.js";
|
|
3
|
+
export { DuplexExchange } from "./duplex.js";
|
|
4
|
+
export { WebSocket, WebSocketClosed } from "./websocket.js";
|
|
5
|
+
export type { WebSocketOptions } from "./websocket.js";
|
|
6
|
+
export { ServerSentEvent, SSEStream } from "./sse.js";
|
|
7
|
+
export type { SSEOptions } from "./sse.js";
|
|
8
|
+
export { Response, StreamResponse } from "./response.js";
|
|
9
|
+
export { Headers } from "./headers.js";
|
|
10
|
+
export { Cookies } from "./cookies.js";
|
|
11
|
+
export type { Cookie, CookieOptions } from "./cookies.js";
|
|
12
|
+
export { FastlsError, FastlsHTTPError, AbortError, FastlsDecodeError, FastlsTimeoutError } from "./errors.js";
|
|
13
|
+
export type { QueryScalar, QueryParams, FormFields, JSONValue, MultipartReader, MultipartBody, MultipartFile, MultipartFiles } from "./request-options.js";
|
|
14
|
+
export type { TimeoutPhase } from "./errors.js";
|
|
15
|
+
export type { TimeoutPolicy, Timeout } from "./timeout.js";
|
|
16
|
+
export type { UploadBody, ByteSource } from "./upload.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FastlsTimeoutError = exports.FastlsDecodeError = exports.AbortError = exports.FastlsHTTPError = exports.FastlsError = exports.Cookies = exports.Headers = exports.StreamResponse = exports.Response = exports.SSEStream = exports.ServerSentEvent = exports.WebSocketClosed = exports.WebSocket = exports.DuplexExchange = exports.Session = void 0;
|
|
4
|
+
var session_js_1 = require("./session.js");
|
|
5
|
+
Object.defineProperty(exports, "Session", { enumerable: true, get: function () { return session_js_1.Session; } });
|
|
6
|
+
var duplex_js_1 = require("./duplex.js");
|
|
7
|
+
Object.defineProperty(exports, "DuplexExchange", { enumerable: true, get: function () { return duplex_js_1.DuplexExchange; } });
|
|
8
|
+
var websocket_js_1 = require("./websocket.js");
|
|
9
|
+
Object.defineProperty(exports, "WebSocket", { enumerable: true, get: function () { return websocket_js_1.WebSocket; } });
|
|
10
|
+
Object.defineProperty(exports, "WebSocketClosed", { enumerable: true, get: function () { return websocket_js_1.WebSocketClosed; } });
|
|
11
|
+
var sse_js_1 = require("./sse.js");
|
|
12
|
+
Object.defineProperty(exports, "ServerSentEvent", { enumerable: true, get: function () { return sse_js_1.ServerSentEvent; } });
|
|
13
|
+
Object.defineProperty(exports, "SSEStream", { enumerable: true, get: function () { return sse_js_1.SSEStream; } });
|
|
14
|
+
var response_js_1 = require("./response.js");
|
|
15
|
+
Object.defineProperty(exports, "Response", { enumerable: true, get: function () { return response_js_1.Response; } });
|
|
16
|
+
Object.defineProperty(exports, "StreamResponse", { enumerable: true, get: function () { return response_js_1.StreamResponse; } });
|
|
17
|
+
var headers_js_1 = require("./headers.js");
|
|
18
|
+
Object.defineProperty(exports, "Headers", { enumerable: true, get: function () { return headers_js_1.Headers; } });
|
|
19
|
+
var cookies_js_1 = require("./cookies.js");
|
|
20
|
+
Object.defineProperty(exports, "Cookies", { enumerable: true, get: function () { return cookies_js_1.Cookies; } });
|
|
21
|
+
var errors_js_1 = require("./errors.js");
|
|
22
|
+
Object.defineProperty(exports, "FastlsError", { enumerable: true, get: function () { return errors_js_1.FastlsError; } });
|
|
23
|
+
Object.defineProperty(exports, "FastlsHTTPError", { enumerable: true, get: function () { return errors_js_1.FastlsHTTPError; } });
|
|
24
|
+
Object.defineProperty(exports, "AbortError", { enumerable: true, get: function () { return errors_js_1.AbortError; } });
|
|
25
|
+
Object.defineProperty(exports, "FastlsDecodeError", { enumerable: true, get: function () { return errors_js_1.FastlsDecodeError; } });
|
|
26
|
+
Object.defineProperty(exports, "FastlsTimeoutError", { enumerable: true, get: function () { return errors_js_1.FastlsTimeoutError; } });
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.libraryPath = libraryPath;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_module_1 = require("node:module");
|
|
8
|
+
const errors_js_1 = require("./errors.js");
|
|
9
|
+
const verified = new Set();
|
|
10
|
+
let glibcRuntime;
|
|
11
|
+
let glibcChecked = false;
|
|
12
|
+
/** Explicit overrides take priority; a present candidate manifest must validate. */
|
|
13
|
+
function libraryPath(library, packageRoot = (0, node_path_1.resolve)(__dirname, "..")) {
|
|
14
|
+
const override = library ?? process.env.FASTLS_LIBRARY;
|
|
15
|
+
const name = process.platform === "win32" ? "fastls.dll"
|
|
16
|
+
: process.platform === "darwin" ? "libfastls.dylib" : "libfastls.so";
|
|
17
|
+
try {
|
|
18
|
+
if (override !== undefined)
|
|
19
|
+
return (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(override));
|
|
20
|
+
let nativeRoot = packageRoot;
|
|
21
|
+
let manifestPath = (0, node_path_1.resolve)(nativeRoot, "native/manifest.json");
|
|
22
|
+
if (!(0, node_fs_1.existsSync)(manifestPath) && !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(packageRoot, "src/native.ts"))) {
|
|
23
|
+
const suffix = `${process.platform}-${process.arch}${process.platform === "linux" ? "-glibc" : ""}`;
|
|
24
|
+
const require = (0, node_module_1.createRequire)((0, node_path_1.resolve)(packageRoot, "package.json"));
|
|
25
|
+
let metadataPath;
|
|
26
|
+
try {
|
|
27
|
+
metadataPath = require.resolve(`@fastls/fastls-${suffix}/package.json`);
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
throw new Error(`no native manifest or optional package for ${suffix}; reinstall with optional dependencies enabled or supply an external library`, { cause });
|
|
31
|
+
}
|
|
32
|
+
const installed = JSON.parse((0, node_fs_1.readFileSync)(metadataPath, "utf8"));
|
|
33
|
+
const wrapper = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(packageRoot, "package.json"), "utf8"));
|
|
34
|
+
if (installed.name !== `@fastls/fastls-${suffix}` || installed.version !== wrapper.version) {
|
|
35
|
+
throw new Error("native package version mismatch; reinstall matching Fastls packages");
|
|
36
|
+
}
|
|
37
|
+
nativeRoot = (0, node_path_1.resolve)(metadataPath, "..");
|
|
38
|
+
manifestPath = (0, node_path_1.resolve)(nativeRoot, "native/manifest.json");
|
|
39
|
+
}
|
|
40
|
+
if (!(0, node_fs_1.existsSync)(manifestPath)) {
|
|
41
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(packageRoot, "src/native.ts")))
|
|
42
|
+
throw new Error("installed package has no native manifest; supply an external library");
|
|
43
|
+
return (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(packageRoot, "../dist", name));
|
|
44
|
+
}
|
|
45
|
+
const manifest = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, "utf8"));
|
|
46
|
+
if (manifest.schema !== 1 || manifest.abi !== 1 || manifest.platform !== process.platform || manifest.arch !== process.arch) {
|
|
47
|
+
throw new Error(`candidate platform mismatch: expected ${process.platform}/${process.arch}, received ${manifest.platform}/${manifest.arch}`);
|
|
48
|
+
}
|
|
49
|
+
if (manifest.file !== name || !/^[a-f0-9]{64}$/.test(manifest.sha256))
|
|
50
|
+
throw new Error("invalid candidate library manifest");
|
|
51
|
+
if (process.platform === "linux") {
|
|
52
|
+
if (!glibcChecked) {
|
|
53
|
+
const report = process.report.getReport();
|
|
54
|
+
glibcRuntime = report.header?.glibcVersionRuntime;
|
|
55
|
+
glibcChecked = true;
|
|
56
|
+
}
|
|
57
|
+
const current = glibcRuntime;
|
|
58
|
+
const minimum = manifest.requirements?.glibc;
|
|
59
|
+
if (!current || !/^\d+\.\d+$/.test(minimum))
|
|
60
|
+
throw new Error("this candidate requires Linux glibc; musl is not supported");
|
|
61
|
+
const [major, minor] = current.split(".").map(Number), [minMajor, minMinor] = minimum.split(".").map(Number);
|
|
62
|
+
if (major < minMajor || (major === minMajor && minor < minMinor))
|
|
63
|
+
throw new Error(`candidate requires glibc >= ${minimum}; found ${current}`);
|
|
64
|
+
}
|
|
65
|
+
const path = (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(nativeRoot, "native", name)), stat = (0, node_fs_1.statSync)(path);
|
|
66
|
+
const key = `${path}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${manifest.sha256}`;
|
|
67
|
+
if (!verified.has(key)) {
|
|
68
|
+
if ((0, node_crypto_1.createHash)("sha256").update((0, node_fs_1.readFileSync)(path)).digest("hex") !== manifest.sha256)
|
|
69
|
+
throw new Error("candidate library SHA-256 mismatch");
|
|
70
|
+
verified.add(key);
|
|
71
|
+
}
|
|
72
|
+
return path;
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
throw new errors_js_1.FastlsError("Cannot find Fastls library or validate native candidate; set FASTLS_LIBRARY or pass { library: absolutePath }", { cause });
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export declare function parseNativeJSON(text: string): unknown;
|
|
2
|
+
export declare function encodeNativeJSON(value: unknown, limit?: number): Buffer;
|
|
3
|
+
export declare function asHandle(value: unknown): bigint;
|
|
4
|
+
export interface NativeInfo {
|
|
5
|
+
state: "pending" | "headers" | "eof" | "error";
|
|
6
|
+
status?: number;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
header_values?: Record<string, string[]>;
|
|
9
|
+
match: Readonly<Record<string, unknown>>;
|
|
10
|
+
error?: string;
|
|
11
|
+
error_code?: string;
|
|
12
|
+
timeout_phase?: string;
|
|
13
|
+
decoded?: boolean;
|
|
14
|
+
protocol?: string;
|
|
15
|
+
final_url?: string;
|
|
16
|
+
trailer_values?: Record<string, string[]>;
|
|
17
|
+
wire_header_values?: Record<string, string[]>;
|
|
18
|
+
upload_discarded?: boolean;
|
|
19
|
+
websocket?: {
|
|
20
|
+
subprotocol: string;
|
|
21
|
+
write_pending: boolean;
|
|
22
|
+
close_code: number;
|
|
23
|
+
close_reason: string;
|
|
24
|
+
close_received: boolean;
|
|
25
|
+
pings: number;
|
|
26
|
+
pongs: number;
|
|
27
|
+
last_pong_base64: string;
|
|
28
|
+
max_message_bytes: number;
|
|
29
|
+
alpn_policy: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare class Native {
|
|
33
|
+
private functions;
|
|
34
|
+
readonly notificationsAvailable: boolean;
|
|
35
|
+
readonly websocketAvailable: boolean;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
static load(library?: string): Native;
|
|
38
|
+
private constructor();
|
|
39
|
+
call<T = unknown>(name: string, ...args: unknown[]): T;
|
|
40
|
+
start(input: unknown): bigint;
|
|
41
|
+
wsStart(input: unknown): bigint;
|
|
42
|
+
wsSend(handle: bigint, opcode: number, data: Uint8Array): number;
|
|
43
|
+
wsRead(handle: bigint, buffer: Buffer): {
|
|
44
|
+
code: number;
|
|
45
|
+
length: number;
|
|
46
|
+
opcode: number;
|
|
47
|
+
end: boolean;
|
|
48
|
+
};
|
|
49
|
+
read(handle: bigint, buffer: Buffer): {
|
|
50
|
+
code: number;
|
|
51
|
+
length: number;
|
|
52
|
+
};
|
|
53
|
+
write(handle: bigint, buffer: Uint8Array, end?: boolean): number;
|
|
54
|
+
}
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Native = void 0;
|
|
7
|
+
exports.parseNativeJSON = parseNativeJSON;
|
|
8
|
+
exports.encodeNativeJSON = encodeNativeJSON;
|
|
9
|
+
exports.asHandle = asHandle;
|
|
10
|
+
const koffi_1 = __importDefault(require("koffi"));
|
|
11
|
+
const lossless_json_1 = require("lossless-json");
|
|
12
|
+
const library_path_js_1 = require("./library-path.js");
|
|
13
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
14
|
+
const errors_js_1 = require("./errors.js");
|
|
15
|
+
function parseNativeJSON(text) {
|
|
16
|
+
return (0, lossless_json_1.parse)(text, undefined, (token) => {
|
|
17
|
+
const number = Number(token);
|
|
18
|
+
return /^-?\d+$/.test(token) && !Number.isSafeInteger(number) ? BigInt(token) : number;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function encodeNativeJSON(value, limit = 1024 * 1024) {
|
|
22
|
+
const raw = Buffer.from((0, lossless_json_1.stringify)(value), "utf8");
|
|
23
|
+
if (!raw.length || raw.length > limit)
|
|
24
|
+
throw new RangeError(`native JSON exceeds ${limit} bytes`);
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
27
|
+
function asHandle(value) {
|
|
28
|
+
if (typeof value !== "bigint" && !(typeof value === "number" && Number.isSafeInteger(value))) {
|
|
29
|
+
throw new errors_js_1.FastlsError("native library returned an invalid handle");
|
|
30
|
+
}
|
|
31
|
+
const handle = BigInt(value);
|
|
32
|
+
if (handle < 1n || handle > 0xffffffffffffffffn)
|
|
33
|
+
throw new errors_js_1.FastlsError("invalid uint64 handle");
|
|
34
|
+
return handle;
|
|
35
|
+
}
|
|
36
|
+
// Keep successful and partially loaded libraries alive until process exit.
|
|
37
|
+
// Worker teardown may unload a Go runtime, so worker_threads are not yet supported.
|
|
38
|
+
const libraries = new Map();
|
|
39
|
+
const bindings = new Map();
|
|
40
|
+
const pinned = new Set();
|
|
41
|
+
let systemLoader;
|
|
42
|
+
function pinLibrary(path) {
|
|
43
|
+
if (pinned.has(path))
|
|
44
|
+
return;
|
|
45
|
+
// N-API finalizers can unload libraries during environment teardown even when
|
|
46
|
+
// JavaScript retained them. Go runtime threads require OS-level process lifetime.
|
|
47
|
+
systemLoader ??= koffi_1.default.load(process.platform === "win32" ? "kernel32.dll" : null);
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
const module = [null];
|
|
50
|
+
const pin = systemLoader.func("int __stdcall GetModuleHandleExW(uint32_t flags, str16 name, _Out_ void **module)");
|
|
51
|
+
if (!pin(1, path, module))
|
|
52
|
+
throw new errors_js_1.FastlsError("cannot pin the Go runtime library for process lifetime");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const dlopen = systemLoader.func("void *dlopen(str path, int flags)");
|
|
56
|
+
// RTLD_NOW | RTLD_NODELETE; values from Darwin / glibc dlfcn.h.
|
|
57
|
+
if (!dlopen(path, 2 | (process.platform === "darwin" ? 0x80 : 0x1000))) {
|
|
58
|
+
throw new errors_js_1.FastlsError("cannot pin the Go runtime library for process lifetime");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
pinned.add(path);
|
|
62
|
+
}
|
|
63
|
+
class Native {
|
|
64
|
+
functions = {};
|
|
65
|
+
notificationsAvailable;
|
|
66
|
+
websocketAvailable;
|
|
67
|
+
path;
|
|
68
|
+
static load(library) {
|
|
69
|
+
if (!node_worker_threads_1.isMainThread)
|
|
70
|
+
throw new errors_js_1.FastlsError("Fastls must be loaded on the main thread; worker_threads are not supported yet");
|
|
71
|
+
const path = (0, library_path_js_1.libraryPath)(library);
|
|
72
|
+
let native = bindings.get(path);
|
|
73
|
+
if (!native) {
|
|
74
|
+
native = new Native(path);
|
|
75
|
+
bindings.set(path, native);
|
|
76
|
+
}
|
|
77
|
+
return native;
|
|
78
|
+
}
|
|
79
|
+
constructor(path) {
|
|
80
|
+
this.path = path;
|
|
81
|
+
try {
|
|
82
|
+
let lib = libraries.get(path);
|
|
83
|
+
if (!lib) {
|
|
84
|
+
lib = koffi_1.default.load(path);
|
|
85
|
+
libraries.set(path, lib);
|
|
86
|
+
}
|
|
87
|
+
pinLibrary(path);
|
|
88
|
+
if (lib.func("uint32_t fastls_abi_version(void)")() !== 1) {
|
|
89
|
+
throw new errors_js_1.FastlsError("unsupported Fastls ABI version (expected 1)");
|
|
90
|
+
}
|
|
91
|
+
const signatures = {
|
|
92
|
+
profiles: [], stats: [], features: [], session_create: [],
|
|
93
|
+
session_close: ["uint64_t"], session_close_idle: ["uint64_t"],
|
|
94
|
+
session_cookies: ["uint64_t", "void *", "size_t"],
|
|
95
|
+
start: ["void *", "size_t"], info: ["uint64_t"],
|
|
96
|
+
cancel: ["uint64_t"], close: ["uint64_t"],
|
|
97
|
+
};
|
|
98
|
+
for (const [name, args] of Object.entries(signatures)) {
|
|
99
|
+
// Return an owned pointer, never an auto-converted string: we must free it.
|
|
100
|
+
this.functions[name] = lib.func(`fastls_${name}`, "void *", args);
|
|
101
|
+
}
|
|
102
|
+
this.functions.free = lib.func("void fastls_free(void *ptr)");
|
|
103
|
+
this.functions.read = lib.func("int fastls_read(uint64_t handle, _Out_ void *buffer, size_t capacity, _Out_ size_t *length)");
|
|
104
|
+
this.functions.write = lib.func("int fastls_write(uint64_t handle, const void *buffer, size_t length, int end)");
|
|
105
|
+
const features = new Set(this.call("features"));
|
|
106
|
+
let websocketAvailable = false;
|
|
107
|
+
if (features.has("websocket_v1")) {
|
|
108
|
+
try {
|
|
109
|
+
const start = lib.func("void *fastls_ws_start(void *json, size_t length)");
|
|
110
|
+
const send = lib.func("int fastls_ws_send(uint64_t handle, int opcode, const void *data, size_t length)");
|
|
111
|
+
const read = lib.func("int fastls_ws_read(uint64_t handle, _Out_ void *buffer, size_t capacity, _Out_ size_t *length, _Out_ int *opcode, _Out_ int *end)");
|
|
112
|
+
Object.assign(this.functions, { ws_start: start, ws_send: send, ws_read: read });
|
|
113
|
+
websocketAvailable = true;
|
|
114
|
+
}
|
|
115
|
+
catch { /* Optional as a group; HTTP stays available with old libraries. */ }
|
|
116
|
+
}
|
|
117
|
+
this.websocketAvailable = websocketAvailable;
|
|
118
|
+
let notificationsAvailable = false;
|
|
119
|
+
if (features.has("session_notifications")) {
|
|
120
|
+
try {
|
|
121
|
+
for (const [name, args] of Object.entries({ session_watch: ["uint64_t", "uint16_t", "void *", "size_t"], session_events: ["uint64_t"], session_unwatch: ["uint64_t"] })) {
|
|
122
|
+
this.functions[name] = lib.func(`fastls_${name}`, "void *", args);
|
|
123
|
+
}
|
|
124
|
+
this.functions.wait = lib.func("int fastls_wait(uint64_t handle, uint32_t events, uint32_t timeout_ms)");
|
|
125
|
+
notificationsAvailable = true;
|
|
126
|
+
}
|
|
127
|
+
catch { /* Old ABI-v1 libraries can still use polling. */ }
|
|
128
|
+
}
|
|
129
|
+
this.notificationsAvailable = notificationsAvailable;
|
|
130
|
+
for (const feature of ["stream_decode", "phase_timeouts", "stream_upload", "write_timeout"]) {
|
|
131
|
+
if (!features.has(feature))
|
|
132
|
+
throw new errors_js_1.FastlsError(`native feature required: ${feature}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
throw new errors_js_1.FastlsError(`Cannot bind Fastls library at ${path}; ABI v1 with decoding, phase timeouts and streaming uploads is required`, { cause });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
call(name, ...args) {
|
|
140
|
+
const pointer = this.functions[name](...args);
|
|
141
|
+
if (!pointer)
|
|
142
|
+
throw new errors_js_1.FastlsError("native library returned a null result");
|
|
143
|
+
let envelope;
|
|
144
|
+
try {
|
|
145
|
+
const json = koffi_1.default.decode.string(pointer);
|
|
146
|
+
// Starts, sessions and notification batches contain uint64 handles. Catalogs, headers
|
|
147
|
+
// and info metadata use strings and bounded numeric fields; avoid the
|
|
148
|
+
// allocating JS numeric parser on every poll and catalog lookup.
|
|
149
|
+
envelope = (name === "start" || name === "ws_start" || name === "session_create" || name === "session_events" ? parseNativeJSON(json) : JSON.parse(json));
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
this.functions.free(pointer);
|
|
153
|
+
}
|
|
154
|
+
if (!envelope?.ok)
|
|
155
|
+
throw new errors_js_1.FastlsError(envelope?.error ?? "invalid native result");
|
|
156
|
+
return envelope.result;
|
|
157
|
+
}
|
|
158
|
+
start(input) {
|
|
159
|
+
const raw = encodeNativeJSON(input);
|
|
160
|
+
return asHandle(this.call("start", raw, raw.length));
|
|
161
|
+
}
|
|
162
|
+
wsStart(input) {
|
|
163
|
+
if (!this.websocketAvailable)
|
|
164
|
+
throw new errors_js_1.FastlsError("WebSocket requires a newer Fastls native library");
|
|
165
|
+
const raw = encodeNativeJSON(input);
|
|
166
|
+
return asHandle(this.call("ws_start", raw, raw.length));
|
|
167
|
+
}
|
|
168
|
+
wsSend(handle, opcode, data) {
|
|
169
|
+
return this.functions.ws_send(handle, opcode, data, data.byteLength);
|
|
170
|
+
}
|
|
171
|
+
wsRead(handle, buffer) {
|
|
172
|
+
const length = [0], opcode = [0], end = [0];
|
|
173
|
+
const code = this.functions.ws_read(handle, buffer, buffer.length, length, opcode, end);
|
|
174
|
+
if (![0, 1, 2].includes(code) && code >= 0)
|
|
175
|
+
throw new errors_js_1.FastlsError("invalid WebSocket read status");
|
|
176
|
+
if (!Number.isSafeInteger(Number(length[0])) || Number(length[0]) < 0 || Number(length[0]) > buffer.length || ![0, 1].includes(end[0]))
|
|
177
|
+
throw new errors_js_1.FastlsError("invalid WebSocket read result");
|
|
178
|
+
return { code, length: Number(length[0]), opcode: opcode[0], end: end[0] === 1 };
|
|
179
|
+
}
|
|
180
|
+
read(handle, buffer) {
|
|
181
|
+
const out = [0];
|
|
182
|
+
const code = this.functions.read(handle, buffer, buffer.length, out);
|
|
183
|
+
const length = Number(out[0]);
|
|
184
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > buffer.length) {
|
|
185
|
+
throw new errors_js_1.FastlsError("native library returned an invalid read length");
|
|
186
|
+
}
|
|
187
|
+
if (code < 0) {
|
|
188
|
+
const info = this.call("info", handle);
|
|
189
|
+
throw (0, errors_js_1.responseError)(info);
|
|
190
|
+
}
|
|
191
|
+
if (![0, 1, 2].includes(code))
|
|
192
|
+
throw new errors_js_1.FastlsError("native library returned an invalid read status");
|
|
193
|
+
return { code, length };
|
|
194
|
+
}
|
|
195
|
+
write(handle, buffer, end = false) {
|
|
196
|
+
const code = this.functions.write(handle, buffer, buffer.byteLength, Number(end));
|
|
197
|
+
if (code < 0) {
|
|
198
|
+
const info = this.call("info", handle);
|
|
199
|
+
if (info.state === "error")
|
|
200
|
+
throw (0, errors_js_1.responseError)(info);
|
|
201
|
+
if (info.state === "headers" || info.state === "eof")
|
|
202
|
+
return 2;
|
|
203
|
+
throw new errors_js_1.FastlsError("upload is closed or invalid");
|
|
204
|
+
}
|
|
205
|
+
return code;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
exports.Native = Native;
|