@tileflow/static 0.1.0-alpha.14 → 0.1.0-alpha.17
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 +21 -1
- package/dist/index.d.ts +33 -3
- package/dist/index.js +199 -12
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
Static map schemas, overlay helpers, and request utilities for Tileflow static map rendering.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createStaticMapIdempotencyKey,
|
|
8
|
+
marker,
|
|
9
|
+
precacheStaticMap,
|
|
10
|
+
validateStaticScene,
|
|
11
|
+
} from '@tileflow/static';
|
|
7
12
|
|
|
8
13
|
const scene = validateStaticScene({
|
|
9
14
|
map: 'madrid',
|
|
@@ -14,7 +19,22 @@ const scene = validateStaticScene({
|
|
|
14
19
|
|
|
15
20
|
await precacheStaticMap(scene.scene, {
|
|
16
21
|
apiKey: process.env.TILEFLOW_API_KEY,
|
|
22
|
+
idempotencyKey: createStaticMapIdempotencyKey(),
|
|
17
23
|
});
|
|
18
24
|
```
|
|
19
25
|
|
|
26
|
+
Create one idempotency key per intentional create/precache action and retain it across network
|
|
27
|
+
retries. Reusing the key with the same normalized scene returns the stored result; reusing it with a
|
|
28
|
+
different scene returns 409. A successful paid operation reports `unitCost: 15`, shared
|
|
29
|
+
`remainingUnits`, its durable `operationId`, cache reuse, hash, and immutable image URL. A live
|
|
30
|
+
equal-hash render may return `202`; the helper polls with the same body/key within its bounded wait.
|
|
31
|
+
`maxWaitMs` is the total client completion budget, including network, response parsing, and polling
|
|
32
|
+
delays; `signal` cancels the same complete operation. The helper aborts in-flight work when either
|
|
33
|
+
boundary is reached. It rejects malformed responses, non-HTTP(S) image URLs, and any change of
|
|
34
|
+
durable operation ID while polling.
|
|
35
|
+
|
|
36
|
+
Free cannot initiate Static Maps work. Starter consumes 15 shared API units for each successful
|
|
37
|
+
logical operation, including a distinct-key cache reuse. Downloading the immutable PNG consumes
|
|
38
|
+
zero units.
|
|
39
|
+
|
|
20
40
|
Docs: https://tileflow.dev/docs
|
package/dist/index.d.ts
CHANGED
|
@@ -202,13 +202,42 @@ type StaticMapResult = {
|
|
|
202
202
|
cached: boolean;
|
|
203
203
|
hash: string;
|
|
204
204
|
imageUrl: string;
|
|
205
|
+
operationId: string | null;
|
|
206
|
+
remainingUnits: number | null;
|
|
205
207
|
status: 'ready';
|
|
208
|
+
unitCost: 0 | 15;
|
|
206
209
|
};
|
|
207
210
|
type StaticMapCreateOptions = {
|
|
208
211
|
apiKey?: string;
|
|
209
212
|
apiUrl?: string;
|
|
210
213
|
fetch?: typeof fetch;
|
|
214
|
+
idempotencyKey: string;
|
|
215
|
+
maxWaitMs?: number;
|
|
216
|
+
pollIntervalMs?: number;
|
|
217
|
+
signal?: AbortSignal;
|
|
211
218
|
};
|
|
219
|
+
declare const staticMapReadyResultSchema: z.ZodObject<{
|
|
220
|
+
cached: z.ZodBoolean;
|
|
221
|
+
hash: z.ZodString;
|
|
222
|
+
imageUrl: z.ZodString;
|
|
223
|
+
operationId: z.ZodString;
|
|
224
|
+
remainingUnits: z.ZodNumber;
|
|
225
|
+
status: z.ZodLiteral<"ready">;
|
|
226
|
+
unitCost: z.ZodLiteral<15>;
|
|
227
|
+
}, z.core.$strict>;
|
|
228
|
+
declare const staticMapProcessingResultSchema: z.ZodObject<{
|
|
229
|
+
operationId: z.ZodString;
|
|
230
|
+
retryAfterMs: z.ZodNumber;
|
|
231
|
+
status: z.ZodLiteral<"processing">;
|
|
232
|
+
}, z.core.$strict>;
|
|
233
|
+
declare function validateStaticMapIdempotencyKey(value: unknown): {
|
|
234
|
+
key: string;
|
|
235
|
+
ok: true;
|
|
236
|
+
} | {
|
|
237
|
+
error: string;
|
|
238
|
+
ok: false;
|
|
239
|
+
};
|
|
240
|
+
declare function createStaticMapIdempotencyKey(): string;
|
|
212
241
|
declare function line(input: Omit<z.input<typeof lineOverlaySchema>, 'type'>): StaticOverlay;
|
|
213
242
|
declare function circle(input: Omit<z.input<typeof circleOverlaySchema>, 'type'>): StaticOverlay;
|
|
214
243
|
declare function marker(input: Omit<z.input<typeof markerOverlaySchema>, 'type'>): StaticOverlay;
|
|
@@ -237,12 +266,13 @@ declare function createRenderManifest(input: {
|
|
|
237
266
|
styleUrl: string;
|
|
238
267
|
}): StaticRenderManifest;
|
|
239
268
|
declare function hashRenderManifest(manifest: StaticRenderManifest): Promise<string>;
|
|
269
|
+
declare function hashStaticSceneRequest(scene: StaticSceneInput): Promise<string>;
|
|
240
270
|
declare function compileStaticOverlays(overlays: StaticOverlay[]): {
|
|
241
271
|
layers: Record<string, unknown>[];
|
|
242
272
|
sources: Record<string, Record<string, unknown>>;
|
|
243
273
|
};
|
|
244
|
-
declare function createStaticMap(scene: StaticSceneInput, options
|
|
245
|
-
declare function precacheStaticMap(scene: StaticSceneInput, options
|
|
274
|
+
declare function createStaticMap(scene: StaticSceneInput, options: StaticMapCreateOptions): Promise<StaticMapResult>;
|
|
275
|
+
declare function precacheStaticMap(scene: StaticSceneInput, options: StaticMapCreateOptions): Promise<StaticMapResult>;
|
|
246
276
|
declare function stableStringify(value: unknown): string;
|
|
247
277
|
|
|
248
|
-
export { type StaticCoordinate, type StaticMapCreateOptions, type StaticMapResult, type StaticOverlay, type StaticOverlayInput, type StaticRenderManifest, type StaticScene, type StaticSceneInput, circle, compileStaticOverlays, createRenderManifest, createStaticMap, hashRenderManifest, line, marker, normalizeStaticScene, polygon, precacheStaticMap, stableStringify, staticOverlaySchema, staticRenderManifestSchema, staticRendererSchemaVersion, staticSceneLimits, staticSceneSchema, staticSceneSchemaVersion, validateStaticRenderManifest, validateStaticScene };
|
|
278
|
+
export { type StaticCoordinate, type StaticMapCreateOptions, type StaticMapResult, type StaticOverlay, type StaticOverlayInput, type StaticRenderManifest, type StaticScene, type StaticSceneInput, circle, compileStaticOverlays, createRenderManifest, createStaticMap, createStaticMapIdempotencyKey, hashRenderManifest, hashStaticSceneRequest, line, marker, normalizeStaticScene, polygon, precacheStaticMap, stableStringify, staticMapProcessingResultSchema, staticMapReadyResultSchema, staticOverlaySchema, staticRenderManifestSchema, staticRendererSchemaVersion, staticSceneLimits, staticSceneSchema, staticSceneSchemaVersion, validateStaticMapIdempotencyKey, validateStaticRenderManifest, validateStaticScene };
|
package/dist/index.js
CHANGED
|
@@ -110,6 +110,37 @@ var staticRenderManifestSchema = z.object({
|
|
|
110
110
|
message: "Expected an http(s) style URL"
|
|
111
111
|
})
|
|
112
112
|
});
|
|
113
|
+
var staticMapIdempotencyKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
114
|
+
var staticMapReadyResultSchema = z.object({
|
|
115
|
+
cached: z.boolean(),
|
|
116
|
+
hash: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
117
|
+
imageUrl: z.string().trim().url().max(2048).refine(isSafeHttpUrl, { message: "Expected an http(s) URL without credentials" }),
|
|
118
|
+
operationId: z.string().min(20).max(68),
|
|
119
|
+
remainingUnits: z.number().int().nonnegative(),
|
|
120
|
+
status: z.literal("ready"),
|
|
121
|
+
unitCost: z.literal(15)
|
|
122
|
+
}).strict();
|
|
123
|
+
var staticMapProcessingResultSchema = z.object({
|
|
124
|
+
operationId: z.string().min(20).max(68),
|
|
125
|
+
retryAfterMs: z.number().int().min(0).max(5e3),
|
|
126
|
+
status: z.literal("processing")
|
|
127
|
+
}).strict();
|
|
128
|
+
function validateStaticMapIdempotencyKey(value) {
|
|
129
|
+
if (typeof value !== "string" || !staticMapIdempotencyKeyPattern.test(value)) {
|
|
130
|
+
return {
|
|
131
|
+
error: "Idempotency key must contain 8-128 ASCII letters, digits, dot, underscore, colon, or hyphen and start with a letter or digit",
|
|
132
|
+
ok: false
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { key: value, ok: true };
|
|
136
|
+
}
|
|
137
|
+
function createStaticMapIdempotencyKey() {
|
|
138
|
+
const key = `static_${crypto.randomUUID()}`;
|
|
139
|
+
if (!validateStaticMapIdempotencyKey(key).ok) {
|
|
140
|
+
throw new Error("Unable to create a valid Tileflow Static Maps idempotency key");
|
|
141
|
+
}
|
|
142
|
+
return key;
|
|
143
|
+
}
|
|
113
144
|
function line(input) {
|
|
114
145
|
return normalizeOverlay({ ...input, type: "line" });
|
|
115
146
|
}
|
|
@@ -211,6 +242,11 @@ async function hashRenderManifest(manifest) {
|
|
|
211
242
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
212
243
|
return base64Url(new Uint8Array(digest));
|
|
213
244
|
}
|
|
245
|
+
async function hashStaticSceneRequest(scene) {
|
|
246
|
+
const validation = validateStaticScene(scene);
|
|
247
|
+
if (!validation.ok) throw new Error(`Invalid Tileflow static scene: ${validation.error}`);
|
|
248
|
+
return hashStableValue(validation.scene);
|
|
249
|
+
}
|
|
214
250
|
function compileStaticOverlays(overlays) {
|
|
215
251
|
const sources = {};
|
|
216
252
|
const layers = [];
|
|
@@ -282,10 +318,10 @@ function compileStaticOverlays(overlays) {
|
|
|
282
318
|
}
|
|
283
319
|
return { layers, sources };
|
|
284
320
|
}
|
|
285
|
-
async function createStaticMap(scene, options
|
|
321
|
+
async function createStaticMap(scene, options) {
|
|
286
322
|
return requestStaticMap(scene, options, "/v1/static/maps");
|
|
287
323
|
}
|
|
288
|
-
async function precacheStaticMap(scene, options
|
|
324
|
+
async function precacheStaticMap(scene, options) {
|
|
289
325
|
return requestStaticMap(scene, options, "/v1/static/maps/precache");
|
|
290
326
|
}
|
|
291
327
|
async function requestStaticMap(scene, options, path) {
|
|
@@ -293,24 +329,66 @@ async function requestStaticMap(scene, options, path) {
|
|
|
293
329
|
if (!validation.ok) {
|
|
294
330
|
throw new Error(`Invalid Tileflow static scene: ${validation.error}`);
|
|
295
331
|
}
|
|
332
|
+
const idempotency = validateStaticMapIdempotencyKey(options.idempotencyKey);
|
|
333
|
+
if (!idempotency.ok) {
|
|
334
|
+
throw new Error(`Invalid Tileflow Static Maps idempotency key: ${idempotency.error}`);
|
|
335
|
+
}
|
|
336
|
+
const maxWaitMs = boundedClientDuration(options.maxWaitMs, 3e4, 100, 12e4, "maxWaitMs");
|
|
337
|
+
const pollIntervalMs = boundedClientDuration(
|
|
338
|
+
options.pollIntervalMs,
|
|
339
|
+
500,
|
|
340
|
+
0,
|
|
341
|
+
5e3,
|
|
342
|
+
"pollIntervalMs"
|
|
343
|
+
);
|
|
296
344
|
const fetcher = options.fetch ?? fetch;
|
|
297
345
|
const apiUrl = normalizeUrl(options.apiUrl ?? "https://api.tileflow.dev");
|
|
298
346
|
const headers = {
|
|
299
|
-
"Content-Type": "application/json"
|
|
347
|
+
"Content-Type": "application/json",
|
|
348
|
+
"Idempotency-Key": idempotency.key
|
|
300
349
|
};
|
|
301
350
|
if (options.apiKey) {
|
|
302
351
|
headers.Authorization = `Bearer ${options.apiKey}`;
|
|
303
352
|
}
|
|
304
|
-
|
|
305
|
-
body
|
|
306
|
-
|
|
307
|
-
|
|
353
|
+
return runWithinStaticMapBudget(maxWaitMs, options.signal, async (signal) => {
|
|
354
|
+
const body = JSON.stringify(validation.scene);
|
|
355
|
+
let operationId = null;
|
|
356
|
+
while (true) {
|
|
357
|
+
const response = await fetcher(`${apiUrl}${path}`, {
|
|
358
|
+
body,
|
|
359
|
+
headers,
|
|
360
|
+
method: "POST",
|
|
361
|
+
signal
|
|
362
|
+
});
|
|
363
|
+
throwIfAborted(signal);
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
const error = await response.text();
|
|
366
|
+
throwIfAborted(signal);
|
|
367
|
+
throw new Error(`Tileflow static map failed: ${response.status} ${error}`);
|
|
368
|
+
}
|
|
369
|
+
const json = await readJsonResponse(response);
|
|
370
|
+
throwIfAborted(signal);
|
|
371
|
+
if (response.status !== 202) {
|
|
372
|
+
const parsed = staticMapReadyResultSchema.safeParse(json);
|
|
373
|
+
if (!parsed.success) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Tileflow static map returned an invalid response: ${parsed.error.message}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
assertStaticMapOperationIdentity(operationId, parsed.data.operationId);
|
|
379
|
+
return parsed.data;
|
|
380
|
+
}
|
|
381
|
+
const pending = staticMapProcessingResultSchema.safeParse(json);
|
|
382
|
+
if (!pending.success) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Tileflow static map returned an invalid response: ${pending.error.message}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
assertStaticMapOperationIdentity(operationId, pending.data.operationId);
|
|
388
|
+
operationId ??= pending.data.operationId;
|
|
389
|
+
await delay(Math.max(pollIntervalMs, pending.data.retryAfterMs), signal);
|
|
390
|
+
}
|
|
308
391
|
});
|
|
309
|
-
if (!response.ok) {
|
|
310
|
-
const error = await response.text();
|
|
311
|
-
throw new Error(`Tileflow static map failed: ${response.status} ${error}`);
|
|
312
|
-
}
|
|
313
|
-
return await response.json();
|
|
314
392
|
}
|
|
315
393
|
function stableStringify(value) {
|
|
316
394
|
return JSON.stringify(sortJson(value));
|
|
@@ -378,6 +456,110 @@ function base64Url(bytes) {
|
|
|
378
456
|
}
|
|
379
457
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
380
458
|
}
|
|
459
|
+
async function hashStableValue(value) {
|
|
460
|
+
const bytes = new TextEncoder().encode(stableStringify(value));
|
|
461
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
462
|
+
return base64Url(new Uint8Array(digest));
|
|
463
|
+
}
|
|
464
|
+
function boundedClientDuration(value, fallback, minimum, maximum, name) {
|
|
465
|
+
const resolved = value ?? fallback;
|
|
466
|
+
if (!Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Tileflow Static Maps ${name} must be an integer from ${minimum} to ${maximum}`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return resolved;
|
|
472
|
+
}
|
|
473
|
+
async function readJsonResponse(response) {
|
|
474
|
+
try {
|
|
475
|
+
return await response.json();
|
|
476
|
+
} catch {
|
|
477
|
+
throw new Error("Tileflow static map returned an invalid response: expected JSON");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function runWithinStaticMapBudget(maxWaitMs, externalSignal, operation) {
|
|
481
|
+
const controller = new AbortController();
|
|
482
|
+
const timeoutError = new Error(`Tileflow static map timed out after ${maxWaitMs}ms`);
|
|
483
|
+
const abortFromCaller = () => controller.abort(externalSignal?.reason);
|
|
484
|
+
if (externalSignal?.aborted) {
|
|
485
|
+
abortFromCaller();
|
|
486
|
+
} else {
|
|
487
|
+
externalSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
488
|
+
}
|
|
489
|
+
const timeout = setTimeout(() => controller.abort(timeoutError), maxWaitMs);
|
|
490
|
+
try {
|
|
491
|
+
throwIfAborted(controller.signal);
|
|
492
|
+
const pending = operation(controller.signal);
|
|
493
|
+
return await raceWithAbort(pending, controller.signal);
|
|
494
|
+
} finally {
|
|
495
|
+
clearTimeout(timeout);
|
|
496
|
+
externalSignal?.removeEventListener("abort", abortFromCaller);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function delay(milliseconds, signal) {
|
|
500
|
+
if (milliseconds === 0) {
|
|
501
|
+
throwIfAborted(signal);
|
|
502
|
+
return Promise.resolve();
|
|
503
|
+
}
|
|
504
|
+
return new Promise((resolve, reject) => {
|
|
505
|
+
const timeout = setTimeout(() => {
|
|
506
|
+
signal.removeEventListener("abort", onAbort);
|
|
507
|
+
resolve();
|
|
508
|
+
}, milliseconds);
|
|
509
|
+
const onAbort = () => {
|
|
510
|
+
clearTimeout(timeout);
|
|
511
|
+
reject(abortReason(signal));
|
|
512
|
+
};
|
|
513
|
+
if (signal.aborted) {
|
|
514
|
+
onAbort();
|
|
515
|
+
} else {
|
|
516
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function raceWithAbort(operation, signal) {
|
|
521
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
522
|
+
return new Promise((resolve, reject) => {
|
|
523
|
+
const onAbort = () => {
|
|
524
|
+
cleanup();
|
|
525
|
+
reject(abortReason(signal));
|
|
526
|
+
};
|
|
527
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
528
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
529
|
+
operation.then(
|
|
530
|
+
(value) => {
|
|
531
|
+
cleanup();
|
|
532
|
+
resolve(value);
|
|
533
|
+
},
|
|
534
|
+
(error) => {
|
|
535
|
+
cleanup();
|
|
536
|
+
reject(error);
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function throwIfAborted(signal) {
|
|
542
|
+
if (signal.aborted) throw abortReason(signal);
|
|
543
|
+
}
|
|
544
|
+
function abortReason(signal) {
|
|
545
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
546
|
+
const error = new Error("Tileflow static map request was aborted");
|
|
547
|
+
error.name = "AbortError";
|
|
548
|
+
return error;
|
|
549
|
+
}
|
|
550
|
+
function assertStaticMapOperationIdentity(expected, actual) {
|
|
551
|
+
if (expected !== null && actual !== expected) {
|
|
552
|
+
throw new Error("Tileflow static map changed operation identity while polling");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function isSafeHttpUrl(value) {
|
|
556
|
+
try {
|
|
557
|
+
const url = new URL(value);
|
|
558
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
559
|
+
} catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
381
563
|
function normalizeUrl(value) {
|
|
382
564
|
return value.replace(/\/+$/, "");
|
|
383
565
|
}
|
|
@@ -386,19 +568,24 @@ export {
|
|
|
386
568
|
compileStaticOverlays,
|
|
387
569
|
createRenderManifest,
|
|
388
570
|
createStaticMap,
|
|
571
|
+
createStaticMapIdempotencyKey,
|
|
389
572
|
hashRenderManifest,
|
|
573
|
+
hashStaticSceneRequest,
|
|
390
574
|
line,
|
|
391
575
|
marker,
|
|
392
576
|
normalizeStaticScene,
|
|
393
577
|
polygon,
|
|
394
578
|
precacheStaticMap,
|
|
395
579
|
stableStringify,
|
|
580
|
+
staticMapProcessingResultSchema,
|
|
581
|
+
staticMapReadyResultSchema,
|
|
396
582
|
staticOverlaySchema,
|
|
397
583
|
staticRenderManifestSchema,
|
|
398
584
|
staticRendererSchemaVersion,
|
|
399
585
|
staticSceneLimits,
|
|
400
586
|
staticSceneSchema,
|
|
401
587
|
staticSceneSchemaVersion,
|
|
588
|
+
validateStaticMapIdempotencyKey,
|
|
402
589
|
validateStaticRenderManifest,
|
|
403
590
|
validateStaticScene
|
|
404
591
|
};
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/static",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.17",
|
|
4
4
|
"description": "Static map request schemas and helpers for Tileflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"homepage": "https://tileflow.dev",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/tileflow/tileflow.git",
|
|
10
|
+
"url": "git+https://github.com/tileflow/tileflow-sdk.git",
|
|
11
11
|
"directory": "packages/static"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/tileflow/tileflow/issues"
|
|
14
|
+
"url": "https://github.com/tileflow/tileflow-sdk/issues"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"tileflow",
|