@gasboost/client 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/dist/AppsScriptClient.d.ts +30 -0
- package/dist/AppsScriptClient.js +43 -0
- package/dist/AppsScriptJob.d.ts +28 -0
- package/dist/AppsScriptJob.js +84 -0
- package/dist/AppsScriptJobQueue.d.ts +14 -0
- package/dist/AppsScriptJobQueue.js +40 -0
- package/dist/AppsScriptJobRunner.d.ts +20 -0
- package/dist/AppsScriptJobRunner.js +93 -0
- package/dist/AppsScriptJobStore.d.ts +7 -0
- package/dist/AppsScriptJobStore.js +9 -0
- package/dist/google.d.ts +36 -0
- package/dist/google.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/job/AppsScriptJob.d.ts +28 -0
- package/dist/job/AppsScriptJob.js +84 -0
- package/dist/job/AppsScriptJobQueue.d.ts +14 -0
- package/dist/job/AppsScriptJobQueue.js +40 -0
- package/dist/job/AppsScriptJobRunner.d.ts +20 -0
- package/dist/job/AppsScriptJobRunner.js +93 -0
- package/dist/job/AppsScriptJobStore.d.ts +7 -0
- package/dist/job/AppsScriptJobStore.js +9 -0
- package/dist/navigation/AppsScriptContainer.d.ts +9 -0
- package/dist/navigation/AppsScriptContainer.js +38 -0
- package/dist/navigation/AppsScriptHistoryPipeline.d.ts +9 -0
- package/dist/navigation/AppsScriptHistoryPipeline.js +28 -0
- package/dist/navigation/AppsScriptIframe.d.ts +9 -0
- package/dist/navigation/AppsScriptIframe.js +45 -0
- package/dist/navigation/HashProperty.d.ts +7 -0
- package/dist/navigation/HashProperty.js +27 -0
- package/dist/navigation/NavigationEntry.d.ts +9 -0
- package/dist/navigation/NavigationEntry.js +54 -0
- package/dist/navigation/NavigationLocation.d.ts +8 -0
- package/dist/navigation/NavigationLocation.js +43 -0
- package/package.json +20 -0
- package/src/AppsScriptClient.ts +81 -0
- package/src/google.ts +55 -0
- package/src/index.ts +4 -0
- package/src/job/AppsScriptJob.ts +85 -0
- package/src/job/AppsScriptJobQueue.ts +48 -0
- package/src/job/AppsScriptJobRunner.ts +107 -0
- package/src/job/AppsScriptJobStore.ts +16 -0
- package/src/navigation/AppsScriptContainer.ts +47 -0
- package/src/navigation/AppsScriptHistoryPipeline.ts +29 -0
- package/src/navigation/AppsScriptIframe.ts +63 -0
- package/src/navigation/HashProperty.ts +20 -0
- package/src/navigation/NavigationEntry.ts +82 -0
- package/src/navigation/NavigationLocation.ts +54 -0
- package/tests/AppsScriptClient.spec.ts +322 -0
- package/tests/AppsScriptClient.type.spec.ts +52 -0
- package/tests/AppsScriptContainer.spec.ts +502 -0
- package/tests/AppsScriptHistoryPipeline.spec.ts +303 -0
- package/tests/AppsScriptIframe.spec.ts +452 -0
- package/tests/AppsScriptJob.spec.ts +83 -0
- package/tests/AppsScriptJobQueue.spec.ts +125 -0
- package/tests/AppsScriptJobRunner.spec.ts +361 -0
- package/tests/HashProperty.spec.ts +29 -0
- package/tests/NavigationEntry.spec.ts +507 -0
- package/tests/NavigationLocation.spec.ts +189 -0
- package/tests/setup.ts +15 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +6 -0
- package/vitest.config.mts +8 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AppsScriptJob } from "./AppsScriptJob";
|
|
2
|
+
|
|
3
|
+
export interface RunnableJob {
|
|
4
|
+
run(): Promise<void>;
|
|
5
|
+
addJob<T>(job: AppsScriptJob<T>): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class AppsScriptJobQueue {
|
|
9
|
+
private jobs: AppsScriptJob<any>[] = [];
|
|
10
|
+
private listeners: Set<RunnableJob> = new Set();
|
|
11
|
+
|
|
12
|
+
public enqueue<T>(label: string, execute: () => Promise<T>): Promise<T> {
|
|
13
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
14
|
+
// キューにジョブを追加
|
|
15
|
+
const job = new AppsScriptJob<T>(label, execute, resolve, reject);
|
|
16
|
+
this.jobs.push(job);
|
|
17
|
+
this.notify(job);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return promise;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public dequeue(): AppsScriptJob<any> | undefined {
|
|
24
|
+
const job = this.jobs.shift();
|
|
25
|
+
if (!job) return undefined;
|
|
26
|
+
return job;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private notify<T>(job: AppsScriptJob<T>) {
|
|
30
|
+
this.listeners.forEach((listener) => {
|
|
31
|
+
listener.addJob(job);
|
|
32
|
+
listener.run();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public subscribe(listener: RunnableJob) {
|
|
37
|
+
this.listeners.add(listener);
|
|
38
|
+
return () => this.listeners.delete(listener);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
remove(jobId: string) {
|
|
42
|
+
const index = this.jobs.findIndex((job) => job.id === jobId);
|
|
43
|
+
|
|
44
|
+
if (index !== -1) {
|
|
45
|
+
this.jobs.splice(index, 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { AppsScriptJob } from "./AppsScriptJob";
|
|
2
|
+
import { AppsScriptJobQueue, RunnableJob } from "./AppsScriptJobQueue";
|
|
3
|
+
|
|
4
|
+
export class AppsScriptJobRunner implements RunnableJob {
|
|
5
|
+
private jobs: AppsScriptJob<any>[] = [];
|
|
6
|
+
private listeners: Set<() => void> = new Set();
|
|
7
|
+
private MAX_CONCURRENT = 30;
|
|
8
|
+
private running = false;
|
|
9
|
+
|
|
10
|
+
private snapshot: AppsScriptJob<any>[] | null = null;
|
|
11
|
+
private dirty = true;
|
|
12
|
+
|
|
13
|
+
constructor(public readonly queue: AppsScriptJobQueue) {
|
|
14
|
+
this.queue.subscribe(this);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public addJob<T>(job: AppsScriptJob<T>) {
|
|
18
|
+
this.jobs.push(job);
|
|
19
|
+
this.notify();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getJobs() {
|
|
23
|
+
if (this.dirty || !this.snapshot) {
|
|
24
|
+
this.snapshot = [...this.jobs];
|
|
25
|
+
this.dirty = false;
|
|
26
|
+
}
|
|
27
|
+
return this.snapshot;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async run(): Promise<void> {
|
|
31
|
+
if (this.running) return;
|
|
32
|
+
this.running = true;
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
const runningCount = this.jobs.filter((j) => j.isRunning()).length;
|
|
36
|
+
|
|
37
|
+
if (runningCount >= this.MAX_CONCURRENT) break;
|
|
38
|
+
|
|
39
|
+
const job = this.queue.dequeue();
|
|
40
|
+
if (!job) break;
|
|
41
|
+
|
|
42
|
+
job.start();
|
|
43
|
+
this.notify();
|
|
44
|
+
|
|
45
|
+
job
|
|
46
|
+
.execute()
|
|
47
|
+
.then((result) => {
|
|
48
|
+
job.success(result);
|
|
49
|
+
const index = this.jobs.indexOf(job);
|
|
50
|
+
if (index !== -1) {
|
|
51
|
+
this.jobs.splice(index, 1);
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.catch((error) => {
|
|
55
|
+
job.fail(error);
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
this.notify();
|
|
59
|
+
this.running = false;
|
|
60
|
+
this.run();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.running = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public remove(jobId: string) {
|
|
68
|
+
const index = this.jobs.findIndex((job) => job.id === jobId);
|
|
69
|
+
|
|
70
|
+
if (index === -1) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.jobs.splice(index, 1);
|
|
75
|
+
this.notify();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public cancel(jobId: string) {
|
|
79
|
+
const job = this.jobs.find((job) => job.id === jobId);
|
|
80
|
+
|
|
81
|
+
if (!job || !job.cancel()) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.queue.remove(jobId);
|
|
86
|
+
this.jobs.splice(this.jobs.indexOf(job), 1);
|
|
87
|
+
this.notify();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public retry(jobId: AppsScriptJob<any>["id"]) {
|
|
91
|
+
const job = this.jobs.find((j) => j.id === jobId);
|
|
92
|
+
if (job) {
|
|
93
|
+
this.remove(job.id);
|
|
94
|
+
this.queue.enqueue(job.label, job.execute);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public subscribe(listener: () => void) {
|
|
99
|
+
this.listeners.add(listener);
|
|
100
|
+
return () => this.listeners.delete(listener);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private notify() {
|
|
104
|
+
this.dirty = true;
|
|
105
|
+
this.listeners.forEach((listener) => listener());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AppsScriptJob } from "./AppsScriptJob";
|
|
2
|
+
import { AppsScriptJobRunner } from "./AppsScriptJobRunner";
|
|
3
|
+
|
|
4
|
+
export interface AppsScriptJobStore {
|
|
5
|
+
subscribe: (listener: () => void) => () => void;
|
|
6
|
+
getSnapshot: () => AppsScriptJob<any>[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function appsScriptJobStore(
|
|
10
|
+
runner: AppsScriptJobRunner,
|
|
11
|
+
): AppsScriptJobStore {
|
|
12
|
+
return {
|
|
13
|
+
subscribe: runner.subscribe.bind(runner),
|
|
14
|
+
getSnapshot: runner.getJobs.bind(runner),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { google } from "../google";
|
|
2
|
+
|
|
3
|
+
import { NavigationEntry } from "./NavigationEntry";
|
|
4
|
+
|
|
5
|
+
export class AppsScriptContainer {
|
|
6
|
+
private constructor(private entry: NavigationEntry) {}
|
|
7
|
+
|
|
8
|
+
static load(callback: (container: AppsScriptContainer) => void) {
|
|
9
|
+
google.script.url.getLocation((location) => {
|
|
10
|
+
callback(new AppsScriptContainer(NavigationEntry.fromGoogle(location)));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
observe(handler: (entry: NavigationEntry) => void) {
|
|
15
|
+
google.script.history.setChangeHandler((event) => {
|
|
16
|
+
const entry = NavigationEntry.fromGoogle(event);
|
|
17
|
+
|
|
18
|
+
this.entry = entry;
|
|
19
|
+
|
|
20
|
+
handler(entry);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
sync(entry: NavigationEntry) {
|
|
25
|
+
if (this.entry.equals(entry)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.entry = entry;
|
|
30
|
+
|
|
31
|
+
const parameters: Record<string, string[]> = {};
|
|
32
|
+
|
|
33
|
+
for (const key of new Set(entry.location.searchParams.keys())) {
|
|
34
|
+
parameters[key] = entry.location.searchParams.getAll(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
google.script.history.push(
|
|
38
|
+
entry.state,
|
|
39
|
+
parameters,
|
|
40
|
+
entry.location.hash.normalize(),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
current() {
|
|
45
|
+
return this.entry;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AppsScriptContainer } from "./AppsScriptContainer";
|
|
2
|
+
import { AppsScriptIframe } from "./AppsScriptIframe";
|
|
3
|
+
|
|
4
|
+
export class AppsScriptHistoryPipeline {
|
|
5
|
+
constructor(
|
|
6
|
+
private readonly container: AppsScriptContainer,
|
|
7
|
+
private readonly iframe: AppsScriptIframe,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
static create(callback: (pipeline: AppsScriptHistoryPipeline) => void) {
|
|
11
|
+
AppsScriptContainer.load((container) => {
|
|
12
|
+
callback(
|
|
13
|
+
new AppsScriptHistoryPipeline(container, new AppsScriptIframe()),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
sync() {
|
|
19
|
+
this.iframe.sync(this.container.current());
|
|
20
|
+
|
|
21
|
+
this.container.observe((entry) => {
|
|
22
|
+
this.iframe.sync(entry);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return this.iframe.observe((entry) => {
|
|
26
|
+
this.container.sync(entry);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HashProperty } from "./HashProperty";
|
|
2
|
+
import { NavigationEntry } from "./NavigationEntry";
|
|
3
|
+
import { NavigationLocation } from "./NavigationLocation";
|
|
4
|
+
|
|
5
|
+
export class AppsScriptIframe {
|
|
6
|
+
private entry: NavigationEntry;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.entry = this.read();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
observe(handler: (entry: NavigationEntry) => void) {
|
|
13
|
+
const listener = () => {
|
|
14
|
+
const entry = this.read();
|
|
15
|
+
|
|
16
|
+
this.entry = entry;
|
|
17
|
+
|
|
18
|
+
handler(entry);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
window.addEventListener("hashchange", listener);
|
|
22
|
+
window.addEventListener("popstate", listener);
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
window.removeEventListener("hashchange", listener);
|
|
26
|
+
window.removeEventListener("popstate", listener);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sync(entry: NavigationEntry) {
|
|
31
|
+
if (this.entry.equals(entry)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hash = entry.location.hash.normalize();
|
|
36
|
+
const search = entry.location.searchParams.toString();
|
|
37
|
+
const nextHash = search ? `${hash}?${search}` : hash;
|
|
38
|
+
|
|
39
|
+
history.replaceState(entry.state, "", nextHash);
|
|
40
|
+
|
|
41
|
+
this.entry = entry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
current() {
|
|
45
|
+
return this.entry;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private read() {
|
|
49
|
+
const normalized = new HashProperty(window.location.hash)
|
|
50
|
+
.toString()
|
|
51
|
+
.replace(/^#/, "");
|
|
52
|
+
|
|
53
|
+
const [path = "/", query = ""] = normalized.split("?");
|
|
54
|
+
|
|
55
|
+
return new NavigationEntry(
|
|
56
|
+
history.state ?? {},
|
|
57
|
+
new NavigationLocation(
|
|
58
|
+
new HashProperty(path),
|
|
59
|
+
new URLSearchParams(query),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class HashProperty {
|
|
2
|
+
constructor(private readonly value: string) {}
|
|
3
|
+
|
|
4
|
+
normalize() {
|
|
5
|
+
if (!this.value) return "#/";
|
|
6
|
+
if (this.value.startsWith("#/")) return this.value;
|
|
7
|
+
if (this.value.startsWith("#")) return `#/${this.value.slice(1)}`;
|
|
8
|
+
if (this.value.startsWith("/")) return `#${this.value}`;
|
|
9
|
+
|
|
10
|
+
return `#/${this.value}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
equals(other: HashProperty) {
|
|
14
|
+
return this.normalize() === other.normalize();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toString() {
|
|
18
|
+
return this.normalize();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GoogleScriptHistoryEvent,
|
|
3
|
+
GoogleScriptUrlLocation,
|
|
4
|
+
} from "../google";
|
|
5
|
+
|
|
6
|
+
import { HashProperty } from "./HashProperty";
|
|
7
|
+
import { NavigationLocation } from "./NavigationLocation";
|
|
8
|
+
|
|
9
|
+
export class NavigationEntry {
|
|
10
|
+
constructor(
|
|
11
|
+
readonly state: Record<string, unknown>,
|
|
12
|
+
readonly location: NavigationLocation,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
static fromGoogle(
|
|
16
|
+
source: GoogleScriptHistoryEvent | GoogleScriptUrlLocation,
|
|
17
|
+
) {
|
|
18
|
+
const location = "location" in source ? source.location : source;
|
|
19
|
+
const state = "state" in source ? source.state : {};
|
|
20
|
+
|
|
21
|
+
const searchParams = new URLSearchParams();
|
|
22
|
+
|
|
23
|
+
for (const [key, values] of Object.entries(location.parameters)) {
|
|
24
|
+
for (const value of values) {
|
|
25
|
+
searchParams.append(key, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new NavigationEntry(
|
|
30
|
+
state,
|
|
31
|
+
new NavigationLocation(new HashProperty(location.hash), searchParams),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
equals(other: NavigationEntry) {
|
|
36
|
+
const equalsState = (left: unknown, right: unknown): boolean => {
|
|
37
|
+
if (Object.is(left, right)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
typeof left !== "object" ||
|
|
43
|
+
left === null ||
|
|
44
|
+
typeof right !== "object" ||
|
|
45
|
+
right === null
|
|
46
|
+
) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
51
|
+
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
left.length === right.length &&
|
|
57
|
+
left.every((value, index) => equalsState(value, right[index]))
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const leftObject = left as Record<string, unknown>;
|
|
62
|
+
const rightObject = right as Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
const leftKeys = Object.keys(leftObject);
|
|
65
|
+
const rightKeys = Object.keys(rightObject);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
leftKeys.length === rightKeys.length &&
|
|
69
|
+
leftKeys.every(
|
|
70
|
+
(key) =>
|
|
71
|
+
Object.hasOwn(rightObject, key) &&
|
|
72
|
+
equalsState(leftObject[key], rightObject[key]),
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
equalsState(this.state, other.state) &&
|
|
79
|
+
this.location.equals(other.location)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { HashProperty } from "./HashProperty";
|
|
2
|
+
|
|
3
|
+
export class NavigationLocation {
|
|
4
|
+
constructor(
|
|
5
|
+
readonly hash: HashProperty,
|
|
6
|
+
readonly searchParams: URLSearchParams,
|
|
7
|
+
) {}
|
|
8
|
+
|
|
9
|
+
equals(other: NavigationLocation) {
|
|
10
|
+
const thisHash = this.hash.normalize().split("?")[0];
|
|
11
|
+
const otherHash = other.hash.normalize().split("?")[0];
|
|
12
|
+
|
|
13
|
+
if (thisHash !== otherHash) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const thisKeys = [...new Set(this.searchParams.keys())].sort();
|
|
18
|
+
const otherKeys = [...new Set(other.searchParams.keys())].sort();
|
|
19
|
+
|
|
20
|
+
if (thisKeys.length !== otherKeys.length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return thisKeys.every((key, index) => {
|
|
25
|
+
if (key !== otherKeys[index]) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const thisValues = this.searchParams.getAll(key);
|
|
30
|
+
const otherValues = other.searchParams.getAll(key);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
thisValues.length === otherValues.length &&
|
|
34
|
+
thisValues.every(
|
|
35
|
+
(value, valueIndex) => value === otherValues[valueIndex],
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private normalizeSearchParams(searchParams: URLSearchParams) {
|
|
42
|
+
const keys = [...new Set(searchParams.keys())].sort();
|
|
43
|
+
|
|
44
|
+
const normalized = new URLSearchParams();
|
|
45
|
+
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
for (const value of searchParams.getAll(key)) {
|
|
48
|
+
normalized.append(key, value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return normalized.toString();
|
|
53
|
+
}
|
|
54
|
+
}
|