@jsm-mit/map-routes-package 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # @jsm-mit/map-routes-package
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The package of map-routes: the fastest route by car through 2–5 points, with the line of each
4
+ leg, from the `map-routes-rust` canister — an OpenStreetMap road graph of
5
+ Nordrhein-Westfalen (© OpenStreetMap contributors, ODbL) inside an Internet Computer
6
+ canister. The canister answers anyone (anonymous queries, no cost to the caller).
7
+ Not published under this name yet: 0.1.0 was published as `@jsm-mit/tigi-map-package`.
8
+
9
+ ```ts
10
+ import { errorKeyOf, isTimeoutError, MapRoutesActor } from "@jsm-mit/map-routes-package";
11
+
12
+ const map = new MapRoutesActor(); // mainnet, MAP_ROUTES_CANISTER_ID
13
+ const route = await map.routeAsyncUnsafe(
14
+ [me, pickup, destination], // { lat, lng }, 2..=5 points
15
+ { line: true, timeoutMs: 5_000 },
16
+ );
17
+ // route.legs[i]: { meters, seconds, line? } line = encoded polyline, precision 5 (as ORS)
18
+ // route.snapMeters, route.osmDate, route.graphSha256
19
+ ```
20
+
21
+ Errors: `CanisterError` with `errorKeyOf(error)` = `OutsideArea` (a point farther than
22
+ 1 km from a road of the map — e.g. outside NRW), `NoRoute`, `NotReady` (no graph yet, or
23
+ one of an older format right after an upgrade), `InvalidData`; `Timeout`
24
+ (`isTimeoutError`); `CriticalCanisterError` (transport). A stopped canister (an upgrade
25
+ runs, IC0508/IC0509) is waited out within the time limit.
26
+
27
+ Local replica: `new MapRoutesActor(localId, { host: "http://127.0.0.1:4943" })`.
28
+
29
+ ## Scripts
30
+
31
+ - `npm run test-unit` — no canister.
32
+ - `npm run test-smoke` — the canister from this tree on the LOCAL replica (deploys it,
33
+ uploads the newest `data/*.tmc` when the active graph differs).
34
+ - `npm run sync-declarations` — `declarations/map-routes/` from `../map-routes-rust.did`.
35
+ - `npm run publish-public` — human-run; see the script for its refusals.
@@ -0,0 +1,93 @@
1
+ type BeginUploadArgs = record {
2
+ // sha256 (hex) of the graph file.
3
+ sha256 : text;
4
+ // Bytes of the graph file (`map-routes-build build` output, format TMC3).
5
+ size : nat64;
6
+ osmDate : text;
7
+ };
8
+ type BufferInfo = record {
9
+ sha256 : text;
10
+ size : nat64;
11
+ startedAtNs : nat64;
12
+ state : BufferState;
13
+ buffer : nat8;
14
+ osmDate : text;
15
+ };
16
+ type BufferState = variant { Uploading; Active; Ready };
17
+ // The fastest route by car between two points.
18
+ type Distance = record {
19
+ meters : nat64;
20
+ snapToMeters : nat32;
21
+ seconds : nat64;
22
+ // Instructions this query took (measurement of the PoC).
23
+ instructions : nat64;
24
+ // sha256 (hex) of the graph, as printed by `map-routes-build build`.
25
+ graphSha256 : text;
26
+ // Date of the OpenStreetMap extract the graph was built from.
27
+ osmDate : text;
28
+ // How far each point lies from the junction the route starts or ends at.
29
+ snapFromMeters : nat32;
30
+ };
31
+ type DistanceArgs = record { to : Point; from : Point };
32
+ type Error = variant {
33
+ // No road within `MAX_SNAP_M` of the point named ("from" / "to" / "points[i]").
34
+ OutsideArea : text;
35
+ // No graph is active yet, or the active one is of an older format.
36
+ NotReady;
37
+ NotFound;
38
+ NotAuthorized : text;
39
+ NoRoute;
40
+ InvalidData : text;
41
+ };
42
+ // The fastest route from one point of a `route` query to the next.
43
+ type Leg = record {
44
+ meters : nat64;
45
+ // Encoded polyline of precision 5 (as ORS sends it), from the junction the leg starts
46
+ // at to the one it ends at; "" when the line was not asked for.
47
+ line : text;
48
+ seconds : nat64;
49
+ };
50
+ type Point = record { lat : float64; lon : float64 };
51
+ type Result = variant { Ok : BufferInfo; Err : Error };
52
+ type Result_1 = variant { Ok : Distance; Err : Error };
53
+ type Result_2 = variant { Ok : Status; Err : Error };
54
+ type Result_3 = variant { Ok : RouteAnswer; Err : Error };
55
+ type Result_4 = variant { Ok; Err : Error };
56
+ type RouteAnswer = record {
57
+ // How far each point lies from the junction its legs start or end at.
58
+ snapMeters : vec nat32;
59
+ // One leg for each pair of consecutive points.
60
+ legs : vec Leg;
61
+ instructions : nat64;
62
+ graphSha256 : text;
63
+ osmDate : text;
64
+ };
65
+ type RouteArgs = record {
66
+ // Whether each leg carries its line.
67
+ line : bool;
68
+ // 2..=`MAX_ROUTE_POINTS` points, driven in this order.
69
+ points : vec Point;
70
+ };
71
+ type Status = record {
72
+ stableBytes : nat64;
73
+ heapBytes : nat64;
74
+ // Nodes of the graph that answers `distance`; 0 when none is active.
75
+ nodes : nat64;
76
+ buffers : vec BufferInfo;
77
+ };
78
+ type UploadChunkArgs = record { offset : nat64; bytes : blob };
79
+ service : () -> {
80
+ // Switches `distance` to the inactive buffer — a new graph, or back to the previous one.
81
+ activate : () -> (Result);
82
+ // Starts an upload into the inactive buffer; whatever was there is dropped.
83
+ beginUpload : (BeginUploadArgs) -> (Result);
84
+ // Checks the hash and the header of the uploaded graph; the buffer becomes Ready.
85
+ commitUpload : () -> (Result);
86
+ // The fastest route by car from one point to another. Public.
87
+ distance : (DistanceArgs) -> (Result_1) query;
88
+ getStatus : () -> (Result_2) query;
89
+ // The fastest route by car through 2..=5 points, leg by leg, with each leg's line when
90
+ // asked. Public.
91
+ route : (RouteArgs) -> (Result_3) query;
92
+ uploadChunk : (UploadChunkArgs) -> (Result_4);
93
+ }
@@ -0,0 +1,153 @@
1
+ import type { Principal } from '@icp-sdk/core/principal';
2
+ import type { ActorMethod } from '@icp-sdk/core/agent';
3
+ import type { IDL } from '@icp-sdk/core/candid';
4
+
5
+ export interface BeginUploadArgs {
6
+ /**
7
+ * sha256 (hex) of the graph file.
8
+ */
9
+ 'sha256' : string,
10
+ /**
11
+ * Bytes of the graph file (`map-routes-build build` output, format TMC3).
12
+ */
13
+ 'size' : bigint,
14
+ 'osmDate' : string,
15
+ }
16
+ export interface BufferInfo {
17
+ 'sha256' : string,
18
+ 'size' : bigint,
19
+ 'startedAtNs' : bigint,
20
+ 'state' : BufferState,
21
+ 'buffer' : number,
22
+ 'osmDate' : string,
23
+ }
24
+ export type BufferState = { 'Uploading' : null } |
25
+ { 'Active' : null } |
26
+ { 'Ready' : null };
27
+ /**
28
+ * The fastest route by car between two points.
29
+ */
30
+ export interface Distance {
31
+ 'meters' : bigint,
32
+ 'snapToMeters' : number,
33
+ 'seconds' : bigint,
34
+ /**
35
+ * Instructions this query took (measurement of the PoC).
36
+ */
37
+ 'instructions' : bigint,
38
+ /**
39
+ * sha256 (hex) of the graph, as printed by `map-routes-build build`.
40
+ */
41
+ 'graphSha256' : string,
42
+ /**
43
+ * Date of the OpenStreetMap extract the graph was built from.
44
+ */
45
+ 'osmDate' : string,
46
+ /**
47
+ * How far each point lies from the junction the route starts or ends at.
48
+ */
49
+ 'snapFromMeters' : number,
50
+ }
51
+ export interface DistanceArgs { 'to' : Point, 'from' : Point }
52
+ export type Error = {
53
+ /**
54
+ * No road within `MAX_SNAP_M` of the point named ("from" / "to" / "points[i]").
55
+ */
56
+ 'OutsideArea' : string
57
+ } |
58
+ {
59
+ /**
60
+ * No graph is active yet, or the active one is of an older format.
61
+ */
62
+ 'NotReady' : null
63
+ } |
64
+ { 'NotFound' : null } |
65
+ { 'NotAuthorized' : string } |
66
+ { 'NoRoute' : null } |
67
+ { 'InvalidData' : string };
68
+ /**
69
+ * The fastest route from one point of a `route` query to the next.
70
+ */
71
+ export interface Leg {
72
+ 'meters' : bigint,
73
+ /**
74
+ * Encoded polyline of precision 5 (as ORS sends it), from the junction the leg starts
75
+ * at to the one it ends at; "" when the line was not asked for.
76
+ */
77
+ 'line' : string,
78
+ 'seconds' : bigint,
79
+ }
80
+ export interface Point { 'lat' : number, 'lon' : number }
81
+ export type Result = { 'Ok' : BufferInfo } |
82
+ { 'Err' : Error };
83
+ export type Result_1 = { 'Ok' : Distance } |
84
+ { 'Err' : Error };
85
+ export type Result_2 = { 'Ok' : Status } |
86
+ { 'Err' : Error };
87
+ export type Result_3 = { 'Ok' : RouteAnswer } |
88
+ { 'Err' : Error };
89
+ export type Result_4 = { 'Ok' : null } |
90
+ { 'Err' : Error };
91
+ export interface RouteAnswer {
92
+ /**
93
+ * How far each point lies from the junction its legs start or end at.
94
+ */
95
+ 'snapMeters' : Uint32Array | number[],
96
+ /**
97
+ * One leg for each pair of consecutive points.
98
+ */
99
+ 'legs' : Array<Leg>,
100
+ 'instructions' : bigint,
101
+ 'graphSha256' : string,
102
+ 'osmDate' : string,
103
+ }
104
+ export interface RouteArgs {
105
+ /**
106
+ * Whether each leg carries its line.
107
+ */
108
+ 'line' : boolean,
109
+ /**
110
+ * 2..=`MAX_ROUTE_POINTS` points, driven in this order.
111
+ */
112
+ 'points' : Array<Point>,
113
+ }
114
+ export interface Status {
115
+ 'stableBytes' : bigint,
116
+ 'heapBytes' : bigint,
117
+ /**
118
+ * Nodes of the graph that answers `distance`; 0 when none is active.
119
+ */
120
+ 'nodes' : bigint,
121
+ 'buffers' : Array<BufferInfo>,
122
+ }
123
+ export interface UploadChunkArgs {
124
+ 'offset' : bigint,
125
+ 'bytes' : Uint8Array | number[],
126
+ }
127
+ export interface _SERVICE {
128
+ /**
129
+ * Switches `distance` to the inactive buffer — a new graph, or back to the previous one.
130
+ */
131
+ 'activate' : ActorMethod<[], Result>,
132
+ /**
133
+ * Starts an upload into the inactive buffer; whatever was there is dropped.
134
+ */
135
+ 'beginUpload' : ActorMethod<[BeginUploadArgs], Result>,
136
+ /**
137
+ * Checks the hash and the header of the uploaded graph; the buffer becomes Ready.
138
+ */
139
+ 'commitUpload' : ActorMethod<[], Result>,
140
+ /**
141
+ * The fastest route by car from one point to another. Public.
142
+ */
143
+ 'distance' : ActorMethod<[DistanceArgs], Result_1>,
144
+ 'getStatus' : ActorMethod<[], Result_2>,
145
+ /**
146
+ * The fastest route by car through 2..=5 points, leg by leg, with each leg's line when
147
+ * asked. Public.
148
+ */
149
+ 'route' : ActorMethod<[RouteArgs], Result_3>,
150
+ 'uploadChunk' : ActorMethod<[UploadChunkArgs], Result_4>,
151
+ }
152
+ export declare const idlFactory: IDL.InterfaceFactory;
153
+ export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];
@@ -0,0 +1,80 @@
1
+ export const idlFactory = ({ IDL }) => {
2
+ const BufferState = IDL.Variant({
3
+ 'Uploading' : IDL.Null,
4
+ 'Active' : IDL.Null,
5
+ 'Ready' : IDL.Null,
6
+ });
7
+ const BufferInfo = IDL.Record({
8
+ 'sha256' : IDL.Text,
9
+ 'size' : IDL.Nat64,
10
+ 'startedAtNs' : IDL.Nat64,
11
+ 'state' : BufferState,
12
+ 'buffer' : IDL.Nat8,
13
+ 'osmDate' : IDL.Text,
14
+ });
15
+ const Error = IDL.Variant({
16
+ 'OutsideArea' : IDL.Text,
17
+ 'NotReady' : IDL.Null,
18
+ 'NotFound' : IDL.Null,
19
+ 'NotAuthorized' : IDL.Text,
20
+ 'NoRoute' : IDL.Null,
21
+ 'InvalidData' : IDL.Text,
22
+ });
23
+ const Result = IDL.Variant({ 'Ok' : BufferInfo, 'Err' : Error });
24
+ const BeginUploadArgs = IDL.Record({
25
+ 'sha256' : IDL.Text,
26
+ 'size' : IDL.Nat64,
27
+ 'osmDate' : IDL.Text,
28
+ });
29
+ const Point = IDL.Record({ 'lat' : IDL.Float64, 'lon' : IDL.Float64 });
30
+ const DistanceArgs = IDL.Record({ 'to' : Point, 'from' : Point });
31
+ const Distance = IDL.Record({
32
+ 'meters' : IDL.Nat64,
33
+ 'snapToMeters' : IDL.Nat32,
34
+ 'seconds' : IDL.Nat64,
35
+ 'instructions' : IDL.Nat64,
36
+ 'graphSha256' : IDL.Text,
37
+ 'osmDate' : IDL.Text,
38
+ 'snapFromMeters' : IDL.Nat32,
39
+ });
40
+ const Result_1 = IDL.Variant({ 'Ok' : Distance, 'Err' : Error });
41
+ const Status = IDL.Record({
42
+ 'stableBytes' : IDL.Nat64,
43
+ 'heapBytes' : IDL.Nat64,
44
+ 'nodes' : IDL.Nat64,
45
+ 'buffers' : IDL.Vec(BufferInfo),
46
+ });
47
+ const Result_2 = IDL.Variant({ 'Ok' : Status, 'Err' : Error });
48
+ const RouteArgs = IDL.Record({
49
+ 'line' : IDL.Bool,
50
+ 'points' : IDL.Vec(Point),
51
+ });
52
+ const Leg = IDL.Record({
53
+ 'meters' : IDL.Nat64,
54
+ 'line' : IDL.Text,
55
+ 'seconds' : IDL.Nat64,
56
+ });
57
+ const RouteAnswer = IDL.Record({
58
+ 'snapMeters' : IDL.Vec(IDL.Nat32),
59
+ 'legs' : IDL.Vec(Leg),
60
+ 'instructions' : IDL.Nat64,
61
+ 'graphSha256' : IDL.Text,
62
+ 'osmDate' : IDL.Text,
63
+ });
64
+ const Result_3 = IDL.Variant({ 'Ok' : RouteAnswer, 'Err' : Error });
65
+ const UploadChunkArgs = IDL.Record({
66
+ 'offset' : IDL.Nat64,
67
+ 'bytes' : IDL.Vec(IDL.Nat8),
68
+ });
69
+ const Result_4 = IDL.Variant({ 'Ok' : IDL.Null, 'Err' : Error });
70
+ return IDL.Service({
71
+ 'activate' : IDL.Func([], [Result], []),
72
+ 'beginUpload' : IDL.Func([BeginUploadArgs], [Result], []),
73
+ 'commitUpload' : IDL.Func([], [Result], []),
74
+ 'distance' : IDL.Func([DistanceArgs], [Result_1], ['query']),
75
+ 'getStatus' : IDL.Func([], [Result_2], ['query']),
76
+ 'route' : IDL.Func([RouteArgs], [Result_3], ['query']),
77
+ 'uploadChunk' : IDL.Func([UploadChunkArgs], [Result_4], []),
78
+ });
79
+ };
80
+ export const init = ({ IDL }) => { return []; };
@@ -0,0 +1,58 @@
1
+ import { type ActorSubclass } from "@icp-sdk/core/agent";
2
+ import type { IDL } from "@icp-sdk/core/candid";
3
+ export interface MapRoutesOptions {
4
+ /**
5
+ * The gateway or replica to talk to. Default: mainnet (`https://icp0.io`). Pass a local
6
+ * replica (`http://127.0.0.1:4943`) for runs that must not touch mainnet. For a local
7
+ * host — and only then — the agent fetches the replica's root key.
8
+ */
9
+ host?: string;
10
+ }
11
+ export declare const MAINNET_HOST = "https://icp0.io";
12
+ /** True for a replica on this machine: localhost, *.localhost, 127.0.0.1 or [::1]. */
13
+ export declare function isLocalHost(host: string): boolean;
14
+ export declare function rawActorAsync<S>(idlFactory: IDL.InterfaceFactory, canisterId: string, options: MapRoutesOptions): Promise<ActorSubclass<S>>;
15
+ /**
16
+ * A canister that is stopped or stopping — an upgrade of it runs — refuses every call with
17
+ * IC0508 / IC0509. The call did NOT run, so it is tried again: doubling pauses from
18
+ * `STOPPED_RETRY_FIRST_DELAY_MS`, at most `STOPPED_RETRY_MAX_FACTOR` times it, each 0.5–1.5
19
+ * of its length at random, `STOPPED_RETRY_ATTEMPTS` tries in all: about a minute — unless
20
+ * the time limit of the call ends first.
21
+ */
22
+ export declare const STOPPED_RETRY_FIRST_DELAY_MS = 1000;
23
+ export declare const STOPPED_RETRY_MAX_FACTOR = 8;
24
+ export declare const STOPPED_RETRY_ATTEMPTS = 10;
25
+ /** True for the refusal of a stopped or stopping canister. */
26
+ export declare function isCanisterStoppedError(error: unknown): boolean;
27
+ /** The wait before try `attempt + 1`: doubling, capped, 0.5–1.5 of it at random. */
28
+ export declare function stoppedRetryDelayMs(attempt: number, firstDelayMs?: number): number;
29
+ /** Tests shorten the waits. */
30
+ export declare const retrySettings: {
31
+ stoppedFirstDelayMs: number;
32
+ stoppedAttempts: number;
33
+ };
34
+ export declare function waitOutStoppedAsync<T>(fnAsync: () => Promise<T>): Promise<T>;
35
+ /** The error of a call that did not answer within its time limit. */
36
+ export declare function timeoutError(timeoutMs: number): Error;
37
+ export declare function isTimeoutError(error: unknown): boolean;
38
+ /**
39
+ * The call, or `Timeout` after `timeoutMs` (none when undefined). The call itself goes on
40
+ * (a query cannot be called back); its late answer is dropped.
41
+ */
42
+ export declare function withTimeoutAsync<T>(fnAsync: () => Promise<T>, timeoutMs: number | undefined): Promise<T>;
43
+ export type RustResult<T> = {
44
+ Ok: T;
45
+ } | {
46
+ Err: Record<string, unknown>;
47
+ };
48
+ /**
49
+ * `{ Ok: T }` resolves with `T`; `{ Err }` is thrown as `Error("CanisterError", { cause:
50
+ * { errorKey, errorMessage } })` with the variant's name as the errorKey (`OutsideArea`,
51
+ * `NoRoute`, `NotReady`, `InvalidData`); any transport failure as
52
+ * `Error("CriticalCanisterError", { cause: { rawError } })`. A stopped canister is waited
53
+ * out first.
54
+ */
55
+ export declare function executeRustAsyncUnsafe<T>(fnAsync: () => Promise<RustResult<T>>): Promise<T>;
56
+ /** The errorKey of a `CanisterError`, or undefined for any other error. */
57
+ export declare function errorKeyOf(error: unknown): string | undefined;
58
+ //# sourceMappingURL=canister-actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canister-actor.d.ts","sourceRoot":"","sources":["../src/canister-actor.ts"],"names":[],"mappings":"AAIA,OAAO,EAAS,KAAK,aAAa,EAAa,MAAM,qBAAqB,CAAC;AAC3E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAEhD,MAAM,WAAW,gBAAgB;IAC7B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAE9C,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUjD;AAiBD,wBAAsB,aAAa,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,gBAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAKjJ;AAID;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AACjD,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAKzC,8DAA8D;AAC9D,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAW9D;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,GAAE,MAAqC,GAAG,MAAM,CAGhH;AAED,+BAA+B;AAC/B,eAAO,MAAM,aAAa;;;CAGzB,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAUlF;AAID,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAErD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAa9G;AAID,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAEzE;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAYjG;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAG7D"}
@@ -0,0 +1,133 @@
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
+ // ----- a stopped canister (an upgrade runs) -----
35
+ /**
36
+ * A canister that is stopped or stopping — an upgrade of it runs — refuses every call with
37
+ * IC0508 / IC0509. The call did NOT run, so it is tried again: doubling pauses from
38
+ * `STOPPED_RETRY_FIRST_DELAY_MS`, at most `STOPPED_RETRY_MAX_FACTOR` times it, each 0.5–1.5
39
+ * of its length at random, `STOPPED_RETRY_ATTEMPTS` tries in all: about a minute — unless
40
+ * the time limit of the call ends first.
41
+ */
42
+ export const STOPPED_RETRY_FIRST_DELAY_MS = 1000;
43
+ export const STOPPED_RETRY_MAX_FACTOR = 8;
44
+ export const STOPPED_RETRY_ATTEMPTS = 10;
45
+ const STOPPED_ERROR_CODES = ["IC0508", "IC0509"];
46
+ const STOPPED_REJECTS = [/ is stopped\b/, / is stopping\b/];
47
+ /** True for the refusal of a stopped or stopping canister. */
48
+ export function isCanisterStoppedError(error) {
49
+ if (!(error instanceof Error))
50
+ return false;
51
+ const raw = error.message === "CriticalCanisterError" ? error.cause?.rawError : error;
52
+ if (!(raw instanceof Error) && (typeof raw !== "object" || raw === null))
53
+ return false;
54
+ const code = raw.cause?.code;
55
+ if (typeof code?.rejectErrorCode === "string" && STOPPED_ERROR_CODES.includes(code.rejectErrorCode))
56
+ return true;
57
+ const text = typeof code?.rejectMessage === "string" ? code.rejectMessage : raw instanceof Error ? raw.message : "";
58
+ return STOPPED_REJECTS.some((pattern) => pattern.test(text));
59
+ }
60
+ /** The wait before try `attempt + 1`: doubling, capped, 0.5–1.5 of it at random. */
61
+ export function stoppedRetryDelayMs(attempt, firstDelayMs = STOPPED_RETRY_FIRST_DELAY_MS) {
62
+ const base = firstDelayMs * Math.min(2 ** (attempt - 1), STOPPED_RETRY_MAX_FACTOR);
63
+ return base * (0.5 + Math.random());
64
+ }
65
+ /** Tests shorten the waits. */
66
+ export const retrySettings = {
67
+ stoppedFirstDelayMs: STOPPED_RETRY_FIRST_DELAY_MS,
68
+ stoppedAttempts: STOPPED_RETRY_ATTEMPTS,
69
+ };
70
+ export async function waitOutStoppedAsync(fnAsync) {
71
+ for (let attempt = 1;; attempt++) {
72
+ try {
73
+ return await fnAsync();
74
+ }
75
+ catch (err) {
76
+ if (!isCanisterStoppedError(err) || attempt >= retrySettings.stoppedAttempts)
77
+ throw err;
78
+ }
79
+ await new Promise((resolve) => setTimeout(resolve, stoppedRetryDelayMs(attempt, retrySettings.stoppedFirstDelayMs)));
80
+ }
81
+ }
82
+ // ----- the time limit -----
83
+ /** The error of a call that did not answer within its time limit. */
84
+ export function timeoutError(timeoutMs) {
85
+ return new Error("Timeout", { cause: { timeoutMs } });
86
+ }
87
+ export function isTimeoutError(error) {
88
+ return error instanceof Error && error.message === "Timeout";
89
+ }
90
+ /**
91
+ * The call, or `Timeout` after `timeoutMs` (none when undefined). The call itself goes on
92
+ * (a query cannot be called back); its late answer is dropped.
93
+ */
94
+ export async function withTimeoutAsync(fnAsync, timeoutMs) {
95
+ if (timeoutMs === undefined)
96
+ return fnAsync();
97
+ let timer;
98
+ const limit = new Promise((_, reject) => {
99
+ timer = setTimeout(() => reject(timeoutError(timeoutMs)), timeoutMs);
100
+ });
101
+ try {
102
+ return await Promise.race([fnAsync(), limit]);
103
+ }
104
+ finally {
105
+ clearTimeout(timer);
106
+ }
107
+ }
108
+ /**
109
+ * `{ Ok: T }` resolves with `T`; `{ Err }` is thrown as `Error("CanisterError", { cause:
110
+ * { errorKey, errorMessage } })` with the variant's name as the errorKey (`OutsideArea`,
111
+ * `NoRoute`, `NotReady`, `InvalidData`); any transport failure as
112
+ * `Error("CriticalCanisterError", { cause: { rawError } })`. A stopped canister is waited
113
+ * out first.
114
+ */
115
+ export async function executeRustAsyncUnsafe(fnAsync) {
116
+ let result;
117
+ try {
118
+ result = await waitOutStoppedAsync(fnAsync);
119
+ }
120
+ catch (err) {
121
+ throw new Error("CriticalCanisterError", { cause: { rawError: err } });
122
+ }
123
+ if ("Ok" in result)
124
+ return result.Ok;
125
+ const errorKey = Object.keys(result.Err)[0] ?? "Unknown";
126
+ throw new Error("CanisterError", { cause: { errorKey, errorMessage: result.Err[errorKey] } });
127
+ }
128
+ /** The errorKey of a `CanisterError`, or undefined for any other error. */
129
+ export function errorKeyOf(error) {
130
+ if (!(error instanceof Error) || error.message !== "CanisterError")
131
+ return undefined;
132
+ return error.cause?.errorKey;
133
+ }
@@ -0,0 +1,47 @@
1
+ import type { Point, RouteAnswer } from "../declarations/map-routes/map-routes.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 = {
9
+ lat: number;
10
+ lng: number;
11
+ };
12
+ export type RouteLeg = {
13
+ meters: number;
14
+ seconds: number;
15
+ /** Encoded polyline of precision 5, junction to junction; absent when not asked for. */
16
+ line?: string;
17
+ };
18
+ export type Route = {
19
+ /** One leg for each pair of consecutive points. */
20
+ legs: RouteLeg[];
21
+ /** How far each point lies from the junction its legs start or end at. */
22
+ snapMeters: number[];
23
+ /** Date of the OpenStreetMap extract of the graph. */
24
+ osmDate: string;
25
+ graphSha256: string;
26
+ };
27
+ export type RouteOptions = {
28
+ /** Each leg carries its line. Default: false. */
29
+ line?: boolean;
30
+ /** `Timeout` after this many milliseconds. Default: none. */
31
+ timeoutMs?: number;
32
+ };
33
+ export declare function toPoint(point: LatLng): Point;
34
+ export declare function fromAnswer(answer: RouteAnswer): Route;
35
+ export declare class MapRoutesActor {
36
+ private readonly canisterId;
37
+ private readonly options;
38
+ constructor(canisterId?: string, options?: MapRoutesOptions);
39
+ /**
40
+ * The fastest route through `points` (2..=MAX_ROUTE_POINTS), leg by leg. Throws
41
+ * `CanisterError` (errorKey `OutsideArea` — a point farther than 1 km from a road of
42
+ * the map, `NoRoute`, `NotReady` — no graph yet, `InvalidData`), `Timeout`, or
43
+ * `CriticalCanisterError`.
44
+ */
45
+ routeAsyncUnsafe(points: LatLng[], options?: RouteOptions): Promise<Route>;
46
+ }
47
+ //# 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,8CAA8C,CAAC;AAEjG,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,MAAM,MAAM,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,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,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,44 @@
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/map-routes.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 class MapRoutesActor {
27
+ canisterId;
28
+ options;
29
+ constructor(canisterId = MAP_ROUTES_CANISTER_ID, options = {}) {
30
+ this.canisterId = canisterId;
31
+ this.options = options;
32
+ }
33
+ /**
34
+ * The fastest route through `points` (2..=MAX_ROUTE_POINTS), leg by leg. Throws
35
+ * `CanisterError` (errorKey `OutsideArea` — a point farther than 1 km from a road of
36
+ * the map, `NoRoute`, `NotReady` — no graph yet, `InvalidData`), `Timeout`, or
37
+ * `CriticalCanisterError`.
38
+ */
39
+ async routeAsyncUnsafe(points, options = {}) {
40
+ const actor = await rawActorAsync(idlFactory, this.canisterId, this.options);
41
+ const answer = await withTimeoutAsync(() => executeRustAsyncUnsafe(() => actor.route({ points: points.map(toPoint), line: options.line ?? false })), options.timeoutMs);
42
+ return fromAnswer(answer);
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
- "name": "@jsm-mit/map-routes-package",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@jsm-mit/map-routes-package",
3
+ "version": "0.1.0",
4
+ "description": "The package of map-routes: the fastest route by car through 2–5 points, with the line of each leg, from an IC canister that holds an OpenStreetMap road graph.",
5
+ "homepage": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes/tree/master/package#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/JSM-Internet-Computer-Map-Service/map-routes.git",
9
+ "directory": "package"
10
+ },
11
+ "license": "ISC",
12
+ "type": "module",
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./declarations/*": "./declarations/*"
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "declarations/"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc && tsc -p tsconfig.tests.json",
28
+ "clean": "rm -rf dist",
29
+ "sync-declarations": "bash scripts/sync-declarations.sh",
30
+ "prepare": "npm run build",
31
+ "publish-public": "bash scripts/publish-public.sh",
32
+ "test-unit": "npx tsx tests/test-unit.ts",
33
+ "test-smoke": "bash scripts/run-tests-local.sh tests/test-smoke.ts"
34
+ },
35
+ "dependencies": {
36
+ "@icp-sdk/core": "^6.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.5.2",
40
+ "tsx": "^4.18.0",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes/issues"
45
+ },
46
+ "mapRoutes": {
47
+ "repo": "map-routes",
48
+ "repoUrl": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes",
49
+ "commit": "21a20fa3d79c1703edf29352d616e81d84428610"
50
+ }
51
+ }