@jsm-mit/map-routes-package 0.0.0-stage → 0.2.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.
@@ -0,0 +1,150 @@
1
+ // The agent and the error handling of the package: one HttpAgent per host, a stopped canister
2
+ // waited out, `{ Ok } | { Err }` turned into a value or a thrown `CanisterError`, a time
3
+ // limit. (The retry and the error shapes are a copy of @jsm-mit/tigi-sharded-package's.)
4
+ import { Actor, HttpAgent } from "@icp-sdk/core/agent";
5
+ export const MAINNET_HOST = "https://icp0.io";
6
+ /** True for a replica on this machine: localhost, *.localhost, 127.0.0.1 or [::1]. */
7
+ export function isLocalHost(host) {
8
+ let hostname;
9
+ try {
10
+ hostname = new URL(host).hostname;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
16
+ }
17
+ /** One anonymous HttpAgent per host: the canister answers anyone. */
18
+ const agents = new Map();
19
+ function agentOf(host) {
20
+ let entry = agents.get(host);
21
+ if (!entry) {
22
+ const agent = HttpAgent.createSync({ host });
23
+ const ready = isLocalHost(host) ? agent.fetchRootKey().then(() => undefined) : Promise.resolve();
24
+ entry = { agent, ready };
25
+ agents.set(host, entry);
26
+ }
27
+ return entry;
28
+ }
29
+ export async function rawActorAsync(idlFactory, canisterId, options) {
30
+ const { agent, ready } = agentOf(options.host ?? MAINNET_HOST);
31
+ await ready;
32
+ return Actor.createActor(idlFactory, { agent, canisterId });
33
+ }
34
+ /** One agent per host and identity: a pass names the principal that calls the worker. */
35
+ const signedAgents = new Map();
36
+ export async function signedActorAsync(idlFactory, canisterId, host, identity) {
37
+ if (!identity) {
38
+ return rawActorAsync(idlFactory, canisterId, { host });
39
+ }
40
+ const key = `${host} ${identity.getPrincipal().toText()}`;
41
+ let entry = signedAgents.get(key);
42
+ if (!entry) {
43
+ const agent = HttpAgent.createSync({ host, identity });
44
+ const ready = isLocalHost(host) ? agent.fetchRootKey().then(() => undefined) : Promise.resolve();
45
+ entry = { agent, ready };
46
+ signedAgents.set(key, entry);
47
+ }
48
+ await entry.ready;
49
+ return Actor.createActor(idlFactory, { agent: entry.agent, canisterId });
50
+ }
51
+ // ----- a stopped canister (an upgrade runs) -----
52
+ /**
53
+ * A canister that is stopped or stopping — an upgrade of it runs — refuses every call with
54
+ * IC0508 / IC0509. The call did NOT run, so it is tried again: doubling pauses from
55
+ * `STOPPED_RETRY_FIRST_DELAY_MS`, at most `STOPPED_RETRY_MAX_FACTOR` times it, each 0.5–1.5
56
+ * of its length at random, `STOPPED_RETRY_ATTEMPTS` tries in all: about a minute — unless
57
+ * the time limit of the call ends first.
58
+ */
59
+ export const STOPPED_RETRY_FIRST_DELAY_MS = 1000;
60
+ export const STOPPED_RETRY_MAX_FACTOR = 8;
61
+ export const STOPPED_RETRY_ATTEMPTS = 10;
62
+ const STOPPED_ERROR_CODES = ["IC0508", "IC0509"];
63
+ const STOPPED_REJECTS = [/ is stopped\b/, / is stopping\b/];
64
+ /** True for the refusal of a stopped or stopping canister. */
65
+ export function isCanisterStoppedError(error) {
66
+ if (!(error instanceof Error))
67
+ return false;
68
+ const raw = error.message === "CriticalCanisterError" ? error.cause?.rawError : error;
69
+ if (!(raw instanceof Error) && (typeof raw !== "object" || raw === null))
70
+ return false;
71
+ const code = raw.cause?.code;
72
+ if (typeof code?.rejectErrorCode === "string" && STOPPED_ERROR_CODES.includes(code.rejectErrorCode))
73
+ return true;
74
+ const text = typeof code?.rejectMessage === "string" ? code.rejectMessage : raw instanceof Error ? raw.message : "";
75
+ return STOPPED_REJECTS.some((pattern) => pattern.test(text));
76
+ }
77
+ /** The wait before try `attempt + 1`: doubling, capped, 0.5–1.5 of it at random. */
78
+ export function stoppedRetryDelayMs(attempt, firstDelayMs = STOPPED_RETRY_FIRST_DELAY_MS) {
79
+ const base = firstDelayMs * Math.min(2 ** (attempt - 1), STOPPED_RETRY_MAX_FACTOR);
80
+ return base * (0.5 + Math.random());
81
+ }
82
+ /** Tests shorten the waits. */
83
+ export const retrySettings = {
84
+ stoppedFirstDelayMs: STOPPED_RETRY_FIRST_DELAY_MS,
85
+ stoppedAttempts: STOPPED_RETRY_ATTEMPTS,
86
+ };
87
+ export async function waitOutStoppedAsync(fnAsync) {
88
+ for (let attempt = 1;; attempt++) {
89
+ try {
90
+ return await fnAsync();
91
+ }
92
+ catch (err) {
93
+ if (!isCanisterStoppedError(err) || attempt >= retrySettings.stoppedAttempts)
94
+ throw err;
95
+ }
96
+ await new Promise((resolve) => setTimeout(resolve, stoppedRetryDelayMs(attempt, retrySettings.stoppedFirstDelayMs)));
97
+ }
98
+ }
99
+ // ----- the time limit -----
100
+ /** The error of a call that did not answer within its time limit. */
101
+ export function timeoutError(timeoutMs) {
102
+ return new Error("Timeout", { cause: { timeoutMs } });
103
+ }
104
+ export function isTimeoutError(error) {
105
+ return error instanceof Error && error.message === "Timeout";
106
+ }
107
+ /**
108
+ * The call, or `Timeout` after `timeoutMs` (none when undefined). The call itself goes on
109
+ * (a query cannot be called back); its late answer is dropped.
110
+ */
111
+ export async function withTimeoutAsync(fnAsync, timeoutMs) {
112
+ if (timeoutMs === undefined)
113
+ return fnAsync();
114
+ let timer;
115
+ const limit = new Promise((_, reject) => {
116
+ timer = setTimeout(() => reject(timeoutError(timeoutMs)), timeoutMs);
117
+ });
118
+ try {
119
+ return await Promise.race([fnAsync(), limit]);
120
+ }
121
+ finally {
122
+ clearTimeout(timer);
123
+ }
124
+ }
125
+ /**
126
+ * `{ Ok: T }` resolves with `T`; `{ Err }` is thrown as `Error("CanisterError", { cause:
127
+ * { errorKey, errorMessage } })` with the variant's name as the errorKey (`OutsideArea`,
128
+ * `NoRoute`, `NotReady`, `InvalidData`); any transport failure as
129
+ * `Error("CriticalCanisterError", { cause: { rawError } })`. A stopped canister is waited
130
+ * out first.
131
+ */
132
+ export async function executeRustAsyncUnsafe(fnAsync) {
133
+ let result;
134
+ try {
135
+ result = await waitOutStoppedAsync(fnAsync);
136
+ }
137
+ catch (err) {
138
+ throw new Error("CriticalCanisterError", { cause: { rawError: err } });
139
+ }
140
+ if ("Ok" in result)
141
+ return result.Ok;
142
+ const errorKey = Object.keys(result.Err)[0] ?? "Unknown";
143
+ throw new Error("CanisterError", { cause: { errorKey, errorMessage: result.Err[errorKey] } });
144
+ }
145
+ /** The errorKey of a `CanisterError`, or undefined for any other error. */
146
+ export function errorKeyOf(error) {
147
+ if (!(error instanceof Error) || error.message !== "CanisterError")
148
+ return undefined;
149
+ return error.cause?.errorKey;
150
+ }
@@ -0,0 +1,109 @@
1
+ import type { Identity } from "@icp-sdk/core/agent";
2
+ import { Principal } from "@icp-sdk/core/principal";
3
+ import type { RouteAnswer } from "../declarations/map-routes-worker/map-routes-worker.did.js";
4
+ import { type AreaView, type Bbox } from "./passes.js";
5
+ import type { LatLng } from "./types.js";
6
+ export type { AreaView, Bbox, HeldPass } from "./passes.js";
7
+ /** What a pass provider is asked: a pass for this area (the provider may narrow the bbox). */
8
+ export type PassRequest = {
9
+ area: string;
10
+ bbox: Bbox;
11
+ };
12
+ /** A pass as the pass core issues it (`issuePass`), whoever relays it. */
13
+ export type IssuedPass = {
14
+ pass: Uint8Array | number[];
15
+ copy: string | Principal;
16
+ validUntilNs: bigint;
17
+ area: string;
18
+ bbox: Bbox;
19
+ };
20
+ /**
21
+ * Where passes come from. A system that is registered at the coordinator asks the pass core
22
+ * of its user itself (`coordinatorPassProvider`); a front asks its own backend canister, which
23
+ * asks the core for its user. Throws like the package (`CanisterError` with `Blocked`,
24
+ * `RateLimited`, `Waiting`, `NotAuthorized`).
25
+ */
26
+ export type PassProvider = (request: PassRequest) => Promise<IssuedPass>;
27
+ /** How the client reaches the group; the tests give their own. */
28
+ export interface Transport {
29
+ areas(): Promise<{
30
+ areas: AreaView[];
31
+ ttlSeconds: number;
32
+ }>;
33
+ /** `RouteAnswer`, or throws `CanisterError` / `CriticalCanisterError`. */
34
+ route(copy: string, points: LatLng[], line: boolean, pass: Uint8Array | undefined): Promise<RouteAnswer>;
35
+ }
36
+ export type MapRoutesClientOptions = {
37
+ /** The coordinator of the group. */
38
+ coordinatorId: string;
39
+ /** Default: mainnet. A local replica: `http://127.0.0.1:4943`. */
40
+ host?: string;
41
+ /** The user: the caller of every query (a pass names it). Default: anonymous. */
42
+ identity?: Identity;
43
+ /** Without it: no pass, a random live copy (only while the copies do not require passes). */
44
+ passProvider?: PassProvider;
45
+ /** @internal tests */
46
+ transport?: Transport;
47
+ /** @internal tests */
48
+ now?: () => number;
49
+ };
50
+ /** A route, and the copy that computed it. */
51
+ export type ClientRoute = {
52
+ legs: {
53
+ meters: number;
54
+ seconds: number;
55
+ line?: string;
56
+ }[];
57
+ snapMeters: number[];
58
+ osmDate: string;
59
+ graphSha256: string;
60
+ copy: string;
61
+ };
62
+ export type ClientRouteOptions = {
63
+ line?: boolean;
64
+ /** `Timeout` after this many milliseconds, renewals and retries included. */
65
+ timeoutMs?: number;
66
+ };
67
+ /** The group over the IC: anonymous to the coordinator (a public query), as the user to the copies. */
68
+ export declare function icTransport(coordinatorId: string, host: string, identity: Identity | undefined): Transport;
69
+ /** How long a system keeps the core of a user (the list TTL of the coordinator). */
70
+ export declare const ISSUER_TTL_MS = 300000;
71
+ /**
72
+ * A pass provider for a system registered at the coordinator: `system` asks the coordinator
73
+ * which core issues the passes of `user` (kept `ISSUER_TTL_MS`, asked again after a failure)
74
+ * and signs `issuePass` at that core for `user` (the principal that will call the copies —
75
+ * often the system itself).
76
+ */
77
+ export declare function coordinatorPassProvider(options: {
78
+ coordinatorId: string;
79
+ host?: string;
80
+ system: Identity;
81
+ user: Principal | string;
82
+ now?: () => number;
83
+ }): PassProvider;
84
+ export declare class MapRoutesClient {
85
+ private readonly transport;
86
+ private readonly provider;
87
+ private readonly now;
88
+ private areasCache;
89
+ private readonly passes;
90
+ private readonly renewals;
91
+ constructor(options: MapRoutesClientOptions);
92
+ /**
93
+ * The fastest route through `points` (2..=5) in the area that holds them all. Throws
94
+ * `CanisterError` — `OutsideArea` (no area of the group holds every point, or a point far
95
+ * from a road), `NoRoute`, `NotReady` (no live copy), `InvalidData`, the pass refusals of
96
+ * the worker after one renewal (`PassExpired`, `PassInvalid`, `PassMissing`,
97
+ * `OutsideBbox`) and of the provider (`Blocked`, `RateLimited`, `Waiting`, `NotAuthorized`)
98
+ * — `Timeout`, or `CriticalCanisterError`.
99
+ */
100
+ routeAsyncUnsafe(points: LatLng[], options?: ClientRouteOptions): Promise<ClientRoute>;
101
+ /** The areas and their live copies (kept `ttlSeconds`). */
102
+ areasAsync(): Promise<AreaView[]>;
103
+ private attemptAsync;
104
+ /** The held pass of the area when it serves (a renewal starts in the background near its end), else a new one. */
105
+ private passForAsync;
106
+ /** One renewal per area at a time; the pass is kept from when it was asked. */
107
+ private renewAsync;
108
+ }
109
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,174 @@
1
+ // MapRoutesClient: the group (`replicated-shards.md`). It asks the coordinator WHERE (the areas
2
+ // and their live copies, kept `ttlSeconds`), takes a pass for the area of the points from a
3
+ // pass provider (kept, renewed in the background before its end, at once on `PassExpired`),
4
+ // and asks the copy the pass names. Without a provider it draws a live copy (a group whose
5
+ // copies do not require passes yet).
6
+ import { Principal } from "@icp-sdk/core/principal";
7
+ import { idlFactory as coordinatorIdl } from "../declarations/map-routes-coordinator/map-routes-coordinator.did.js";
8
+ import { idlFactory as coreIdl } from "../declarations/map-routes-core/map-routes-core.did.js";
9
+ import { idlFactory as workerIdl } from "../declarations/map-routes-worker/map-routes-worker.did.js";
10
+ import { errorKeyOf, executeRustAsyncUnsafe, MAINNET_HOST, signedActorAsync, withTimeoutAsync } from "./canister-actor.js";
11
+ import { areaFor, PASS_ERROR_KEYS, passServes, pick, renewSoon } from "./passes.js";
12
+ function canisterError(errorKey, errorMessage) {
13
+ return new Error("CanisterError", { cause: { errorKey, errorMessage } });
14
+ }
15
+ function toHeld(issued) {
16
+ return {
17
+ pass: issued.pass instanceof Uint8Array ? issued.pass : Uint8Array.from(issued.pass),
18
+ copy: typeof issued.copy === "string" ? issued.copy : issued.copy.toText(),
19
+ validUntilMs: Number(issued.validUntilNs / 1000000n),
20
+ area: issued.area,
21
+ bbox: issued.bbox,
22
+ };
23
+ }
24
+ /** The group over the IC: anonymous to the coordinator (a public query), as the user to the copies. */
25
+ export function icTransport(coordinatorId, host, identity) {
26
+ return {
27
+ async areas() {
28
+ const coordinator = await signedActorAsync(coordinatorIdl, coordinatorId, host, undefined);
29
+ const answer = await executeRustAsyncUnsafe(async () => ({ Ok: await coordinator.areas() }));
30
+ return {
31
+ ttlSeconds: answer.ttlSeconds,
32
+ areas: answer.areas.map((a) => ({ area: a.area, bbox: a.bbox, copies: a.copies.map((c) => c.toText()) })),
33
+ };
34
+ },
35
+ async route(copy, points, line, pass) {
36
+ const worker = await signedActorAsync(workerIdl, copy, host, identity);
37
+ return executeRustAsyncUnsafe(() => worker.route({ points: points.map((p) => ({ lat: p.lat, lon: p.lng })), line, pass: pass ? [pass] : [] }));
38
+ },
39
+ };
40
+ }
41
+ /** How long a system keeps the core of a user (the list TTL of the coordinator). */
42
+ export const ISSUER_TTL_MS = 300_000;
43
+ /**
44
+ * A pass provider for a system registered at the coordinator: `system` asks the coordinator
45
+ * which core issues the passes of `user` (kept `ISSUER_TTL_MS`, asked again after a failure)
46
+ * and signs `issuePass` at that core for `user` (the principal that will call the copies —
47
+ * often the system itself).
48
+ */
49
+ export function coordinatorPassProvider(options) {
50
+ const host = options.host ?? MAINNET_HOST;
51
+ const user = typeof options.user === "string" ? Principal.fromText(options.user) : options.user;
52
+ const now = options.now ?? Date.now;
53
+ let issuer;
54
+ return async ({ area, bbox }) => {
55
+ if (!issuer || issuer.untilMs <= now()) {
56
+ const coordinator = await signedActorAsync(coordinatorIdl, options.coordinatorId, host, undefined);
57
+ const found = await executeRustAsyncUnsafe(async () => ({ Ok: await coordinator.issuerFor(user) }));
58
+ if (found.length === 0) {
59
+ throw canisterError("Waiting", "the group has no pass core yet");
60
+ }
61
+ issuer = { id: found[0].toText(), untilMs: now() + ISSUER_TTL_MS };
62
+ }
63
+ const core = await signedActorAsync(coreIdl, issuer.id, host, options.system);
64
+ try {
65
+ return await executeRustAsyncUnsafe(() => core.issuePass({ user, area, bbox, system: [] }));
66
+ }
67
+ catch (error) {
68
+ if (error instanceof Error && error.message === "CriticalCanisterError") {
69
+ issuer = undefined;
70
+ }
71
+ throw error;
72
+ }
73
+ };
74
+ }
75
+ export class MapRoutesClient {
76
+ transport;
77
+ provider;
78
+ now;
79
+ areasCache;
80
+ passes = new Map();
81
+ renewals = new Map();
82
+ constructor(options) {
83
+ this.transport = options.transport ?? icTransport(options.coordinatorId, options.host ?? MAINNET_HOST, options.identity);
84
+ this.provider = options.passProvider;
85
+ this.now = options.now ?? Date.now;
86
+ }
87
+ /**
88
+ * The fastest route through `points` (2..=5) in the area that holds them all. Throws
89
+ * `CanisterError` — `OutsideArea` (no area of the group holds every point, or a point far
90
+ * from a road), `NoRoute`, `NotReady` (no live copy), `InvalidData`, the pass refusals of
91
+ * the worker after one renewal (`PassExpired`, `PassInvalid`, `PassMissing`,
92
+ * `OutsideBbox`) and of the provider (`Blocked`, `RateLimited`, `Waiting`, `NotAuthorized`)
93
+ * — `Timeout`, or `CriticalCanisterError`.
94
+ */
95
+ async routeAsyncUnsafe(points, options = {}) {
96
+ return withTimeoutAsync(() => this.attemptAsync(points, options.line ?? false, false), options.timeoutMs);
97
+ }
98
+ /** The areas and their live copies (kept `ttlSeconds`). */
99
+ async areasAsync() {
100
+ if (this.areasCache && this.areasCache.untilMs > this.now()) {
101
+ return this.areasCache.areas;
102
+ }
103
+ const { areas, ttlSeconds } = await this.transport.areas();
104
+ this.areasCache = { areas, untilMs: this.now() + ttlSeconds * 1000 };
105
+ return areas;
106
+ }
107
+ async attemptAsync(points, line, retried) {
108
+ const area = areaFor(await this.areasAsync(), points);
109
+ if (!area) {
110
+ throw canisterError("OutsideArea", "no area of the group holds every point");
111
+ }
112
+ const held = this.provider ? await this.passForAsync(area, points) : undefined;
113
+ const copy = held?.copy ?? pick(area.copies);
114
+ if (!copy) {
115
+ throw canisterError("NotReady", `${area.area}: no live copy`);
116
+ }
117
+ try {
118
+ const answer = await this.transport.route(copy, points, line, held?.pass);
119
+ return {
120
+ legs: answer.legs.map((leg) => ({ meters: Number(leg.meters), seconds: Number(leg.seconds), ...(leg.line ? { line: leg.line } : {}) })),
121
+ snapMeters: Array.from(answer.snapMeters),
122
+ osmDate: answer.osmDate,
123
+ graphSha256: answer.graphSha256,
124
+ copy,
125
+ };
126
+ }
127
+ catch (error) {
128
+ if (retried) {
129
+ throw error;
130
+ }
131
+ // The pass itself: a new one (the key rotated, the copy changed, its time ended).
132
+ if (this.provider && PASS_ERROR_KEYS.includes(errorKeyOf(error) ?? "")) {
133
+ this.passes.delete(area.area);
134
+ return this.attemptAsync(points, line, true);
135
+ }
136
+ // The copy did not answer (deleted, moved): the list again, and a pass for another.
137
+ if (error instanceof Error && error.message === "CriticalCanisterError") {
138
+ this.areasCache = undefined;
139
+ this.passes.delete(area.area);
140
+ return this.attemptAsync(points, line, true);
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+ /** The held pass of the area when it serves (a renewal starts in the background near its end), else a new one. */
146
+ async passForAsync(area, points) {
147
+ const entry = this.passes.get(area.area);
148
+ const now = this.now();
149
+ if (entry && passServes(entry.held, area.area, points, now)) {
150
+ if (renewSoon(entry.held, entry.issuedAtMs, now)) {
151
+ void this.renewAsync(area).catch(() => undefined);
152
+ }
153
+ return entry.held;
154
+ }
155
+ return this.renewAsync(area);
156
+ }
157
+ /** One renewal per area at a time; the pass is kept from when it was asked. */
158
+ renewAsync(area) {
159
+ const running = this.renewals.get(area.area);
160
+ if (running) {
161
+ return running;
162
+ }
163
+ const askedAt = this.now();
164
+ const renewal = this.provider({ area: area.area, bbox: area.bbox })
165
+ .then((issued) => {
166
+ const held = toHeld(issued);
167
+ this.passes.set(area.area, { held, issuedAtMs: askedAt });
168
+ return held;
169
+ })
170
+ .finally(() => this.renewals.delete(area.area));
171
+ this.renewals.set(area.area, renewal);
172
+ return renewal;
173
+ }
174
+ }
@@ -0,0 +1,50 @@
1
+ import type { Point, RouteAnswer } from "../declarations/map-routes-worker/map-routes-worker.did.js";
2
+ import { type MapRoutesOptions } from "./canister-actor.js";
3
+ export { errorKeyOf, isCanisterStoppedError, isLocalHost, isTimeoutError, MAINNET_HOST, retrySettings, type MapRoutesOptions } from "./canister-actor.js";
4
+ /** The canister on mainnet (crafting-table slot backend7). */
5
+ export declare const MAP_ROUTES_CANISTER_ID = "6aigz-vyaaa-aaaag-ayjqa-cai";
6
+ /** Points of one route: a start, up to three stops, an end. */
7
+ export declare const MAX_ROUTE_POINTS = 5;
8
+ export type { LatLng } from "./types.js";
9
+ import type { LatLng } from "./types.js";
10
+ export type RouteLeg = {
11
+ meters: number;
12
+ seconds: number;
13
+ /** Encoded polyline of precision 5, junction to junction; absent when not asked for. */
14
+ line?: string;
15
+ };
16
+ export type Route = {
17
+ /** One leg for each pair of consecutive points. */
18
+ legs: RouteLeg[];
19
+ /** How far each point lies from the junction its legs start or end at. */
20
+ snapMeters: number[];
21
+ /** Date of the OpenStreetMap extract of the graph. */
22
+ osmDate: string;
23
+ graphSha256: string;
24
+ };
25
+ export type RouteOptions = {
26
+ /** Each leg carries its line. Default: false. */
27
+ line?: boolean;
28
+ /** `Timeout` after this many milliseconds. Default: none. */
29
+ timeoutMs?: number;
30
+ };
31
+ export declare function toPoint(point: LatLng): Point;
32
+ export declare function fromAnswer(answer: RouteAnswer): Route;
33
+ export * from "./client.js";
34
+ /**
35
+ * ONE worker copy, asked directly, without the coordinator and without a pass: a copy that
36
+ * does not require passes (`passRequired` off), a local test. Groups use `MapRoutesClient`.
37
+ */
38
+ export declare class MapRoutesActor {
39
+ private readonly canisterId;
40
+ private readonly options;
41
+ constructor(canisterId?: string, options?: MapRoutesOptions);
42
+ /**
43
+ * The fastest route through `points` (2..=MAX_ROUTE_POINTS), leg by leg. Throws
44
+ * `CanisterError` (errorKey `OutsideArea` — a point farther than 1 km from a road of
45
+ * the map, `NoRoute`, `NotReady` — no graph yet, `InvalidData`), `Timeout`, or
46
+ * `CriticalCanisterError`.
47
+ */
48
+ routeAsyncUnsafe(points: LatLng[], options?: RouteOptions): Promise<Route>;
49
+ }
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAY,KAAK,EAAE,WAAW,EAAE,MAAM,4DAA4D,CAAC;AAE/G,OAAO,EAAyC,KAAK,gBAAgB,EAAoB,MAAM,qBAAqB,CAAC;AAErH,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE1J,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,gCAAgC,CAAC;AAEpE,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,MAAM,QAAQ,GAAG;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,wFAAwF;IACxF,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG;IAChB,mDAAmD;IACnD,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB,iDAAiD;IACjD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAE5C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,CAWrD;AAED,cAAc,aAAa,CAAC;AAE5B;;;GAGG;AACH,qBAAa,cAAc;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;gBAE/B,UAAU,GAAE,MAA+B,EAAE,OAAO,GAAE,gBAAqB;IAKvF;;;;;OAKG;IACU,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC;CAS9F"}
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ // @jsm-mit/map-routes-package: the fastest route by car through 2–5 points from the map-routes
2
+ // canister (an OpenStreetMap road graph of Nordrhein-Westfalen), with the line of each leg
3
+ // as an encoded polyline of precision 5 — the format ORS sends.
4
+ import { idlFactory } from "../declarations/map-routes-worker/map-routes-worker.did.js";
5
+ import { executeRustAsyncUnsafe, rawActorAsync, withTimeoutAsync } from "./canister-actor.js";
6
+ export { errorKeyOf, isCanisterStoppedError, isLocalHost, isTimeoutError, MAINNET_HOST, retrySettings } from "./canister-actor.js";
7
+ /** The canister on mainnet (crafting-table slot backend7). */
8
+ export const MAP_ROUTES_CANISTER_ID = "6aigz-vyaaa-aaaag-ayjqa-cai";
9
+ /** Points of one route: a start, up to three stops, an end. */
10
+ export const MAX_ROUTE_POINTS = 5;
11
+ export function toPoint(point) {
12
+ return { lat: point.lat, lon: point.lng };
13
+ }
14
+ export function fromAnswer(answer) {
15
+ return {
16
+ legs: answer.legs.map((leg) => ({
17
+ meters: Number(leg.meters),
18
+ seconds: Number(leg.seconds),
19
+ ...(leg.line ? { line: leg.line } : {}),
20
+ })),
21
+ snapMeters: Array.from(answer.snapMeters),
22
+ osmDate: answer.osmDate,
23
+ graphSha256: answer.graphSha256,
24
+ };
25
+ }
26
+ export * from "./client.js";
27
+ /**
28
+ * ONE worker copy, asked directly, without the coordinator and without a pass: a copy that
29
+ * does not require passes (`passRequired` off), a local test. Groups use `MapRoutesClient`.
30
+ */
31
+ export class MapRoutesActor {
32
+ canisterId;
33
+ options;
34
+ constructor(canisterId = MAP_ROUTES_CANISTER_ID, options = {}) {
35
+ this.canisterId = canisterId;
36
+ this.options = options;
37
+ }
38
+ /**
39
+ * The fastest route through `points` (2..=MAX_ROUTE_POINTS), leg by leg. Throws
40
+ * `CanisterError` (errorKey `OutsideArea` — a point farther than 1 km from a road of
41
+ * the map, `NoRoute`, `NotReady` — no graph yet, `InvalidData`), `Timeout`, or
42
+ * `CriticalCanisterError`.
43
+ */
44
+ async routeAsyncUnsafe(points, options = {}) {
45
+ const actor = await rawActorAsync(idlFactory, this.canisterId, this.options);
46
+ const answer = await withTimeoutAsync(() => executeRustAsyncUnsafe(() => actor.route({ points: points.map(toPoint), line: options.line ?? false, pass: [] })), options.timeoutMs);
47
+ return fromAnswer(answer);
48
+ }
49
+ }
@@ -0,0 +1,37 @@
1
+ import type { LatLng } from "./types.js";
2
+ /** A rectangle in degrees, edges included. */
3
+ export type Bbox = {
4
+ minLat: number;
5
+ minLon: number;
6
+ maxLat: number;
7
+ maxLon: number;
8
+ };
9
+ export type AreaView = {
10
+ area: string;
11
+ bbox: Bbox;
12
+ copies: string[];
13
+ };
14
+ /** A pass from the coordinator: valid on `copy`, for `bbox`, until `validUntilMs`. */
15
+ export type HeldPass = {
16
+ pass: Uint8Array;
17
+ copy: string;
18
+ validUntilMs: number;
19
+ area: string;
20
+ bbox: Bbox;
21
+ };
22
+ /** A held pass is renewed in the background when less than this part of its life is left. */
23
+ export declare const RENEW_PART = 0.25;
24
+ /** A pass this close to its end is not used any more (the call itself takes time). */
25
+ export declare const PASS_MARGIN_MS = 10000;
26
+ export declare function contains(b: Bbox, p: LatLng): boolean;
27
+ /** The first area whose bbox holds every point; undefined when none does. */
28
+ export declare function areaFor(areas: AreaView[], points: LatLng[]): AreaView | undefined;
29
+ /** A held pass serves this call: the same area, every point inside, not too close to its end. */
30
+ export declare function passServes(held: HeldPass | undefined, area: string, points: LatLng[], nowMs: number): held is HeldPass;
31
+ /** Time to get the next pass in the background (the held one still serves). */
32
+ export declare function renewSoon(held: HeldPass, issuedAtMs: number, nowMs: number): boolean;
33
+ /** The worker refused the pass itself: a new pass may help, a retry of the same one not. */
34
+ export declare const PASS_ERROR_KEYS: string[];
35
+ /** A random element (the copies of an area without a pass). */
36
+ export declare function pick<T>(items: T[], random?: () => number): T | undefined;
37
+ //# sourceMappingURL=passes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passes.d.ts","sourceRoot":"","sources":["../src/passes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,8CAA8C;AAC9C,MAAM,MAAM,IAAI,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtF,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEtE,sFAAsF;AACtF,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAE1G,6FAA6F;AAC7F,eAAO,MAAM,UAAU,OAAO,CAAC;AAC/B,sFAAsF;AACtF,eAAO,MAAM,cAAc,QAAS,CAAC;AAErC,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,GAAG,SAAS,CAEjF;AAED,iGAAiG;AACjG,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,IAAI,QAAQ,CAEtH;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIpF;AAED,4FAA4F;AAC5F,eAAO,MAAM,eAAe,UAA+D,CAAC;AAE5F,+DAA+D;AAC/D,wBAAgB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,GAAE,MAAM,MAAoB,GAAG,CAAC,GAAG,SAAS,CAErF"}
package/dist/passes.js ADDED
@@ -0,0 +1,28 @@
1
+ // The decisions of the client: which area holds the points, whether a held pass serves,
2
+ // when to renew it. Plain functions (tests/test-unit.ts).
3
+ /** A held pass is renewed in the background when less than this part of its life is left. */
4
+ export const RENEW_PART = 0.25;
5
+ /** A pass this close to its end is not used any more (the call itself takes time). */
6
+ export const PASS_MARGIN_MS = 10_000;
7
+ export function contains(b, p) {
8
+ return p.lat >= b.minLat && p.lat <= b.maxLat && p.lng >= b.minLon && p.lng <= b.maxLon;
9
+ }
10
+ /** The first area whose bbox holds every point; undefined when none does. */
11
+ export function areaFor(areas, points) {
12
+ return areas.find((a) => points.every((p) => contains(a.bbox, p)));
13
+ }
14
+ /** A held pass serves this call: the same area, every point inside, not too close to its end. */
15
+ export function passServes(held, area, points, nowMs) {
16
+ return held !== undefined && held.area === area && held.validUntilMs - PASS_MARGIN_MS > nowMs && points.every((p) => contains(held.bbox, p));
17
+ }
18
+ /** Time to get the next pass in the background (the held one still serves). */
19
+ export function renewSoon(held, issuedAtMs, nowMs) {
20
+ const life = held.validUntilMs - issuedAtMs;
21
+ return life > 0 && held.validUntilMs - nowMs < life * RENEW_PART;
22
+ }
23
+ /** The worker refused the pass itself: a new pass may help, a retry of the same one not. */
24
+ export const PASS_ERROR_KEYS = ["PassExpired", "PassInvalid", "PassMissing", "OutsideBbox"];
25
+ /** A random element (the copies of an area without a pass). */
26
+ export function pick(items, random = Math.random) {
27
+ return items.length === 0 ? undefined : items[Math.floor(random() * items.length) % items.length];
28
+ }
@@ -0,0 +1,5 @@
1
+ export type LatLng = {
2
+ lat: number;
3
+ lng: number;
4
+ };
5
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};