@flowselections/google-maps-api-installatie-techniek 1.0.5 → 1.1.1
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 +33 -0
- package/dist-lib/components/MapsBeheerPage.d.ts.map +1 -1
- package/dist-lib/components/MapsBeheerPage.js +26 -1
- package/dist-lib/components/maps/MapsErrorsPanel.d.ts +2 -0
- package/dist-lib/components/maps/MapsErrorsPanel.d.ts.map +1 -0
- package/dist-lib/components/maps/MapsErrorsPanel.js +26 -0
- package/dist-lib/integrations/supabase/types.d.ts +7538 -428
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/maps-admin.server.d.ts +9 -1
- package/dist-lib/lib/maps-admin.server.d.ts.map +1 -1
- package/dist-lib/lib/maps-admin.server.js +60 -6
- package/dist-lib/lib/maps-errors.d.ts +24 -0
- package/dist-lib/lib/maps-errors.d.ts.map +1 -0
- package/dist-lib/lib/maps-errors.js +57 -0
- package/dist-lib/lib/maps-errors.test.d.ts +2 -0
- package/dist-lib/lib/maps-errors.test.d.ts.map +1 -0
- package/dist-lib/lib/maps-errors.test.js +40 -0
- package/dist-lib/lib/maps-processor.server.d.ts +2 -0
- package/dist-lib/lib/maps-processor.server.d.ts.map +1 -1
- package/dist-lib/lib/maps-processor.server.js +117 -22
- package/dist-lib/lib/maps-processor.test.d.ts +2 -0
- package/dist-lib/lib/maps-processor.test.d.ts.map +1 -0
- package/dist-lib/lib/maps-processor.test.js +182 -0
- package/dist-lib/lib/maps.functions.d.ts +44 -0
- package/dist-lib/lib/maps.functions.d.ts.map +1 -1
- package/dist-lib/lib/maps.functions.js +23 -0
- package/package.json +7 -5
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import type { MapsOverview } from "./maps.functions";
|
|
1
|
+
import type { MapsErrorPage, MapsOverview } from "./maps.functions";
|
|
2
2
|
export declare function getMapsOverviewData(): Promise<MapsOverview>;
|
|
3
3
|
export declare function saveMapsSettings(input: {
|
|
4
4
|
cacheTtlDays: number;
|
|
5
5
|
dailyCallLimit: number;
|
|
6
6
|
allowEstimateFallback: boolean;
|
|
7
7
|
provider: "free" | "google";
|
|
8
|
+
claimViaRpcEnabled?: boolean;
|
|
9
|
+
perCallLimitEnabled?: boolean;
|
|
10
|
+
maxAttemptsEnabled?: boolean;
|
|
11
|
+
stopBijSleutelfoutEnabled?: boolean;
|
|
12
|
+
maxAttempts?: number;
|
|
13
|
+
timezone?: string;
|
|
14
|
+
expectedUpdatedAt?: string | null;
|
|
8
15
|
}): Promise<{
|
|
9
16
|
ok: boolean;
|
|
10
17
|
}>;
|
|
@@ -14,4 +21,5 @@ export declare function clearCache(): Promise<{
|
|
|
14
21
|
export declare function purgeExpired(): Promise<{
|
|
15
22
|
ok: boolean;
|
|
16
23
|
}>;
|
|
24
|
+
export declare function listMapsErrors(page: number, pageSize: number): Promise<MapsErrorPage>;
|
|
17
25
|
//# sourceMappingURL=maps-admin.server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maps-admin.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-admin.server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"maps-admin.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-admin.server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAkCpE,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,YAAY,CAAC,CA8EjE;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;;GA8BA;AAED,wBAAsB,UAAU;;GAK/B;AAED,wBAAsB,YAAY;;GAQjC;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA0B3F"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Server-only beheerlogica voor de kaarten-module.
|
|
2
2
|
import { createClient } from "@supabase/supabase-js";
|
|
3
3
|
import { getGoogleConnectionStatus } from "./central-auth.server";
|
|
4
|
+
import { classifyGoogleError, resolveProcessorSettings } from "./maps-errors";
|
|
4
5
|
// Ruwe schatting: Routes/Places kosten ~ USD 5 per 1000 elementen.
|
|
5
6
|
// Alleen relevant zodra de betaalde Google-provider actief is.
|
|
6
7
|
const COST_PER_ELEMENT = 0.005;
|
|
@@ -66,6 +67,13 @@ export async function getMapsOverviewData() {
|
|
|
66
67
|
dailyCallLimit: settings.daily_call_limit,
|
|
67
68
|
allowEstimateFallback: settings.allow_estimate_fallback,
|
|
68
69
|
provider: settings.provider === "google" ? "google" : "free",
|
|
70
|
+
claimViaRpcEnabled: settings.claim_via_rpc_enabled === true,
|
|
71
|
+
perCallLimitEnabled: settings.per_call_limit_enabled === true,
|
|
72
|
+
maxAttemptsEnabled: settings.max_attempts_enabled === true,
|
|
73
|
+
stopBijSleutelfoutEnabled: settings.stop_bij_sleutelfout_enabled === true,
|
|
74
|
+
maxAttempts: resolveProcessorSettings({ ...settings, max_attempts_enabled: true }).maxAttempts,
|
|
75
|
+
timezone: resolveProcessorSettings(settings).timezone,
|
|
76
|
+
updatedAt: settings.updated_at,
|
|
69
77
|
lastSuccessAt: settings.last_success_at,
|
|
70
78
|
lastErrorAt: settings.last_error_at,
|
|
71
79
|
lastError: settings.last_error,
|
|
@@ -89,18 +97,37 @@ export async function getMapsOverviewData() {
|
|
|
89
97
|
export async function saveMapsSettings(input) {
|
|
90
98
|
const db = adminClient();
|
|
91
99
|
const settings = await ensureSettings(db);
|
|
92
|
-
const
|
|
93
|
-
.from("maps_settings")
|
|
94
|
-
.update({
|
|
100
|
+
const patch = {
|
|
95
101
|
cache_ttl_days: input.cacheTtlDays,
|
|
96
102
|
daily_call_limit: input.dailyCallLimit,
|
|
97
103
|
allow_estimate_fallback: input.allowEstimateFallback,
|
|
98
104
|
provider: input.provider,
|
|
99
105
|
updated_at: new Date().toISOString(),
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
};
|
|
107
|
+
if (input.claimViaRpcEnabled !== undefined)
|
|
108
|
+
patch.claim_via_rpc_enabled = input.claimViaRpcEnabled;
|
|
109
|
+
if (input.perCallLimitEnabled !== undefined)
|
|
110
|
+
patch.per_call_limit_enabled = input.perCallLimitEnabled;
|
|
111
|
+
if (input.maxAttemptsEnabled !== undefined)
|
|
112
|
+
patch.max_attempts_enabled = input.maxAttemptsEnabled;
|
|
113
|
+
if (input.stopBijSleutelfoutEnabled !== undefined)
|
|
114
|
+
patch.stop_bij_sleutelfout_enabled = input.stopBijSleutelfoutEnabled;
|
|
115
|
+
if (input.maxAttempts !== undefined)
|
|
116
|
+
patch.max_attempts = input.maxAttempts;
|
|
117
|
+
if (input.timezone !== undefined)
|
|
118
|
+
patch.timezone = input.timezone;
|
|
119
|
+
let query = db.from("maps_settings").update(patch).eq("id", settings.id);
|
|
120
|
+
// Optimistische vergrendeling op updated_at.
|
|
121
|
+
if (input.expectedUpdatedAt)
|
|
122
|
+
query = query.eq("updated_at", input.expectedUpdatedAt);
|
|
123
|
+
const { data, error } = await query.select("id");
|
|
124
|
+
if (error) {
|
|
125
|
+
console.error("[maps] instellingen opslaan", error);
|
|
103
126
|
throw new Error(error.message);
|
|
127
|
+
}
|
|
128
|
+
if (input.expectedUpdatedAt && (!Array.isArray(data) || data.length === 0)) {
|
|
129
|
+
throw new Error("Dit is intussen door iemand anders gewijzigd. Ververs de pagina.");
|
|
130
|
+
}
|
|
104
131
|
return { ok: true };
|
|
105
132
|
}
|
|
106
133
|
export async function clearCache() {
|
|
@@ -119,3 +146,30 @@ export async function purgeExpired() {
|
|
|
119
146
|
throw new Error(error.message);
|
|
120
147
|
return { ok: true };
|
|
121
148
|
}
|
|
149
|
+
export async function listMapsErrors(page, pageSize) {
|
|
150
|
+
const db = adminClient();
|
|
151
|
+
const from = page * pageSize;
|
|
152
|
+
const { data, count: total, error } = await db
|
|
153
|
+
.from("maps_usage_log")
|
|
154
|
+
.select("id, call_type, outcome, detail, created_at", { count: "exact" })
|
|
155
|
+
.in("outcome", ["error", "no_result"])
|
|
156
|
+
.order("created_at", { ascending: false })
|
|
157
|
+
.range(from, from + pageSize - 1);
|
|
158
|
+
if (error) {
|
|
159
|
+
console.error("[maps] fouten lezen", error);
|
|
160
|
+
throw new Error(error.message);
|
|
161
|
+
}
|
|
162
|
+
const rows = Array.isArray(data) ? data : [];
|
|
163
|
+
return {
|
|
164
|
+
total: total ?? 0,
|
|
165
|
+
page,
|
|
166
|
+
pageSize,
|
|
167
|
+
items: rows.map((row) => ({
|
|
168
|
+
id: String(row.id),
|
|
169
|
+
at: row.created_at,
|
|
170
|
+
callType: row.call_type,
|
|
171
|
+
kind: row.outcome === "no_result" ? "not_found" : classifyGoogleError(row.detail),
|
|
172
|
+
message: row.detail ?? (row.outcome === "no_result" ? "Adres niet gevonden" : "Onbekende fout"),
|
|
173
|
+
})),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type GoogleErrorKind = "quota" | "invalid_key" | "not_found" | "token" | "other";
|
|
2
|
+
/** Deelt een foutmelding in. Volgorde is bewust: token vóór sleutel. */
|
|
3
|
+
export declare function classifyGoogleError(message: string | null | undefined): GoogleErrorKind;
|
|
4
|
+
/** Fouten waarbij doorgaan zinloos is (alleen relevant als de schakelaar aan staat). */
|
|
5
|
+
export declare function isFatalKeyError(kind: GoogleErrorKind): boolean;
|
|
6
|
+
export declare const DEFAULT_MAX_ATTEMPTS = 3;
|
|
7
|
+
export declare const DEFAULT_DAILY_LIMIT = 2000;
|
|
8
|
+
export declare const DEFAULT_TIMEZONE = "Europe/Amsterdam";
|
|
9
|
+
export interface ProcessorSettings {
|
|
10
|
+
dailyCallLimit: number;
|
|
11
|
+
maxAttempts: number;
|
|
12
|
+
claimViaRpc: boolean;
|
|
13
|
+
perCallLimit: boolean;
|
|
14
|
+
stopOnKeyError: boolean;
|
|
15
|
+
timezone: string;
|
|
16
|
+
}
|
|
17
|
+
/** Ontbrekende waarden vallen terug op het huidige gedrag (alle schakelaars uit). */
|
|
18
|
+
export declare function resolveProcessorSettings(row: Record<string, unknown> | null | undefined): ProcessorSettings;
|
|
19
|
+
/** Kalenderdatum (yyyy-mm-dd) in de opgegeven tijdzone. */
|
|
20
|
+
export declare function calendarDay(date: Date, timeZone: string): string;
|
|
21
|
+
export declare class DailyLimitReachedError extends Error {
|
|
22
|
+
constructor();
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=maps-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"maps-errors.d.ts","sourceRoot":"","sources":["../../src/lib/maps-errors.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC;AAExF,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,CAUvF;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAE9D;AAED,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qFAAqF;AACrF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAa3G;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUhE;AAED,qBAAa,sBAAuB,SAAQ,KAAK;;CAKhD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Pure helpers voor foutindeling en instellingen van de wachtrijverwerking.
|
|
2
|
+
/** Deelt een foutmelding in. Volgorde is bewust: token vóór sleutel. */
|
|
3
|
+
export function classifyGoogleError(message) {
|
|
4
|
+
const m = (message ?? "").toLowerCase();
|
|
5
|
+
if (!m)
|
|
6
|
+
return "other";
|
|
7
|
+
if (/central auth|token|credential|niet geconfigureerd/.test(m))
|
|
8
|
+
return "token";
|
|
9
|
+
if (/api[_ -]?key|api key not valid|permission_denied|request_denied|ongeldige sleutel|\b403\b|\b401\b/.test(m))
|
|
10
|
+
return "invalid_key";
|
|
11
|
+
if (/quota|resource_exhausted|rate limit|over_query_limit|\b429\b|daglimiet/.test(m))
|
|
12
|
+
return "quota";
|
|
13
|
+
if (/zero_results|not[_ ]found|geen resultaat|niet gevonden|geen route|niet worden omgezet/.test(m))
|
|
14
|
+
return "not_found";
|
|
15
|
+
return "other";
|
|
16
|
+
}
|
|
17
|
+
/** Fouten waarbij doorgaan zinloos is (alleen relevant als de schakelaar aan staat). */
|
|
18
|
+
export function isFatalKeyError(kind) {
|
|
19
|
+
return kind === "invalid_key" || kind === "token";
|
|
20
|
+
}
|
|
21
|
+
export const DEFAULT_MAX_ATTEMPTS = 3;
|
|
22
|
+
export const DEFAULT_DAILY_LIMIT = 2000;
|
|
23
|
+
export const DEFAULT_TIMEZONE = "Europe/Amsterdam";
|
|
24
|
+
/** Ontbrekende waarden vallen terug op het huidige gedrag (alle schakelaars uit). */
|
|
25
|
+
export function resolveProcessorSettings(row) {
|
|
26
|
+
const r = row ?? {};
|
|
27
|
+
const limit = Number(r.daily_call_limit);
|
|
28
|
+
const max = Number(r.max_attempts);
|
|
29
|
+
const maxEnabled = r.max_attempts_enabled === true;
|
|
30
|
+
return {
|
|
31
|
+
dailyCallLimit: Number.isFinite(limit) && r.daily_call_limit != null ? limit : DEFAULT_DAILY_LIMIT,
|
|
32
|
+
maxAttempts: maxEnabled && Number.isFinite(max) && max >= 1 ? Math.round(max) : DEFAULT_MAX_ATTEMPTS,
|
|
33
|
+
claimViaRpc: r.claim_via_rpc_enabled === true,
|
|
34
|
+
perCallLimit: r.per_call_limit_enabled === true,
|
|
35
|
+
stopOnKeyError: r.stop_bij_sleutelfout_enabled === true,
|
|
36
|
+
timezone: typeof r.timezone === "string" && r.timezone ? r.timezone : DEFAULT_TIMEZONE,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Kalenderdatum (yyyy-mm-dd) in de opgegeven tijdzone. */
|
|
40
|
+
export function calendarDay(date, timeZone) {
|
|
41
|
+
try {
|
|
42
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
43
|
+
timeZone, year: "numeric", month: "2-digit", day: "2-digit",
|
|
44
|
+
}).format(date);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
48
|
+
timeZone: DEFAULT_TIMEZONE, year: "numeric", month: "2-digit", day: "2-digit",
|
|
49
|
+
}).format(date);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class DailyLimitReachedError extends Error {
|
|
53
|
+
constructor() {
|
|
54
|
+
super("Daglimiet bereikt");
|
|
55
|
+
this.name = "DailyLimitReachedError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"maps-errors.test.d.ts","sourceRoot":"","sources":["../../src/lib/maps-errors.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { calendarDay, classifyGoogleError, isFatalKeyError, resolveProcessorSettings, } from "./maps-errors";
|
|
3
|
+
describe("classifyGoogleError", () => {
|
|
4
|
+
it("herkent quota", () => expect(classifyGoogleError("RESOURCE_EXHAUSTED: quota exceeded (429)")).toBe("quota"));
|
|
5
|
+
it("herkent ongeldige sleutel", () => expect(classifyGoogleError("API key not valid. Please pass a valid API key.")).toBe("invalid_key"));
|
|
6
|
+
it("herkent adres niet gevonden", () => expect(classifyGoogleError("Geen resultaat van Google")).toBe("not_found"));
|
|
7
|
+
it("herkent tokenfout van Central", () => expect(classifyGoogleError("Central Auth weigert het secret van deze module")).toBe("token"));
|
|
8
|
+
it("onbekend is other", () => expect(classifyGoogleError("iets raars")).toBe("other"));
|
|
9
|
+
it("alleen sleutel- en tokenfouten zijn fataal", () => {
|
|
10
|
+
expect(isFatalKeyError("invalid_key")).toBe(true);
|
|
11
|
+
expect(isFatalKeyError("token")).toBe(true);
|
|
12
|
+
expect(isFatalKeyError("quota")).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe("resolveProcessorSettings", () => {
|
|
16
|
+
it("ontbrekende instellingen = huidig gedrag (limiet 2000, 3 pogingen, alles uit)", () => {
|
|
17
|
+
expect(resolveProcessorSettings(null)).toEqual({
|
|
18
|
+
dailyCallLimit: 2000,
|
|
19
|
+
maxAttempts: 3,
|
|
20
|
+
claimViaRpc: false,
|
|
21
|
+
perCallLimit: false,
|
|
22
|
+
stopOnKeyError: false,
|
|
23
|
+
timezone: "Europe/Amsterdam",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it("max_attempts telt alleen als de schakelaar aan staat", () => {
|
|
27
|
+
expect(resolveProcessorSettings({ max_attempts: 7 }).maxAttempts).toBe(3);
|
|
28
|
+
expect(resolveProcessorSettings({ max_attempts: 7, max_attempts_enabled: true }).maxAttempts).toBe(7);
|
|
29
|
+
});
|
|
30
|
+
it("neemt de daglimiet uit de instellingen", () => {
|
|
31
|
+
expect(resolveProcessorSettings({ daily_call_limit: 50 }).dailyCallLimit).toBe(50);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("calendarDay", () => {
|
|
35
|
+
it("gebruikt de tijdzone voor de daggrens", () => {
|
|
36
|
+
// 23:30 UTC op 9 nov = 00:30 op 10 nov in Amsterdam
|
|
37
|
+
expect(calendarDay(new Date(Date.UTC(2026, 10, 9, 23, 30)), "Europe/Amsterdam")).toBe("2026-11-10");
|
|
38
|
+
expect(calendarDay(new Date(Date.UTC(2026, 10, 9, 23, 30)), "UTC")).toBe("2026-11-09");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maps-processor.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-processor.server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"maps-processor.server.d.ts","sourceRoot":"","sources":["../../src/lib/maps-processor.server.ts"],"names":[],"mappings":"AAeA,uEAAuE;AACvE,eAAO,MAAM,YAAY,IAAI,CAAC;AAG9B,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,+DAA+D;AAC/D,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9D;AAcD,0EAA0E;AAC1E,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAUxD;AAED,2CAA2C;AAC3C,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMvD;AA2LD,yEAAyE;AACzE,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,aAAa,CAAC,CAyNpE"}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Leest maps_request_queue, geocodet adressen en haalt reistijden op.
|
|
3
3
|
import { createClient } from "@supabase/supabase-js";
|
|
4
4
|
import { computeRouteMatrix, geocodeAddress } from "./google-maps.server";
|
|
5
|
+
import { DailyLimitReachedError, calendarDay, classifyGoogleError, isFatalKeyError, resolveProcessorSettings, } from "./maps-errors";
|
|
5
6
|
const BATCH_SIZE = 50;
|
|
6
|
-
|
|
7
|
+
/** Terugvalwaarde (huidig gedrag) voor het maximum aantal pogingen. */
|
|
8
|
+
export const MAX_ATTEMPTS = 3;
|
|
7
9
|
const MAX_MATRIX_SIDE = 25;
|
|
8
10
|
/** SHA-256 hash via Web Crypto (browser, Node 15+ en Deno). */
|
|
9
11
|
export async function addressHash(raw) {
|
|
@@ -69,15 +71,81 @@ async function callsToday(db) {
|
|
|
69
71
|
const rows = Array.isArray(data) ? data : [];
|
|
70
72
|
return rows.reduce((sum, row) => sum + (row.elements ?? 0), 0);
|
|
71
73
|
}
|
|
74
|
+
const noop = async () => { };
|
|
75
|
+
/**
|
|
76
|
+
* Verhoogt de dagteller atomair in de database (alleen bij per_call_limit_enabled).
|
|
77
|
+
* Gooit DailyLimitReachedError als de limiet bereikt is; dan wordt Google niet aangeroepen.
|
|
78
|
+
*/
|
|
79
|
+
function makeConsumeCall(db, cfg, initialUsed) {
|
|
80
|
+
if (!cfg.perCallLimit)
|
|
81
|
+
return noop;
|
|
82
|
+
return async () => {
|
|
83
|
+
const { data, error } = await db.rpc("maps_try_consume_call", {
|
|
84
|
+
p_day: calendarDay(new Date(), cfg.timezone),
|
|
85
|
+
p_limit: cfg.dailyCallLimit,
|
|
86
|
+
p_initial: initialUsed,
|
|
87
|
+
});
|
|
88
|
+
if (error) {
|
|
89
|
+
console.error("[maps] maps_try_consume_call", error);
|
|
90
|
+
throw new Error(`Daglimietteller niet bereikbaar: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (data == null)
|
|
93
|
+
throw new DailyLimitReachedError();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function claimItems(db, cfg) {
|
|
97
|
+
if (cfg.claimViaRpc) {
|
|
98
|
+
const { data, error } = await db.rpc("maps_claim_queue_items", {
|
|
99
|
+
p_limit: BATCH_SIZE,
|
|
100
|
+
p_max_attempts: cfg.maxAttempts,
|
|
101
|
+
});
|
|
102
|
+
if (error) {
|
|
103
|
+
console.error("[maps] maps_claim_queue_items", error);
|
|
104
|
+
throw new Error(`Wachtrij claimen mislukt: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
return Array.isArray(data) ? data : [];
|
|
107
|
+
}
|
|
108
|
+
// Huidig gedrag (schakelaar uit): eerst lezen, dan op processing zetten.
|
|
109
|
+
const { data: queue, error } = await db
|
|
110
|
+
.from("maps_request_queue")
|
|
111
|
+
.select("*")
|
|
112
|
+
.eq("status", "pending")
|
|
113
|
+
.lt("attempts", cfg.maxAttempts)
|
|
114
|
+
.order("created_at", { ascending: true })
|
|
115
|
+
.limit(BATCH_SIZE);
|
|
116
|
+
if (error)
|
|
117
|
+
console.error("[maps] wachtrij lezen", error);
|
|
118
|
+
const items = Array.isArray(queue) ? queue : [];
|
|
119
|
+
if (items.length === 0)
|
|
120
|
+
return items;
|
|
121
|
+
const { error: updError } = await db
|
|
122
|
+
.from("maps_request_queue")
|
|
123
|
+
.update({ status: "processing", updated_at: new Date().toISOString() })
|
|
124
|
+
.in("id", items.map((item) => item.id));
|
|
125
|
+
if (updError)
|
|
126
|
+
console.error("[maps] wachtrij claimen", updError);
|
|
127
|
+
return items;
|
|
128
|
+
}
|
|
129
|
+
async function releaseItem(db, id) {
|
|
130
|
+
// Terug naar pending zonder extra poging (limiet of afgebroken batch).
|
|
131
|
+
const { error } = await db
|
|
132
|
+
.from("maps_request_queue")
|
|
133
|
+
.update({ status: "pending", updated_at: new Date().toISOString() })
|
|
134
|
+
.eq("id", id);
|
|
135
|
+
if (error)
|
|
136
|
+
console.error("[maps] item vrijgeven", error);
|
|
137
|
+
}
|
|
72
138
|
async function logUsage(db, callType, elements, outcome, detail) {
|
|
73
|
-
await db.from("maps_usage_log").insert({
|
|
139
|
+
const { error } = await db.from("maps_usage_log").insert({
|
|
74
140
|
call_type: callType,
|
|
75
141
|
elements,
|
|
76
142
|
outcome,
|
|
77
143
|
detail: detail ?? null,
|
|
78
144
|
});
|
|
145
|
+
if (error)
|
|
146
|
+
console.error("[maps] usage log", error);
|
|
79
147
|
}
|
|
80
|
-
async function ensureAddress(db, raw) {
|
|
148
|
+
async function ensureAddress(db, raw, beforeCall = noop) {
|
|
81
149
|
const hash = await addressHash(raw);
|
|
82
150
|
const { data: existing } = await db
|
|
83
151
|
.from("maps_addresses")
|
|
@@ -97,6 +165,7 @@ async function ensureAddress(db, raw) {
|
|
|
97
165
|
.single()).data?.id;
|
|
98
166
|
if (!id)
|
|
99
167
|
return null;
|
|
168
|
+
await beforeCall();
|
|
100
169
|
try {
|
|
101
170
|
const result = await geocodeAddress(raw);
|
|
102
171
|
if (!result) {
|
|
@@ -141,6 +210,7 @@ async function ensureAddress(db, raw) {
|
|
|
141
210
|
export async function processMapsQueueBatch() {
|
|
142
211
|
const db = adminClient();
|
|
143
212
|
const settings = await getSettings(db);
|
|
213
|
+
const cfg = resolveProcessorSettings(settings);
|
|
144
214
|
const result = {
|
|
145
215
|
processed: 0,
|
|
146
216
|
geocoded: 0,
|
|
@@ -149,36 +219,38 @@ export async function processMapsQueueBatch() {
|
|
|
149
219
|
skipped: null,
|
|
150
220
|
};
|
|
151
221
|
const used = await callsToday(db);
|
|
152
|
-
if (settings && used >=
|
|
222
|
+
if (settings && used >= cfg.dailyCallLimit) {
|
|
153
223
|
await logUsage(db, "limit", 0, "skipped", "Daglimiet bereikt");
|
|
154
224
|
result.skipped = "Daglimiet bereikt";
|
|
155
225
|
return result;
|
|
156
226
|
}
|
|
157
|
-
const
|
|
158
|
-
.from("maps_request_queue")
|
|
159
|
-
.select("*")
|
|
160
|
-
.eq("status", "pending")
|
|
161
|
-
.lt("attempts", MAX_ATTEMPTS)
|
|
162
|
-
.order("created_at", { ascending: true })
|
|
163
|
-
.limit(BATCH_SIZE);
|
|
164
|
-
const items = Array.isArray(queue) ? queue : [];
|
|
227
|
+
const items = await claimItems(db, cfg);
|
|
165
228
|
if (items.length === 0)
|
|
166
229
|
return result;
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
230
|
+
const beforeCall = makeConsumeCall(db, cfg, used);
|
|
231
|
+
let aborted = false;
|
|
232
|
+
let limitLogged = false;
|
|
233
|
+
const onLimit = async () => {
|
|
234
|
+
if (!limitLogged) {
|
|
235
|
+
limitLogged = true;
|
|
236
|
+
await logUsage(db, "limit", 0, "skipped", "Daglimiet bereikt");
|
|
237
|
+
}
|
|
238
|
+
result.skipped = "Daglimiet bereikt";
|
|
239
|
+
};
|
|
172
240
|
const ttlDays = settings?.cache_ttl_days ?? 30;
|
|
173
241
|
const expiresAt = new Date(Date.now() + ttlDays * 86400000).toISOString();
|
|
174
242
|
const pairs = [];
|
|
175
243
|
for (const item of items) {
|
|
244
|
+
if (aborted) {
|
|
245
|
+
await releaseItem(db, item.id);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
176
248
|
try {
|
|
177
249
|
if (item.kind === "geocode") {
|
|
178
250
|
const raw = item.raw_origin ?? item.raw_destination;
|
|
179
251
|
if (!raw)
|
|
180
252
|
throw new Error("Geen adres in wachtrijregel");
|
|
181
|
-
const address = await ensureAddress(db, raw);
|
|
253
|
+
const address = await ensureAddress(db, raw, beforeCall);
|
|
182
254
|
if (address)
|
|
183
255
|
result.geocoded += 1;
|
|
184
256
|
else
|
|
@@ -196,9 +268,9 @@ export async function processMapsQueueBatch() {
|
|
|
196
268
|
result.processed += 1;
|
|
197
269
|
continue;
|
|
198
270
|
}
|
|
199
|
-
const origin = item.raw_origin ? await ensureAddress(db, item.raw_origin) : null;
|
|
271
|
+
const origin = item.raw_origin ? await ensureAddress(db, item.raw_origin, beforeCall) : null;
|
|
200
272
|
const destination = item.raw_destination
|
|
201
|
-
? await ensureAddress(db, item.raw_destination)
|
|
273
|
+
? await ensureAddress(db, item.raw_destination, beforeCall)
|
|
202
274
|
: null;
|
|
203
275
|
if (!origin || !destination) {
|
|
204
276
|
result.failed += 1;
|
|
@@ -218,12 +290,20 @@ export async function processMapsQueueBatch() {
|
|
|
218
290
|
pairs.push({ item, origin, destination });
|
|
219
291
|
}
|
|
220
292
|
catch (error) {
|
|
293
|
+
if (error instanceof DailyLimitReachedError) {
|
|
294
|
+
await releaseItem(db, item.id);
|
|
295
|
+
await onLimit();
|
|
296
|
+
aborted = true;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
221
299
|
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
+
if (cfg.stopOnKeyError && isFatalKeyError(classifyGoogleError(message)))
|
|
301
|
+
aborted = true;
|
|
222
302
|
result.failed += 1;
|
|
223
303
|
await db
|
|
224
304
|
.from("maps_request_queue")
|
|
225
305
|
.update({
|
|
226
|
-
status: (item.attempts ?? 0) + 1 >=
|
|
306
|
+
status: (item.attempts ?? 0) + 1 >= cfg.maxAttempts ? "failed" : "pending",
|
|
227
307
|
attempts: (item.attempts ?? 0) + 1,
|
|
228
308
|
last_error: message,
|
|
229
309
|
updated_at: new Date().toISOString(),
|
|
@@ -241,7 +321,13 @@ export async function processMapsQueueBatch() {
|
|
|
241
321
|
const origins = chunk.map((pair) => pair.origin);
|
|
242
322
|
const destinations = chunk.map((pair) => pair.destination);
|
|
243
323
|
const mode = chunk[0]?.item.mode ?? "drive";
|
|
324
|
+
if (aborted) {
|
|
325
|
+
for (const pair of chunk)
|
|
326
|
+
await releaseItem(db, pair.item.id);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
244
329
|
try {
|
|
330
|
+
await beforeCall();
|
|
245
331
|
const elements = await computeRouteMatrix(origins, destinations, mode);
|
|
246
332
|
await logUsage(db, "matrix", chunk.length, "ok");
|
|
247
333
|
for (let position = 0; position < chunk.length; position += 1) {
|
|
@@ -290,7 +376,16 @@ export async function processMapsQueueBatch() {
|
|
|
290
376
|
.eq("id", settings.id);
|
|
291
377
|
}
|
|
292
378
|
catch (error) {
|
|
379
|
+
if (error instanceof DailyLimitReachedError) {
|
|
380
|
+
for (const pair of chunk)
|
|
381
|
+
await releaseItem(db, pair.item.id);
|
|
382
|
+
await onLimit();
|
|
383
|
+
aborted = true;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
293
386
|
const message = error instanceof Error ? error.message : String(error);
|
|
387
|
+
if (cfg.stopOnKeyError && isFatalKeyError(classifyGoogleError(message)))
|
|
388
|
+
aborted = true;
|
|
294
389
|
await logUsage(db, "matrix", chunk.length, "error", message);
|
|
295
390
|
await db
|
|
296
391
|
.from("maps_settings")
|
|
@@ -302,7 +397,7 @@ export async function processMapsQueueBatch() {
|
|
|
302
397
|
await db
|
|
303
398
|
.from("maps_request_queue")
|
|
304
399
|
.update({
|
|
305
|
-
status: (pair.item.attempts ?? 0) + 1 >=
|
|
400
|
+
status: (pair.item.attempts ?? 0) + 1 >= cfg.maxAttempts ? "failed" : "pending",
|
|
306
401
|
attempts: (pair.item.attempts ?? 0) + 1,
|
|
307
402
|
last_error: message,
|
|
308
403
|
updated_at: new Date().toISOString(),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"maps-processor.test.d.ts","sourceRoot":"","sources":["../../src/lib/maps-processor.test.ts"],"names":[],"mappings":""}
|