@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 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 {marker, precacheStaticMap, validateStaticScene} from '@tileflow/static';
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?: StaticMapCreateOptions): Promise<StaticMapResult>;
245
- declare function precacheStaticMap(scene: StaticSceneInput, options?: StaticMapCreateOptions): Promise<StaticMapResult>;
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
- const response = await fetcher(`${apiUrl}${path}`, {
305
- body: JSON.stringify(validation.scene),
306
- headers,
307
- method: "POST"
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.14",
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",