@botejs/core 0.1.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.
@@ -0,0 +1,4 @@
1
+ export type { JsonPointer } from './pointer.ts';
2
+ export { open, type Cursor, type RootCursor, type SessionOptions } from './open.ts';
3
+ export { fromBuffer, fromFile, fromHttpRange, type FactoryOptions, type Source, type SourceReader, type HttpRangeOptions, } from './sources.ts';
4
+ export { ValidationError, type StandardSchemaV1 } from './validate.ts';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ValidationError = exports.fromHttpRange = exports.fromFile = exports.fromBuffer = void 0;
4
+ // Node 18 and Node 20.3 predate `Symbol.asyncDispose`; mirror what TS emits for
5
+ // `await using` so the well-known symbol is available across our engine range.
6
+ if (!Symbol.asyncDispose) {
7
+ ;
8
+ Symbol.asyncDispose = Symbol.for('Symbol.asyncDispose');
9
+ }
10
+ var open_ts_1 = require("./open.js");
11
+ Object.defineProperty(exports, "open", { enumerable: true, get: function () { return open_ts_1.open; } });
12
+ var sources_ts_1 = require("./sources.js");
13
+ Object.defineProperty(exports, "fromBuffer", { enumerable: true, get: function () { return sources_ts_1.fromBuffer; } });
14
+ Object.defineProperty(exports, "fromFile", { enumerable: true, get: function () { return sources_ts_1.fromFile; } });
15
+ Object.defineProperty(exports, "fromHttpRange", { enumerable: true, get: function () { return sources_ts_1.fromHttpRange; } });
16
+ var validate_ts_1 = require("./validate.js");
17
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return validate_ts_1.ValidationError; } });
package/dist/open.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { JsonPointer } from './pointer.ts';
2
+ import type { Source } from './sources.ts';
3
+ import { type StandardSchemaV1 } from './validate.ts';
4
+ export interface SessionOptions {
5
+ /**
6
+ * Maximum number of source chunks held resident at once. Each slot
7
+ * accounts for one chunk's bytes plus its bitmaps; the cache also
8
+ * enforces a derived byte ceiling at roughly `maxResidentChunks x
9
+ * source.chunkBytes x 2` to bound total native memory.
10
+ *
11
+ * Defaults to 512 chunks.
12
+ */
13
+ maxResidentChunks?: number;
14
+ }
15
+ type InferOutput<Sch> = Sch extends StandardSchemaV1<unknown, infer O> ? O : never;
16
+ export interface Cursor {
17
+ /** Object-member key or array-element index that this cursor was yielded under by `walk`. `null` on the root cursor. */
18
+ readonly key: string | number | null;
19
+ has<S extends string>(pointer: JsonPointer<S>): Promise<boolean>;
20
+ has<S extends string>(pointer: JsonPointer<S>, schema: StandardSchemaV1): Promise<boolean>;
21
+ get<S extends string>(pointer: JsonPointer<S>): Promise<unknown>;
22
+ get<S extends string, Sch extends StandardSchemaV1>(pointer: JsonPointer<S>, schema: Sch): Promise<InferOutput<Sch>>;
23
+ iter<S extends string>(pointer: JsonPointer<S>): AsyncIterable<unknown>;
24
+ iter<S extends string, Sch extends StandardSchemaV1>(pointer: JsonPointer<S>, schema: Sch): AsyncIterable<InferOutput<Sch>>;
25
+ walk<S extends string>(pointer: JsonPointer<S>): AsyncIterable<Cursor>;
26
+ }
27
+ /**
28
+ * The cursor returned by `open()`. Owns the underlying `Source` and exposes
29
+ * both an explicit `close()` and `Symbol.asyncDispose` so callers can choose
30
+ * between manual cleanup and `await using` scoping.
31
+ */
32
+ export interface RootCursor extends Cursor, AsyncDisposable {
33
+ /** Close the underlying source. Idempotent. */
34
+ close(): Promise<void>;
35
+ }
36
+ /**
37
+ * Open a cursor over a seekable source.
38
+ *
39
+ * Calls `source.open()` to acquire a reader, then constructs the native cursor
40
+ * over it. The reader's `read(offset, buf)` is invoked with chunk-aligned
41
+ * `offset` and a `buf` whose `byteLength` equals the configured chunk size;
42
+ * the reader fills `buf` and resolves with `bytesRead`. `buf` is a view over
43
+ * native-owned memory and **MUST** not be retained past the returned promise.
44
+ *
45
+ * The returned `RootCursor` owns the reader: `close()` (or `await using`)
46
+ * drives the reader's own `close()` exactly once.
47
+ */
48
+ export declare function open(source: Source, options?: SessionOptions): Promise<RootCursor>;
49
+ export {};
package/dist/open.js ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.open = open;
4
+ const native_1 = require("@botejs/native");
5
+ const validate_ts_1 = require("./validate.js");
6
+ /**
7
+ * Open a cursor over a seekable source.
8
+ *
9
+ * Calls `source.open()` to acquire a reader, then constructs the native cursor
10
+ * over it. The reader's `read(offset, buf)` is invoked with chunk-aligned
11
+ * `offset` and a `buf` whose `byteLength` equals the configured chunk size;
12
+ * the reader fills `buf` and resolves with `bytesRead`. `buf` is a view over
13
+ * native-owned memory and **MUST** not be retained past the returned promise.
14
+ *
15
+ * The returned `RootCursor` owns the reader: `close()` (or `await using`)
16
+ * drives the reader's own `close()` exactly once.
17
+ */
18
+ async function open(source, options) {
19
+ const reader = await source.open();
20
+ let native;
21
+ try {
22
+ native = (0, native_1.open)({
23
+ size: reader.size,
24
+ chunkBytes: reader.chunkBytes,
25
+ read: async ({ offset, buf }) => reader.read(offset, buf),
26
+ }, {
27
+ maxResidentChunks: options?.maxResidentChunks,
28
+ });
29
+ }
30
+ catch (err) {
31
+ await closeReader(reader);
32
+ throw err;
33
+ }
34
+ let closed = false;
35
+ const close = async () => {
36
+ if (closed)
37
+ return;
38
+ closed = true;
39
+ await closeReader(reader);
40
+ };
41
+ return Object.assign(wrap(native), {
42
+ close,
43
+ [Symbol.asyncDispose]: close,
44
+ });
45
+ }
46
+ async function closeReader(reader) {
47
+ if (reader.close)
48
+ await reader.close();
49
+ }
50
+ function wrap(native) {
51
+ const cursor = {
52
+ get key() {
53
+ return native.key;
54
+ },
55
+ async has(pointer, schema) {
56
+ if (!schema)
57
+ return native.has(pointer);
58
+ if (!(await native.has(pointer)))
59
+ return false;
60
+ const result = await schema['~standard'].validate(await native.get(pointer));
61
+ return result.issues === undefined;
62
+ },
63
+ async get(pointer, schema) {
64
+ const value = await native.get(pointer);
65
+ return schema ? (0, validate_ts_1.runStandardSchema)(schema, value, pointer) : value;
66
+ },
67
+ iter(pointer, schema) {
68
+ const inner = native.iter(pointer);
69
+ if (!schema)
70
+ return inner;
71
+ return {
72
+ async *[Symbol.asyncIterator]() {
73
+ let i = 0;
74
+ for await (const v of inner) {
75
+ yield await (0, validate_ts_1.runStandardSchema)(schema, v, `${pointer}/${i++}`);
76
+ }
77
+ },
78
+ };
79
+ },
80
+ walk(pointer) {
81
+ return {
82
+ async *[Symbol.asyncIterator]() {
83
+ for await (const child of native.walk(pointer)) {
84
+ yield wrap(child);
85
+ }
86
+ },
87
+ };
88
+ },
89
+ };
90
+ return cursor;
91
+ }
@@ -0,0 +1,5 @@
1
+ type ValidateTokenChars<S extends string> = S extends `${string}~${infer Rest}` ? Rest extends `0${infer After}` | `1${infer After}` ? ValidateTokenChars<After> : false : true;
2
+ type ValidateTokens<S extends string> = S extends `${infer Token}/${infer Rest}` ? ValidateTokenChars<Token> extends true ? ValidateTokens<Rest> : false : ValidateTokenChars<S>;
3
+ type IsJsonPointer<S extends string> = S extends '' ? true : S extends `/${infer Rest}` ? ValidateTokens<Rest> : false;
4
+ export type JsonPointer<S extends string> = IsJsonPointer<S> extends true ? S : `Error: invalid JSON pointer "${S}"`;
5
+ export {};
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // RFC 6901 JSON Pointer Static Typing Validator
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A handle on an opened seekable byte stream. The reader owns whatever
3
+ * resources back the stream (a file handle, an `AbortController`, etc.) and
4
+ * surfaces them through `close()`. Constructed by `Source.open()`; never by
5
+ * library callers directly.
6
+ */
7
+ export interface SourceReader {
8
+ /** Total length of the underlying byte stream. */
9
+ readonly size: number;
10
+ /** Preferred read granularity in bytes. Must be a non-zero multiple of 64. */
11
+ readonly chunkBytes?: number;
12
+ /**
13
+ * Fill `buf` with up to `buf.byteLength` bytes starting at `offset` and
14
+ * resolve with the number of bytes written. The implementation must not
15
+ * retain a reference to `buf` or read from it after the returned promise
16
+ * resolves: `buf` is a view over native-owned memory whose lifetime ends
17
+ * once the promise settles.
18
+ */
19
+ read(offset: number, buf: Uint8Array): Promise<number>;
20
+ /** Release resources held by the reader. Driven once by the `open()` lifecycle. */
21
+ close?(): Promise<void> | void;
22
+ }
23
+ /**
24
+ * Describes how to obtain a seekable byte stream. Construction is cheap and
25
+ * synchronous - no I/O happens until `open()` runs, which the top-level
26
+ * `open()` API drives. Provide your own object implementing this interface to
27
+ * plug in custom backends.
28
+ */
29
+ export interface Source {
30
+ /** Acquire the stream. Resolves to a `SourceReader` that owns any underlying resources. */
31
+ open(): Promise<SourceReader>;
32
+ }
33
+ export interface FactoryOptions {
34
+ /** Override the factory's default chunk size. Must be a non-zero multiple of 64. */
35
+ chunkBytes?: number;
36
+ }
37
+ export interface HttpRangeOptions extends FactoryOptions {
38
+ /** Merged into every request (headers, credentials, signal, etc.). */
39
+ init?: RequestInit;
40
+ }
41
+ export declare function fromBuffer(buf: Uint8Array | ArrayBuffer, options?: FactoryOptions): Source;
42
+ export declare function fromFile(path: string, options?: FactoryOptions): Source;
43
+ export declare function fromHttpRange(url: string, options?: HttpRangeOptions): Source;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fromBuffer = fromBuffer;
4
+ exports.fromFile = fromFile;
5
+ exports.fromHttpRange = fromHttpRange;
6
+ const promises_1 = require("node:fs/promises");
7
+ /** Default chunk size, in bytes, for in-memory sources. */
8
+ const DEFAULT_BUFFER_CHUNK_BYTES = 4 * 1024;
9
+ /** Default chunk size, in bytes, for local files: matches typical filesystem readahead. */
10
+ const DEFAULT_FILE_CHUNK_BYTES = 64 * 1024;
11
+ /** Default chunk size, in bytes, for HTTP range reads: amortizes RTT across more data. */
12
+ const DEFAULT_URL_CHUNK_BYTES = 256 * 1024;
13
+ function fromBuffer(buf, options) {
14
+ const view = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
15
+ const chunkBytes = options?.chunkBytes ?? DEFAULT_BUFFER_CHUNK_BYTES;
16
+ return {
17
+ open: () => Promise.resolve({
18
+ size: view.byteLength,
19
+ chunkBytes,
20
+ read: async (offset, dst) => {
21
+ const end = Math.min(offset + dst.byteLength, view.byteLength);
22
+ const n = Math.max(0, end - offset);
23
+ if (n > 0)
24
+ dst.set(view.subarray(offset, end));
25
+ return n;
26
+ },
27
+ }),
28
+ };
29
+ }
30
+ function fromFile(path, options) {
31
+ const chunkBytes = options?.chunkBytes ?? DEFAULT_FILE_CHUNK_BYTES;
32
+ return {
33
+ open: async () => {
34
+ const handle = await (0, promises_1.open)(path, 'r');
35
+ const stat = await handle.stat();
36
+ let closed = false;
37
+ return {
38
+ size: stat.size,
39
+ chunkBytes,
40
+ read: async (offset, dst) => {
41
+ const { bytesRead } = await handle.read(dst, 0, dst.byteLength, offset);
42
+ return bytesRead;
43
+ },
44
+ close: async () => {
45
+ if (closed)
46
+ return;
47
+ closed = true;
48
+ await handle.close();
49
+ },
50
+ };
51
+ },
52
+ };
53
+ }
54
+ function fromHttpRange(url, options) {
55
+ const init = options?.init;
56
+ const chunkBytes = options?.chunkBytes ?? DEFAULT_URL_CHUNK_BYTES;
57
+ return {
58
+ open: async () => {
59
+ const controller = new AbortController();
60
+ const userSignal = init?.signal;
61
+ if (userSignal) {
62
+ if (userSignal.aborted) {
63
+ controller.abort(userSignal.reason);
64
+ }
65
+ else {
66
+ userSignal.addEventListener('abort', () => controller.abort(userSignal.reason), { once: true });
67
+ }
68
+ }
69
+ const head = await fetch(url, { ...init, method: 'HEAD', signal: controller.signal });
70
+ if (!head.ok) {
71
+ throw new Error(`HEAD ${url} failed: ${head.status} ${head.statusText}`);
72
+ }
73
+ const sizeHeader = head.headers.get('content-length');
74
+ const size = sizeHeader === null ? NaN : Number.parseInt(sizeHeader, 10);
75
+ if (!Number.isFinite(size) || size < 0) {
76
+ throw new Error(`HEAD ${url} returned no valid Content-Length`);
77
+ }
78
+ const acceptsRanges = (head.headers.get('accept-ranges') ?? '').toLowerCase().includes('bytes');
79
+ if (!acceptsRanges) {
80
+ throw new Error(`HEAD ${url} does not advertise Accept-Ranges: bytes`);
81
+ }
82
+ let closed = false;
83
+ return {
84
+ size,
85
+ chunkBytes,
86
+ read: async (offset, dst) => {
87
+ // HTTP ranges are inclusive on both ends.
88
+ const end = Math.min(offset + dst.byteLength, size) - 1;
89
+ const headers = new Headers(init?.headers);
90
+ headers.set('Range', `bytes=${offset}-${end}`);
91
+ const res = await fetch(url, { ...init, headers, method: 'GET', signal: controller.signal });
92
+ if (res.status === 206) {
93
+ const body = new Uint8Array(await res.arrayBuffer());
94
+ dst.set(body);
95
+ return body.byteLength;
96
+ }
97
+ // A 200 means the server ignored our Range request and returned the full
98
+ // body. We throw here since the point of using ranges is to not have to
99
+ // buffer the whole thing in memory.
100
+ if (res.status === 200) {
101
+ throw new Error(`Range GET ${url} (bytes=${offset}-${end}) ignored Range and returned 200.`);
102
+ }
103
+ throw new Error(`Range GET ${url} (bytes=${offset}-${end}) failed: ${res.status}`);
104
+ },
105
+ close: async () => {
106
+ if (closed)
107
+ return;
108
+ closed = true;
109
+ controller.abort();
110
+ },
111
+ };
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,8 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ export type { StandardSchemaV1 };
3
+ export declare class ValidationError extends Error {
4
+ readonly issues: readonly StandardSchemaV1.Issue[];
5
+ readonly pointer: string;
6
+ constructor(issues: readonly StandardSchemaV1.Issue[], pointer: string);
7
+ }
8
+ export declare function runStandardSchema<O>(schema: StandardSchemaV1<unknown, O>, value: unknown, pointer: string): Promise<O>;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ValidationError = void 0;
4
+ exports.runStandardSchema = runStandardSchema;
5
+ class ValidationError extends Error {
6
+ issues;
7
+ pointer;
8
+ constructor(issues, pointer) {
9
+ super(`bote: schema validation failed at ${pointer || '/'}: ${issues[0]?.message ?? 'unknown'}`);
10
+ this.name = 'ValidationError';
11
+ this.issues = issues;
12
+ this.pointer = pointer;
13
+ }
14
+ }
15
+ exports.ValidationError = ValidationError;
16
+ async function runStandardSchema(schema, value, pointer) {
17
+ const result = await schema['~standard'].validate(value);
18
+ if (result.issues)
19
+ throw new ValidationError(result.issues, pointer);
20
+ return result.value;
21
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@botejs/core",
3
+ "version": "0.1.2",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/jankdc/bote.git",
8
+ "directory": "packages/bote"
9
+ },
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "engines": {
16
+ "node": ">= 18.17.0 < 19 || >= 20.3.0 < 21 || >= 21.1.0"
17
+ },
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org/",
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "build:debug": "tsc --sourceMap",
25
+ "test": "node --test --experimental-strip-types --no-warnings=ExperimentalWarning __test__/*.spec.ts",
26
+ "lint": "oxlint src",
27
+ "prepublishOnly": "tsc"
28
+ },
29
+ "dependencies": {
30
+ "@botejs/native": "workspace:*"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "oxlint": "^1.14.0",
35
+ "typescript": "^6.0.0"
36
+ },
37
+ "optionalDependencies": {
38
+ "@standard-schema/spec": "^1.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@standard-schema/spec": "^1.0.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@standard-schema/spec": {
45
+ "optional": true
46
+ }
47
+ }
48
+ }