@milaboratories/pframes-rs-serv 1.0.66 → 1.0.68

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/README.md CHANGED
@@ -4,15 +4,15 @@ A high-performance HTTP server for serving Parquet files with full RFC 9110 comp
4
4
 
5
5
  ## Overview
6
6
 
7
- This package provides HTTP server functionality for the PFrames ecosystem, specifically designed for serving Parquet files with proper HTTP semantics. It implements a complete HTTP/1.1 server that handles range requests, conditional requests, authentication, and caching according to web standards.
7
+ This package provides HTTP(S) server functionality for the PFrames ecosystem, specifically designed for serving Parquet files with proper HTTP semantics. It implements a complete HTTP/1.1 server that handles range requests, conditional requests, Bearer token authentication, and range requests caching.
8
8
 
9
9
  ## Architecture
10
10
 
11
11
  ### Core Components
12
12
 
13
- - **Server** (`src/serve.ts`) - Generic HTTP server with lifecycle management
14
- - **HTTP Handler** (`src/handler.ts`) - RFC 9110 compliant HTTP request handler
15
- - **File System Store** (`src/fs-store.ts`) - Reader for local filesystem
13
+ - **Server** (`src/serve.ts`) - Generic HTTP server with lifecycle management and optional authentication
14
+ - **HTTP Handler** (`src/handler.ts`) - RFC 9110 compliant HTTP request handler with object store integration
15
+ - **File System Store** (`src/fs-store.ts`) - Object store implementation for local filesystem
16
16
 
17
17
  ### Exports
18
18
 
@@ -22,17 +22,18 @@ This package provides HTTP server functionality for the PFrames ecosystem, speci
22
22
  - `createRequestHandler()` - HTTP request handler factory
23
23
  - `FileSystemStore` - Local file system object store implementation
24
24
 
25
- #### HttpHelpers (for re-export)
26
-
27
- The `HttpHelpers` export contains utility functions intended for re-export through the `@milaboratories/pframes-node` package and consumption by PFrameDriver in public monorepo:
25
+ The `HttpHelpers` export contains functions intended for re-export through the `@milaboratories/pframes-rs-node` package.
28
26
 
29
27
  ### Binary Script
30
28
 
31
- The `bin/parquet-server.mjs` script is used by the `core` package tests via the `ParquetServer` helper class for integration testing. It provides a standalone server that can be spawned programmatically for testing HTTP compliance and file serving functionality.
29
+ The `bin/parquet-server.mjs` script can be used for integration testing. It provides a standalone server that can be spawned programmatically and serve Parquet files over HTTP(S) from a local directory.
32
30
 
33
31
  ## HTTP Handler Flow
34
32
 
35
- The request handler implements a complete HTTP/1.1 server following [RFC 9110 (HTTP Semantics)](https://datatracker.ietf.org/doc/html/rfc9110) and [RFC 9111 (HTTP Caching)](https://datatracker.ietf.org/doc/html/rfc9111). Here's the detailed request processing flow:
33
+ The request handler implements a complete HTTP/1.1 server following [RFC 9110 (HTTP Semantics)](https://datatracker.ietf.org/doc/html/rfc9110) and [RFC 9111 (HTTP Caching)](https://datatracker.ietf.org/doc/html/rfc9111). The handler consists of two layers:
34
+
35
+ 1. **Authorization Layer** (`authorizeRequestHandler`) - Optional Bearer token authentication
36
+ 2. **Core Handler** (`handleRequest`) - Main HTTP processing logic
36
37
 
37
38
  ## Request Processing Flow
38
39
 
@@ -48,25 +49,31 @@ The request handler implements a complete HTTP/1.1 server following [RFC 9110 (H
48
49
  └─────────┬───────────┘
49
50
 
50
51
 
51
- ┌─────────────────────┐ [FAIL] No Token
52
- │ Check Authentication│────────────┐
52
+ ┌─────────────────────┐ [FAIL] Invalid/Missing Token
53
+ │ Check Bearer Token │────────────┐
54
+ │ (Timing-Safe) │ │
53
55
  └─────────┬───────────┘ │
54
- │ [PASS] Authorized │
55
- ▼ ▼
56
- ┌─────────────────────┐ ┌──────────────┐
57
- Set Cache Headers Return 401 ← WWW-Authenticate: Bearer
58
- └─────────┬───────────┘ │ Unauthorized │
59
- │ └──────────────┘
56
+ │ [PASS] Valid Token ▼
57
+ │ ┌──────────────┐
58
+ │ │ Return 401 │ ← WWW-Authenticate: Bearer
59
+ Unauthorized
60
+ ▼ └──────────────┘
61
+ ┌─────────────────────┐
62
+ │ Set Cache Control │ ← Cache-Control: public, max-age=31536000
63
+ │ Headers │
64
+ └─────────┬───────────┘
65
+
60
66
 
61
67
  ┌─────────────────────┐ [FAIL] Not GET/HEAD
62
68
  │ Check HTTP Method │────────────┐
63
69
  └─────────┬───────────┘ │
64
- │ [PASS] Valid Method
70
+ │ [PASS] GET or HEAD
65
71
  ▼ ▼
66
72
  ┌─────────────────────┐ ┌──────────────┐
67
- │ Parse URL │ │ Return 405 │ ← Allow: GET, HEAD
68
- └─────────┬───────────┘ │ Not Allowed │
69
- └──────────────┘
73
+ │ Parse URL & │ │ Return 405 │ ← Allow: GET, HEAD
74
+ │ Extract Filename Not Allowed │
75
+ └─────────┬───────────┘ └──────────────┘
76
+
70
77
 
71
78
  ┌─────────────────────┐ [FAIL] Invalid Pattern
72
79
  │ Validate .parquet │────────────┐
@@ -75,68 +82,69 @@ The request handler implements a complete HTTP/1.1 server following [RFC 9110 (H
75
82
  │ [PASS] Valid .parquet │
76
83
  ▼ ▼
77
84
  ┌─────────────────────┐ ┌──────────────┐
78
- │ Set Content Headers │ │ Return 410 │ ← Permanently Gone
85
+ │ Set Content Headers │ │ Return 410 │
79
86
  │ (Accept-Ranges, │ │ Gone │
80
- │ Content-Type) │ └──────────────┘
87
+ │ Content-Type), │ └──────────────┘
88
+ │ Generate ETag & │ ← ETag from filename
89
+ │ Set Cache Headers │ Last-Modified: UTC(0)
81
90
  └─────────┬───────────┘
82
91
 
83
92
 
84
- ┌─────────────────────┐ [FAIL] File Missing
85
- │ Check File Exists │────────────┐
86
- └─────────┬───────────┘ │
87
- │ [PASS] File Found │
88
- ▼ ▼
89
- ┌─────────────────────┐ ┌──────────────┐
90
- │ Generate ETag │ │ Return 404 │ ← File Not Found
91
- │ Set ETag Header │ │ Not Found │
92
- └─────────┬───────────┘ └──────────────┘
93
-
94
-
95
93
  ┌─────────────────────┐ [FAIL] Precondition Failed
96
- │ Check If-Match │────────────┐
97
- Conditional Headers │ │
94
+ │ Check If-Match & │────────────┐
95
+ If-Unmodified-Since │ │
98
96
  └─────────┬───────────┘ │
99
- [PASS] Conditions Met
97
+
100
98
  ▼ ▼
101
- ┌─────────────────────┐ ┌──────────────┐
102
- │ Check If-None-Match │ │ Return 412 ← Precondition Failed
103
- └─────────┬───────────┘ │ Precondition │
104
- │ │ Failed │
105
- ▼ └──────────────┘
99
+ ┌─────────────────────┐ ┌─────────────────────┐
100
+ │ Check If-None-Match │ │ Return 412
101
+ │ & If-Modified-Since │ │ Precondition Failed
102
+ └─────────┬───────────┘ └─────────────────────┘
106
103
  │ [FAIL] Not Modified
107
104
  ├────────────────────────┐
108
105
  │ ▼
109
106
  │ ┌──────────────┐
110
- │ │ Return 304 │ ← ETag, Not Modified
107
+ │ │ Return 304 │
111
108
  │ │ Not Modified │
112
109
  │ └──────────────┘
113
- │ [PASS] Modified
114
-
115
- ┌─────────────────────┐
116
- │ Parse Range Header │
117
- └─────────┬───────────┘
118
-
119
110
 
120
111
  ┌─────────────────────┐ [FAIL] Invalid Range
121
- Validate Range │────────────┐
122
- │ Against File Size │ │
112
+ Parse Range Header │────────────┐
123
113
  └─────────┬───────────┘ │
124
114
  │ [PASS] Valid Range │
125
115
  ▼ ▼
126
116
  ┌─────────────────────┐ ┌──────────────┐
127
- Set Status & Length │ │ Return 416 ← Content-Range: bytes */size
128
- 200 (full) or │ │ Range Not
129
- │ 206 (partial) │ │ Satisfiable │
117
+ Proxy Request to │ │ Return 400
118
+ Object Store │ │ Bad Request
130
119
  └─────────┬───────────┘ └──────────────┘
120
+
121
+ ├─────────┬─────────┬─────────┐
122
+ │ │ │ │
123
+ ▼ ▼ ▼ ▼
124
+ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
125
+ │ Ok │ │ Not │ │Invalid│ │ Server│
126
+ │Success│ │ Found │ │ Range │ │ Error │
127
+ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
128
+ │ │ │ │
129
+ ▼ ▼ ▼ ▼
130
+ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
131
+ │200/206│ │ 404 │ │ 416 │ │ 500 │
132
+ └───┬───┘ └───────┘ └───────┘ └───────┘
133
+
134
+
135
+ ┌─────────────────────┐
136
+ │ Set Content-Length │ ← Full size or range size
137
+ │ & Content-Range │ Content-Range for 206 responses
138
+ └─────────┬───────────┘
131
139
 
132
140
 
133
- ┌─────────────────────┐ [HEAD] Request
134
- │ Check Request Type │────────────┐
141
+ ┌─────────────────────┐ [HEAD] Headers Only
142
+ │ Check Request │────────────┐
143
+ │ Method │ │
135
144
  └─────────┬───────────┘ │
136
- │ [GET] Request
145
+ │ [GET] Send Body
137
146
  ▼ ▼
138
147
  ┌─────────────────────┐ ┌──────────────┐
139
- │ Stream File Content │ │ Send Headers │ ← Headers Only
140
- │ (with range support)│ │ Only (HEAD) │
148
+ │ Stream File Content │ │ Send Headers │
141
149
  └─────────────────────┘ └──────────────┘
142
150
  ```
package/dist/handler.cjs CHANGED
@@ -112,7 +112,7 @@ async function handleRequest(request, response, store) {
112
112
  * - <https://datatracker.ietf.org/doc/html/rfc9110>
113
113
  * - <https://datatracker.ietf.org/doc/html/rfc9111>
114
114
  *
115
- * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise
115
+ * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
116
116
  * Assumes that files are immutable (and sets cache headers accordingly)
117
117
  */
118
118
  function createRequestHandler(options) {
@@ -127,7 +127,7 @@ function authorizeRequest(request, response, handler, authHeader) {
127
127
  response.strictContentLength = true;
128
128
  response.setHeader(headers.HeaderName.ContentLength, 0);
129
129
  // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked
130
- const actualHeader = request.headers['authorization'];
130
+ const actualHeader = request.headers[headers.HeaderName.Authorization];
131
131
  // Early length check to avoid unnecessary processing
132
132
  if (!actualHeader || actualHeader.length !== authHeader.length) {
133
133
  // RFC 9110 section 11.6.1: WWW-Authenticate header field
@@ -1 +1 @@
1
- {"version":3,"file":"handler.cjs","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue,\n isGet\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers['authorization'];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":["HeaderName","HeaderValue","method","isGetOrHead","StatusCode","filename","getFilenameFromUrl","etag","createETag","options","Options","range","parseRange","isGet","isHead","pipeline","timingSafeEqual"],"mappings":"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"}
1
+ {"version":3,"file":"handler.cjs","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":["HeaderName","HeaderValue","method","isGetOrHead","StatusCode","filename","getFilenameFromUrl","etag","createETag","options","Options","range","parseRange","isGet","isHead","pipeline","timingSafeEqual"],"mappings":"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"}
package/dist/handler.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
6
6
  * - <https://datatracker.ietf.org/doc/html/rfc9110>
7
7
  * - <https://datatracker.ietf.org/doc/html/rfc9111>
8
8
  *
9
- * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise
9
+ * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
10
10
  * Assumes that files are immutable (and sets cache headers accordingly)
11
11
  */
12
12
  export declare function createRequestHandler(options: PFrameInternal.RequestHandlerOptions): RequestListener;
package/dist/handler.js CHANGED
@@ -110,7 +110,7 @@ async function handleRequest(request, response, store) {
110
110
  * - <https://datatracker.ietf.org/doc/html/rfc9110>
111
111
  * - <https://datatracker.ietf.org/doc/html/rfc9111>
112
112
  *
113
- * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise
113
+ * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
114
114
  * Assumes that files are immutable (and sets cache headers accordingly)
115
115
  */
116
116
  function createRequestHandler(options) {
@@ -125,7 +125,7 @@ function authorizeRequest(request, response, handler, authHeader) {
125
125
  response.strictContentLength = true;
126
126
  response.setHeader(HeaderName.ContentLength, 0);
127
127
  // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked
128
- const actualHeader = request.headers['authorization'];
128
+ const actualHeader = request.headers[HeaderName.Authorization];
129
129
  // Early length check to avoid unnecessary processing
130
130
  if (!actualHeader || actualHeader.length !== authHeader.length) {
131
131
  // RFC 9110 section 11.6.1: WWW-Authenticate header field
@@ -1 +1 @@
1
- {"version":3,"file":"handler.js","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue,\n isGet\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers['authorization'];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.js","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -13,5 +13,6 @@ exports.createRequestHandler = handler.createRequestHandler;
13
13
  exports.FileSystemStore = fsStore.FileSystemStore;
14
14
  exports.serve = serve.serve;
15
15
  exports.HttpHelpers = _export.HttpHelpers;
16
+ exports.ParquetServer = parquetServer.ParquetServer;
16
17
  exports.runParquetServer = parquetServer.runParquetServer;
17
18
  //# 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":";;;;;;;;;;;;;;;;"}
package/dist/index.js CHANGED
@@ -2,5 +2,5 @@ export { authorizeRequestHandler, createRequestHandler } from './handler.js';
2
2
  export { FileSystemStore } from './fs-store.js';
3
3
  export { serve } from './serve.js';
4
4
  export { HttpHelpers } from './export.js';
5
- export { runParquetServer } from './parquet-server.js';
5
+ export { ParquetServer, runParquetServer } from './parquet-server.js';
6
6
  //# sourceMappingURL=index.js.map
@@ -1,10 +1,20 @@
1
1
  'use strict';
2
2
 
3
+ var node_child_process = require('node:child_process');
4
+ var promises = require('node:readline/promises');
5
+ var node_path = require('node:path');
6
+ var node_url = require('node:url');
3
7
  var commander = require('commander');
4
8
  var fsStore = require('./fs-store.cjs');
5
9
  var handler = require('./handler.cjs');
6
10
  var serve = require('./serve.cjs');
11
+ var plModelCommon = require('@milaboratories/pl-model-common');
7
12
 
13
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
14
+ const Options = {
15
+ Http: '--http',
16
+ NoAuth: '--no-auth'
17
+ };
8
18
  /**
9
19
  * Serves parquet files from the given root directory.
10
20
  * Manages the server lifecycle with graceful shutdown.
@@ -15,8 +25,8 @@ async function runParquetServer() {
15
25
  .name('parquet-server')
16
26
  .description('Serve parquet files from a directory over HTTP(S)')
17
27
  .argument('<root-directory>', 'Root directory containing parquet files')
18
- .option('--http', 'Use HTTP instead of HTTPS', false)
19
- .option('--no-auth', 'Disable authentication')
28
+ .option(Options.Http, 'Use HTTP instead of HTTPS', false)
29
+ .option(Options.NoAuth, 'Disable authentication')
20
30
  .action(async (rootDir, options) => {
21
31
  const abortController = new AbortController();
22
32
  process
@@ -39,15 +49,58 @@ async function runParquetServer() {
39
49
  });
40
50
  abortController.signal.onabort = () => server.stop();
41
51
  abortController.signal.throwIfAborted();
42
- console.log(JSON.stringify({
52
+ const serverConfig = plModelCommon.stringifyJson({
43
53
  url: server.address,
44
54
  ...(server.authToken && { authToken: server.authToken }),
45
55
  ...(server.encodedCaCert && { caCert: server.encodedCaCert })
46
- }));
56
+ });
57
+ console.log(serverConfig);
47
58
  await server.stopped;
48
59
  });
49
60
  await program.parseAsync();
50
61
  }
62
+ /**
63
+ * Reference implementation of a parquet server runner for tests:
64
+ * - Reads the server configuration from the spawned process stdout
65
+ * - Forwards the server logs to the console
66
+ * - Shuts down the server on dispose
67
+ */
68
+ class ParquetServer {
69
+ #process;
70
+ #config;
71
+ #lineReader;
72
+ constructor(process, config, lineReader) {
73
+ this.#process = process;
74
+ this.#config = config;
75
+ this.#lineReader = lineReader;
76
+ }
77
+ get config() {
78
+ return this.#config;
79
+ }
80
+ static async serve(rootDir, options) {
81
+ const nodeFileUrl = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('parquet-server.cjs', document.baseURI).href));
82
+ const nodeDirname = node_path.dirname(node_url.fileURLToPath(nodeFileUrl));
83
+ const binPath = node_path.join(nodeDirname, '..', 'bin', 'parquet-server.mjs');
84
+ const serverProcess = node_child_process.spawn('node', [
85
+ binPath,
86
+ rootDir,
87
+ ...(options?.http ? [Options.Http] : []),
88
+ ...(options?.noAuth ? [Options.NoAuth] : [])
89
+ ], {
90
+ stdio: ['ignore', 'pipe', 'ignore']
91
+ });
92
+ const lineReader = promises.createInterface({ input: serverProcess.stdout });
93
+ const firstLine = await lineReader[Symbol.asyncIterator]().next();
94
+ const serverConfig = plModelCommon.parseJson(firstLine.value);
95
+ lineReader.on('line', console.log);
96
+ return new ParquetServer(serverProcess, serverConfig, lineReader);
97
+ }
98
+ [Symbol.dispose]() {
99
+ this.#lineReader.close();
100
+ this.#process.kill();
101
+ }
102
+ }
51
103
 
104
+ exports.ParquetServer = ParquetServer;
52
105
  exports.runParquetServer = runParquetServer;
53
106
  //# sourceMappingURL=parquet-server.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"parquet-server.cjs","sources":["../src/parquet-server.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option('--http', 'Use HTTP instead of HTTPS', false)\n .option('--no-auth', 'Disable authentication')\n .action(\n async (rootDir: string, options: { http: boolean; auth: boolean }) => {\n const abortController = new AbortController();\n process\n .on('SIGINT', () => abortController.abort())\n .on('SIGTERM', () => abortController.abort());\n abortController.signal.throwIfAborted();\n\n const store = await FileSystemStore.init({\n rootDir,\n logger: (level, message) => {\n const timestamp = new Date(Date.now()).toISOString();\n console.log(`[${timestamp}] [${level}] ${message}`);\n }\n });\n abortController.signal.throwIfAborted();\n const handler = createRequestHandler({ store });\n\n const server = await serve({\n handler,\n ...(options.http && { http: true }),\n ...(!options.auth && { noAuth: true })\n });\n abortController.signal.onabort = () => server.stop();\n abortController.signal.throwIfAborted();\n\n console.log(\n JSON.stringify({\n url: server.address,\n ...(server.authToken && { authToken: server.authToken }),\n ...(server.encodedCaCert && { caCert: server.encodedCaCert })\n })\n );\n\n await server.stopped;\n }\n );\n\n await program.parseAsync();\n}\n"],"names":["Command","FileSystemStore","handler","createRequestHandler","serve"],"mappings":";;;;;;;AAKA;;;AAGG;AACI,eAAe,gBAAgB,GAAA;AACpC,IAAA,MAAM,OAAO,GAAG,IAAIA,iBAAO,EAAE;IAE7B;SACG,IAAI,CAAC,gBAAgB;SACrB,WAAW,CAAC,mDAAmD;AAC/D,SAAA,QAAQ,CAAC,kBAAkB,EAAE,yCAAyC;AACtE,SAAA,MAAM,CAAC,QAAQ,EAAE,2BAA2B,EAAE,KAAK;AACnD,SAAA,MAAM,CAAC,WAAW,EAAE,wBAAwB;AAC5C,SAAA,MAAM,CACL,OAAO,OAAe,EAAE,OAAyC,KAAI;AACnE,QAAA,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE;QAC7C;aACG,EAAE,CAAC,QAAQ,EAAE,MAAM,eAAe,CAAC,KAAK,EAAE;aAC1C,EAAE,CAAC,SAAS,EAAE,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC;AAC/C,QAAA,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE;AAEvC,QAAA,MAAM,KAAK,GAAG,MAAMC,uBAAe,CAAC,IAAI,CAAC;YACvC,OAAO;AACP,YAAA,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAI;AACzB,gBAAA,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE;gBACpD,OAAO,CAAC,GAAG,CAAC,CAAA,CAAA,EAAI,SAAS,CAAA,GAAA,EAAM,KAAK,CAAA,EAAA,EAAK,OAAO,CAAA,CAAE,CAAC;YACrD;AACD,SAAA,CAAC;AACF,QAAA,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE;QACvC,MAAMC,SAAO,GAAGC,4BAAoB,CAAC,EAAE,KAAK,EAAE,CAAC;AAE/C,QAAA,MAAM,MAAM,GAAG,MAAMC,WAAK,CAAC;qBACzBF,SAAO;YACP,IAAI,OAAO,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;AACtC,SAAA,CAAC;AACF,QAAA,eAAe,CAAC,MAAM,CAAC,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE;AACpD,QAAA,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE;AAEvC,QAAA,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CAAC;YACb,GAAG,EAAE,MAAM,CAAC,OAAO;AACnB,YAAA,IAAI,MAAM,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;AACxD,YAAA,IAAI,MAAM,CAAC,aAAa,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,EAAE;AAC7D,SAAA,CAAC,CACH;QAED,MAAM,MAAM,CAAC,OAAO;AACtB,IAAA,CAAC,CACF;AAEH,IAAA,MAAM,OAAO,CAAC,UAAU,EAAE;AAC5B;;;;"}
1
+ {"version":3,"file":"parquet-server.cjs","sources":["../src/parquet-server.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport { createInterface, type Interface } from 'node:readline/promises';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\nimport {\n parseJson,\n type StringifiedJson,\n stringifyJson\n} from '@milaboratories/pl-model-common';\nimport { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';\n\nconst Options = {\n Http: '--http',\n NoAuth: '--no-auth'\n} as const;\n\ntype Config = StringifiedJson<PFrameInternal.ParquetServerConfig>;\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option(Options.Http, 'Use HTTP instead of HTTPS', false)\n .option(Options.NoAuth, 'Disable authentication')\n .action(\n async (rootDir: string, options: { http: boolean; auth: boolean }) => {\n const abortController = new AbortController();\n process\n .on('SIGINT', () => abortController.abort())\n .on('SIGTERM', () => abortController.abort());\n abortController.signal.throwIfAborted();\n\n const store = await FileSystemStore.init({\n rootDir,\n logger: (level, message) => {\n const timestamp = new Date(Date.now()).toISOString();\n console.log(`[${timestamp}] [${level}] ${message}`);\n }\n });\n abortController.signal.throwIfAborted();\n const handler = createRequestHandler({ store });\n\n const server = await serve({\n handler,\n ...(options.http && { http: true }),\n ...(!options.auth && { noAuth: true })\n });\n abortController.signal.onabort = () => server.stop();\n abortController.signal.throwIfAborted();\n\n const serverConfig: Config = stringifyJson({\n url: server.address,\n ...(server.authToken && { authToken: server.authToken }),\n ...(server.encodedCaCert && { caCert: server.encodedCaCert })\n });\n console.log(serverConfig);\n\n await server.stopped;\n }\n );\n\n await program.parseAsync();\n}\n\n/**\n * Reference implementation of a parquet server runner for tests:\n * - Reads the server configuration from the spawned process stdout\n * - Forwards the server logs to the console\n * - Shuts down the server on dispose\n */\nexport class ParquetServer implements Disposable {\n readonly #process: ChildProcess;\n readonly #config: PFrameInternal.ParquetServerConfig;\n readonly #lineReader: Interface;\n\n private constructor(\n process: ChildProcess,\n config: PFrameInternal.ParquetServerConfig,\n lineReader: Interface\n ) {\n this.#process = process;\n this.#config = config;\n this.#lineReader = lineReader;\n }\n\n get config(): PFrameInternal.ParquetServerConfig {\n return this.#config;\n }\n\n static async serve(\n rootDir: string,\n options?: {\n http?: boolean;\n noAuth?: boolean;\n }\n ): Promise<ParquetServer> {\n const nodeFileUrl = import.meta.url;\n const nodeDirname = dirname(fileURLToPath(nodeFileUrl));\n const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');\n\n const serverProcess = spawn(\n 'node',\n [\n binPath,\n rootDir,\n ...(options?.http ? [Options.Http] : []),\n ...(options?.noAuth ? [Options.NoAuth] : [])\n ],\n {\n stdio: ['ignore', 'pipe', 'ignore']\n }\n );\n\n const lineReader = createInterface({ input: serverProcess.stdout! });\n\n const firstLine = await lineReader[Symbol.asyncIterator]().next();\n const serverConfig = parseJson(firstLine.value as Config);\n\n lineReader.on('line', console.log);\n\n return new ParquetServer(serverProcess, serverConfig, lineReader);\n }\n\n [Symbol.dispose](): void {\n this.#lineReader.close();\n this.#process.kill();\n }\n}\n"],"names":["Command","FileSystemStore","handler","createRequestHandler","serve","stringifyJson","dirname","fileURLToPath","join","spawn","createInterface","parseJson"],"mappings":"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"}
@@ -1,6 +1,23 @@
1
+ import { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';
1
2
  /**
2
3
  * Serves parquet files from the given root directory.
3
4
  * Manages the server lifecycle with graceful shutdown.
4
5
  */
5
6
  export declare function runParquetServer(): Promise<void>;
7
+ /**
8
+ * Reference implementation of a parquet server runner for tests:
9
+ * - Reads the server configuration from the spawned process stdout
10
+ * - Forwards the server logs to the console
11
+ * - Shuts down the server on dispose
12
+ */
13
+ export declare class ParquetServer implements Disposable {
14
+ #private;
15
+ private constructor();
16
+ get config(): PFrameInternal.ParquetServerConfig;
17
+ static serve(rootDir: string, options?: {
18
+ http?: boolean;
19
+ noAuth?: boolean;
20
+ }): Promise<ParquetServer>;
21
+ [Symbol.dispose](): void;
22
+ }
6
23
  //# sourceMappingURL=parquet-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parquet-server.d.ts","sourceRoot":"","sources":["../src/parquet-server.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAgDtD"}
1
+ {"version":3,"file":"parquet-server.d.ts","sourceRoot":"","sources":["../src/parquet-server.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAS5E;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+CtD;AAED;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,UAAU;;IAK9C,OAAO;IAUP,IAAI,MAAM,IAAI,cAAc,CAAC,mBAAmB,CAE/C;WAEY,KAAK,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,GACA,OAAO,CAAC,aAAa,CAAC;IA4BzB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAIzB"}
@@ -1,8 +1,17 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
1
5
  import { Command } from 'commander';
2
6
  import { FileSystemStore } from './fs-store.js';
3
7
  import { createRequestHandler } from './handler.js';
4
8
  import { serve } from './serve.js';
9
+ import { stringifyJson, parseJson } from '@milaboratories/pl-model-common';
5
10
 
11
+ const Options = {
12
+ Http: '--http',
13
+ NoAuth: '--no-auth'
14
+ };
6
15
  /**
7
16
  * Serves parquet files from the given root directory.
8
17
  * Manages the server lifecycle with graceful shutdown.
@@ -13,8 +22,8 @@ async function runParquetServer() {
13
22
  .name('parquet-server')
14
23
  .description('Serve parquet files from a directory over HTTP(S)')
15
24
  .argument('<root-directory>', 'Root directory containing parquet files')
16
- .option('--http', 'Use HTTP instead of HTTPS', false)
17
- .option('--no-auth', 'Disable authentication')
25
+ .option(Options.Http, 'Use HTTP instead of HTTPS', false)
26
+ .option(Options.NoAuth, 'Disable authentication')
18
27
  .action(async (rootDir, options) => {
19
28
  const abortController = new AbortController();
20
29
  process
@@ -37,15 +46,57 @@ async function runParquetServer() {
37
46
  });
38
47
  abortController.signal.onabort = () => server.stop();
39
48
  abortController.signal.throwIfAborted();
40
- console.log(JSON.stringify({
49
+ const serverConfig = stringifyJson({
41
50
  url: server.address,
42
51
  ...(server.authToken && { authToken: server.authToken }),
43
52
  ...(server.encodedCaCert && { caCert: server.encodedCaCert })
44
- }));
53
+ });
54
+ console.log(serverConfig);
45
55
  await server.stopped;
46
56
  });
47
57
  await program.parseAsync();
48
58
  }
59
+ /**
60
+ * Reference implementation of a parquet server runner for tests:
61
+ * - Reads the server configuration from the spawned process stdout
62
+ * - Forwards the server logs to the console
63
+ * - Shuts down the server on dispose
64
+ */
65
+ class ParquetServer {
66
+ #process;
67
+ #config;
68
+ #lineReader;
69
+ constructor(process, config, lineReader) {
70
+ this.#process = process;
71
+ this.#config = config;
72
+ this.#lineReader = lineReader;
73
+ }
74
+ get config() {
75
+ return this.#config;
76
+ }
77
+ static async serve(rootDir, options) {
78
+ const nodeFileUrl = import.meta.url;
79
+ const nodeDirname = dirname(fileURLToPath(nodeFileUrl));
80
+ const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');
81
+ const serverProcess = spawn('node', [
82
+ binPath,
83
+ rootDir,
84
+ ...(options?.http ? [Options.Http] : []),
85
+ ...(options?.noAuth ? [Options.NoAuth] : [])
86
+ ], {
87
+ stdio: ['ignore', 'pipe', 'ignore']
88
+ });
89
+ const lineReader = createInterface({ input: serverProcess.stdout });
90
+ const firstLine = await lineReader[Symbol.asyncIterator]().next();
91
+ const serverConfig = parseJson(firstLine.value);
92
+ lineReader.on('line', console.log);
93
+ return new ParquetServer(serverProcess, serverConfig, lineReader);
94
+ }
95
+ [Symbol.dispose]() {
96
+ this.#lineReader.close();
97
+ this.#process.kill();
98
+ }
99
+ }
49
100
 
50
- export { runParquetServer };
101
+ export { ParquetServer, runParquetServer };
51
102
  //# sourceMappingURL=parquet-server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"parquet-server.js","sources":["../src/parquet-server.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option('--http', 'Use HTTP instead of HTTPS', false)\n .option('--no-auth', 'Disable authentication')\n .action(\n async (rootDir: string, options: { http: boolean; auth: boolean }) => {\n const abortController = new AbortController();\n process\n .on('SIGINT', () => abortController.abort())\n .on('SIGTERM', () => abortController.abort());\n abortController.signal.throwIfAborted();\n\n const store = await FileSystemStore.init({\n rootDir,\n logger: (level, message) => {\n const timestamp = new Date(Date.now()).toISOString();\n console.log(`[${timestamp}] [${level}] ${message}`);\n }\n });\n abortController.signal.throwIfAborted();\n const handler = createRequestHandler({ store });\n\n const server = await serve({\n handler,\n ...(options.http && { http: true }),\n ...(!options.auth && { noAuth: true })\n });\n abortController.signal.onabort = () => server.stop();\n abortController.signal.throwIfAborted();\n\n console.log(\n JSON.stringify({\n url: server.address,\n ...(server.authToken && { authToken: server.authToken }),\n ...(server.encodedCaCert && { caCert: server.encodedCaCert })\n })\n );\n\n await server.stopped;\n }\n );\n\n await program.parseAsync();\n}\n"],"names":[],"mappings":";;;;;AAKA;;;AAGG;AACI,eAAe,gBAAgB,GAAA;AACpC,IAAA,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE;IAE7B;SACG,IAAI,CAAC,gBAAgB;SACrB,WAAW,CAAC,mDAAmD;AAC/D,SAAA,QAAQ,CAAC,kBAAkB,EAAE,yCAAyC;AACtE,SAAA,MAAM,CAAC,QAAQ,EAAE,2BAA2B,EAAE,KAAK;AACnD,SAAA,MAAM,CAAC,WAAW,EAAE,wBAAwB;AAC5C,SAAA,MAAM,CACL,OAAO,OAAe,EAAE,OAAyC,KAAI;AACnE,QAAA,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE;QAC7C;aACG,EAAE,CAAC,QAAQ,EAAE,MAAM,eAAe,CAAC,KAAK,EAAE;aAC1C,EAAE,CAAC,SAAS,EAAE,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC;AAC/C,QAAA,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE;AAEvC,QAAA,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC;YACvC,OAAO;AACP,YAAA,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAI;AACzB,gBAAA,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE;gBACpD,OAAO,CAAC,GAAG,CAAC,CAAA,CAAA,EAAI,SAAS,CAAA,GAAA,EAAM,KAAK,CAAA,EAAA,EAAK,OAAO,CAAA,CAAE,CAAC;YACrD;AACD,SAAA,CAAC;AACF,QAAA,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE;QACvC,MAAM,OAAO,GAAG,oBAAoB,CAAC,EAAE,KAAK,EAAE,CAAC;AAE/C,QAAA,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC;YACzB,OAAO;YACP,IAAI,OAAO,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;AACtC,SAAA,CAAC;AACF,QAAA,eAAe,CAAC,MAAM,CAAC,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE;AACpD,QAAA,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE;AAEvC,QAAA,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CAAC;YACb,GAAG,EAAE,MAAM,CAAC,OAAO;AACnB,YAAA,IAAI,MAAM,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;AACxD,YAAA,IAAI,MAAM,CAAC,aAAa,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,EAAE;AAC7D,SAAA,CAAC,CACH;QAED,MAAM,MAAM,CAAC,OAAO;AACtB,IAAA,CAAC,CACF;AAEH,IAAA,MAAM,OAAO,CAAC,UAAU,EAAE;AAC5B;;;;"}
1
+ {"version":3,"file":"parquet-server.js","sources":["../src/parquet-server.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport { createInterface, type Interface } from 'node:readline/promises';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Command } from 'commander';\nimport { FileSystemStore } from './fs-store';\nimport { createRequestHandler } from './handler';\nimport { serve } from './serve';\nimport {\n parseJson,\n type StringifiedJson,\n stringifyJson\n} from '@milaboratories/pl-model-common';\nimport { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';\n\nconst Options = {\n Http: '--http',\n NoAuth: '--no-auth'\n} as const;\n\ntype Config = StringifiedJson<PFrameInternal.ParquetServerConfig>;\n\n/**\n * Serves parquet files from the given root directory.\n * Manages the server lifecycle with graceful shutdown.\n */\nexport async function runParquetServer(): Promise<void> {\n const program = new Command();\n\n program\n .name('parquet-server')\n .description('Serve parquet files from a directory over HTTP(S)')\n .argument('<root-directory>', 'Root directory containing parquet files')\n .option(Options.Http, 'Use HTTP instead of HTTPS', false)\n .option(Options.NoAuth, 'Disable authentication')\n .action(\n async (rootDir: string, options: { http: boolean; auth: boolean }) => {\n const abortController = new AbortController();\n process\n .on('SIGINT', () => abortController.abort())\n .on('SIGTERM', () => abortController.abort());\n abortController.signal.throwIfAborted();\n\n const store = await FileSystemStore.init({\n rootDir,\n logger: (level, message) => {\n const timestamp = new Date(Date.now()).toISOString();\n console.log(`[${timestamp}] [${level}] ${message}`);\n }\n });\n abortController.signal.throwIfAborted();\n const handler = createRequestHandler({ store });\n\n const server = await serve({\n handler,\n ...(options.http && { http: true }),\n ...(!options.auth && { noAuth: true })\n });\n abortController.signal.onabort = () => server.stop();\n abortController.signal.throwIfAborted();\n\n const serverConfig: Config = stringifyJson({\n url: server.address,\n ...(server.authToken && { authToken: server.authToken }),\n ...(server.encodedCaCert && { caCert: server.encodedCaCert })\n });\n console.log(serverConfig);\n\n await server.stopped;\n }\n );\n\n await program.parseAsync();\n}\n\n/**\n * Reference implementation of a parquet server runner for tests:\n * - Reads the server configuration from the spawned process stdout\n * - Forwards the server logs to the console\n * - Shuts down the server on dispose\n */\nexport class ParquetServer implements Disposable {\n readonly #process: ChildProcess;\n readonly #config: PFrameInternal.ParquetServerConfig;\n readonly #lineReader: Interface;\n\n private constructor(\n process: ChildProcess,\n config: PFrameInternal.ParquetServerConfig,\n lineReader: Interface\n ) {\n this.#process = process;\n this.#config = config;\n this.#lineReader = lineReader;\n }\n\n get config(): PFrameInternal.ParquetServerConfig {\n return this.#config;\n }\n\n static async serve(\n rootDir: string,\n options?: {\n http?: boolean;\n noAuth?: boolean;\n }\n ): Promise<ParquetServer> {\n const nodeFileUrl = import.meta.url;\n const nodeDirname = dirname(fileURLToPath(nodeFileUrl));\n const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');\n\n const serverProcess = spawn(\n 'node',\n [\n binPath,\n rootDir,\n ...(options?.http ? [Options.Http] : []),\n ...(options?.noAuth ? [Options.NoAuth] : [])\n ],\n {\n stdio: ['ignore', 'pipe', 'ignore']\n }\n );\n\n const lineReader = createInterface({ input: serverProcess.stdout! });\n\n const firstLine = await lineReader[Symbol.asyncIterator]().next();\n const serverConfig = parseJson(firstLine.value as Config);\n\n lineReader.on('line', console.log);\n\n return new ParquetServer(serverProcess, serverConfig, lineReader);\n }\n\n [Symbol.dispose](): void {\n this.#lineReader.close();\n this.#process.kill();\n }\n}\n"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@
4
4
  const HeaderName = {
5
5
  AcceptRanges: 'accept-ranges',
6
6
  Allow: 'allow',
7
+ Authorization: 'authorization',
7
8
  CacheControl: 'cache-control',
8
9
  ContentLength: 'content-length',
9
10
  ContentRange: 'content-range',
@@ -1 +1 @@
1
- {"version":3,"file":"headers.cjs","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":";;AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,aAAa,EAAE,gBAAgB;AAC/B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,WAAW,EAAE,cAAc;AAC3B,IACA,IAAI,EAAE,MAAM;AACZ,IAIA,YAAY,EAAE,eAAe;AAC7B,IACA,eAAe,EAAE;;AAGnB;AACO,MAAM,WAAW,GAAG;AACzB,IAAA,YAAY,EAAE,OAAO;AACrB,IAAA,KAAK,EAAE,WAAW;AAClB,IAAA,YAAY,EAAE,qCAAqC;AACnD,IAAA,WAAW,EAAE,0BAA0B;AACvC,IAAA,eAAe,EAAE;;;;;;"}
1
+ {"version":3,"file":"headers.cjs","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":";;AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,aAAa,EAAE,gBAAgB;AAC/B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,WAAW,EAAE,cAAc;AAC3B,IACA,IAAI,EAAE,MAAM;AACZ,IAIA,YAAY,EAAE,eAAe;AAC7B,IACA,eAAe,EAAE;;AAGnB;AACO,MAAM,WAAW,GAAG;AACzB,IAAA,YAAY,EAAE,OAAO;AACrB,IAAA,KAAK,EAAE,WAAW;AAClB,IAAA,YAAY,EAAE,qCAAqC;AACnD,IAAA,WAAW,EAAE,0BAA0B;AACvC,IAAA,eAAe,EAAE;;;;;;"}
@@ -2,6 +2,7 @@
2
2
  const HeaderName = {
3
3
  AcceptRanges: 'accept-ranges',
4
4
  Allow: 'allow',
5
+ Authorization: 'authorization',
5
6
  CacheControl: 'cache-control',
6
7
  ContentLength: 'content-length',
7
8
  ContentRange: 'content-range',
@@ -1 +1 @@
1
- {"version":3,"file":"headers.js","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":"AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,aAAa,EAAE,gBAAgB;AAC/B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,WAAW,EAAE,cAAc;AAC3B,IACA,IAAI,EAAE,MAAM;AACZ,IAIA,YAAY,EAAE,eAAe;AAC7B,IACA,eAAe,EAAE;;AAGnB;AACO,MAAM,WAAW,GAAG;AACzB,IAAA,YAAY,EAAE,OAAO;AACrB,IAAA,KAAK,EAAE,WAAW;AAClB,IAAA,YAAY,EAAE,qCAAqC;AACnD,IAAA,WAAW,EAAE,0BAA0B;AACvC,IAAA,eAAe,EAAE;;;;;"}
1
+ {"version":3,"file":"headers.js","sources":["../../src/utils/headers.ts"],"sourcesContent":["/** HTTP header names used in the parquet server handler */\nexport const HeaderName = {\n Accept: 'accept',\n AcceptRanges: 'accept-ranges',\n Allow: 'allow',\n Authorization: 'authorization',\n CacheControl: 'cache-control',\n ContentLength: 'content-length',\n ContentRange: 'content-range',\n ContentType: 'content-type',\n Date: 'date',\n ETag: 'etag',\n IfMatch: 'if-match',\n IfModifiedSince: 'if-modified-since',\n IfNoneMatch: 'if-none-match',\n IfUnmodifiedSince: 'if-unmodified-since',\n LastModified: 'last-modified',\n Range: 'range',\n WWWAuthenticate: 'www-authenticate'\n} as const;\n\n/** HTTP header values used in the parquet server handler */\nexport const HeaderValue = {\n AcceptRanges: 'bytes',\n Allow: 'GET, HEAD',\n CacheControl: 'public, immutable, max-age=31536000',\n ContentType: 'application/octet-stream',\n WWWAuthenticate: 'Bearer realm=\"parquet-server\"'\n} as const;\n"],"names":[],"mappings":"AAAA;AACO,MAAM,UAAU,GAAG;AACxB,IACA,YAAY,EAAE,eAAe;AAC7B,IAAA,KAAK,EAAE,OAAO;AACd,IAAA,aAAa,EAAE,eAAe;AAC9B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,aAAa,EAAE,gBAAgB;AAC/B,IAAA,YAAY,EAAE,eAAe;AAC7B,IAAA,WAAW,EAAE,cAAc;AAC3B,IACA,IAAI,EAAE,MAAM;AACZ,IAIA,YAAY,EAAE,eAAe;AAC7B,IACA,eAAe,EAAE;;AAGnB;AACO,MAAM,WAAW,GAAG;AACzB,IAAA,YAAY,EAAE,OAAO;AACrB,IAAA,KAAK,EAAE,WAAW;AAClB,IAAA,YAAY,EAAE,qCAAqC;AACnD,IAAA,WAAW,EAAE,0BAA0B;AACvC,IAAA,eAAe,EAAE;;;;;"}
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@milaboratories/pframes-rs-serv",
3
3
  "description": "PFrames - Node.js HTTP(S) Parquet Server",
4
- "version": "1.0.66",
4
+ "keywords": [
5
+ "http",
6
+ "https",
7
+ "parquet",
8
+ "proxy",
9
+ "server"
10
+ ],
11
+ "version": "1.0.68",
5
12
  "type": "module",
6
13
  "types": "./dist/index.d.ts",
7
14
  "main": "./dist/index.js",
@@ -23,7 +30,7 @@
23
30
  "dependencies": {
24
31
  "@milaboratories/helpers": "1.6.22",
25
32
  "@milaboratories/pl-model-common": "1.19.14",
26
- "@milaboratories/pl-model-middle-layer": "1.8.16",
33
+ "@milaboratories/pl-model-middle-layer": "1.8.17",
27
34
  "commander": "^14.0.0",
28
35
  "selfsigned": "^3.0.1"
29
36
  },
@@ -34,6 +41,7 @@
34
41
  "@types/node": "^20.19.11",
35
42
  "@vitest/coverage-istanbul": "^3.2.4",
36
43
  "autocannon": "^8.0.0",
44
+ "tslib": "^2.8.1",
37
45
  "typescript": "^5.9.2",
38
46
  "undici": "^7.14.0",
39
47
  "vite": "^7.1.3",
package/src/handler.ts CHANGED
@@ -11,12 +11,12 @@ import {
11
11
  getFilenameFromUrl,
12
12
  parseRange,
13
13
  isGetOrHead,
14
+ isGet,
14
15
  isHead,
15
16
  Options,
16
17
  StatusCode,
17
18
  HeaderName,
18
- HeaderValue,
19
- isGet
19
+ HeaderValue
20
20
  } from './utils';
21
21
 
22
22
  /** Main request handler for parquet files */
@@ -146,7 +146,7 @@ async function handleRequest(
146
146
  * - <https://datatracker.ietf.org/doc/html/rfc9110>
147
147
  * - <https://datatracker.ietf.org/doc/html/rfc9111>
148
148
  *
149
- * Accepts only paths of the form `/<filename>.parquet`, returns 404 otherwise
149
+ * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise
150
150
  * Assumes that files are immutable (and sets cache headers accordingly)
151
151
  */
152
152
  export function createRequestHandler(
@@ -170,7 +170,7 @@ function authorizeRequest(
170
170
  response.setHeader(HeaderName.ContentLength, 0);
171
171
  // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked
172
172
 
173
- const actualHeader = request.headers['authorization'];
173
+ const actualHeader = request.headers[HeaderName.Authorization];
174
174
 
175
175
  // Early length check to avoid unnecessary processing
176
176
  if (!actualHeader || actualHeader.length !== authHeader.length) {
@@ -1,7 +1,24 @@
1
+ import { type ChildProcess, spawn } from 'node:child_process';
2
+ import { createInterface, type Interface } from 'node:readline/promises';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
1
5
  import { Command } from 'commander';
2
6
  import { FileSystemStore } from './fs-store';
3
7
  import { createRequestHandler } from './handler';
4
8
  import { serve } from './serve';
9
+ import {
10
+ parseJson,
11
+ type StringifiedJson,
12
+ stringifyJson
13
+ } from '@milaboratories/pl-model-common';
14
+ import { type PFrameInternal } from '@milaboratories/pl-model-middle-layer';
15
+
16
+ const Options = {
17
+ Http: '--http',
18
+ NoAuth: '--no-auth'
19
+ } as const;
20
+
21
+ type Config = StringifiedJson<PFrameInternal.ParquetServerConfig>;
5
22
 
6
23
  /**
7
24
  * Serves parquet files from the given root directory.
@@ -14,8 +31,8 @@ export async function runParquetServer(): Promise<void> {
14
31
  .name('parquet-server')
15
32
  .description('Serve parquet files from a directory over HTTP(S)')
16
33
  .argument('<root-directory>', 'Root directory containing parquet files')
17
- .option('--http', 'Use HTTP instead of HTTPS', false)
18
- .option('--no-auth', 'Disable authentication')
34
+ .option(Options.Http, 'Use HTTP instead of HTTPS', false)
35
+ .option(Options.NoAuth, 'Disable authentication')
19
36
  .action(
20
37
  async (rootDir: string, options: { http: boolean; auth: boolean }) => {
21
38
  const abortController = new AbortController();
@@ -42,13 +59,12 @@ export async function runParquetServer(): Promise<void> {
42
59
  abortController.signal.onabort = () => server.stop();
43
60
  abortController.signal.throwIfAborted();
44
61
 
45
- console.log(
46
- JSON.stringify({
47
- url: server.address,
48
- ...(server.authToken && { authToken: server.authToken }),
49
- ...(server.encodedCaCert && { caCert: server.encodedCaCert })
50
- })
51
- );
62
+ const serverConfig: Config = stringifyJson({
63
+ url: server.address,
64
+ ...(server.authToken && { authToken: server.authToken }),
65
+ ...(server.encodedCaCert && { caCert: server.encodedCaCert })
66
+ });
67
+ console.log(serverConfig);
52
68
 
53
69
  await server.stopped;
54
70
  }
@@ -56,3 +72,68 @@ export async function runParquetServer(): Promise<void> {
56
72
 
57
73
  await program.parseAsync();
58
74
  }
75
+
76
+ /**
77
+ * Reference implementation of a parquet server runner for tests:
78
+ * - Reads the server configuration from the spawned process stdout
79
+ * - Forwards the server logs to the console
80
+ * - Shuts down the server on dispose
81
+ */
82
+ export class ParquetServer implements Disposable {
83
+ readonly #process: ChildProcess;
84
+ readonly #config: PFrameInternal.ParquetServerConfig;
85
+ readonly #lineReader: Interface;
86
+
87
+ private constructor(
88
+ process: ChildProcess,
89
+ config: PFrameInternal.ParquetServerConfig,
90
+ lineReader: Interface
91
+ ) {
92
+ this.#process = process;
93
+ this.#config = config;
94
+ this.#lineReader = lineReader;
95
+ }
96
+
97
+ get config(): PFrameInternal.ParquetServerConfig {
98
+ return this.#config;
99
+ }
100
+
101
+ static async serve(
102
+ rootDir: string,
103
+ options?: {
104
+ http?: boolean;
105
+ noAuth?: boolean;
106
+ }
107
+ ): Promise<ParquetServer> {
108
+ const nodeFileUrl = import.meta.url;
109
+ const nodeDirname = dirname(fileURLToPath(nodeFileUrl));
110
+ const binPath = join(nodeDirname, '..', 'bin', 'parquet-server.mjs');
111
+
112
+ const serverProcess = spawn(
113
+ 'node',
114
+ [
115
+ binPath,
116
+ rootDir,
117
+ ...(options?.http ? [Options.Http] : []),
118
+ ...(options?.noAuth ? [Options.NoAuth] : [])
119
+ ],
120
+ {
121
+ stdio: ['ignore', 'pipe', 'ignore']
122
+ }
123
+ );
124
+
125
+ const lineReader = createInterface({ input: serverProcess.stdout! });
126
+
127
+ const firstLine = await lineReader[Symbol.asyncIterator]().next();
128
+ const serverConfig = parseJson(firstLine.value as Config);
129
+
130
+ lineReader.on('line', console.log);
131
+
132
+ return new ParquetServer(serverProcess, serverConfig, lineReader);
133
+ }
134
+
135
+ [Symbol.dispose](): void {
136
+ this.#lineReader.close();
137
+ this.#process.kill();
138
+ }
139
+ }