@volter/twin-googlemaps 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +96 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +23 -0
  5. package/dist/src/googlemaps-auth.d.ts +31 -0
  6. package/dist/src/googlemaps-auth.js +37 -0
  7. package/dist/src/googlemaps-budget.d.ts +61 -0
  8. package/dist/src/googlemaps-budget.js +142 -0
  9. package/dist/src/googlemaps-capabilities.d.ts +3 -0
  10. package/dist/src/googlemaps-capabilities.js +648 -0
  11. package/dist/src/googlemaps-conformance.d.ts +8 -0
  12. package/dist/src/googlemaps-conformance.js +18 -0
  13. package/dist/src/googlemaps-connector.d.ts +94 -0
  14. package/dist/src/googlemaps-connector.js +243 -0
  15. package/dist/src/googlemaps-data.d.ts +87 -0
  16. package/dist/src/googlemaps-data.js +370 -0
  17. package/dist/src/googlemaps-perform-harness.d.ts +5 -0
  18. package/dist/src/googlemaps-perform-harness.js +17 -0
  19. package/dist/src/googlemaps-server.d.ts +14 -0
  20. package/dist/src/googlemaps-server.js +25 -0
  21. package/dist/src/googlemaps-twin.d.ts +2 -0
  22. package/dist/src/googlemaps-twin.js +1116 -0
  23. package/dist/src/googlemaps-types.d.ts +45 -0
  24. package/dist/src/googlemaps-types.js +1 -0
  25. package/dist/src/index.d.ts +11 -0
  26. package/dist/src/index.js +65 -0
  27. package/package.json +52 -0
  28. package/src/cli.ts +22 -0
  29. package/src/googlemaps-auth.ts +52 -0
  30. package/src/googlemaps-budget.ts +168 -0
  31. package/src/googlemaps-capabilities.ts +766 -0
  32. package/src/googlemaps-conformance.ts +24 -0
  33. package/src/googlemaps-connector.ts +250 -0
  34. package/src/googlemaps-data.ts +399 -0
  35. package/src/googlemaps-perform-harness.ts +17 -0
  36. package/src/googlemaps-server.ts +33 -0
  37. package/src/googlemaps-twin.ts +1110 -0
  38. package/src/googlemaps-types.ts +47 -0
  39. package/src/index.ts +124 -0
@@ -0,0 +1,370 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Deterministic, NON-real geodata. A Google Maps twin cannot reproduce Google's
4
+ * real geospatial index, live traffic, or imagery; instead it returns faithful
5
+ * response SHAPES with stable, deterministic values. A given query always maps
6
+ * to the same canned result so verifies and tests are repeatable offline.
7
+ */
8
+ /** Stable FNV-ish hash → hex, used to derive deterministic ids/coords. */
9
+ export function stableHash(input) {
10
+ const s = typeof input === 'string' ? input : JSON.stringify(input);
11
+ return createHash('sha256').update(s).digest('hex').slice(0, 16);
12
+ }
13
+ /** Normalize a free-text address for stable keying (lowercase, collapse ws). */
14
+ export function normalizeAddress(address) {
15
+ return address.trim().toLowerCase().replace(/\s+/g, ' ');
16
+ }
17
+ /** Deterministic lat/lng in a plausible range derived from a seed string. */
18
+ export function deterministicLatLng(seed) {
19
+ const h = stableHash(seed);
20
+ const a = parseInt(h.slice(0, 8), 16);
21
+ const b = parseInt(h.slice(8, 16), 16);
22
+ // lat in [-85, 85], lng in [-180, 180], rounded to 7 decimals like Google.
23
+ const lat = Math.round((((a % 17000000) / 100000) - 85) * 1e7) / 1e7;
24
+ const lng = Math.round((((b % 36000000) / 100000) - 180) * 1e7) / 1e7;
25
+ return { lat, lng };
26
+ }
27
+ export function viewportAround(loc, delta = 0.01) {
28
+ return {
29
+ northeast: { lat: Math.round((loc.lat + delta) * 1e7) / 1e7, lng: Math.round((loc.lng + delta) * 1e7) / 1e7 },
30
+ southwest: { lat: Math.round((loc.lat - delta) * 1e7) / 1e7, lng: Math.round((loc.lng - delta) * 1e7) / 1e7 },
31
+ };
32
+ }
33
+ // Place ids use Google's `ChIJ`-prefixed opaque token shape. These are canned.
34
+ const PLACES = [
35
+ {
36
+ place_id: 'ChIJE9on3F3HwoAR9AhGJW_fL-I',
37
+ name: 'Los Angeles',
38
+ formatted_address: 'Los Angeles, CA, USA',
39
+ location: { lat: 34.0522342, lng: -118.2436849 },
40
+ types: ['locality', 'political'],
41
+ components: [
42
+ { long_name: 'Los Angeles', short_name: 'Los Angeles', types: ['locality', 'political'] },
43
+ { long_name: 'Los Angeles County', short_name: 'Los Angeles County', types: ['administrative_area_level_2', 'political'] },
44
+ { long_name: 'California', short_name: 'CA', types: ['administrative_area_level_1', 'political'] },
45
+ { long_name: 'United States', short_name: 'US', types: ['country', 'political'] },
46
+ ],
47
+ },
48
+ {
49
+ place_id: 'ChIJOwg_06VPwokRYv534QaPC8g',
50
+ name: 'New York',
51
+ formatted_address: 'New York, NY, USA',
52
+ location: { lat: 40.7127753, lng: -74.0059728 },
53
+ types: ['locality', 'political'],
54
+ components: [
55
+ { long_name: 'New York', short_name: 'New York', types: ['locality', 'political'] },
56
+ { long_name: 'New York', short_name: 'NY', types: ['administrative_area_level_1', 'political'] },
57
+ { long_name: 'United States', short_name: 'US', types: ['country', 'political'] },
58
+ ],
59
+ },
60
+ {
61
+ place_id: 'ChIJj61dQgK6j4AR4GeTYWZsKWw',
62
+ name: 'Mountain View',
63
+ formatted_address: '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA',
64
+ location: { lat: 37.4220656, lng: -122.0840897 },
65
+ types: ['street_address'],
66
+ components: [
67
+ { long_name: '1600', short_name: '1600', types: ['street_number'] },
68
+ { long_name: 'Amphitheatre Parkway', short_name: 'Amphitheatre Pkwy', types: ['route'] },
69
+ { long_name: 'Mountain View', short_name: 'Mountain View', types: ['locality', 'political'] },
70
+ { long_name: 'California', short_name: 'CA', types: ['administrative_area_level_1', 'political'] },
71
+ { long_name: 'United States', short_name: 'US', types: ['country', 'political'] },
72
+ { long_name: '94043', short_name: '94043', types: ['postal_code'] },
73
+ ],
74
+ },
75
+ {
76
+ place_id: 'ChIJdd4hrwug2EcRmSrV3Vo6llI',
77
+ name: 'London',
78
+ formatted_address: 'London, UK',
79
+ location: { lat: 51.5072178, lng: -0.1275862 },
80
+ types: ['locality', 'political'],
81
+ components: [
82
+ { long_name: 'London', short_name: 'London', types: ['locality', 'political'] },
83
+ { long_name: 'United Kingdom', short_name: 'GB', types: ['country', 'political'] },
84
+ ],
85
+ },
86
+ {
87
+ place_id: 'ChIJ51cu8IcbXWARiRtXIothAS4',
88
+ name: 'Tokyo',
89
+ formatted_address: 'Tokyo, Japan',
90
+ location: { lat: 35.6761919, lng: 139.6503106 },
91
+ types: ['locality', 'political'],
92
+ components: [
93
+ { long_name: 'Tokyo', short_name: 'Tokyo', types: ['locality', 'political'] },
94
+ { long_name: 'Japan', short_name: 'JP', types: ['country', 'political'] },
95
+ ],
96
+ },
97
+ ];
98
+ const ZERO_RESULTS_MARKERS = ['nowhere', 'zzzznoresult', 'zero_results', 'this-place-does-not-exist'];
99
+ /** Returns true for a query the twin deliberately maps to ZERO_RESULTS. */
100
+ export function isZeroResultsQuery(q) {
101
+ const n = normalizeAddress(q);
102
+ return ZERO_RESULTS_MARKERS.some((m) => n.includes(m));
103
+ }
104
+ /** Find a canned place by fuzzy name/address substring match. */
105
+ export function findPlaceByText(query) {
106
+ const n = normalizeAddress(query);
107
+ for (const p of PLACES) {
108
+ if (n.includes(normalizeAddress(p.name)))
109
+ return p;
110
+ if (normalizeAddress(p.formatted_address).includes(n) && n.length >= 3)
111
+ return p;
112
+ }
113
+ return null;
114
+ }
115
+ export function findPlaceById(placeId) {
116
+ return PLACES.find((p) => p.place_id === placeId) ?? null;
117
+ }
118
+ /** The country short_name (ccTLD-ish) for a canned place, for component filtering. */
119
+ export function countryOf(p) {
120
+ return p.components.find((c) => c.types.includes('country'))?.short_name?.toLowerCase();
121
+ }
122
+ /** The locality long_name for a canned place (used in plus-code compound codes). */
123
+ export function localityOf(p) {
124
+ return p.components.find((c) => c.types.includes('locality'))?.long_name
125
+ ?? p.components.find((c) => c.types.includes('administrative_area_level_1'))?.long_name
126
+ ?? p.name;
127
+ }
128
+ /** Nearest canned place to a lat/lng (for reverse geocoding). */
129
+ export function findPlaceByLatLng(loc) {
130
+ let best = PLACES[0];
131
+ let bestD = Number.POSITIVE_INFINITY;
132
+ for (const p of PLACES) {
133
+ const d = (p.location.lat - loc.lat) ** 2 + (p.location.lng - loc.lng) ** 2;
134
+ if (d < bestD) {
135
+ bestD = d;
136
+ best = p;
137
+ }
138
+ }
139
+ return best;
140
+ }
141
+ export function allPlaces() {
142
+ return PLACES;
143
+ }
144
+ /**
145
+ * Localized display names for canned places, keyed by language code. The Geocoding
146
+ * API returns localized `formatted_address` / component names per the `language`
147
+ * parameter; the twin models this faithfully for a small set of languages and
148
+ * falls back to the English name otherwise.
149
+ */
150
+ const LOCALIZED_NAMES = {
151
+ 'Tokyo, Japan': { ja: '日本、東京都', fr: 'Tokyo, Japon', es: 'Tokio, Japón', de: 'Tokio, Japan' },
152
+ 'London, UK': { ja: 'イギリス ロンドン', fr: 'Londres, Royaume-Uni', es: 'Londres, Reino Unido', de: 'London, Vereinigtes Königreich' },
153
+ 'Los Angeles, CA, USA': { ja: 'アメリカ合衆国 カリフォルニア州 ロサンゼルス', fr: 'Los Angeles, Californie, États-Unis', es: 'Los Ángeles, California, EE. UU.', de: 'Los Angeles, Kalifornien, USA' },
154
+ 'New York, NY, USA': { ja: 'アメリカ合衆国 ニューヨーク州 ニューヨーク', fr: 'New York, État de New York, États-Unis', es: 'Nueva York, EE. UU.', de: 'New York, USA' },
155
+ };
156
+ /** Localize a formatted_address for a `language` code (no-op for English/unknown). */
157
+ export function localizeFormattedAddress(formatted_address, language) {
158
+ if (!language)
159
+ return formatted_address;
160
+ const lang = language.toLowerCase().split('-')[0];
161
+ if (lang === 'en')
162
+ return formatted_address;
163
+ return LOCALIZED_NAMES[formatted_address]?.[lang] ?? formatted_address;
164
+ }
165
+ /** Build a faithful GeocodeResult from a canned place. */
166
+ export function geocodeResultFromPlace(p, locationType = 'ROOFTOP') {
167
+ return {
168
+ address_components: p.components,
169
+ formatted_address: p.formatted_address,
170
+ geometry: {
171
+ location: p.location,
172
+ location_type: locationType,
173
+ viewport: viewportAround(p.location),
174
+ },
175
+ place_id: p.place_id,
176
+ types: p.types,
177
+ };
178
+ }
179
+ /**
180
+ * Synthesize a deterministic GeocodeResult for an arbitrary (non-canned but
181
+ * valid) address so the twin never falsely 404s a well-formed query. Coordinates
182
+ * are derived from the address hash; shape is faithful.
183
+ */
184
+ export function synthGeocodeResult(address) {
185
+ const loc = deterministicLatLng(normalizeAddress(address));
186
+ const placeId = `ChIJ${stableHash(address)}twin`;
187
+ return {
188
+ address_components: [
189
+ { long_name: address, short_name: address, types: ['route'] },
190
+ { long_name: 'United States', short_name: 'US', types: ['country', 'political'] },
191
+ ],
192
+ formatted_address: address,
193
+ geometry: { location: loc, location_type: 'GEOMETRIC_CENTER', viewport: viewportAround(loc) },
194
+ place_id: placeId,
195
+ types: ['route'],
196
+ };
197
+ }
198
+ /** Great-circle distance (haversine) in meters between two points. */
199
+ export function haversineMeters(a, b) {
200
+ const R = 6371000;
201
+ const toRad = (d) => (d * Math.PI) / 180;
202
+ const dLat = toRad(b.lat - a.lat);
203
+ const dLng = toRad(b.lng - a.lng);
204
+ const lat1 = toRad(a.lat);
205
+ const lat2 = toRad(b.lat);
206
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
207
+ return Math.round(2 * R * Math.asin(Math.sqrt(h)));
208
+ }
209
+ /** Human "5.0 mi" / "1.2 km" text for a distance in meters (imperial default). */
210
+ export function distanceText(meters, metric = false) {
211
+ if (metric) {
212
+ const km = meters / 1000;
213
+ return km >= 1 ? `${km.toFixed(1)} km` : `${meters} m`;
214
+ }
215
+ const mi = meters / 1609.344;
216
+ return mi >= 0.1 ? `${mi.toFixed(1)} mi` : `${Math.round(meters * 3.28084)} ft`;
217
+ }
218
+ /** Human "15 mins" / "1 hour 5 mins" text for a duration in seconds. */
219
+ export function durationText(seconds) {
220
+ const mins = Math.round(seconds / 60);
221
+ if (mins < 60)
222
+ return `${mins} min${mins === 1 ? '' : 's'}`;
223
+ const h = Math.floor(mins / 60);
224
+ const m = mins % 60;
225
+ return m === 0 ? `${h} hour${h === 1 ? '' : 's'}` : `${h} hour${h === 1 ? '' : 's'} ${m} min${m === 1 ? '' : 's'}`;
226
+ }
227
+ /** Deterministic driving duration (seconds) for a distance — ~40 km/h average. */
228
+ export function deterministicDuration(meters) {
229
+ return Math.max(60, Math.round((meters / 1000 / 40) * 3600));
230
+ }
231
+ /**
232
+ * A tiny polyline encoder (Google's Encoded Polyline Algorithm Format), used to
233
+ * produce a faithful `overview_polyline.points` for Directions responses.
234
+ */
235
+ export function encodePolyline(points) {
236
+ let last = [0, 0];
237
+ let out = '';
238
+ const enc = (cur, prev) => {
239
+ let v = Math.round(cur * 1e5) - Math.round(prev * 1e5);
240
+ v = v < 0 ? ~(v << 1) : v << 1;
241
+ let s = '';
242
+ while (v >= 0x20) {
243
+ s += String.fromCharCode((0x20 | (v & 0x1f)) + 63);
244
+ v >>= 5;
245
+ }
246
+ s += String.fromCharCode(v + 63);
247
+ return s;
248
+ };
249
+ for (const p of points) {
250
+ out += enc(p.lat, last[0]);
251
+ out += enc(p.lng, last[1]);
252
+ last = [p.lat, p.lng];
253
+ }
254
+ return out;
255
+ }
256
+ /** Deterministic elevation (meters) for a lat/lng. */
257
+ export function deterministicElevation(loc) {
258
+ const h = parseInt(stableHash(`${loc.lat},${loc.lng}`).slice(0, 8), 16);
259
+ return Math.round(((h % 400000) / 100 - 100) * 1e2) / 1e2; // -100m .. 3900m
260
+ }
261
+ /**
262
+ * Deterministic IANA time zone id + offsets for a lat/lng. When a UTC `timestamp`
263
+ * (seconds) is supplied, northern-hemisphere DST is computed faithfully: zones
264
+ * that observe DST (US/UK) return a non-zero `dstOffset` for timestamps that fall
265
+ * between mid-March and early November; UTC/Japan never observe DST.
266
+ */
267
+ export function deterministicTimeZone(loc, timestamp) {
268
+ const zones = [
269
+ { timeZoneId: 'America/Los_Angeles', stdName: 'Pacific Standard Time', dstName: 'Pacific Daylight Time', rawOffset: -28800, observesDst: true },
270
+ { timeZoneId: 'America/New_York', stdName: 'Eastern Standard Time', dstName: 'Eastern Daylight Time', rawOffset: -18000, observesDst: true },
271
+ { timeZoneId: 'Europe/London', stdName: 'Greenwich Mean Time', dstName: 'British Summer Time', rawOffset: 0, observesDst: true },
272
+ { timeZoneId: 'Asia/Tokyo', stdName: 'Japan Standard Time', dstName: 'Japan Standard Time', rawOffset: 32400, observesDst: false },
273
+ ];
274
+ const idx = parseInt(stableHash(`${loc.lat},${loc.lng}`).slice(0, 4), 16) % zones.length;
275
+ const z = zones[idx];
276
+ let dstOffset = 0;
277
+ if (z.observesDst && timestamp !== undefined && Number.isFinite(timestamp)) {
278
+ const month = new Date(timestamp * 1000).getUTCMonth(); // 0..11
279
+ // Northern-hemisphere DST window: roughly mid-March (2) .. October (9).
280
+ if (month >= 2 && month <= 9)
281
+ dstOffset = 3600;
282
+ }
283
+ return {
284
+ timeZoneId: z.timeZoneId,
285
+ timeZoneName: dstOffset > 0 ? z.dstName : z.stdName,
286
+ rawOffset: z.rawOffset,
287
+ dstOffset,
288
+ };
289
+ }
290
+ /**
291
+ * Open Location Code (Plus Code) encoder. Produces a faithful global_code (the
292
+ * 11th char is '+' after position 8) and a compound_code (global suffix + a
293
+ * locality reference). This is the real OLC algorithm over a deterministic grid.
294
+ */
295
+ const OLC_ALPHABET = '23456789CFGHJMPQRVWX';
296
+ export function encodePlusCode(loc, codeLength = 10) {
297
+ const lat = Math.min(Math.max(loc.lat, -90), 90) + 90;
298
+ const lng = ((loc.lng + 180) % 360 + 360) % 360;
299
+ let code = '';
300
+ // First 10 chars: 5 pairs, each a base-20 digit for lat then lng.
301
+ let latPrecision = 20;
302
+ let lngPrecision = 20;
303
+ for (let i = 0; i < 5; i++) {
304
+ const latDigit = Math.floor(lat / latPrecision) % 20;
305
+ const lngDigit = Math.floor(lng / lngPrecision) % 20;
306
+ code += OLC_ALPHABET[latDigit];
307
+ code += OLC_ALPHABET[lngDigit];
308
+ if (i === 3)
309
+ code += '+';
310
+ latPrecision /= 20;
311
+ lngPrecision /= 20;
312
+ }
313
+ return code.slice(0, codeLength + 1);
314
+ }
315
+ export function plusCodeFor(loc, localityName) {
316
+ const global_code = encodePlusCode(loc);
317
+ // compound_code drops the 4-char area prefix and appends a locality reference.
318
+ const compound_code = `${global_code.slice(4)} ${localityName}`;
319
+ return { global_code, compound_code };
320
+ }
321
+ /** Deterministic next_page_token <-> offset codec for Places pagination. */
322
+ export function encodePageToken(query, offset) {
323
+ return Buffer.from(`${normalizeAddress(query)}::${offset}`).toString('base64url');
324
+ }
325
+ export function decodePageToken(token) {
326
+ try {
327
+ const [query, offsetStr] = Buffer.from(token, 'base64url').toString('utf8').split('::');
328
+ const offset = Number(offsetStr);
329
+ if (query === undefined || !Number.isFinite(offset))
330
+ return null;
331
+ return { query, offset };
332
+ }
333
+ catch {
334
+ return null;
335
+ }
336
+ }
337
+ /** A faithful opening_hours block (weekday_text + periods) for a place. */
338
+ export function openingHoursFor(p) {
339
+ const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
340
+ const periods = days.map((_d, i) => ({
341
+ open: { day: i, time: '0900' },
342
+ close: { day: i, time: '1700' },
343
+ }));
344
+ return {
345
+ open_now: true,
346
+ periods,
347
+ weekday_text: days.map((d) => `${d}: 9:00 AM – 5:00 PM`),
348
+ };
349
+ }
350
+ /** A faithful, deterministic set of reviews for a place. */
351
+ export function reviewsFor(p) {
352
+ return [
353
+ {
354
+ author_name: 'A. Traveler',
355
+ rating: 5,
356
+ relative_time_description: 'a month ago',
357
+ text: `Great experience at ${p.name}.`,
358
+ time: 1700000000,
359
+ language: 'en',
360
+ },
361
+ {
362
+ author_name: 'B. Local',
363
+ rating: 4,
364
+ relative_time_description: '2 months ago',
365
+ text: `Solid spot in ${p.name}.`,
366
+ time: 1697000000,
367
+ language: 'en',
368
+ },
369
+ ];
370
+ }
@@ -0,0 +1,5 @@
1
+ import { type GoogleMapsExecute } from './googlemaps-connector.js';
2
+ export declare function performPending(execute: GoogleMapsExecute, opts?: {
3
+ root?: string;
4
+ occurredAt?: string;
5
+ }): Promise<number>;
@@ -0,0 +1,17 @@
1
+ // A MINIATURE OF THE HEAD, for this pack's own claims and suites (protocol 2). Not on the serve path.
2
+ import { confirmAction, deployableEntries, worldNow } from '@volter/world-core';
3
+ import { pushGoogleMapsAction } from "./googlemaps-connector.js";
4
+ export async function performPending(execute, opts = {}) {
5
+ let pushed = 0;
6
+ for (const entry of deployableEntries('googlemaps', opts.root)) {
7
+ if (!(await pushGoogleMapsAction(entry, execute)))
8
+ continue;
9
+ confirmAction({
10
+ service: 'googlemaps', actionId: entry.id, subject: entry.subject, fields: entry.fields ?? {},
11
+ occurredAt: opts.occurredAt ?? worldNow(), receipt: { status: 'deployed' },
12
+ ...(opts.root !== undefined ? { root: opts.root } : {}),
13
+ });
14
+ pushed += 1;
15
+ }
16
+ return pushed;
17
+ }
@@ -0,0 +1,14 @@
1
+ /** Options every Google Maps-twin HTTP surface needs, independent of who owns the socket. */
2
+ export interface GoogleMapsTwinFetchOptions {
3
+ root?: string;
4
+ readOnly?: boolean;
5
+ }
6
+ export declare function createGoogleMapsTwinFetch(options?: GoogleMapsTwinFetchOptions): (request: Request) => Promise<Response>;
7
+ export declare function createGoogleMapsTwinServer(options?: {
8
+ root?: string;
9
+ port?: number;
10
+ readOnly?: boolean;
11
+ }): Promise<{
12
+ port: number;
13
+ stop: () => void;
14
+ }>;
@@ -0,0 +1,25 @@
1
+ // Google Maps twin HTTP server — serve the full Google Maps Platform twin handler over HTTP so
2
+ // the real clients (pointed at this base URL) work unmodified.
3
+ //
4
+ // FETCH-FIRST (runtime contract R12b): the pack's whole HTTP surface is the plain fetch below,
5
+ // built from the kernel's ONE adaptation (`createTwinFetchFromHandler` — manifest door, body
6
+ // read, header map, worldNow() stamp, JSON reply). This file contributes only VALUES. The
7
+ // server is one line of Bun.serve around that same closure, so the standalone (R1) and hosted
8
+ // lanes execute identical serving bytes.
9
+ import { serveHttp } from '@volter/world-core';
10
+ import { handleGoogleMapsTwinRequest } from "./googlemaps-twin.js";
11
+ import { createTwinFetchFromHandler, statefulTwinManifest } from '@volter/world-core';
12
+ export function createGoogleMapsTwinFetch(options = {}) {
13
+ return createTwinFetchFromHandler(handleGoogleMapsTwinRequest, {
14
+ ...options,
15
+ manifest: statefulTwinManifest({ vendor: 'googlemaps', twinOf: 'the Google Maps Platform web services', stores: 'deterministic geocoding/places/roads answers derived from the request' }),
16
+ });
17
+ }
18
+ export async function createGoogleMapsTwinServer(options = {}) {
19
+ const server = await serveHttp({
20
+ port: options.port ?? 0,
21
+ idleTimeout: 60,
22
+ fetch: createGoogleMapsTwinFetch(options),
23
+ });
24
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { GoogleMapsRequest, GoogleMapsResponse } from './googlemaps-types.js';
2
+ export declare function handleGoogleMapsTwinRequest(req: GoogleMapsRequest): Promise<GoogleMapsResponse>;