@jskit-ai/connectors-catalog 0.2.5 → 0.2.6
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/connectors-catalog",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Provider definitions and focused API operations for JSKIT connectors.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -125,14 +125,14 @@
|
|
|
125
125
|
"@aws-sdk/client-redshift-data": "3.1128.0",
|
|
126
126
|
"@aws-sdk/client-s3": "3.1128.0",
|
|
127
127
|
"@aws-sdk/s3-request-presigner": "3.1128.0",
|
|
128
|
-
"@jskit-ai/connectors-core": "0.2.
|
|
128
|
+
"@jskit-ai/connectors-core": "0.2.6",
|
|
129
129
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
130
130
|
"@smithy/fetch-http-handler": "5.8.0",
|
|
131
131
|
"jose": "^6.1.3",
|
|
132
132
|
"json-rest-schema": "^1.0.17"
|
|
133
133
|
},
|
|
134
134
|
"peerDependencies": {
|
|
135
|
-
"@jskit-ai/kernel": "0.2.
|
|
135
|
+
"@jskit-ai/kernel": "0.2.5"
|
|
136
136
|
},
|
|
137
137
|
"jskit": {
|
|
138
138
|
"kind": "runtime",
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Optional browser implementation. The application owns consent and route hooks.
|
|
2
2
|
const installations = new WeakMap();
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} options
|
|
6
|
+
* @param {string} options.measurementId
|
|
7
|
+
* @param {Window & typeof globalThis} [options.window] The page to install on, the current one by default.
|
|
8
|
+
*/
|
|
9
|
+
function createGoogleAnalytics({ measurementId, window: browser = globalThis.window }) {
|
|
5
10
|
if (typeof measurementId !== "string" || !/^G-[A-Z0-9]+$/.test(measurementId) || measurementId.length > 64) throw new TypeError("Supply a valid GA4 Measurement ID.");
|
|
6
11
|
if (!browser?.document) throw new TypeError("Initialize Google Analytics in the browser after hydration.");
|
|
7
12
|
const existing = installations.get(browser);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Compose an app-owned Google map after the application has loaded Google's SDK. */
|
|
2
|
-
async function mountGoogleMap({ element, maps = globalThis.google?.maps, center, zoom = 12, mapId, markers = [], path = [], signal
|
|
2
|
+
async function mountGoogleMap({ element, maps = globalThis.google?.maps, center, zoom = 12, mapId, markers = [], path = [], signal = new AbortController().signal }) {
|
|
3
3
|
const coordinate = point => {
|
|
4
4
|
if (!point || !Number.isFinite(point.lat) || Math.abs(point.lat) > 90 || !Number.isFinite(point.lng) || Math.abs(point.lng) > 180) throw new Error("Use finite lat/lng coordinates within Earth bounds.");
|
|
5
5
|
return { lat: point.lat, lng: point.lng };
|
|
6
6
|
};
|
|
7
|
+
// readable-check: allow method-probe - platform object: Google's SDK is on the page only once the application has loaded it
|
|
7
8
|
if (!element?.replaceChildren || typeof maps?.importLibrary !== "function") throw new Error("Provide a map container and load the Google Maps JavaScript SDK first.");
|
|
8
9
|
if (!Number.isFinite(zoom) || zoom < 0 || zoom > 22) throw new Error("Use zoom between 0 and 22.");
|
|
9
10
|
if (!Array.isArray(markers) || markers.length > 100 || !Array.isArray(path) || path.length > 10000) throw new Error("Use at most 100 markers and 10000 route points.");
|
|
@@ -14,16 +15,16 @@ async function mountGoogleMap({ element, maps = globalThis.google?.maps, center,
|
|
|
14
15
|
return { position: coordinate(marker.position), title: marker.title || "" };
|
|
15
16
|
});
|
|
16
17
|
const route = path.map(coordinate);
|
|
17
|
-
signal
|
|
18
|
+
signal.throwIfAborted();
|
|
18
19
|
const library = await maps.importLibrary("maps");
|
|
19
20
|
const markerLibrary = markerOptions.length ? await maps.importLibrary("marker") : null;
|
|
20
|
-
signal
|
|
21
|
+
signal.throwIfAborted();
|
|
21
22
|
const map = new library.Map(element, { center: position, zoom, ...(mapId ? { mapId } : {}) });
|
|
22
23
|
const pins = []; let line; let disposed = false;
|
|
23
24
|
function dispose() {
|
|
24
25
|
if (disposed) return;
|
|
25
26
|
disposed = true;
|
|
26
|
-
signal
|
|
27
|
+
signal.removeEventListener("abort", dispose);
|
|
27
28
|
for (const pin of pins) { maps.event.clearInstanceListeners(pin); pin.map = null; }
|
|
28
29
|
if (line) { maps.event.clearInstanceListeners(line); line.setMap(null); }
|
|
29
30
|
maps.event.clearInstanceListeners(map);
|
|
@@ -32,7 +33,7 @@ async function mountGoogleMap({ element, maps = globalThis.google?.maps, center,
|
|
|
32
33
|
try {
|
|
33
34
|
for (const options of markerOptions) pins.push(new markerLibrary.AdvancedMarkerElement({ ...options, map }));
|
|
34
35
|
if (route.length) line = new library.Polyline({ map, path: route });
|
|
35
|
-
signal
|
|
36
|
+
signal.addEventListener("abort", dispose, { once: true });
|
|
36
37
|
return { map, dispose };
|
|
37
38
|
} catch (error) { dispose(); throw error; }
|
|
38
39
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createGoogleAnalytics } from "../src/client/google-analytics.js";
|
|
4
|
+
import { mountGoogleMap } from "../src/client/google-maps-platform.js";
|
|
5
|
+
|
|
6
|
+
function createMapElement() {
|
|
7
|
+
return {
|
|
8
|
+
clears: 0,
|
|
9
|
+
replaceChildren() {
|
|
10
|
+
this.clears += 1;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createMapsSdk() {
|
|
16
|
+
const imported = [];
|
|
17
|
+
const released = [];
|
|
18
|
+
class Map {
|
|
19
|
+
constructor(element, options) {
|
|
20
|
+
this.element = element;
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
imported,
|
|
26
|
+
released,
|
|
27
|
+
async importLibrary(name) {
|
|
28
|
+
imported.push(name);
|
|
29
|
+
return { Map };
|
|
30
|
+
},
|
|
31
|
+
event: {
|
|
32
|
+
clearInstanceListeners(target) {
|
|
33
|
+
released.push(target);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createAnalyticsPage() {
|
|
40
|
+
const scripts = [];
|
|
41
|
+
return {
|
|
42
|
+
scripts,
|
|
43
|
+
location: { href: "https://example.test/start" },
|
|
44
|
+
document: {
|
|
45
|
+
title: "Start",
|
|
46
|
+
querySelector: () => null,
|
|
47
|
+
createElement: () => ({}),
|
|
48
|
+
head: {
|
|
49
|
+
appendChild(script) {
|
|
50
|
+
scripts.push(script);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const center = { lat: -31.95, lng: 115.86 };
|
|
58
|
+
|
|
59
|
+
test("a map mounts and disposes once without an abort signal", async () => {
|
|
60
|
+
const element = createMapElement();
|
|
61
|
+
const maps = createMapsSdk();
|
|
62
|
+
|
|
63
|
+
const mounted = await mountGoogleMap({ element, maps, center });
|
|
64
|
+
assert.deepEqual(mounted.map.options, { center, zoom: 12 });
|
|
65
|
+
assert.equal(element.clears, 0);
|
|
66
|
+
|
|
67
|
+
mounted.dispose();
|
|
68
|
+
mounted.dispose();
|
|
69
|
+
assert.equal(element.clears, 1);
|
|
70
|
+
assert.deepEqual(maps.released, [mounted.map]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a signal aborted before mounting stops it before Google's SDK loads a library", async () => {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const maps = createMapsSdk();
|
|
76
|
+
controller.abort();
|
|
77
|
+
|
|
78
|
+
await assert.rejects(mountGoogleMap({ element: createMapElement(), maps, center, signal: controller.signal }));
|
|
79
|
+
assert.deepEqual(maps.imported, []);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("aborting a mounted map disposes it, and disposing it stops listening to the signal", async () => {
|
|
83
|
+
const aborted = new AbortController();
|
|
84
|
+
const abortedElement = createMapElement();
|
|
85
|
+
await mountGoogleMap({ element: abortedElement, maps: createMapsSdk(), center, signal: aborted.signal });
|
|
86
|
+
aborted.abort();
|
|
87
|
+
assert.equal(abortedElement.clears, 1);
|
|
88
|
+
|
|
89
|
+
const disposed = new AbortController();
|
|
90
|
+
const disposedElement = createMapElement();
|
|
91
|
+
const mounted = await mountGoogleMap({ element: disposedElement, maps: createMapsSdk(), center, signal: disposed.signal });
|
|
92
|
+
mounted.dispose();
|
|
93
|
+
disposed.abort();
|
|
94
|
+
assert.equal(disposedElement.clears, 1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a page without Google's SDK is told to load it", async () => {
|
|
98
|
+
await assert.rejects(
|
|
99
|
+
mountGoogleMap({ element: createMapElement(), center }),
|
|
100
|
+
/load the Google Maps JavaScript SDK first/u
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a page has one Analytics owner, and nothing loads before consent", () => {
|
|
105
|
+
const page = createAnalyticsPage();
|
|
106
|
+
const analytics = createGoogleAnalytics({ measurementId: "G-FIXTURE1", window: page });
|
|
107
|
+
|
|
108
|
+
assert.equal(createGoogleAnalytics({ measurementId: "G-FIXTURE1", window: page }), analytics);
|
|
109
|
+
assert.throws(() => createGoogleAnalytics({ measurementId: "G-OTHER1", window: page }), /already has an Analytics owner/u);
|
|
110
|
+
assert.equal(page.scripts.length, 0);
|
|
111
|
+
|
|
112
|
+
analytics.setConsent(true);
|
|
113
|
+
assert.equal(page.scripts.length, 1);
|
|
114
|
+
assert.match(page.scripts[0].src, /googletagmanager\.com\/gtag\/js\?id=G-FIXTURE1/u);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("Analytics refuses an invalid Measurement ID and a page with no document", () => {
|
|
118
|
+
assert.throws(() => createGoogleAnalytics({ measurementId: "UA-1", window: createAnalyticsPage() }), /valid GA4 Measurement ID/u);
|
|
119
|
+
assert.throws(() => createGoogleAnalytics({ measurementId: "G-FIXTURE1", window: {} }), /in the browser after hydration/u);
|
|
120
|
+
});
|