@milaboratories/pl-model-middle-layer 1.8.13 → 1.8.15

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.
@@ -1,4 +1,5 @@
1
1
  import { AxisSpec, ValueType } from '@milaboratories/pl-model-common';
2
+ export type Logger = (level: 'info' | 'warn' | 'error', message: string) => void;
2
3
  export interface SingleAxisSelector {
3
4
  name: string;
4
5
  type?: ValueType;
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/pframe/internal_api/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,QAAQ,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAE9E,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,kBAAkB,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,cAAc,EAAE,iBAAiB,EAAE,CAAC;CACrC;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B,CAAA"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/pframe/internal_api/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEtE,MAAM,MAAM,MAAM,GAAG,CACnB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAChC,OAAO,EAAE,MAAM,KACZ,IAAI,CAAC;AAEV,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,kBAAkB,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,cAAc,EAAE,iBAAiB,EAAE,CAAC;CACrC;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B,CAAA"}
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ /** File system abstraction for request handler factory, @see HttpHelpers.createRequestHandler */
4
+ class ObjectStore {
5
+ logger;
6
+ constructor(options) {
7
+ this.logger = options.logger ?? (() => { });
8
+ }
9
+ /** Translate HTTP range to file range, @returns null if the range is not satisfiable */
10
+ translate(fileSize, range) {
11
+ if (!range)
12
+ return { start: 0, end: fileSize - 1 };
13
+ switch (range.type) {
14
+ case 'bounded':
15
+ if (range.end >= fileSize)
16
+ return null;
17
+ return { start: range.start, end: range.end };
18
+ case 'offset':
19
+ if (range.offset >= fileSize)
20
+ return null;
21
+ return { start: range.offset, end: fileSize - 1 };
22
+ case 'suffix':
23
+ if (range.suffix > fileSize)
24
+ return null;
25
+ return { start: fileSize - range.suffix, end: fileSize - 1 };
26
+ }
27
+ }
28
+ }
29
+
30
+ exports.ObjectStore = ObjectStore;
31
+ //# sourceMappingURL=http_helpers.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http_helpers.cjs","sources":["../../../src/pframe/internal_api/http_helpers.ts"],"sourcesContent":["import type { Readable } from 'node:stream';\nimport type { RequestListener } from 'node:http';\nimport type { Branded, Base64Encoded } from '@milaboratories/pl-model-common';\nimport type { Logger } from './common';\n\n/** Parquet file name */\nexport type ParquetFileName = Branded<`${string}.parquet`, 'PFrameInternal.ParquetFileName'>;\n\nexport type FileRange = {\n /** Start byte position (inclusive) */\n start: number;\n /** End byte position (inclusive) */\n end: number;\n}\n\n/** HTTP range as of RFC 9110 <https://datatracker.ietf.org/doc/html/rfc9110#name-range> */\nexport type HttpRange =\n | {\n /**\n * Get file content in the specified byte range\n * \n * @example\n * ```\n * GET /file.parquet HTTP/1.1\n * Range: bytes=0-1023\n * ```\n */\n type: 'bounded';\n /** Start byte position (inclusive) */\n start: number;\n /** End byte position (inclusive) */\n end: number;\n }\n | {\n /**\n * Get byte range starting from the specified offset\n * \n * @example\n * ```\n * GET /file.parquet HTTP/1.1\n * Range: bytes=1024-\n * ```\n */\n type: 'offset';\n /** Start byte position (inclusive) */\n offset: number;\n }\n | {\n /**\n * Get byte range starting from the specified suffix\n * \n * @example\n * ```\n * GET /file.parquet HTTP/1.1\n * Range: bytes=-1024\n * ```\n */\n type: 'suffix';\n /** End byte position (inclusive) */\n suffix: number;\n };\n\n/** HTTP method passed to object store */\nexport type HttpMethod = 'GET' | 'HEAD';\n\n/** HTTP response from object store */\nexport type ObjectStoreResponse =\n | {\n /** Will be translated to 500 Internal Server Error by the handler */\n type: 'InternalError';\n }\n | {\n /** Will be translated to 404 Not Found by the handler */\n type: 'NotFound';\n }\n | {\n /** Will be translated to 416 Range Not Satisfiable by the handler */\n type: 'RangeNotSatisfiable';\n /** Total file size in bytes */\n size: number;\n }\n | {\n /** Will be translated to 200 OK or 206 Partial Content by the handler */\n type: 'Ok';\n /** Total file size in bytes */\n size: number;\n /** File range translated from HTTP range */\n range: FileRange;\n /** Stream of file content, undefined for HEAD requests */\n data?: Readable;\n }\n\n/** Common options for object store creation */\nexport interface ObjectStoreOptions {\n /** Logger instance, no logging is performed when not provided */\n logger?: Logger;\n}\n\n/** Options for file system object store creation */\nexport interface FsStoreOptions extends ObjectStoreOptions {\n /** Local directory to serve files from */\n rootDir: string;\n}\n\n/** File system abstraction for request handler factory, @see HttpHelpers.createRequestHandler */\nexport abstract class ObjectStore {\n protected readonly logger: Logger;\n\n constructor(options: ObjectStoreOptions) {\n this.logger = options.logger ?? (() => {});\n }\n\n /** Translate HTTP range to file range, @returns null if the range is not satisfiable */\n protected translate(fileSize: number, range?: HttpRange): FileRange | null {\n if (!range) return { start: 0, end: fileSize - 1 };\n switch (range.type) {\n case 'bounded':\n if (range.end >= fileSize) return null;\n return { start: range.start, end: range.end };\n case 'offset':\n if (range.offset >= fileSize) return null;\n return { start: range.offset, end: fileSize - 1 };\n case 'suffix':\n if (range.suffix > fileSize) return null;\n return { start: fileSize - range.suffix, end: fileSize - 1 };\n }\n }\n\n /**\n * Proxy HTTP(S) request for parquet file to object store.\n * Callback promise resolves when stream is closed by handler @see HttpHelpers.createRequestHandler\n * Callback API is used so that ObjectStore can limit the number of concurrent requests.\n */\n abstract request(\n filename: ParquetFileName,\n params: {\n method: HttpMethod;\n range?: HttpRange;\n signal: AbortSignal;\n callback: (response: ObjectStoreResponse) => Promise<void>;\n }\n ): void;\n}\n\n/** Object store base URL in format accepted by Apache DataFusion and DuckDB */\nexport type ObjectStoreUrl = Branded<string, 'PFrameInternal.ObjectStoreUrl'>;\n\n/** HTTP(S) request handler creation options */\nexport type RequestHandlerOptions = {\n /** Object store to serve files from, @see HttpHelpers.createFsStore */\n store: ObjectStore;\n /** Here will go caching options... */\n}\n\n/** Server configuration options */\nexport type HttpServerOptions = {\n /** HTTP(S) request handler function, @see HttpHelpers.createRequestHandler */\n handler: RequestListener;\n /** Port to bind to, @default 0 for auto-assignment */\n port?: number;\n /** Do not apply authorization middleware to @param handler */\n noAuth?: true;\n /** Downgrade default HTTPS server to plain HTTP, @warning use only for testing */\n http?: true;\n};\n\n/**\n * Long unique opaque string for use in Bearer authorization header\n * \n * @example\n * ```ts\n * request.setHeader('Authorization', `Bearer ${authToken}`);\n * ```\n */\nexport type HttpAuthorizationToken = Branded<string, 'PFrameInternal.HttpAuthorizationToken'>;\n\n/**\n * TLS certificate in PEM format\n * \n * @example\n * ```txt\n * -----BEGIN CERTIFICATE-----\n * MIIC2zCCAcOgAwIBAgIJaVW7...\n * ...\n * ...Yf9CRK8fgnukKM7TJ\n * -----END CERTIFICATE-----\n * ```\n */\nexport type PemCertificate = Branded<string, 'PFrameInternal.PemCertificate'>;\n\n/** HTTP(S) server information and controls, @see HttpHelpers.createHttpServer */\nexport interface HttpServer {\n /** Server address info formatted as `http{s}://<host>:<port>/` */\n get address(): ObjectStoreUrl;\n /** Authorization token for Bearer scheme, undefined when @see HttpServerOptions.noAuth flag is set */\n get authToken(): HttpAuthorizationToken | undefined;\n /** Base64-encoded CA certificate in PEM format, undefined when @see HttpServerOptions.http flag is set */\n get encodedCaCert(): Base64Encoded<PemCertificate> | undefined;\n /** Promise that resolves when the server is stopped */\n get stopped(): Promise<void>;\n /** Request server stop, returns the same promise as @see HttpServer.stopped */\n stop(): Promise<void>;\n}\n\n/** List of HTTP(S) related helper functions exposed by PFrame module */\nexport interface HttpHelpers {\n /**\n * Create an object store for serving files from a local directory.\n * Rejects if the provided path does not exist or is not a directory.\n */\n createFsStore(options: FsStoreOptions): Promise<ObjectStore>;\n\n /**\n * Create an HTTP request handler for serving files from an object store.\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 otherwise.\n * Assumes that files are immutable (and sets cache headers accordingly).\n */\n createRequestHandler(options: RequestHandlerOptions): RequestListener;\n\n /**\n * Serve HTTP(S) requests using the provided handler on localhost port.\n * @returns promise that resolves when the server has stopped.\n *\n * @example\n * ```ts\n * const rootDir = '/path/to/directory/with/parquet/files';\n *\n * let store = await HttpHelpers.createFsStore({ rootDir }).catch((err: unknown) => {\n * throw new Error(`Failed to create file store for ${rootDir} - ${ensureError(err)}`);\n * });\n *\n * const server = await HttpHelpers.createHttpServer({\n * handler: HttpHelpers.createRequestHandler(store),\n * }).catch((err: unknown) => {\n * throw new Error(`Failed to start HTTP server - ${ensureError(err)}`);\n * });\n *\n * const { address, authToken, base64EncodedCaCert } = server;\n *\n * await server.stop();\n * ```\n */\n createHttpServer(options: HttpServerOptions): Promise<HttpServer>;\n}\n"],"names":[],"mappings":";;AAwGA;MACsB,WAAW,CAAA;AACZ,IAAA,MAAM;AAEzB,IAAA,WAAA,CAAY,OAA2B,EAAA;AACrC,QAAA,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,MAAK,EAAE,CAAC,CAAC;IAC5C;;IAGU,SAAS,CAAC,QAAgB,EAAE,KAAiB,EAAA;AACrD,QAAA,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE;AAClD,QAAA,QAAQ,KAAK,CAAC,IAAI;AAChB,YAAA,KAAK,SAAS;AACZ,gBAAA,IAAI,KAAK,CAAC,GAAG,IAAI,QAAQ;AAAE,oBAAA,OAAO,IAAI;AACtC,gBAAA,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE;AAC/C,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ;AAAE,oBAAA,OAAO,IAAI;AACzC,gBAAA,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE;AACnD,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ;AAAE,oBAAA,OAAO,IAAI;AACxC,gBAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE;;IAElE;AAgBD;;;;"}
@@ -1,70 +1,109 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  import type { RequestListener } from 'node:http';
3
3
  import type { Branded, Base64Encoded } from '@milaboratories/pl-model-common';
4
- /** File range specification */
4
+ import type { Logger } from './common';
5
+ /** Parquet file name */
6
+ export type ParquetFileName = Branded<`${string}.parquet`, 'PFrameInternal.ParquetFileName'>;
5
7
  export type FileRange = {
6
8
  /** Start byte position (inclusive) */
7
9
  start: number;
8
10
  /** End byte position (inclusive) */
9
11
  end: number;
10
12
  };
11
- /**
12
- * File system abstraction for request handler factory,
13
- * @see HttpHelpers.createRequestHandler.
14
- * Assumes that it is working with flat directory structure.
15
- * Accepts filenames with extension as input (e.g. `file.parquet`).
16
- */
17
- export interface ObjectStore {
13
+ /** HTTP range as of RFC 9110 <https://datatracker.ietf.org/doc/html/rfc9110#name-range> */
14
+ export type HttpRange = {
18
15
  /**
19
- * @returns file size in bytes or `-1` if file does not exist or permissions do not allow access.
20
- * @throws if file can become accessible after retry (e.g. on network error)
16
+ * Get file content in the specified byte range
21
17
  *
22
18
  * @example
23
- * ```ts
24
- * async getFileSize(filename: string): Promise<number> {
25
- * const filePath = this.resolve(filename);
26
- * return await fs
27
- * .stat(filePath)
28
- * .then((stat) => ({ size: stat.isFile() ? stat.size : -1 }))
29
- * .catch(() => ({ size: -1 }));
30
- * }
19
+ * ```
20
+ * GET /file.parquet HTTP/1.1
21
+ * Range: bytes=0-1023
31
22
  * ```
32
23
  */
33
- getFileSize(filename: string): Promise<number>;
24
+ type: 'bounded';
25
+ /** Start byte position (inclusive) */
26
+ start: number;
27
+ /** End byte position (inclusive) */
28
+ end: number;
29
+ } | {
34
30
  /**
35
- * Execute action with readable stream (actions can be concurrency limited by the store).
36
- * Action resolves when stream is closed by handler @see HttpHelpers.createRequestHandler
37
- *
38
- * @param filename - existing file name (for which @see ObjectStore.getFileSize returned non-negative value)
39
- * @param range - valid range of bytes to read from the file (store may skip validation)
40
- * @param action - function to execute with the stream, responsible for closing the stream
41
- * @returns promise that resolves after the action is completed
31
+ * Get byte range starting from the specified offset
42
32
  *
43
33
  * @example
44
- * ```ts
45
- * async withReadStream(params: {
46
- * filename: string;
47
- * range: FileRange;
48
- * action: (stream: Readable) => Promise<void>;
49
- * }): Promise<void> {
50
- * const { filename, range, action } = params;
51
- * const filePath = this.resolve(filename);
34
+ * ```
35
+ * GET /file.parquet HTTP/1.1
36
+ * Range: bytes=1024-
37
+ * ```
38
+ */
39
+ type: 'offset';
40
+ /** Start byte position (inclusive) */
41
+ offset: number;
42
+ } | {
43
+ /**
44
+ * Get byte range starting from the specified suffix
52
45
  *
53
- * try {
54
- * const stream = createReadStream(filePath, range);
55
- * return await action(stream);
56
- * } catch (err: unknown) {
57
- * console.error(`failed to create read stream for ${filename} - ${ensureError(err)}`);
58
- * throw;
59
- * }
60
- * }
46
+ * @example
47
+ * ```
48
+ * GET /file.parquet HTTP/1.1
49
+ * Range: bytes=-1024
61
50
  * ```
62
51
  */
63
- withReadStream(params: {
64
- filename: string;
65
- range: FileRange;
66
- action: (stream: Readable) => Promise<void>;
67
- }): Promise<void>;
52
+ type: 'suffix';
53
+ /** End byte position (inclusive) */
54
+ suffix: number;
55
+ };
56
+ /** HTTP method passed to object store */
57
+ export type HttpMethod = 'GET' | 'HEAD';
58
+ /** HTTP response from object store */
59
+ export type ObjectStoreResponse = {
60
+ /** Will be translated to 500 Internal Server Error by the handler */
61
+ type: 'InternalError';
62
+ } | {
63
+ /** Will be translated to 404 Not Found by the handler */
64
+ type: 'NotFound';
65
+ } | {
66
+ /** Will be translated to 416 Range Not Satisfiable by the handler */
67
+ type: 'RangeNotSatisfiable';
68
+ /** Total file size in bytes */
69
+ size: number;
70
+ } | {
71
+ /** Will be translated to 200 OK or 206 Partial Content by the handler */
72
+ type: 'Ok';
73
+ /** Total file size in bytes */
74
+ size: number;
75
+ /** File range translated from HTTP range */
76
+ range: FileRange;
77
+ /** Stream of file content, undefined for HEAD requests */
78
+ data?: Readable;
79
+ };
80
+ /** Common options for object store creation */
81
+ export interface ObjectStoreOptions {
82
+ /** Logger instance, no logging is performed when not provided */
83
+ logger?: Logger;
84
+ }
85
+ /** Options for file system object store creation */
86
+ export interface FsStoreOptions extends ObjectStoreOptions {
87
+ /** Local directory to serve files from */
88
+ rootDir: string;
89
+ }
90
+ /** File system abstraction for request handler factory, @see HttpHelpers.createRequestHandler */
91
+ export declare abstract class ObjectStore {
92
+ protected readonly logger: Logger;
93
+ constructor(options: ObjectStoreOptions);
94
+ /** Translate HTTP range to file range, @returns null if the range is not satisfiable */
95
+ protected translate(fileSize: number, range?: HttpRange): FileRange | null;
96
+ /**
97
+ * Proxy HTTP(S) request for parquet file to object store.
98
+ * Callback promise resolves when stream is closed by handler @see HttpHelpers.createRequestHandler
99
+ * Callback API is used so that ObjectStore can limit the number of concurrent requests.
100
+ */
101
+ abstract request(filename: ParquetFileName, params: {
102
+ method: HttpMethod;
103
+ range?: HttpRange;
104
+ signal: AbortSignal;
105
+ callback: (response: ObjectStoreResponse) => Promise<void>;
106
+ }): void;
68
107
  }
69
108
  /** Object store base URL in format accepted by Apache DataFusion and DuckDB */
70
109
  export type ObjectStoreUrl = Branded<string, 'PFrameInternal.ObjectStoreUrl'>;
@@ -77,7 +116,7 @@ export type RequestHandlerOptions = {
77
116
  export type HttpServerOptions = {
78
117
  /** HTTP(S) request handler function, @see HttpHelpers.createRequestHandler */
79
118
  handler: RequestListener;
80
- /** Port to bind to (@default 0 for auto-assignment) */
119
+ /** Port to bind to, @default 0 for auto-assignment */
81
120
  port?: number;
82
121
  /** Do not apply authorization middleware to @param handler */
83
122
  noAuth?: true;
@@ -125,7 +164,7 @@ export interface HttpHelpers {
125
164
  * Create an object store for serving files from a local directory.
126
165
  * Rejects if the provided path does not exist or is not a directory.
127
166
  */
128
- createFsStore(rootDir: string): Promise<ObjectStore>;
167
+ createFsStore(options: FsStoreOptions): Promise<ObjectStore>;
129
168
  /**
130
169
  * Create an HTTP request handler for serving files from an object store.
131
170
  * Accepts only paths of the form `/<filename>.parquet`, returns 410 otherwise.
@@ -140,7 +179,7 @@ export interface HttpHelpers {
140
179
  * ```ts
141
180
  * const rootDir = '/path/to/directory/with/parquet/files';
142
181
  *
143
- * let store = await HttpHelpers.createFsStore(rootDir).catch((err: unknown) => {
182
+ * let store = await HttpHelpers.createFsStore({ rootDir }).catch((err: unknown) => {
144
183
  * throw new Error(`Failed to create file store for ${rootDir} - ${ensureError(err)}`);
145
184
  * });
146
185
  *
@@ -1 +1 @@
1
- {"version":3,"file":"http_helpers.d.ts","sourceRoot":"","sources":["../../../src/pframe/internal_api/http_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAE9E,+BAA+B;AAC/B,MAAM,MAAM,SAAS,GAAG;IACtB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,cAAc,CAAC,MAAM,EAAE;QACrB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,SAAS,CAAC;QACjB,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC7C,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,+BAA+B,CAAC,CAAC;AAE9E,+CAA+C;AAC/C,MAAM,MAAM,qBAAqB,GAAG;IAClC,uEAAuE;IACvE,KAAK,EAAE,WAAW,CAAC;CAEpB,CAAA;AAED,mCAAmC;AACnC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,8EAA8E;IAC9E,OAAO,EAAE,eAAe,CAAC;IACzB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,kFAAkF;IAClF,IAAI,CAAC,EAAE,IAAI,CAAC;CACb,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,uCAAuC,CAAC,CAAC;AAE9F;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,+BAA+B,CAAC,CAAC;AAE9E,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,IAAI,OAAO,IAAI,cAAc,CAAC;IAC9B,sGAAsG;IACtG,IAAI,SAAS,IAAI,sBAAsB,GAAG,SAAS,CAAC;IACpD,0GAA0G;IAC1G,IAAI,aAAa,IAAI,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAC/D,uDAAuD;IACvD,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,+EAA+E;IAC/E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAErD;;;;OAIG;IACH,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,eAAe,CAAC;IAEtE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACnE"}
1
+ {"version":3,"file":"http_helpers.d.ts","sourceRoot":"","sources":["../../../src/pframe/internal_api/http_helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /** File system abstraction for request handler factory, @see HttpHelpers.createRequestHandler */
2
+ class ObjectStore {
3
+ logger;
4
+ constructor(options) {
5
+ this.logger = options.logger ?? (() => { });
6
+ }
7
+ /** Translate HTTP range to file range, @returns null if the range is not satisfiable */
8
+ translate(fileSize, range) {
9
+ if (!range)
10
+ return { start: 0, end: fileSize - 1 };
11
+ switch (range.type) {
12
+ case 'bounded':
13
+ if (range.end >= fileSize)
14
+ return null;
15
+ return { start: range.start, end: range.end };
16
+ case 'offset':
17
+ if (range.offset >= fileSize)
18
+ return null;
19
+ return { start: range.offset, end: fileSize - 1 };
20
+ case 'suffix':
21
+ if (range.suffix > fileSize)
22
+ return null;
23
+ return { start: fileSize - range.suffix, end: fileSize - 1 };
24
+ }
25
+ }
26
+ }
27
+
28
+ export { ObjectStore };
29
+ //# sourceMappingURL=http_helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http_helpers.js","sources":["../../../src/pframe/internal_api/http_helpers.ts"],"sourcesContent":["import type { Readable } from 'node:stream';\nimport type { RequestListener } from 'node:http';\nimport type { Branded, Base64Encoded } from '@milaboratories/pl-model-common';\nimport type { Logger } from './common';\n\n/** Parquet file name */\nexport type ParquetFileName = Branded<`${string}.parquet`, 'PFrameInternal.ParquetFileName'>;\n\nexport type FileRange = {\n /** Start byte position (inclusive) */\n start: number;\n /** End byte position (inclusive) */\n end: number;\n}\n\n/** HTTP range as of RFC 9110 <https://datatracker.ietf.org/doc/html/rfc9110#name-range> */\nexport type HttpRange =\n | {\n /**\n * Get file content in the specified byte range\n * \n * @example\n * ```\n * GET /file.parquet HTTP/1.1\n * Range: bytes=0-1023\n * ```\n */\n type: 'bounded';\n /** Start byte position (inclusive) */\n start: number;\n /** End byte position (inclusive) */\n end: number;\n }\n | {\n /**\n * Get byte range starting from the specified offset\n * \n * @example\n * ```\n * GET /file.parquet HTTP/1.1\n * Range: bytes=1024-\n * ```\n */\n type: 'offset';\n /** Start byte position (inclusive) */\n offset: number;\n }\n | {\n /**\n * Get byte range starting from the specified suffix\n * \n * @example\n * ```\n * GET /file.parquet HTTP/1.1\n * Range: bytes=-1024\n * ```\n */\n type: 'suffix';\n /** End byte position (inclusive) */\n suffix: number;\n };\n\n/** HTTP method passed to object store */\nexport type HttpMethod = 'GET' | 'HEAD';\n\n/** HTTP response from object store */\nexport type ObjectStoreResponse =\n | {\n /** Will be translated to 500 Internal Server Error by the handler */\n type: 'InternalError';\n }\n | {\n /** Will be translated to 404 Not Found by the handler */\n type: 'NotFound';\n }\n | {\n /** Will be translated to 416 Range Not Satisfiable by the handler */\n type: 'RangeNotSatisfiable';\n /** Total file size in bytes */\n size: number;\n }\n | {\n /** Will be translated to 200 OK or 206 Partial Content by the handler */\n type: 'Ok';\n /** Total file size in bytes */\n size: number;\n /** File range translated from HTTP range */\n range: FileRange;\n /** Stream of file content, undefined for HEAD requests */\n data?: Readable;\n }\n\n/** Common options for object store creation */\nexport interface ObjectStoreOptions {\n /** Logger instance, no logging is performed when not provided */\n logger?: Logger;\n}\n\n/** Options for file system object store creation */\nexport interface FsStoreOptions extends ObjectStoreOptions {\n /** Local directory to serve files from */\n rootDir: string;\n}\n\n/** File system abstraction for request handler factory, @see HttpHelpers.createRequestHandler */\nexport abstract class ObjectStore {\n protected readonly logger: Logger;\n\n constructor(options: ObjectStoreOptions) {\n this.logger = options.logger ?? (() => {});\n }\n\n /** Translate HTTP range to file range, @returns null if the range is not satisfiable */\n protected translate(fileSize: number, range?: HttpRange): FileRange | null {\n if (!range) return { start: 0, end: fileSize - 1 };\n switch (range.type) {\n case 'bounded':\n if (range.end >= fileSize) return null;\n return { start: range.start, end: range.end };\n case 'offset':\n if (range.offset >= fileSize) return null;\n return { start: range.offset, end: fileSize - 1 };\n case 'suffix':\n if (range.suffix > fileSize) return null;\n return { start: fileSize - range.suffix, end: fileSize - 1 };\n }\n }\n\n /**\n * Proxy HTTP(S) request for parquet file to object store.\n * Callback promise resolves when stream is closed by handler @see HttpHelpers.createRequestHandler\n * Callback API is used so that ObjectStore can limit the number of concurrent requests.\n */\n abstract request(\n filename: ParquetFileName,\n params: {\n method: HttpMethod;\n range?: HttpRange;\n signal: AbortSignal;\n callback: (response: ObjectStoreResponse) => Promise<void>;\n }\n ): void;\n}\n\n/** Object store base URL in format accepted by Apache DataFusion and DuckDB */\nexport type ObjectStoreUrl = Branded<string, 'PFrameInternal.ObjectStoreUrl'>;\n\n/** HTTP(S) request handler creation options */\nexport type RequestHandlerOptions = {\n /** Object store to serve files from, @see HttpHelpers.createFsStore */\n store: ObjectStore;\n /** Here will go caching options... */\n}\n\n/** Server configuration options */\nexport type HttpServerOptions = {\n /** HTTP(S) request handler function, @see HttpHelpers.createRequestHandler */\n handler: RequestListener;\n /** Port to bind to, @default 0 for auto-assignment */\n port?: number;\n /** Do not apply authorization middleware to @param handler */\n noAuth?: true;\n /** Downgrade default HTTPS server to plain HTTP, @warning use only for testing */\n http?: true;\n};\n\n/**\n * Long unique opaque string for use in Bearer authorization header\n * \n * @example\n * ```ts\n * request.setHeader('Authorization', `Bearer ${authToken}`);\n * ```\n */\nexport type HttpAuthorizationToken = Branded<string, 'PFrameInternal.HttpAuthorizationToken'>;\n\n/**\n * TLS certificate in PEM format\n * \n * @example\n * ```txt\n * -----BEGIN CERTIFICATE-----\n * MIIC2zCCAcOgAwIBAgIJaVW7...\n * ...\n * ...Yf9CRK8fgnukKM7TJ\n * -----END CERTIFICATE-----\n * ```\n */\nexport type PemCertificate = Branded<string, 'PFrameInternal.PemCertificate'>;\n\n/** HTTP(S) server information and controls, @see HttpHelpers.createHttpServer */\nexport interface HttpServer {\n /** Server address info formatted as `http{s}://<host>:<port>/` */\n get address(): ObjectStoreUrl;\n /** Authorization token for Bearer scheme, undefined when @see HttpServerOptions.noAuth flag is set */\n get authToken(): HttpAuthorizationToken | undefined;\n /** Base64-encoded CA certificate in PEM format, undefined when @see HttpServerOptions.http flag is set */\n get encodedCaCert(): Base64Encoded<PemCertificate> | undefined;\n /** Promise that resolves when the server is stopped */\n get stopped(): Promise<void>;\n /** Request server stop, returns the same promise as @see HttpServer.stopped */\n stop(): Promise<void>;\n}\n\n/** List of HTTP(S) related helper functions exposed by PFrame module */\nexport interface HttpHelpers {\n /**\n * Create an object store for serving files from a local directory.\n * Rejects if the provided path does not exist or is not a directory.\n */\n createFsStore(options: FsStoreOptions): Promise<ObjectStore>;\n\n /**\n * Create an HTTP request handler for serving files from an object store.\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 otherwise.\n * Assumes that files are immutable (and sets cache headers accordingly).\n */\n createRequestHandler(options: RequestHandlerOptions): RequestListener;\n\n /**\n * Serve HTTP(S) requests using the provided handler on localhost port.\n * @returns promise that resolves when the server has stopped.\n *\n * @example\n * ```ts\n * const rootDir = '/path/to/directory/with/parquet/files';\n *\n * let store = await HttpHelpers.createFsStore({ rootDir }).catch((err: unknown) => {\n * throw new Error(`Failed to create file store for ${rootDir} - ${ensureError(err)}`);\n * });\n *\n * const server = await HttpHelpers.createHttpServer({\n * handler: HttpHelpers.createRequestHandler(store),\n * }).catch((err: unknown) => {\n * throw new Error(`Failed to start HTTP server - ${ensureError(err)}`);\n * });\n *\n * const { address, authToken, base64EncodedCaCert } = server;\n *\n * await server.stop();\n * ```\n */\n createHttpServer(options: HttpServerOptions): Promise<HttpServer>;\n}\n"],"names":[],"mappings":"AAwGA;MACsB,WAAW,CAAA;AACZ,IAAA,MAAM;AAEzB,IAAA,WAAA,CAAY,OAA2B,EAAA;AACrC,QAAA,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,MAAK,EAAE,CAAC,CAAC;IAC5C;;IAGU,SAAS,CAAC,QAAgB,EAAE,KAAiB,EAAA;AACrD,QAAA,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE;AAClD,QAAA,QAAQ,KAAK,CAAC,IAAI;AAChB,YAAA,KAAK,SAAS;AACZ,gBAAA,IAAI,KAAK,CAAC,GAAG,IAAI,QAAQ;AAAE,oBAAA,OAAO,IAAI;AACtC,gBAAA,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE;AAC/C,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ;AAAE,oBAAA,OAAO,IAAI;AACzC,gBAAA,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE;AACnD,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ;AAAE,oBAAA,OAAO,IAAI;AACxC,gBAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,EAAE;;IAElE;AAgBD;;;;"}
@@ -1,3 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ var http_helpers = require('./http_helpers.cjs');
4
+
5
+
6
+
7
+ exports.ObjectStore = http_helpers.ObjectStore;
3
8
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
@@ -1,2 +1,2 @@
1
-
1
+ export { ObjectStore } from './http_helpers.js';
2
2
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  import type { PFrameFactoryAPIV2, PFrameFactoryAPIV3 } from './api_factory';
2
2
  import type { PFrameReadAPIV8, PFrameReadAPIV9 } from './api_read';
3
- export type Logger = (level: 'info' | 'warn' | 'error', message: string) => void;
3
+ import type { Logger } from './common';
4
4
  export interface PFrameV9 extends PFrameFactoryAPIV2, PFrameReadAPIV8 {
5
5
  }
6
6
  export interface PFrameV10 extends PFrameFactoryAPIV3, PFrameReadAPIV9 {
@@ -1 +1 @@
1
- {"version":3,"file":"pframe.d.ts","sourceRoot":"","sources":["../../../src/pframe/internal_api/pframe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,MAAM,MAAM,GAAG,CACnB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAChC,OAAO,EAAE,MAAM,KACZ,IAAI,CAAC;AAEV,MAAM,WAAW,QAAS,SAAQ,kBAAkB,EAAE,eAAe;CAAG;AAExE,MAAM,WAAW,SAAU,SAAQ,kBAAkB,EAAE,eAAe;CAAG;AAEzE,MAAM,MAAM,aAAa,GAAG;IAC1B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAA;AAED,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;IAEhD;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;CACtC"}
1
+ {"version":3,"file":"pframe.d.ts","sourceRoot":"","sources":["../../../src/pframe/internal_api/pframe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,WAAW,QAAS,SAAQ,kBAAkB,EAAE,eAAe;CAAG;AAExE,MAAM,WAAW,SAAU,SAAQ,kBAAkB,EAAE,eAAe;CAAG;AAEzE,MAAM,MAAM,aAAa,GAAG;IAC1B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAA;AAED,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;IAEhD;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;CACtC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-model-middle-layer",
3
- "version": "1.8.13",
3
+ "version": "1.8.15",
4
4
  "description": "Common model between middle layer and non-block UI code",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
25
25
  "devDependencies": {
26
26
  "typescript": "~5.6.3",
27
27
  "@milaboratories/build-configs": "1.0.8",
28
- "@milaboratories/ts-configs": "1.0.6",
29
- "@milaboratories/ts-builder": "1.0.5"
28
+ "@milaboratories/ts-builder": "1.0.5",
29
+ "@milaboratories/ts-configs": "1.0.6"
30
30
  },
31
31
  "scripts": {
32
32
  "type-check": "ts-builder types --target node",
@@ -1,4 +1,9 @@
1
- import { AxisId, AxisSpec, ValueType } from '@milaboratories/pl-model-common';
1
+ import { AxisSpec, ValueType } from '@milaboratories/pl-model-common';
2
+
3
+ export type Logger = (
4
+ level: 'info' | 'warn' | 'error',
5
+ message: string
6
+ ) => void;
2
7
 
3
8
  export interface SingleAxisSelector {
4
9
  name: string;
@@ -1,73 +1,145 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  import type { RequestListener } from 'node:http';
3
3
  import type { Branded, Base64Encoded } from '@milaboratories/pl-model-common';
4
+ import type { Logger } from './common';
5
+
6
+ /** Parquet file name */
7
+ export type ParquetFileName = Branded<`${string}.parquet`, 'PFrameInternal.ParquetFileName'>;
4
8
 
5
- /** File range specification */
6
9
  export type FileRange = {
7
10
  /** Start byte position (inclusive) */
8
11
  start: number;
9
12
  /** End byte position (inclusive) */
10
13
  end: number;
11
- };
14
+ }
12
15
 
13
- /**
14
- * File system abstraction for request handler factory,
15
- * @see HttpHelpers.createRequestHandler.
16
- * Assumes that it is working with flat directory structure.
17
- * Accepts filenames with extension as input (e.g. `file.parquet`).
18
- */
19
- export interface ObjectStore {
20
- /**
21
- * @returns file size in bytes or `-1` if file does not exist or permissions do not allow access.
22
- * @throws if file can become accessible after retry (e.g. on network error)
23
- *
24
- * @example
25
- * ```ts
26
- * async getFileSize(filename: string): Promise<number> {
27
- * const filePath = this.resolve(filename);
28
- * return await fs
29
- * .stat(filePath)
30
- * .then((stat) => ({ size: stat.isFile() ? stat.size : -1 }))
31
- * .catch(() => ({ size: -1 }));
32
- * }
33
- * ```
34
- */
35
- getFileSize(filename: string): Promise<number>;
16
+ /** HTTP range as of RFC 9110 <https://datatracker.ietf.org/doc/html/rfc9110#name-range> */
17
+ export type HttpRange =
18
+ | {
19
+ /**
20
+ * Get file content in the specified byte range
21
+ *
22
+ * @example
23
+ * ```
24
+ * GET /file.parquet HTTP/1.1
25
+ * Range: bytes=0-1023
26
+ * ```
27
+ */
28
+ type: 'bounded';
29
+ /** Start byte position (inclusive) */
30
+ start: number;
31
+ /** End byte position (inclusive) */
32
+ end: number;
33
+ }
34
+ | {
35
+ /**
36
+ * Get byte range starting from the specified offset
37
+ *
38
+ * @example
39
+ * ```
40
+ * GET /file.parquet HTTP/1.1
41
+ * Range: bytes=1024-
42
+ * ```
43
+ */
44
+ type: 'offset';
45
+ /** Start byte position (inclusive) */
46
+ offset: number;
47
+ }
48
+ | {
49
+ /**
50
+ * Get byte range starting from the specified suffix
51
+ *
52
+ * @example
53
+ * ```
54
+ * GET /file.parquet HTTP/1.1
55
+ * Range: bytes=-1024
56
+ * ```
57
+ */
58
+ type: 'suffix';
59
+ /** End byte position (inclusive) */
60
+ suffix: number;
61
+ };
62
+
63
+ /** HTTP method passed to object store */
64
+ export type HttpMethod = 'GET' | 'HEAD';
65
+
66
+ /** HTTP response from object store */
67
+ export type ObjectStoreResponse =
68
+ | {
69
+ /** Will be translated to 500 Internal Server Error by the handler */
70
+ type: 'InternalError';
71
+ }
72
+ | {
73
+ /** Will be translated to 404 Not Found by the handler */
74
+ type: 'NotFound';
75
+ }
76
+ | {
77
+ /** Will be translated to 416 Range Not Satisfiable by the handler */
78
+ type: 'RangeNotSatisfiable';
79
+ /** Total file size in bytes */
80
+ size: number;
81
+ }
82
+ | {
83
+ /** Will be translated to 200 OK or 206 Partial Content by the handler */
84
+ type: 'Ok';
85
+ /** Total file size in bytes */
86
+ size: number;
87
+ /** File range translated from HTTP range */
88
+ range: FileRange;
89
+ /** Stream of file content, undefined for HEAD requests */
90
+ data?: Readable;
91
+ }
92
+
93
+ /** Common options for object store creation */
94
+ export interface ObjectStoreOptions {
95
+ /** Logger instance, no logging is performed when not provided */
96
+ logger?: Logger;
97
+ }
98
+
99
+ /** Options for file system object store creation */
100
+ export interface FsStoreOptions extends ObjectStoreOptions {
101
+ /** Local directory to serve files from */
102
+ rootDir: string;
103
+ }
104
+
105
+ /** File system abstraction for request handler factory, @see HttpHelpers.createRequestHandler */
106
+ export abstract class ObjectStore {
107
+ protected readonly logger: Logger;
108
+
109
+ constructor(options: ObjectStoreOptions) {
110
+ this.logger = options.logger ?? (() => {});
111
+ }
112
+
113
+ /** Translate HTTP range to file range, @returns null if the range is not satisfiable */
114
+ protected translate(fileSize: number, range?: HttpRange): FileRange | null {
115
+ if (!range) return { start: 0, end: fileSize - 1 };
116
+ switch (range.type) {
117
+ case 'bounded':
118
+ if (range.end >= fileSize) return null;
119
+ return { start: range.start, end: range.end };
120
+ case 'offset':
121
+ if (range.offset >= fileSize) return null;
122
+ return { start: range.offset, end: fileSize - 1 };
123
+ case 'suffix':
124
+ if (range.suffix > fileSize) return null;
125
+ return { start: fileSize - range.suffix, end: fileSize - 1 };
126
+ }
127
+ }
36
128
 
37
129
  /**
38
- * Execute action with readable stream (actions can be concurrency limited by the store).
39
- * Action resolves when stream is closed by handler @see HttpHelpers.createRequestHandler
40
- *
41
- * @param filename - existing file name (for which @see ObjectStore.getFileSize returned non-negative value)
42
- * @param range - valid range of bytes to read from the file (store may skip validation)
43
- * @param action - function to execute with the stream, responsible for closing the stream
44
- * @returns promise that resolves after the action is completed
45
- *
46
- * @example
47
- * ```ts
48
- * async withReadStream(params: {
49
- * filename: string;
50
- * range: FileRange;
51
- * action: (stream: Readable) => Promise<void>;
52
- * }): Promise<void> {
53
- * const { filename, range, action } = params;
54
- * const filePath = this.resolve(filename);
55
- *
56
- * try {
57
- * const stream = createReadStream(filePath, range);
58
- * return await action(stream);
59
- * } catch (err: unknown) {
60
- * console.error(`failed to create read stream for ${filename} - ${ensureError(err)}`);
61
- * throw;
62
- * }
63
- * }
64
- * ```
130
+ * Proxy HTTP(S) request for parquet file to object store.
131
+ * Callback promise resolves when stream is closed by handler @see HttpHelpers.createRequestHandler
132
+ * Callback API is used so that ObjectStore can limit the number of concurrent requests.
65
133
  */
66
- withReadStream(params: {
67
- filename: string;
68
- range: FileRange;
69
- action: (stream: Readable) => Promise<void>;
70
- }): Promise<void>;
134
+ abstract request(
135
+ filename: ParquetFileName,
136
+ params: {
137
+ method: HttpMethod;
138
+ range?: HttpRange;
139
+ signal: AbortSignal;
140
+ callback: (response: ObjectStoreResponse) => Promise<void>;
141
+ }
142
+ ): void;
71
143
  }
72
144
 
73
145
  /** Object store base URL in format accepted by Apache DataFusion and DuckDB */
@@ -84,7 +156,7 @@ export type RequestHandlerOptions = {
84
156
  export type HttpServerOptions = {
85
157
  /** HTTP(S) request handler function, @see HttpHelpers.createRequestHandler */
86
158
  handler: RequestListener;
87
- /** Port to bind to (@default 0 for auto-assignment) */
159
+ /** Port to bind to, @default 0 for auto-assignment */
88
160
  port?: number;
89
161
  /** Do not apply authorization middleware to @param handler */
90
162
  noAuth?: true;
@@ -136,7 +208,7 @@ export interface HttpHelpers {
136
208
  * Create an object store for serving files from a local directory.
137
209
  * Rejects if the provided path does not exist or is not a directory.
138
210
  */
139
- createFsStore(rootDir: string): Promise<ObjectStore>;
211
+ createFsStore(options: FsStoreOptions): Promise<ObjectStore>;
140
212
 
141
213
  /**
142
214
  * Create an HTTP request handler for serving files from an object store.
@@ -153,7 +225,7 @@ export interface HttpHelpers {
153
225
  * ```ts
154
226
  * const rootDir = '/path/to/directory/with/parquet/files';
155
227
  *
156
- * let store = await HttpHelpers.createFsStore(rootDir).catch((err: unknown) => {
228
+ * let store = await HttpHelpers.createFsStore({ rootDir }).catch((err: unknown) => {
157
229
  * throw new Error(`Failed to create file store for ${rootDir} - ${ensureError(err)}`);
158
230
  * });
159
231
  *
@@ -1,10 +1,6 @@
1
1
  import type { PFrameFactoryAPIV2, PFrameFactoryAPIV3 } from './api_factory';
2
2
  import type { PFrameReadAPIV8, PFrameReadAPIV9 } from './api_read';
3
-
4
- export type Logger = (
5
- level: 'info' | 'warn' | 'error',
6
- message: string
7
- ) => void;
3
+ import type { Logger } from './common';
8
4
 
9
5
  export interface PFrameV9 extends PFrameFactoryAPIV2, PFrameReadAPIV8 {}
10
6