styled-map-package 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default as Reader } from "./reader.js";
2
+ export { default as ReaderWatch } from "./reader-watch.js";
2
3
  export { default as Writer } from "./writer.js";
3
4
  export { default as Server } from "./server.js";
4
5
  export { default as StyleDownloader } from "./style-downloader.js";
@@ -0,0 +1,13 @@
1
+ /** @implements {Pick<Reader, keyof Reader>} */
2
+ export default class ReaderWatch implements Pick<Reader, keyof Reader> {
3
+ /**
4
+ * @param {string} filepath
5
+ */
6
+ constructor(filepath: string);
7
+ opened(): Promise<void>;
8
+ getStyle(baseUrl?: string | null | undefined): Promise<import("./types.js").SMPStyle>;
9
+ getResource(path: string): Promise<Resource>;
10
+ close(): Promise<void>;
11
+ #private;
12
+ }
13
+ import Reader from './reader.js';
package/dist/server.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  export default function _default(instance: import("fastify").FastifyInstance<import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>, opts: PluginOptions, done: (err?: Error) => void): void;
2
- export type PluginOptions = {
3
- lazy?: boolean | undefined;
4
- prefix?: string | undefined;
2
+ export type PluginOptionsFilepath = {
5
3
  /**
6
4
  * Path to styled map package (`.smp`) file
7
5
  */
@@ -10,3 +8,14 @@ export type PluginOptions = {
10
8
  */
11
9
  filepath: string;
12
10
  };
11
+ export type PluginOptionsReader = {
12
+ /**
13
+ * SMP Reader interface (also supports ReaderWatch)
14
+ */
15
+ /**
16
+ * SMP Reader interface (also supports ReaderWatch)
17
+ */
18
+ reader: Pick<Reader, keyof Reader>;
19
+ };
20
+ export type PluginOptions = PluginOptionsFilepath | PluginOptionsReader;
21
+ import Reader from './reader.js';
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Returns true if the error if because a file is not found. On Windows, some
3
+ * operations like fs.watch() throw an EPERM error rather than ENOENT.
4
+ *
5
+ * @param {unknown} error
6
+ * @returns {error is Error & { code: 'ENOENT' | 'EPERM' }}
7
+ */
8
+ export function isFileNotThereError(error: unknown): error is Error & {
9
+ code: "ENOENT" | "EPERM";
10
+ };
1
11
  export class ENOENT extends Error {
2
12
  /** @param {string} path */
3
13
  constructor(path: string);
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  /** @typedef {import('./types.js').SMPStyle} SMPStyle */
3
3
 
4
4
  export { default as Reader } from './reader.js'
5
+ export { default as ReaderWatch } from './reader-watch.js'
5
6
  export { default as Writer } from './writer.js'
6
7
  export { default as Server } from './server.js'
7
8
  export { default as StyleDownloader } from './style-downloader.js'
@@ -0,0 +1,133 @@
1
+ import { once } from 'events'
2
+
3
+ import fs from 'node:fs'
4
+ import fsPromises from 'node:fs/promises'
5
+
6
+ import Reader from './reader.js'
7
+ import { ENOENT, isFileNotThereError } from './utils/errors.js'
8
+ import { noop } from './utils/misc.js'
9
+
10
+ /** @implements {Pick<Reader, keyof Reader>} */
11
+ export default class ReaderWatch {
12
+ /** @type {Reader | undefined} */
13
+ #reader
14
+ /** @type {Reader | undefined} */
15
+ #maybeReader
16
+ /** @type {Promise<Reader> | undefined} */
17
+ #readerOpeningPromise
18
+ #filepath
19
+ /** @type {fs.FSWatcher | undefined} */
20
+ #watch
21
+
22
+ /**
23
+ * @param {string} filepath
24
+ */
25
+ constructor(filepath) {
26
+ this.#filepath = filepath
27
+ // Call this now to catch any synchronous errors
28
+ this.#tryToWatchFile()
29
+ // eagerly open Reader
30
+ this.#get().catch(noop)
31
+ }
32
+
33
+ #tryToWatchFile() {
34
+ if (this.#watch) return
35
+ try {
36
+ this.#watch = fs
37
+ .watch(this.#filepath, { persistent: false }, () => {
38
+ this.#reader?.close().catch(noop)
39
+ this.#reader = undefined
40
+ this.#maybeReader = undefined
41
+ this.#readerOpeningPromise = undefined
42
+ // Close the watcher (which on some platforms will continue watching
43
+ // the previous file) so on the next request we will start watching
44
+ // the new file
45
+ this.#watch?.close()
46
+ this.#watch = undefined
47
+ })
48
+ .on('error', noop)
49
+ } catch (error) {
50
+ if (isFileNotThereError(error)) {
51
+ // Ignore: File does not exist yet, but we'll try to open it later
52
+ } else {
53
+ throw error
54
+ }
55
+ }
56
+ }
57
+
58
+ async #get() {
59
+ if (isWin() && (this.#reader || this.#readerOpeningPromise)) {
60
+ // On Windows, the file watcher does not recognize file deletions, so we
61
+ // need to check if the file still exists each time
62
+ try {
63
+ await fsPromises.stat(this.#filepath)
64
+ } catch {
65
+ this.#watch?.close()
66
+ this.#watch = undefined
67
+ this.#reader?.close().catch(noop)
68
+ this.#reader = undefined
69
+ this.#maybeReader = undefined
70
+ this.#readerOpeningPromise = undefined
71
+ }
72
+ }
73
+ // Need to retry this each time in case it failed initially because the file
74
+ // was not present, or if the file was moved or deleted.
75
+ this.#tryToWatchFile()
76
+ // A lovely promise tangle to confuse future readers... sorry.
77
+ //
78
+ // 1. If the reader is already open, return it.
79
+ // 2. If the reader is in the process of opening, return a promise that will
80
+ // return the reader instance if it opened without error, or throw.
81
+ // 3. If the reader threw an error during opening, try to open it again next
82
+ // time this is called.
83
+ if (this.#reader) return this.#reader
84
+ if (this.#readerOpeningPromise) return this.#readerOpeningPromise
85
+ this.#maybeReader = new Reader(this.#filepath)
86
+ this.#readerOpeningPromise = this.#maybeReader
87
+ .opened()
88
+ .then(() => {
89
+ if (!this.#maybeReader) {
90
+ throw new ENOENT(this.#filepath)
91
+ }
92
+ this.#reader = this.#maybeReader
93
+ return this.#reader
94
+ })
95
+ .finally(() => {
96
+ this.#maybeReader = undefined
97
+ this.#readerOpeningPromise = undefined
98
+ })
99
+ return this.#readerOpeningPromise
100
+ }
101
+
102
+ /** @type {Reader['opened']} */
103
+ async opened() {
104
+ const reader = await this.#get()
105
+ return reader.opened()
106
+ }
107
+
108
+ /** @type {Reader['getStyle']} */
109
+ async getStyle(baseUrl = null) {
110
+ const reader = await this.#get()
111
+ return reader.getStyle(baseUrl)
112
+ }
113
+
114
+ /** @type {Reader['getResource']} */
115
+ async getResource(path) {
116
+ const reader = await this.#get()
117
+ return reader.getResource(path)
118
+ }
119
+
120
+ async close() {
121
+ const reader = await this.#get()
122
+ if (this.#watch) {
123
+ this.#watch.close()
124
+ await once(this.#watch, 'close')
125
+ }
126
+ await reader.close()
127
+ }
128
+ }
129
+
130
+ /** @returns {boolean} */
131
+ function isWin() {
132
+ return process.platform === 'win32'
133
+ }
package/lib/server.js CHANGED
@@ -1,17 +1,23 @@
1
1
  import createError from 'http-errors'
2
2
 
3
3
  import Reader from './reader.js'
4
+ import { isFileNotThereError } from './utils/errors.js'
4
5
  import { noop } from './utils/misc.js'
5
6
 
6
7
  /** @import { FastifyPluginCallback, FastifyReply } from 'fastify' */
7
8
  /** @import { Resource } from './reader.js' */
8
9
 
9
10
  /**
10
- * @typedef {object} PluginOptions
11
- * @property {boolean} [lazy=false]
12
- * @property {string} [prefix]
11
+ * @typedef {object} PluginOptionsFilepath
13
12
  * @property {string} filepath Path to styled map package (`.smp`) file
14
13
  */
14
+ /**
15
+ * @typedef {object} PluginOptionsReader
16
+ * @property {Pick<Reader, keyof Reader>} reader SMP Reader interface (also supports ReaderWatch)
17
+ */
18
+ /**
19
+ * @typedef {PluginOptionsFilepath | PluginOptionsReader} PluginOptions
20
+ */
15
21
 
16
22
  /**
17
23
  * @param {FastifyReply} reply
@@ -29,60 +35,28 @@ function sendResource(reply, resource) {
29
35
  }
30
36
 
31
37
  /**
32
- * Fastify plugin for serving a styled map package. User `lazy: true` to defer
33
- * opening the file until the first request.
38
+ * Fastify plugin for serving a styled map package.
39
+ *
40
+ * If you provide a `Reader` (or `ReaderWatch`) instance via the `reader` opt,
41
+ * you must manually close the instance yourself.
34
42
  *
35
43
  * @type {FastifyPluginCallback<PluginOptions>}
36
44
  */
37
- export default function (fastify, { lazy = false, filepath }, done) {
38
- /** @type {Reader | undefined} */
39
- let _reader
40
- /** @type {Promise<Reader> | undefined} */
41
- let _readerOpeningPromise
42
-
43
- async function getReader() {
44
- // A lovely promise tangle to confuse future readers... sorry.
45
- //
46
- // 1. If the reader is already open, return it.
47
- // 2. If the reader is in the process of opening, return a promise that will
48
- // return the reader instance if it opened without error, or throw.
49
- // 3. If the reader threw an error during opening, try to open it again next
50
- // time this is called.
51
- if (_reader) return _reader
52
- if (_readerOpeningPromise) return _readerOpeningPromise
53
- const maybeReader = new Reader(filepath)
54
- _readerOpeningPromise = maybeReader
55
- .opened()
56
- .then(() => {
57
- _reader = maybeReader
58
- return _reader
59
- })
60
- .finally(() => {
61
- _readerOpeningPromise = undefined
62
- })
63
- return _readerOpeningPromise
64
- }
45
+ export default function (fastify, opts, done) {
46
+ const reader = 'reader' in opts ? opts.reader : new Reader(opts.filepath)
65
47
 
66
- if (!lazy) {
67
- getReader().catch(noop)
48
+ // Only close the reader if it was created by this plugin
49
+ if (!('reader' in opts)) {
50
+ fastify.addHook('onClose', () => reader.close().catch(noop))
68
51
  }
69
52
 
70
- fastify.addHook('onClose', async () => {
71
- try {
72
- const reader = await getReader()
73
- await reader.close()
74
- } catch {
75
- // ignore
76
- }
77
- })
78
-
79
53
  fastify.get('/style.json', async () => {
80
54
  try {
81
- const reader = await getReader()
82
55
  const baseUrl = new URL(fastify.prefix, fastify.listeningOrigin)
83
- return reader.getStyle(baseUrl.href)
56
+ const style = await reader.getStyle(baseUrl.href)
57
+ return style
84
58
  } catch (error) {
85
- if (isENOENT(error)) {
59
+ if (isFileNotThereError(error)) {
86
60
  throw createError(404, error.message)
87
61
  }
88
62
  throw error
@@ -94,11 +68,10 @@ export default function (fastify, { lazy = false, filepath }, done) {
94
68
  const path = request.params['*']
95
69
 
96
70
  try {
97
- const reader = await getReader()
98
71
  const resource = await reader.getResource(path)
99
72
  return sendResource(reply, resource)
100
73
  } catch (error) {
101
- if (isENOENT(error)) {
74
+ if (isFileNotThereError(error)) {
102
75
  throw createError(404, error.message)
103
76
  }
104
77
  throw error
@@ -106,11 +79,3 @@ export default function (fastify, { lazy = false, filepath }, done) {
106
79
  })
107
80
  done()
108
81
  }
109
-
110
- /**
111
- * @param {unknown} error
112
- * @returns {error is Error & { code: 'ENOENT' }}
113
- */
114
- function isENOENT(error) {
115
- return error instanceof Error && 'code' in error && error.code === 'ENOENT'
116
- }
@@ -7,3 +7,18 @@ export class ENOENT extends Error {
7
7
  this.path = path
8
8
  }
9
9
  }
10
+
11
+ /**
12
+ * Returns true if the error if because a file is not found. On Windows, some
13
+ * operations like fs.watch() throw an EPERM error rather than ENOENT.
14
+ *
15
+ * @param {unknown} error
16
+ * @returns {error is Error & { code: 'ENOENT' | 'EPERM' }}
17
+ */
18
+ export function isFileNotThereError(error) {
19
+ return (
20
+ error instanceof Error &&
21
+ 'code' in error &&
22
+ (error.code === 'ENOENT' || error.code === 'EPERM')
23
+ )
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "styled-map-package",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -11,6 +11,10 @@
11
11
  "types": "./dist/reader.d.ts",
12
12
  "import": "./lib/reader.js"
13
13
  },
14
+ "./reader-watch": {
15
+ "types": "./dist/reader-watch.d.ts",
16
+ "import": "./lib/reader-watch.js"
17
+ },
14
18
  "./writer": {
15
19
  "types": "./dist/writer.d.ts",
16
20
  "import": "./lib/writer.js"