@k2b/cloud 0.41.0 → 0.42.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 (227) hide show
  1. package/package.json +6 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-timeout.ts +71 -0
  6. package/src/_internal/define-app.ts +145 -89
  7. package/src/_internal/http-drain.ts +56 -0
  8. package/src/_internal/registry-validation.ts +3 -0
  9. package/src/_internal/runtime-context.ts +1 -0
  10. package/src/_internal/status-preserving-ssr.ts +2 -0
  11. package/src/access/PermissionEditor.tsx +16 -17
  12. package/src/access/PrincipalPicker.tsx +45 -10
  13. package/src/access/messages.ts +8 -0
  14. package/src/access/principal-label.ts +18 -0
  15. package/src/access/ui.ts +1 -0
  16. package/src/account/EntitySearch.tsx +5 -4
  17. package/src/ai/action-review.ts +2 -1
  18. package/src/ai/app-skill-store.ts +1 -1
  19. package/src/ai/capabilities.ts +21 -3
  20. package/src/ai/capability-execution.ts +14 -12
  21. package/src/ai/chat/action-card.ts +7 -1
  22. package/src/ai/chat/action-messages.ts +14 -0
  23. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  24. package/src/ai/chat/action-question.tsx +202 -22
  25. package/src/ai/chat/action-text.ts +1 -1
  26. package/src/ai/chat-tasks.ts +2 -1
  27. package/src/ai/client/controller.ts +150 -32
  28. package/src/ai/client/transport.ts +14 -2
  29. package/src/ai/code-capability-routes.ts +27 -23
  30. package/src/ai/code-runtime-tools.ts +14 -16
  31. package/src/ai/code-source-tools.ts +16 -18
  32. package/src/ai/executor.ts +56 -21
  33. package/src/ai/fetch-file-tool.ts +67 -76
  34. package/src/ai/http.ts +1 -0
  35. package/src/ai/migrate.ts +13 -0
  36. package/src/ai/open-questions.ts +4 -8
  37. package/src/ai/projects.ts +1 -2
  38. package/src/ai/protocol.ts +2 -2
  39. package/src/ai/recent-resources.ts +12 -5
  40. package/src/ai/review-content.ts +77 -0
  41. package/src/ai/routes.ts +69 -31
  42. package/src/ai/runtime.ts +1 -0
  43. package/src/ai/skill-tool.ts +17 -6
  44. package/src/ai/skills-routes.ts +72 -27
  45. package/src/ai/skills.ts +139 -60
  46. package/src/ai/store.ts +168 -33
  47. package/src/ai/types.ts +10 -3
  48. package/src/ai/usage.ts +2 -2
  49. package/src/api/admin-core-settings.ts +15 -2
  50. package/src/api/admin-geo.ts +54 -0
  51. package/src/api/admin-linux-identities.ts +6 -3
  52. package/src/api/admin-outgoing-mail.ts +29 -15
  53. package/src/api/approvals-loader.ts +3 -3
  54. package/src/api/approvals.ts +51 -0
  55. package/src/api/apps.ts +4 -2
  56. package/src/api/capabilities.ts +258 -181
  57. package/src/api/capability-streams.ts +21 -13
  58. package/src/api/cli-plugins.ts +4 -1
  59. package/src/api/geo-glyphs.ts +11 -0
  60. package/src/api/geo.ts +128 -0
  61. package/src/api/help.ts +12 -4
  62. package/src/api/index.ts +5 -0
  63. package/src/api/mcp.ts +46 -38
  64. package/src/api/me-panel-consent.ts +8 -5
  65. package/src/api/search.ts +21 -14
  66. package/src/api/sync-ops.ts +19 -13
  67. package/src/api/widgets.ts +42 -18
  68. package/src/approvals/Approvals.tsx +5 -3
  69. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  70. package/src/browser/CloudResourceSearch.tsx +3 -13
  71. package/src/browser/PanelLeaveNotice.tsx +18 -0
  72. package/src/browser/navigation-search.ts +78 -25
  73. package/src/browser/panel-leave.ts +69 -0
  74. package/src/browser/panel-messages.ts +12 -0
  75. package/src/browser/panel-slot.ts +173 -24
  76. package/src/browser/panel.ts +3 -0
  77. package/src/browser/resource-search-messages.ts +2 -0
  78. package/src/capabilities/client.ts +28 -1
  79. package/src/capabilities/executions.ts +22 -3
  80. package/src/capabilities/geo-client.ts +57 -0
  81. package/src/capabilities/response.ts +2 -1
  82. package/src/capabilities/review-storage.ts +24 -0
  83. package/src/capabilities/reviews.ts +17 -3
  84. package/src/capabilities/server.ts +36 -16
  85. package/src/cli/access.ts +15 -23
  86. package/src/cli/admin/linux.ts +13 -2
  87. package/src/cli/admin/outgoing-mail.ts +26 -4
  88. package/src/cli/capabilities.ts +19 -0
  89. package/src/config/env.ts +0 -6
  90. package/src/contracts/app-access.ts +48 -0
  91. package/src/contracts/app.ts +4 -1
  92. package/src/contracts/approvals.ts +2 -2
  93. package/src/contracts/avatar.ts +186 -0
  94. package/src/contracts/capabilities.ts +148 -54
  95. package/src/contracts/geo-admin.ts +127 -0
  96. package/src/contracts/geo-map.ts +19 -0
  97. package/src/contracts/geo-route.ts +168 -0
  98. package/src/contracts/geo.ts +248 -0
  99. package/src/contracts/index.ts +4 -0
  100. package/src/contracts/outgoing-mail.ts +17 -4
  101. package/src/contracts/registry.ts +2 -0
  102. package/src/events/app-access.ts +68 -0
  103. package/src/events/live-delivery.ts +128 -0
  104. package/src/events/live.ts +14 -130
  105. package/src/geo/CloudMap.tsx +95 -0
  106. package/src/geo/map-messages.ts +17 -0
  107. package/src/geo/ui.ts +1 -0
  108. package/src/index.ts +2 -0
  109. package/src/server/client-ip.ts +36 -0
  110. package/src/server/index.ts +3 -4
  111. package/src/server/middleware/app-access.ts +312 -0
  112. package/src/server/middleware/auth.ts +1 -1
  113. package/src/server/middleware/invocation.ts +11 -0
  114. package/src/server/middleware/rate-limit.ts +4 -4
  115. package/src/server/services/access.ts +13 -9
  116. package/src/server/services/index.ts +0 -2
  117. package/src/server/services/services.ts +0 -2
  118. package/src/services/accounts/posix.ts +21 -7
  119. package/src/services/allowed-network-fetch.ts +16 -10
  120. package/src/services/app-access-metrics.ts +24 -0
  121. package/src/services/app-access.ts +123 -0
  122. package/src/services/app-discovery.ts +27 -0
  123. package/src/services/approvals/content.ts +40 -0
  124. package/src/services/approvals/index.ts +14 -6
  125. package/src/services/approvals/list.ts +6 -11
  126. package/src/services/approvals/question.ts +11 -7
  127. package/src/services/downstream-budget.ts +87 -0
  128. package/src/services/geo/admin.ts +262 -0
  129. package/src/services/geo/bucket.ts +67 -0
  130. package/src/services/geo/cache.ts +57 -0
  131. package/src/services/geo/costs.ts +5 -0
  132. package/src/services/geo/errors.ts +41 -0
  133. package/src/services/geo/fixtures/README.md +17 -0
  134. package/src/services/geo/fixtures/along.json +355 -0
  135. package/src/services/geo/fixtures/autocomplete.json +218 -0
  136. package/src/services/geo/fixtures/geocode.json +129 -0
  137. package/src/services/geo/fixtures/places.json +355 -0
  138. package/src/services/geo/fixtures/reverse.json +61 -0
  139. package/src/services/geo/fixtures/routing.json +57 -0
  140. package/src/services/geo/geoapify-route.ts +93 -0
  141. package/src/services/geo/geoapify.ts +281 -0
  142. package/src/services/geo/ledger.ts +122 -0
  143. package/src/services/geo/map.ts +160 -0
  144. package/src/services/geo/migrate.ts +25 -0
  145. package/src/services/geo/polyline.ts +184 -0
  146. package/src/services/geo/route-chain.ts +145 -0
  147. package/src/services/geo/route-costs.ts +9 -0
  148. package/src/services/geo/route-geometry.ts +39 -0
  149. package/src/services/geo/route-plan.ts +172 -0
  150. package/src/services/geo/route-service.ts +433 -0
  151. package/src/services/geo/service.ts +300 -0
  152. package/src/services/geo/settings.ts +49 -0
  153. package/src/services/help/index.ts +16 -7
  154. package/src/services/help/types.ts +6 -1
  155. package/src/services/identity/invocation-actor.ts +20 -2
  156. package/src/services/identity/issue-invocation.ts +67 -0
  157. package/src/services/identity/key-ring.ts +5 -3
  158. package/src/services/identity/runtime-config.ts +23 -0
  159. package/src/services/index.ts +9 -0
  160. package/src/services/mandates/index.ts +40 -3
  161. package/src/services/network-security.ts +6 -3
  162. package/src/services/network-transport.ts +99 -0
  163. package/src/services/notifications/catalog.ts +3 -2
  164. package/src/services/notifications/runtime.ts +1 -0
  165. package/src/services/notifications/web-push-transport.ts +45 -38
  166. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  167. package/src/services/outgoing-mail/index.ts +55 -1
  168. package/src/services/outgoing-mail/messages.ts +82 -3
  169. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  170. package/src/services/outgoing-mail/retention.ts +13 -1
  171. package/src/services/outgoing-mail/store.ts +36 -6
  172. package/src/services/outgoing-mail/test-send.ts +34 -4
  173. package/src/services/outgoing-mail/transport.ts +1 -1
  174. package/src/services/pdf/gotenberg.ts +11 -38
  175. package/src/services/pdf/template-preview.ts +1 -1
  176. package/src/services/public-http.ts +28 -40
  177. package/src/services/settings/app.ts +4 -1
  178. package/src/services/settings/core-settings.ts +88 -0
  179. package/src/services/settings/defaults.ts +8 -2
  180. package/src/services/settings/errors.ts +11 -0
  181. package/src/services/settings/geo-budget.ts +40 -0
  182. package/src/services/settings/index.ts +4 -3
  183. package/src/services/settings/store.ts +25 -17
  184. package/src/services/weather/geo.ts +29 -111
  185. package/src/services/weather/index.ts +4 -3
  186. package/src/services/weather/locations.ts +8 -5
  187. package/src/services/weather/migrate.ts +1 -0
  188. package/src/services/weather/settings.ts +0 -16
  189. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  190. package/src/shared/avatar-art.ts +400 -0
  191. package/src/shared/index.ts +2 -0
  192. package/src/shared/network-address.ts +22 -3
  193. package/src/shared/template-rendering.ts +36 -4
  194. package/src/shared-text/documents.ts +1 -1
  195. package/src/shared-text/index.ts +2 -0
  196. package/src/shared-text/log.ts +51 -3
  197. package/src/shared-text/snapshots.ts +340 -0
  198. package/src/ssr/AdminLayout.tsx +4 -2
  199. package/src/ssr/CloudShell.island.tsx +6 -0
  200. package/src/ssr/Layout.tsx +65 -15
  201. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  202. package/src/ssr/LayoutHelp.tsx +3 -2
  203. package/src/ssr/PanelRuntime.island.tsx +11 -7
  204. package/src/ssr/PwaLayout.tsx +2 -2
  205. package/src/ssr/RailApps.tsx +6 -0
  206. package/src/ssr/ShellRuntime.island.tsx +23 -7
  207. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  208. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
  209. package/src/ssr/admin-navigation.ts +1 -0
  210. package/src/ssr/app-badges.ts +6 -1
  211. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  212. package/src/ssr/help.ts +3 -2
  213. package/src/ssr/index.ts +1 -1
  214. package/src/ssr/layout-context.ts +2 -0
  215. package/src/ssr/mobile-menu-history.ts +4 -0
  216. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  217. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  218. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  219. package/src/ssr/page-swap.ts +28 -0
  220. package/src/ssr/page-trail-runtime.ts +32 -13
  221. package/src/ssr/panel-consent.ts +2 -2
  222. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  223. package/src/ssr/platform-messages.ts +8 -0
  224. package/src/ssr/runtime.ts +17 -0
  225. package/src/ssr/shell-data.tsx +10 -2
  226. package/src/styles/global.css +70 -6
  227. package/src/server/services/geo.ts +0 -215
@@ -0,0 +1,433 @@
1
+ import { z } from "zod";
2
+ import { GeoGeocodeDataSchema, type GeoGeocodeInput } from "../../contracts/geo";
3
+ import {
4
+ GeoRouteCalculateDataSchema,
5
+ type GeoRouteCalculateInput,
6
+ GeoRouteCalculateInputSchema,
7
+ type GeoRouteLeg,
8
+ GeoRouteLegSchema,
9
+ type GeoRouteOptions,
10
+ type GeoRouteResolvedStop,
11
+ GeoRouteResolvedStopSchema,
12
+ type GeoRouteStop,
13
+ type GeoRouteUpdateData,
14
+ GeoRouteUpdateDataSchema,
15
+ type GeoRouteUpdateInput,
16
+ GeoRouteUpdateInputSchema,
17
+ } from "../../contracts/geo-route";
18
+ import { geoCacheKey, normalizeGeoInput } from "./cache";
19
+ import { GeoError, limitExceeded } from "./errors";
20
+ import type { GeoapifyAdapter } from "./geoapify";
21
+ import { geoDistance } from "./polyline";
22
+ import {
23
+ type GeoRouteCallPricing,
24
+ type GeoRouteChainCall,
25
+ type GeoRouteChainDependencies,
26
+ reserveRouteCall,
27
+ runGeoRouteChain,
28
+ } from "./route-chain";
29
+ import {
30
+ assembleRoute,
31
+ insertionGaps,
32
+ routeOptions,
33
+ routeSummary,
34
+ sameRouteOptions,
35
+ validateRoutePlan,
36
+ validateRouteStops,
37
+ validateSavedRoute,
38
+ walkDistance,
39
+ } from "./route-plan";
40
+ import type { GeoContext } from "./service";
41
+ import type { GeoConfig } from "./settings";
42
+
43
+ const providerData = z
44
+ .object({
45
+ stops: z.array(GeoRouteResolvedStopSchema).min(2).max(26),
46
+ legs: z.array(GeoRouteLegSchema).min(1).max(25),
47
+ distanceMeters: z.number().nonnegative(),
48
+ })
49
+ .strict();
50
+ type RouteCall = GeoRouteChainCall;
51
+ const parse = <T>(schema: z.ZodType<T>, input: unknown): T => {
52
+ const parsed = schema.safeParse(input);
53
+ if (!parsed.success) {
54
+ const issue = parsed.error.issues[0]!;
55
+ if (issue.code === "too_big" || issue.code === "too_small") {
56
+ const actual = issue.path.reduce<unknown>(
57
+ (value, key) => (value && typeof value === "object" ? Reflect.get(value, key) : undefined),
58
+ input,
59
+ );
60
+ limitExceeded(
61
+ issue.path.join("."),
62
+ Number(issue.code === "too_big" ? issue.maximum : issue.minimum),
63
+ Array.isArray(actual) || typeof actual === "string" ? actual.length : typeof actual === "number" ? actual : 0,
64
+ );
65
+ }
66
+ throw new GeoError("GEO_UNSUPPORTED_OPTION");
67
+ }
68
+ return parsed.data;
69
+ };
70
+ type PlannedCall = { ids: string[]; options: GeoRouteOptions; optimize: boolean };
71
+ export const createGeoRouteService = (
72
+ dependencies: GeoRouteChainDependencies & {
73
+ configured: (context: GeoContext) => Promise<GeoConfig>;
74
+ adapter: GeoapifyAdapter;
75
+ },
76
+ ) => {
77
+ const prepare = async (stops: readonly GeoRouteStop[], context: GeoContext) => {
78
+ const addresses = new Map<string, z.infer<typeof GeoGeocodeDataSchema>>();
79
+ const missing = new Map<string, GeoGeocodeInput>();
80
+ for (const stop of stops) {
81
+ if (!stop.address) continue;
82
+ const key = geoCacheKey("geocode", stop.address, context.locale ?? "en");
83
+ if (addresses.has(key) || missing.has(key)) continue;
84
+ const hit = await dependencies.cache.get(key, GeoGeocodeDataSchema);
85
+ if (hit) addresses.set(key, hit);
86
+ else missing.set(key, stop.address);
87
+ }
88
+ const known = stops.map((stop) =>
89
+ stop.point
90
+ ? { id: stop.id, label: stop.label, point: stop.point }
91
+ : (() => {
92
+ const found = addresses.get(geoCacheKey("geocode", stop.address, context.locale ?? "en"));
93
+ return found ? { id: stop.id, label: stop.label, point: found.address.point, address: found.address } : undefined;
94
+ })(),
95
+ );
96
+ return {
97
+ cost: missing.size * 1000,
98
+ known,
99
+ async resolve(call: RouteCall, config: GeoConfig): Promise<GeoRouteResolvedStop[]> {
100
+ const result: GeoRouteResolvedStop[] = [];
101
+ for (const stop of stops) {
102
+ if (stop.point) {
103
+ result.push({ id: stop.id, label: stop.label, point: stop.point });
104
+ continue;
105
+ }
106
+ const key = geoCacheKey("geocode", stop.address, context.locale ?? "en");
107
+ let found = addresses.get(key);
108
+ if (!found) {
109
+ found = await call("geocode", stop.address, GeoGeocodeDataSchema, async (signal) => {
110
+ const data = await dependencies.adapter.geocode(stop.address!, config.apiKey, context.locale ?? "en", signal);
111
+ if (!data.candidates.length) throw new GeoError("GEO_ADDRESS_NOT_FOUND", { stopId: stop.id });
112
+ if (data.candidates.length > 1) throw new GeoError("GEO_ADDRESS_AMBIGUOUS", { stopId: stop.id, candidates: data.candidates });
113
+ return { address: data.candidates[0]!, provenance: data.provenance };
114
+ });
115
+ addresses.set(key, found);
116
+ }
117
+ result.push({ id: stop.id, label: stop.label, point: found.address.point, address: found.address });
118
+ }
119
+ return result;
120
+ },
121
+ };
122
+ };
123
+ const pricing = (step: PlannedCall, allowance: number): GeoRouteCallPricing => ({
124
+ pairs: step.ids.length - 1,
125
+ avoids: step.options.avoid?.length ?? 0,
126
+ optimize: step.optimize,
127
+ distanceAllowanceMeters: allowance,
128
+ });
129
+ const preflight = (
130
+ stops: readonly (GeoRouteResolvedStop | undefined)[],
131
+ options: GeoRouteOptions,
132
+ allowance: number,
133
+ optimize = false,
134
+ ) => {
135
+ if (optimize) {
136
+ // Every route connects all known stops. Their minimum spanning tree is an order-independent lower bound.
137
+ const known = stops.filter((stop): stop is GeoRouteResolvedStop => !!stop);
138
+ const connected = new Set<number>([0]);
139
+ let lowerBound = 0;
140
+ while (connected.size < known.length) {
141
+ let shortest = Infinity,
142
+ next = -1;
143
+ for (const from of connected)
144
+ for (let to = 0; to < known.length; to++)
145
+ if (!connected.has(to)) {
146
+ const distance = geoDistance(known[from]!.point, known[to]!.point);
147
+ if (distance < shortest) {
148
+ shortest = distance;
149
+ next = to;
150
+ }
151
+ }
152
+ connected.add(next);
153
+ lowerBound += shortest;
154
+ }
155
+ const pairs = stops.length - 1;
156
+ if (lowerBound > pairs * allowance) limitExceeded("distanceAllowanceMeters", pairs * allowance, lowerBound);
157
+ if (options.mode === "walk" && lowerBound > pairs * 100_000) limitExceeded("walk.legMeters", pairs * 100_000, lowerBound);
158
+ return;
159
+ }
160
+ for (let i = 1; i < stops.length; i++) {
161
+ const a = stops[i - 1],
162
+ b = stops[i];
163
+ if (!a || !b) continue;
164
+ const distance = geoDistance(a.point, b.point);
165
+ if (distance > allowance) limitExceeded("distanceAllowanceMeters", allowance, distance);
166
+ walkDistance([a, b], options);
167
+ }
168
+ };
169
+ const execute = async (
170
+ step: PlannedCall,
171
+ stops: readonly GeoRouteResolvedStop[],
172
+ allowance: number,
173
+ call: RouteCall,
174
+ config: GeoConfig,
175
+ context: GeoContext,
176
+ ) => {
177
+ const selected = step.ids.map((id) => stops.find((stop) => stop.id === id)!);
178
+ preflight(selected, step.options, allowance, step.optimize);
179
+ return call(
180
+ "route",
181
+ { stops: selected, options: step.options, optimize: step.optimize },
182
+ providerData,
183
+ (signal) => dependencies.adapter.route(selected, step.options, step.optimize, config.apiKey, context.locale ?? "en", signal),
184
+ pricing(step, allowance),
185
+ (data) => data.distanceMeters,
186
+ );
187
+ };
188
+ const calculate = async (input: GeoRouteCalculateInput, context: GeoContext) => {
189
+ const config = await dependencies.configured(context);
190
+ const { plan } = parse(GeoRouteCalculateInputSchema, input);
191
+ const { defaults, byDeparture } = validateRoutePlan(plan);
192
+ const allowance = plan.distanceAllowanceMeters ?? 500_000;
193
+ const key = geoCacheKey(
194
+ "route",
195
+ {
196
+ calculate: {
197
+ stops: plan.stops.map((stop) => (stop.address ? { ...stop, address: normalizeGeoInput(stop.address, "geocode") } : stop)),
198
+ options: defaults,
199
+ distanceAllowanceMeters: allowance,
200
+ optimize: plan.optimize ?? false,
201
+ legOptions: plan.stops.flatMap((stop) => {
202
+ const options = byDeparture.get(stop.id);
203
+ return options && !sameRouteOptions(options, defaults) ? [{ fromStopId: stop.id, options }] : [];
204
+ }),
205
+ },
206
+ },
207
+ context.locale ?? "en",
208
+ );
209
+ const cached = await dependencies.cache.get(key, GeoRouteCalculateDataSchema);
210
+ if (cached) return cached;
211
+ const prepared = await prepare(plan.stops, context);
212
+ const steps: PlannedCall[] = [];
213
+ if (plan.optimize) {
214
+ steps.push({ ids: plan.stops.map((stop) => stop.id), options: defaults, optimize: true });
215
+ for (const [id, options] of byDeparture)
216
+ if (!sameRouteOptions(options, defaults)) {
217
+ const i = plan.stops.findIndex((stop) => stop.id === id);
218
+ steps.push({ ids: [id, plan.stops[i + 1]!.id], options, optimize: false });
219
+ }
220
+ } else {
221
+ for (let i = 0; i < plan.stops.length - 1; i++) {
222
+ const options = byDeparture.get(plan.stops[i]!.id) ?? defaults;
223
+ const last = steps.at(-1);
224
+ if (last && sameRouteOptions(last.options, options)) last.ids.push(plan.stops[i + 1]!.id);
225
+ else steps.push({ ids: [plan.stops[i]!.id, plan.stops[i + 1]!.id], options, optimize: false });
226
+ }
227
+ }
228
+ for (const step of plan.optimize ? steps.slice(0, 1) : steps)
229
+ preflight(
230
+ step.ids.map((id) => prepared.known[plan.stops.findIndex((stop) => stop.id === id)]),
231
+ step.options,
232
+ allowance,
233
+ step.optimize,
234
+ );
235
+ const result = await runGeoRouteChain(
236
+ dependencies,
237
+ context,
238
+ config,
239
+ prepared.cost + steps.reduce((sum, step) => sum + reserveRouteCall(pricing(step, allowance)), 0),
240
+ async (call) => {
241
+ let stops = await prepared.resolve(call, config);
242
+ let legs: GeoRouteLeg[] = [];
243
+ const first = steps[0]!;
244
+ if (plan.optimize) {
245
+ const optimized = await execute(first, stops, allowance, call, config, context);
246
+ stops = optimized.stops;
247
+ legs = optimized.legs;
248
+ for (let i = 0; i < legs.length; i++) {
249
+ const options = byDeparture.get(stops[i]!.id);
250
+ if (options && !sameRouteOptions(options, defaults)) {
251
+ const step = { ids: [stops[i]!.id, stops[i + 1]!.id], options, optimize: false };
252
+ legs[i] = (await execute(step, stops, allowance, call, config, context)).legs[0]!;
253
+ }
254
+ }
255
+ } else {
256
+ // Validate the complete resolved route before the first routing call.
257
+ for (const step of steps)
258
+ preflight(
259
+ step.ids.map((id) => stops.find((stop) => stop.id === id)!),
260
+ step.options,
261
+ allowance,
262
+ );
263
+ for (const step of steps) legs.push(...(await execute(step, stops, allowance, call, config, context)).legs);
264
+ }
265
+ return { route: assembleRoute(stops, defaults, legs, dependencies.now(), allowance) };
266
+ },
267
+ );
268
+ await dependencies.cache.put(key, "route", result, false);
269
+ return result;
270
+ };
271
+ const update = async (input: GeoRouteUpdateInput, context: GeoContext): Promise<GeoRouteUpdateData> => {
272
+ const config = await dependencies.configured(context);
273
+ const { route, changes, distanceAllowanceMeters } = parse(GeoRouteUpdateInputSchema, input);
274
+ validateSavedRoute(route);
275
+ const allowance = distanceAllowanceMeters ?? route.distanceAllowanceMeters;
276
+ const key = geoCacheKey("route", { update: input }, context.locale ?? "en");
277
+ const cached = await dependencies.cache.get(key, GeoRouteUpdateDataSchema);
278
+ if (cached) return cached;
279
+ let defaults = routeOptions(route.options),
280
+ stops = [...route.stops];
281
+ const byDeparture = new Map(route.legs.map((leg) => [leg.fromStopId, routeOptions(leg.options)]));
282
+ const steps: PlannedCall[] = [];
283
+ let insertion: GeoRouteUpdateData["insertion"];
284
+ let inserted: Awaited<ReturnType<typeof prepare>> | undefined;
285
+ let fixedGap: number | undefined;
286
+ if (changes.type === "insert") {
287
+ validateRouteStops([...route.stops.map((stop) => ({ id: stop.id, label: stop.label, point: stop.point })), changes.stop]);
288
+ if (changes.afterStopId !== undefined) {
289
+ fixedGap = stops.findIndex((stop) => stop.id === changes.afterStopId);
290
+ if (fixedGap < 0 || fixedGap >= stops.length - 1) throw new GeoError("GEO_UNSUPPORTED_OPTION");
291
+ }
292
+ inserted = await prepare([changes.stop], context);
293
+ // Reserve the most expensive possible three gaps before resolving an address.
294
+ const candidateIndexes = fixedGap !== undefined ? [fixedGap] : stops.slice(0, -1).map((_, i) => i);
295
+ for (const index of candidateIndexes)
296
+ steps.push({
297
+ ids: [stops[index]!.id, changes.stop.id, stops[index + 1]!.id],
298
+ options: route.legs[index]!.options,
299
+ optimize: false,
300
+ });
301
+ steps.sort((a, b) => reserveRouteCall(pricing(b, allowance)) - reserveRouteCall(pricing(a, allowance)));
302
+ steps.splice(3);
303
+ const point = inserted.known[0];
304
+ if (point)
305
+ for (const index of fixedGap !== undefined ? [fixedGap] : insertionGaps(route, point.point))
306
+ preflight([stops[index], point, stops[index + 1]], route.legs[index]!.options, allowance);
307
+ } else if (changes.type === "move") {
308
+ const index = stops.findIndex((stop) => stop.id === changes.stopId);
309
+ if (index < 0 || changes.toIndex >= stops.length) throw new GeoError("GEO_UNSUPPORTED_OPTION");
310
+ const [stop] = stops.splice(index, 1);
311
+ stops.splice(changes.toIndex, 0, stop!);
312
+ } else if (changes.type === "options") {
313
+ if (changes.leg) {
314
+ const index = route.legs.findIndex((leg) => leg.fromStopId === changes.leg!.fromStopId && leg.toStopId === changes.leg!.toStopId);
315
+ if (index < 0) throw new GeoError("GEO_UNSUPPORTED_OPTION");
316
+ byDeparture.set(changes.leg.fromStopId, routeOptions(changes.options, route.legs[index]!.options));
317
+ } else {
318
+ defaults = routeOptions(changes.options, defaults);
319
+ byDeparture.clear();
320
+ }
321
+ } else {
322
+ steps.push({ ids: stops.map((stop) => stop.id), options: defaults, optimize: true });
323
+ for (const leg of route.legs)
324
+ if (!sameRouteOptions(leg.options, defaults))
325
+ steps.push({ ids: [leg.fromStopId, leg.toStopId], options: leg.options, optimize: false });
326
+ }
327
+ if (changes.type !== "insert" && changes.type !== "optimize") {
328
+ for (let i = 0; i < stops.length - 1; i++) {
329
+ const from = stops[i]!,
330
+ to = stops[i + 1]!,
331
+ options = byDeparture.get(from.id) ?? defaults;
332
+ const reusable = route.legs.find(
333
+ (leg) => leg.fromStopId === from.id && leg.toStopId === to.id && sameRouteOptions(leg.options, options),
334
+ );
335
+ if (!reusable) {
336
+ const last = steps.at(-1);
337
+ if (last && last.ids.at(-1) === from.id && sameRouteOptions(last.options, options)) last.ids.push(to.id);
338
+ else steps.push({ ids: [from.id, to.id], options, optimize: false });
339
+ }
340
+ }
341
+ }
342
+ if (changes.type !== "insert")
343
+ for (const step of changes.type === "optimize" ? steps.slice(0, 1) : steps)
344
+ preflight(
345
+ step.ids.map((id) => stops.find((stop) => stop.id === id)!),
346
+ step.options,
347
+ allowance,
348
+ step.optimize,
349
+ );
350
+ const work = async (call: RouteCall) => {
351
+ let legs: GeoRouteLeg[];
352
+ if (changes.type === "insert") {
353
+ const [point] = await inserted!.resolve(call, config);
354
+ const gaps = fixedGap !== undefined ? [fixedGap] : insertionGaps(route, point!.point);
355
+ // Resolve and validate every candidate before the first paid route request.
356
+ for (const index of gaps) preflight([stops[index], point, stops[index + 1]], route.legs[index]!.options, allowance);
357
+ let winner: { index: number; legs: GeoRouteLeg[]; detour: number; meters: number } | undefined;
358
+ for (const index of gaps) {
359
+ let result: z.infer<typeof providerData>;
360
+ try {
361
+ result = await execute(
362
+ { ids: [stops[index]!.id, point!.id, stops[index + 1]!.id], options: route.legs[index]!.options, optimize: false },
363
+ [...stops, point!],
364
+ allowance,
365
+ call,
366
+ config,
367
+ context,
368
+ );
369
+ } catch (error) {
370
+ if (error instanceof GeoError && error.code === "GEO_NO_ROUTE") continue;
371
+ throw error;
372
+ }
373
+ const detour = result.legs.reduce((sum, leg) => sum + leg.durationSeconds, 0) - route.legs[index]!.durationSeconds;
374
+ const meters = result.distanceMeters - route.legs[index]!.distanceMeters;
375
+ if (!winner || detour < winner.detour || (detour === winner.detour && meters < winner.meters))
376
+ winner = { index, legs: result.legs, detour, meters };
377
+ }
378
+ if (!winner) throw new GeoError("GEO_NO_ROUTE");
379
+ legs = [...route.legs];
380
+ legs.splice(winner.index, 1, ...winner.legs);
381
+ insertion = {
382
+ afterStopId: stops[winner!.index]!.id,
383
+ candidateGaps: gaps.length,
384
+ exhaustive: fixedGap !== undefined || route.legs.length <= 3,
385
+ };
386
+ stops.splice(winner!.index + 1, 0, point!);
387
+ } else if (changes.type === "optimize") {
388
+ const result = await execute(steps[0]!, stops, allowance, call, config, context);
389
+ stops = result.stops;
390
+ legs = result.legs;
391
+ for (let i = 0; i < legs.length; i++) {
392
+ const options = byDeparture.get(stops[i]!.id) ?? defaults;
393
+ if (!sameRouteOptions(options, defaults))
394
+ legs[i] = (
395
+ await execute({ ids: [stops[i]!.id, stops[i + 1]!.id], options, optimize: false }, stops, allowance, call, config, context)
396
+ ).legs[0]!;
397
+ }
398
+ } else {
399
+ const replacements = new Map<string, GeoRouteLeg>();
400
+ for (const step of steps)
401
+ for (const leg of (await execute(step, stops, allowance, call, config, context)).legs) replacements.set(leg.fromStopId, leg);
402
+ legs = stops
403
+ .slice(0, -1)
404
+ .map(
405
+ (stop, i) =>
406
+ replacements.get(stop.id) ?? route.legs.find((leg) => leg.fromStopId === stop.id && leg.toStopId === stops[i + 1]!.id)!,
407
+ );
408
+ }
409
+ const next = assembleRoute(stops, defaults, legs, dependencies.now(), allowance);
410
+ const before = routeSummary(route),
411
+ after = routeSummary(next);
412
+ return GeoRouteUpdateDataSchema.parse({
413
+ route: next,
414
+ before,
415
+ after,
416
+ delta: {
417
+ distanceMeters: after.distanceMeters - before.distanceMeters,
418
+ durationSeconds: after.durationSeconds - before.durationSeconds,
419
+ },
420
+ ...(insertion ? { insertion } : {}),
421
+ });
422
+ };
423
+ const cost = (inserted?.cost ?? 0) + steps.reduce((sum, step) => sum + reserveRouteCall(pricing(step, allowance)), 0);
424
+ const result = cost
425
+ ? await runGeoRouteChain(dependencies, context, config, cost, work)
426
+ : await work(async () => {
427
+ throw new GeoError("GEO_PROVIDER_FAILED");
428
+ });
429
+ await dependencies.cache.put(key, "route", result, false);
430
+ return result;
431
+ };
432
+ return { calculateRoute: calculate, updateRoute: update };
433
+ };