@tileflow/core 0.1.0-alpha.16 → 0.1.0-alpha.18

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.js CHANGED
@@ -6821,6 +6821,250 @@ function createTileflowSessionId() {
6821
6821
  }
6822
6822
  return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
6823
6823
  }
6824
+ function createTileflowSessionController(input) {
6825
+ const fetchGrant = input.fetch ?? globalThis.fetch;
6826
+ const grantTimeoutMs = validateSessionGrantTimeout(input.grantTimeoutMs);
6827
+ const now = input.now ?? (() => /* @__PURE__ */ new Date());
6828
+ const sessionIdFactory = input.sessionIdFactory ?? createTileflowSessionId;
6829
+ const createSession = (startedAt) => ({
6830
+ binding: null,
6831
+ grant: null,
6832
+ grantPromise: null,
6833
+ pendingGrantRequests: 0,
6834
+ requestCount: 0,
6835
+ sessionId: sessionIdFactory(),
6836
+ startedAt
6837
+ });
6838
+ let activeSession = createSession(now().getTime());
6839
+ async function resolveGrant(session, analytics, at) {
6840
+ if (session.grant && session.grant.expiresAt - at > 3e4) {
6841
+ return { grant: session.grant, session };
6842
+ }
6843
+ session.pendingGrantRequests += 1;
6844
+ try {
6845
+ if (!session.grantPromise) {
6846
+ const pending = (async () => {
6847
+ try {
6848
+ const nextGrant = await requestSessionGrant({
6849
+ analytics,
6850
+ at,
6851
+ fetchGrant,
6852
+ timeoutMs: grantTimeoutMs,
6853
+ sessionId: session.sessionId,
6854
+ source: input.source
6855
+ });
6856
+ session.grant = nextGrant;
6857
+ return { grant: nextGrant, session };
6858
+ } catch (error) {
6859
+ if (!(error instanceof TileflowSessionRestartError)) throw error;
6860
+ const replacement = createSession(at);
6861
+ replacement.binding = session.binding;
6862
+ replacement.requestCount = session.pendingGrantRequests;
6863
+ if (activeSession === session) activeSession = replacement;
6864
+ const replacementPending = requestSessionGrant({
6865
+ analytics,
6866
+ at,
6867
+ fetchGrant,
6868
+ timeoutMs: grantTimeoutMs,
6869
+ sessionId: replacement.sessionId,
6870
+ source: input.source
6871
+ }).then((nextGrant) => {
6872
+ replacement.grant = nextGrant;
6873
+ return { grant: nextGrant, session: replacement };
6874
+ });
6875
+ replacement.grantPromise = replacementPending;
6876
+ try {
6877
+ return await replacementPending;
6878
+ } finally {
6879
+ if (replacement.grantPromise === replacementPending) {
6880
+ replacement.grantPromise = null;
6881
+ }
6882
+ }
6883
+ } finally {
6884
+ session.grantPromise = null;
6885
+ }
6886
+ })();
6887
+ session.grantPromise = pending;
6888
+ }
6889
+ return await session.grantPromise;
6890
+ } finally {
6891
+ session.pendingGrantRequests -= 1;
6892
+ }
6893
+ }
6894
+ return {
6895
+ get sessionId() {
6896
+ return activeSession.sessionId;
6897
+ },
6898
+ async resolveRequestUrl(url, analytics) {
6899
+ if (!analytics || !analytics.mapId) return void 0;
6900
+ const at = now().getTime();
6901
+ if (at - activeSession.startedAt >= 6 * 60 * 60 * 1e3 || activeSession.requestCount >= 1e4) {
6902
+ activeSession = createSession(at);
6903
+ }
6904
+ const apiOrigin = originOf(getTileflowAnalyticsApiUrl(analytics));
6905
+ const binding = JSON.stringify([getTileflowAnalyticsApiUrl(analytics), analytics.mapId]);
6906
+ let session = activeSession;
6907
+ const grantedOrigins = session.binding === binding ? session.grant?.resourceOrigins : void 0;
6908
+ if (!isEligibleResource(url, analytics.mapId, apiOrigin, grantedOrigins)) {
6909
+ return void 0;
6910
+ }
6911
+ if (session.binding !== null && session.binding !== binding) {
6912
+ session = createSession(at);
6913
+ activeSession = session;
6914
+ }
6915
+ session.binding = binding;
6916
+ session.requestCount += 1;
6917
+ const resolved = await resolveGrant(session, analytics, at);
6918
+ const nextUrl = new URL(url);
6919
+ nextUrl.searchParams.set("session", resolved.session.sessionId);
6920
+ nextUrl.searchParams.set("map", analytics.mapId);
6921
+ if (analytics.styleId) nextUrl.searchParams.set("styleId", analytics.styleId);
6922
+ if (resolved.grant.token) nextUrl.searchParams.set("grant", resolved.grant.token);
6923
+ return nextUrl.toString();
6924
+ }
6925
+ };
6926
+ }
6927
+ async function requestSessionGrant(input) {
6928
+ return runWithinSessionGrantTimeout(input.timeoutMs, async (signal) => {
6929
+ const apiUrl = getTileflowAnalyticsApiUrl(input.analytics);
6930
+ const response = await input.fetchGrant(`${apiUrl}/v1/sessions/start`, {
6931
+ body: JSON.stringify({
6932
+ mapId: input.analytics.mapId,
6933
+ sdkVersion: input.analytics.sdkVersion,
6934
+ sessionId: input.sessionId,
6935
+ source: input.analytics.source ?? input.source,
6936
+ styleId: input.analytics.styleId
6937
+ }),
6938
+ credentials: "omit",
6939
+ headers: { "Content-Type": "application/json" },
6940
+ method: "POST",
6941
+ signal
6942
+ });
6943
+ throwIfSessionGrantAborted(signal);
6944
+ const body = await response.json().catch(() => null);
6945
+ throwIfSessionGrantAborted(signal);
6946
+ if (response.status === 409 && isRecord6(body) && body.code === "COMMERCIAL_SESSION_RESTART_REQUIRED" && body.ok === false && body.retryWithNewSession === true && body.sessionId === input.sessionId) {
6947
+ throw new TileflowSessionRestartError();
6948
+ }
6949
+ if (!response.ok) {
6950
+ throw new Error(`Tileflow session grant failed: ${response.status}`);
6951
+ }
6952
+ const parsed = parseSessionGrantResponse(body, input.sessionId);
6953
+ if (!parsed) {
6954
+ throw new Error("Tileflow session grant response was invalid.");
6955
+ }
6956
+ const expiresAt = parsed.grant ? Date.parse(parsed.expiresAt) : input.at + 6e4;
6957
+ if (response.status === 201 && (!parsed.grant || !parsed.expiresAt) || parsed.grant && (!Number.isFinite(expiresAt) || expiresAt <= input.at)) {
6958
+ throw new Error("Tileflow session grant response was invalid.");
6959
+ }
6960
+ return {
6961
+ expiresAt,
6962
+ resourceOrigins: new Set(
6963
+ [originOf(apiUrl), ...(parsed.resourceOrigins ?? []).map(originOf)].filter(
6964
+ (value) => Boolean(value)
6965
+ )
6966
+ ),
6967
+ token: parsed.grant ?? null
6968
+ };
6969
+ });
6970
+ }
6971
+ function validateSessionGrantTimeout(value) {
6972
+ const resolved = value ?? 1e4;
6973
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > 12e4) {
6974
+ throw new Error("Tileflow session grantTimeoutMs must be an integer from 1 to 120000");
6975
+ }
6976
+ return resolved;
6977
+ }
6978
+ async function runWithinSessionGrantTimeout(timeoutMs, operation) {
6979
+ const controller = new AbortController();
6980
+ const timeoutError = new Error(`Tileflow session grant timed out after ${timeoutMs}ms`);
6981
+ const timeout = setTimeout(() => controller.abort(timeoutError), timeoutMs);
6982
+ try {
6983
+ return await raceSessionGrantWithAbort(operation(controller.signal), controller.signal);
6984
+ } finally {
6985
+ clearTimeout(timeout);
6986
+ }
6987
+ }
6988
+ function raceSessionGrantWithAbort(operation, signal) {
6989
+ if (signal.aborted) return Promise.reject(sessionGrantAbortReason(signal));
6990
+ return new Promise((resolve, reject) => {
6991
+ const onAbort = () => {
6992
+ cleanup();
6993
+ reject(sessionGrantAbortReason(signal));
6994
+ };
6995
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
6996
+ signal.addEventListener("abort", onAbort, { once: true });
6997
+ operation.then(
6998
+ (value) => {
6999
+ cleanup();
7000
+ resolve(value);
7001
+ },
7002
+ (error) => {
7003
+ cleanup();
7004
+ reject(error);
7005
+ }
7006
+ );
7007
+ });
7008
+ }
7009
+ function throwIfSessionGrantAborted(signal) {
7010
+ if (signal.aborted) throw sessionGrantAbortReason(signal);
7011
+ }
7012
+ function sessionGrantAbortReason(signal) {
7013
+ if (signal.reason instanceof Error) return signal.reason;
7014
+ const error = new Error("Tileflow session grant was aborted");
7015
+ error.name = "AbortError";
7016
+ return error;
7017
+ }
7018
+ function parseSessionGrantResponse(value, expectedSessionId) {
7019
+ if (!isRecord6(value) || value.ok !== true || value.sessionId !== expectedSessionId) return null;
7020
+ if (value.usageMode !== void 0 && value.usageMode !== "session") return null;
7021
+ if (value.billingEnabled !== void 0 && value.billingEnabled !== false) return null;
7022
+ if (value.disposition !== void 0 && value.disposition !== "ordinary" && value.disposition !== "unbilled_fail_open") {
7023
+ return null;
7024
+ }
7025
+ if (value.meterMode !== void 0 && value.meterMode !== "disabled" && value.meterMode !== "shadow" && value.meterMode !== "enforced") {
7026
+ return null;
7027
+ }
7028
+ if (value.grant !== void 0 && (typeof value.grant !== "string" || value.grant.length === 0 || value.grant.length > 8192)) {
7029
+ return null;
7030
+ }
7031
+ if (value.expiresAt !== void 0 && typeof value.expiresAt !== "string") return null;
7032
+ if (value.grant !== void 0 && value.expiresAt === void 0) return null;
7033
+ if (value.resourceOrigins !== void 0 && (!Array.isArray(value.resourceOrigins) || value.resourceOrigins.length > 32 || value.resourceOrigins.some((origin) => !originOf(origin)))) {
7034
+ return null;
7035
+ }
7036
+ return value;
7037
+ }
7038
+ function isRecord6(value) {
7039
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
7040
+ }
7041
+ var TileflowSessionRestartError = class extends Error {
7042
+ constructor() {
7043
+ super("Tileflow session must restart");
7044
+ this.name = "TileflowSessionRestartError";
7045
+ }
7046
+ };
7047
+ function isEligibleResource(value, mapId, apiOrigin, grantedOrigins) {
7048
+ try {
7049
+ const url = new URL(value);
7050
+ if (url.username || url.password) return false;
7051
+ const originAllowed = url.origin === apiOrigin || grantedOrigins?.has(url.origin) || apiOrigin === "https://api.tileflow.dev" && url.origin === "https://cdn.tileflow.dev";
7052
+ if (!originAllowed) return false;
7053
+ return url.pathname === `/maps/${encodeURIComponent(mapId)}/style.json` || url.pathname.startsWith("/tiles/") || url.pathname.startsWith("/v1/tiles/") || url.pathname.startsWith("/fonts/") || url.pathname.startsWith("/sprites/");
7054
+ } catch {
7055
+ return false;
7056
+ }
7057
+ }
7058
+ function originOf(value) {
7059
+ if (typeof value !== "string") return null;
7060
+ try {
7061
+ const url = new URL(value);
7062
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return null;
7063
+ return url.origin;
7064
+ } catch {
7065
+ return null;
7066
+ }
7067
+ }
6824
7068
  function resolveTileflowAnalyticsRequestUrl(url, analytics, sessionId) {
6825
7069
  if (!analytics || analytics.enabled === false) {
6826
7070
  return void 0;
@@ -6843,7 +7087,7 @@ function startTileflowSession(analytics, input) {
6843
7087
  if (!analytics || analytics.enabled === false || !input.mapId || typeof navigator === "undefined") {
6844
7088
  return;
6845
7089
  }
6846
- const endpoint = analytics.endpoint ?? `${getTileflowAnalyticsApiUrl(analytics)}/v1/sessions/start`;
7090
+ const endpoint = analytics.endpoint ?? `${getTileflowAnalyticsApiUrl(analytics)}/v1/sessions/analytics`;
6847
7091
  const payload = JSON.stringify({
6848
7092
  mapId: input.mapId,
6849
7093
  metadata: analytics.metadata,
@@ -6866,7 +7110,7 @@ function startTileflowSession(analytics, input) {
6866
7110
  },
6867
7111
  keepalive: true,
6868
7112
  method: "POST"
6869
- });
7113
+ }).catch(() => void 0);
6870
7114
  }
6871
7115
  function getTileflowAnalyticsApiUrl(analytics) {
6872
7116
  if (analytics.apiUrl) {
@@ -6883,9 +7127,6 @@ function getTileflowAnalyticsApiUrl(analytics) {
6883
7127
  return "https://api.tileflow.dev";
6884
7128
  }
6885
7129
  function mergeTileflowAnalytics(userAnalytics, runtimeAnalytics) {
6886
- if (userAnalytics?.enabled === false) {
6887
- return userAnalytics;
6888
- }
6889
7130
  if (!userAnalytics && !runtimeAnalytics?.mapId) {
6890
7131
  return void 0;
6891
7132
  }
@@ -6994,7 +7235,7 @@ function normalizeTileflowStaticImageSize(input) {
6994
7235
  function inferTileflowAnalyticsFromStyleUrl(styleUrl) {
6995
7236
  try {
6996
7237
  const url = new URL(styleUrl, getTileflowLocationOrigin());
6997
- const match = url.pathname.match(/\/(?:v1\/)?maps\/([^/]+)\/style\.json$/);
7238
+ const match = url.pathname.match(/^\/maps\/([^/]+)\/style\.json$/);
6998
7239
  if (!match?.[1]) {
6999
7240
  return void 0;
7000
7241
  }
@@ -7037,6 +7278,7 @@ export {
7037
7278
  createManifest,
7038
7279
  createStyle,
7039
7280
  createStyleFromProject,
7281
+ createTileflowSessionController,
7040
7282
  createTileflowSessionId,
7041
7283
  defaultTileflowManifestUrl,
7042
7284
  defaultTileflowStyleBaseUrl,