Skip to content

Commit 94c825d

Browse files
committed
initial implementation
1 parent 391fb96 commit 94c825d

File tree

11 files changed

+1838
-76
lines changed

11 files changed

+1838
-76
lines changed

package-lock.json

Lines changed: 1448 additions & 47 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
"sodium-universal": "^4.0.0",
192192
"start-stop-state-machine": "^1.2.0",
193193
"streamx": "^2.19.0",
194+
"styled-map-package": "^1.0.1",
194195
"sub-encoder": "^2.1.1",
195196
"throttle-debounce": "^5.0.0",
196197
"tiny-typed-emitter": "^2.1.0",

src/fastify-plugins/maps/index.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '../utils.js'
1010
import { PLUGIN_NAME as MAPEO_STATIC_MAPS } from './static-maps.js'
1111
import { PLUGIN_NAME as MAPEO_OFFLINE_FALLBACK } from './offline-fallback-map.js'
12+
import { PLUGIN_NAME as COMAPEO_STYLED_MAP_PACKAGE } from './styled-map-package.js'
1213

1314
export const PLUGIN_NAME = 'mapeo-maps'
1415
export const DEFAULT_MAPBOX_STYLE_URL =
@@ -31,7 +32,11 @@ export const plugin = fp(mapsPlugin, {
3132
fastify: '4.x',
3233
name: PLUGIN_NAME,
3334
decorators: { fastify: ['mapeoStaticMaps', 'mapeoFallbackMap'] },
34-
dependencies: [MAPEO_STATIC_MAPS, MAPEO_OFFLINE_FALLBACK],
35+
dependencies: [
36+
COMAPEO_STYLED_MAP_PACKAGE,
37+
MAPEO_STATIC_MAPS,
38+
MAPEO_OFFLINE_FALLBACK,
39+
],
3540
})
3641

3742
/**
@@ -76,7 +81,19 @@ async function routes(fastify, opts) {
7681
async (req, rep) => {
7782
const serverAddress = await getFastifyServerAddress(req.server.server)
7883

79-
// 1. Attempt to get "default" local static map's style.json
84+
// 1. Attempt to use the styled map package
85+
{
86+
const style = await fastify.comapeoSmp.getStyle().catch(() => {
87+
fastify.log.warn('Cannot read styled map package archive')
88+
return null
89+
})
90+
91+
if (style) {
92+
return style
93+
}
94+
}
95+
96+
// 2. Attempt to get "default" local static map's style.json
8097
{
8198
const styleId = 'default'
8299

@@ -90,12 +107,16 @@ async function routes(fastify, opts) {
90107

91108
if (results) {
92109
const [stats, styleJson] = results
93-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
110+
rep.headers(
111+
createStyleJsonResponseHeaders({
112+
'Last-Modified': stats.mtime.toUTCString(),
113+
})
114+
)
94115
return styleJson
95116
}
96117
}
97118

98-
// 2. Attempt to get a default style.json from online source
119+
// 3. Attempt to get a default style.json from online source
99120
{
100121
const { key } = req.query
101122

@@ -151,7 +172,7 @@ async function routes(fastify, opts) {
151172
}
152173
}
153174

154-
// 3. Provide offline fallback map's style.json
175+
// 4. Provide offline fallback map's style.json
155176
{
156177
let results = null
157178

@@ -165,7 +186,11 @@ async function routes(fastify, opts) {
165186
}
166187

167188
const [stats, styleJson] = results
168-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
189+
rep.headers(
190+
createStyleJsonResponseHeaders({
191+
'Last-Modified': stats.mtime.toUTCString(),
192+
})
193+
)
169194
return styleJson
170195
}
171196
}

src/fastify-plugins/maps/offline-fallback-map.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ async function routes(fastify, opts) {
107107
throw new NotFoundError(`id = fallback, style.json`)
108108
}
109109

110-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
110+
rep.headers(
111+
createStyleJsonResponseHeaders({
112+
'Last-Modified': stats.mtime.toUTCString(),
113+
})
114+
)
111115

112116
return styleJson
113117
})

src/fastify-plugins/maps/static-maps.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,11 @@ async function routes(fastify, opts) {
190190
throw new NotFoundError(`id = ${styleId}, style.json`)
191191
}
192192

193-
rep.headers(createStyleJsonResponseHeaders(stats.mtime))
193+
rep.headers(
194+
createStyleJsonResponseHeaders({
195+
'Last-Modified': stats.mtime.toUTCString(),
196+
})
197+
)
194198

195199
return styleJson
196200
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import fp from 'fastify-plugin'
2+
import { Reader } from 'styled-map-package'
3+
4+
/** @import {FastifyPluginAsync} from 'fastify' */
5+
/** @import {Resource, SMPStyle} from 'styled-map-package/reader.js' */
6+
7+
export const PLUGIN_NAME = 'comapeo-styled-map-package'
8+
9+
/**
10+
* @typedef {object} StyledMapPackagePluginOpts
11+
* @property {string} filepath
12+
* @property {boolean} [lazy]
13+
* @property {string} [prefix]
14+
*
15+
*/
16+
17+
/**
18+
* @typedef {object} StyledMapPackagePluginDecorator
19+
* @property {(baseUrl?: string) => Promise<SMPStyle>} getStyle
20+
* @property {(path: string) => Promise<Resource>} getResource
21+
*/
22+
23+
export const plugin = fp(styledMapPackagePlugin, {
24+
fastify: '4.x',
25+
name: PLUGIN_NAME,
26+
})
27+
28+
/** @type {FastifyPluginAsync<StyledMapPackagePluginOpts>} */
29+
async function styledMapPackagePlugin(fastify, opts) {
30+
let reader = opts.lazy ? null : new Reader(opts.filepath)
31+
32+
fastify.addHook('onClose', async () => {
33+
if (reader) {
34+
// Can fail to close if `opts.filepath` used for instantiation is invalid
35+
try {
36+
await reader.close()
37+
} catch (err) {
38+
fastify.log.warn('Failed to close SMP reader instance', err)
39+
}
40+
}
41+
})
42+
43+
fastify.decorate('comapeoSmp', {
44+
async getStyle(baseUrl) {
45+
if (!reader) {
46+
reader = new Reader(opts.filepath)
47+
}
48+
49+
const base =
50+
baseUrl || new URL(opts.prefix || '', fastify.listeningOrigin).href
51+
52+
return reader.getStyle(base)
53+
},
54+
55+
async getResource(path) {
56+
if (!reader) {
57+
reader = new Reader(opts.filepath)
58+
}
59+
60+
return reader.getResource(path)
61+
},
62+
})
63+
64+
fastify.register(routes, {
65+
prefix: opts.prefix,
66+
})
67+
}
68+
69+
/** @type {FastifyPluginAsync} */
70+
async function routes(fastify) {
71+
if (!fastify.hasDecorator('comapeoSmp')) {
72+
throw new Error('Could not find `comapeoSmp` decorator')
73+
}
74+
75+
fastify.get('/style.json', async () => {
76+
const baseUrl = fastify.prefix
77+
? new URL(fastify.prefix, fastify.listeningOrigin).href
78+
: fastify.listeningOrigin
79+
80+
return fastify.comapeoSmp.getStyle(baseUrl)
81+
})
82+
83+
fastify.get('*', async (request, reply) => {
84+
/** @type {Resource} */
85+
let resource
86+
try {
87+
// Removes the prefix that might have been registered by a consumer of the plugin
88+
const normalizedPath = fastify.prefix
89+
? request.url.replace(fastify.prefix, '')
90+
: request.url
91+
92+
resource = await fastify.comapeoSmp.getResource(decodeURI(normalizedPath))
93+
} catch (e) {
94+
// @ts-expect-error
95+
e.statusCode = 404
96+
throw e
97+
}
98+
99+
reply
100+
.type(resource.contentType)
101+
.header('content-length', resource.contentLength)
102+
103+
if (resource.contentEncoding) {
104+
reply.header('content-encoding', resource.contentEncoding)
105+
}
106+
107+
return reply.send(resource.stream)
108+
})
109+
}

src/fastify-plugins/utils.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ export async function getFastifyServerAddress(server, { timeout } = {}) {
3939
}
4040

4141
/**
42-
* @param {Readonly<Date>} lastModified
42+
* @param {Parameters<import('fastify').FastifyReply['headers']>[0]} [overrides]
4343
*/
44-
export function createStyleJsonResponseHeaders(lastModified) {
44+
export function createStyleJsonResponseHeaders(overrides) {
4545
return {
4646
'Cache-Control': 'max-age=' + 5 * 60, // 5 minutes
4747
'Access-Control-Allow-Headers':
4848
'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since',
4949
'Access-Control-Allow-Origin': '*',
50-
'Last-Modified': lastModified.toUTCString(),
50+
...overrides,
5151
}
5252
}

test-e2e/manager-fastify-server.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { FastifyController } from '../src/fastify-controller.js'
1515
import { plugin as StaticMapsPlugin } from '../src/fastify-plugins/maps/static-maps.js'
1616
import { plugin as MapServerPlugin } from '../src/fastify-plugins/maps/index.js'
1717
import { plugin as OfflineFallbackMapPlugin } from '../src/fastify-plugins/maps/offline-fallback-map.js'
18+
import { plugin as StyledMapPackagePlugin } from '../src/fastify-plugins/maps/styled-map-package.js'
19+
1820
import { blobMetadata } from '../test/helpers/blob-store.js'
1921

2022
const BLOB_FIXTURES_DIR = fileURLToPath(
@@ -29,6 +31,11 @@ const MAPEO_FALLBACK_MAP_PATH = new URL(
2931
import.meta.url
3032
).pathname
3133

34+
const SMP_FIXTURE_PATH = new URL(
35+
'../fixtures/styled-map-packages/basic.smp',
36+
import.meta.url
37+
).pathname
38+
3239
const projectMigrationsFolder = new URL('../drizzle/project', import.meta.url)
3340
.pathname
3441
const clientMigrationsFolder = new URL('../drizzle/client', import.meta.url)
@@ -349,6 +356,11 @@ test('retrieving style.json using stable url', async (t) => {
349356

350357
const fastify = Fastify()
351358

359+
fastify.register(StyledMapPackagePlugin, {
360+
prefix: 'smp',
361+
lazy: true,
362+
filepath: SMP_FIXTURE_PATH,
363+
})
352364
fastify.register(StaticMapsPlugin, {
353365
prefix: 'static',
354366
staticRootDir: MAP_FIXTURES_PATH,

0 commit comments

Comments
 (0)