@jsm-mit/map-routes-package 0.1.0 → 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,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
+ }
package/dist/index.d.ts CHANGED
@@ -1,14 +1,12 @@
1
- import type { Point, RouteAnswer } from "../declarations/map-routes/map-routes.did.js";
1
+ import type { Point, RouteAnswer } from "../declarations/map-routes-worker/map-routes-worker.did.js";
2
2
  import { type MapRoutesOptions } from "./canister-actor.js";
3
3
  export { errorKeyOf, isCanisterStoppedError, isLocalHost, isTimeoutError, MAINNET_HOST, retrySettings, type MapRoutesOptions } from "./canister-actor.js";
4
4
  /** The canister on mainnet (crafting-table slot backend7). */
5
5
  export declare const MAP_ROUTES_CANISTER_ID = "6aigz-vyaaa-aaaag-ayjqa-cai";
6
6
  /** Points of one route: a start, up to three stops, an end. */
7
7
  export declare const MAX_ROUTE_POINTS = 5;
8
- export type LatLng = {
9
- lat: number;
10
- lng: number;
11
- };
8
+ export type { LatLng } from "./types.js";
9
+ import type { LatLng } from "./types.js";
12
10
  export type RouteLeg = {
13
11
  meters: number;
14
12
  seconds: number;
@@ -32,6 +30,11 @@ export type RouteOptions = {
32
30
  };
33
31
  export declare function toPoint(point: LatLng): Point;
34
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
+ */
35
38
  export declare class MapRoutesActor {
36
39
  private readonly canisterId;
37
40
  private readonly options;
@@ -1 +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"}
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 CHANGED
@@ -1,7 +1,7 @@
1
1
  // @jsm-mit/map-routes-package: the fastest route by car through 2–5 points from the map-routes
2
2
  // canister (an OpenStreetMap road graph of Nordrhein-Westfalen), with the line of each leg
3
3
  // as an encoded polyline of precision 5 — the format ORS sends.
4
- import { idlFactory } from "../declarations/map-routes/map-routes.did.js";
4
+ import { idlFactory } from "../declarations/map-routes-worker/map-routes-worker.did.js";
5
5
  import { executeRustAsyncUnsafe, rawActorAsync, withTimeoutAsync } from "./canister-actor.js";
6
6
  export { errorKeyOf, isCanisterStoppedError, isLocalHost, isTimeoutError, MAINNET_HOST, retrySettings } from "./canister-actor.js";
7
7
  /** The canister on mainnet (crafting-table slot backend7). */
@@ -23,6 +23,11 @@ export function fromAnswer(answer) {
23
23
  graphSha256: answer.graphSha256,
24
24
  };
25
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
+ */
26
31
  export class MapRoutesActor {
27
32
  canisterId;
28
33
  options;
@@ -38,7 +43,7 @@ export class MapRoutesActor {
38
43
  */
39
44
  async routeAsyncUnsafe(points, options = {}) {
40
45
  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);
46
+ const answer = await withTimeoutAsync(() => executeRustAsyncUnsafe(() => actor.route({ points: points.map(toPoint), line: options.line ?? false, pass: [] })), options.timeoutMs);
42
47
  return fromAnswer(answer);
43
48
  }
44
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 {};
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
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",
3
+ "version": "0.2.0",
4
+ "description": "The package of map-routes: the fastest route by car through 2–5 points, with the line of each leg, from a replicated-shards group of IC canisters (a coordinator and worker copies) that hold OpenStreetMap road graphs.",
5
+ "homepage": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes-replicated-shards/tree/master/package#readme",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/JSM-Internet-Computer-Map-Service/map-routes.git",
8
+ "url": "git+https://github.com/JSM-Internet-Computer-Map-Service/map-routes-replicated-shards.git",
9
9
  "directory": "package"
10
10
  },
11
11
  "license": "ISC",
@@ -30,7 +30,8 @@
30
30
  "prepare": "npm run build",
31
31
  "publish-public": "bash scripts/publish-public.sh",
32
32
  "test-unit": "npx tsx tests/test-unit.ts",
33
- "test-smoke": "bash scripts/run-tests-local.sh tests/test-smoke.ts"
33
+ "test-smoke": "bash scripts/run-tests-local.sh tests/test-smoke.ts",
34
+ "test-group": "bash scripts/run-group-tests-local.sh tests/test-group.ts"
34
35
  },
35
36
  "dependencies": {
36
37
  "@icp-sdk/core": "^6.1.0"
@@ -41,11 +42,11 @@
41
42
  "typescript": "^5.9.3"
42
43
  },
43
44
  "bugs": {
44
- "url": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes/issues"
45
+ "url": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes-replicated-shards/issues"
45
46
  },
46
47
  "mapRoutes": {
47
- "repo": "map-routes",
48
- "repoUrl": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes",
49
- "commit": "21a20fa3d79c1703edf29352d616e81d84428610"
48
+ "repo": "map-routes-replicated-shards",
49
+ "repoUrl": "https://github.com/JSM-Internet-Computer-Map-Service/map-routes-replicated-shards",
50
+ "commit": "63152a303365beb6134d98c633a66f6b159eea92"
50
51
  }
51
52
  }
@@ -1,93 +0,0 @@
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
- }