@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,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppsScriptJobRunner = void 0;
|
|
4
|
+
class AppsScriptJobRunner {
|
|
5
|
+
queue;
|
|
6
|
+
jobs = [];
|
|
7
|
+
listeners = new Set();
|
|
8
|
+
MAX_CONCURRENT = 30;
|
|
9
|
+
running = false;
|
|
10
|
+
snapshot = null;
|
|
11
|
+
dirty = true;
|
|
12
|
+
constructor(queue) {
|
|
13
|
+
this.queue = queue;
|
|
14
|
+
this.queue.subscribe(this);
|
|
15
|
+
}
|
|
16
|
+
addJob(job) {
|
|
17
|
+
this.jobs.push(job);
|
|
18
|
+
this.notify();
|
|
19
|
+
}
|
|
20
|
+
getJobs() {
|
|
21
|
+
if (this.dirty || !this.snapshot) {
|
|
22
|
+
this.snapshot = [...this.jobs];
|
|
23
|
+
this.dirty = false;
|
|
24
|
+
}
|
|
25
|
+
return this.snapshot;
|
|
26
|
+
}
|
|
27
|
+
async run() {
|
|
28
|
+
if (this.running)
|
|
29
|
+
return;
|
|
30
|
+
this.running = true;
|
|
31
|
+
while (true) {
|
|
32
|
+
const runningCount = this.jobs.filter((j) => j.isRunning()).length;
|
|
33
|
+
if (runningCount >= this.MAX_CONCURRENT)
|
|
34
|
+
break;
|
|
35
|
+
const job = this.queue.dequeue();
|
|
36
|
+
if (!job)
|
|
37
|
+
break;
|
|
38
|
+
job.start();
|
|
39
|
+
this.notify();
|
|
40
|
+
job
|
|
41
|
+
.execute()
|
|
42
|
+
.then((result) => {
|
|
43
|
+
job.success(result);
|
|
44
|
+
const index = this.jobs.indexOf(job);
|
|
45
|
+
if (index !== -1) {
|
|
46
|
+
this.jobs.splice(index, 1);
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.catch((error) => {
|
|
50
|
+
job.fail(error);
|
|
51
|
+
})
|
|
52
|
+
.finally(() => {
|
|
53
|
+
this.notify();
|
|
54
|
+
this.running = false;
|
|
55
|
+
this.run();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
this.running = false;
|
|
59
|
+
}
|
|
60
|
+
remove(jobId) {
|
|
61
|
+
const index = this.jobs.findIndex((job) => job.id === jobId);
|
|
62
|
+
if (index === -1) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.jobs.splice(index, 1);
|
|
66
|
+
this.notify();
|
|
67
|
+
}
|
|
68
|
+
cancel(jobId) {
|
|
69
|
+
const job = this.jobs.find((job) => job.id === jobId);
|
|
70
|
+
if (!job || !job.cancel()) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.queue.remove(jobId);
|
|
74
|
+
this.jobs.splice(this.jobs.indexOf(job), 1);
|
|
75
|
+
this.notify();
|
|
76
|
+
}
|
|
77
|
+
retry(jobId) {
|
|
78
|
+
const job = this.jobs.find((j) => j.id === jobId);
|
|
79
|
+
if (job) {
|
|
80
|
+
this.remove(job.id);
|
|
81
|
+
this.queue.enqueue(job.label, job.execute);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
subscribe(listener) {
|
|
85
|
+
this.listeners.add(listener);
|
|
86
|
+
return () => this.listeners.delete(listener);
|
|
87
|
+
}
|
|
88
|
+
notify() {
|
|
89
|
+
this.dirty = true;
|
|
90
|
+
this.listeners.forEach((listener) => listener());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.AppsScriptJobRunner = AppsScriptJobRunner;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AppsScriptJob } from "./AppsScriptJob";
|
|
2
|
+
import { AppsScriptJobRunner } from "./AppsScriptJobRunner";
|
|
3
|
+
export interface AppsScriptJobStore {
|
|
4
|
+
subscribe: (listener: () => void) => () => void;
|
|
5
|
+
getSnapshot: () => AppsScriptJob<any>[];
|
|
6
|
+
}
|
|
7
|
+
export declare function appsScriptJobStore(runner: AppsScriptJobRunner): AppsScriptJobStore;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.appsScriptJobStore = appsScriptJobStore;
|
|
4
|
+
function appsScriptJobStore(runner) {
|
|
5
|
+
return {
|
|
6
|
+
subscribe: runner.subscribe.bind(runner),
|
|
7
|
+
getSnapshot: runner.getJobs.bind(runner),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NavigationEntry } from "./NavigationEntry";
|
|
2
|
+
export declare class AppsScriptContainer {
|
|
3
|
+
private entry;
|
|
4
|
+
private constructor();
|
|
5
|
+
static load(callback: (container: AppsScriptContainer) => void): void;
|
|
6
|
+
observe(handler: (entry: NavigationEntry) => void): void;
|
|
7
|
+
sync(entry: NavigationEntry): void;
|
|
8
|
+
current(): NavigationEntry;
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppsScriptContainer = void 0;
|
|
4
|
+
const google_1 = require("../google");
|
|
5
|
+
const NavigationEntry_1 = require("./NavigationEntry");
|
|
6
|
+
class AppsScriptContainer {
|
|
7
|
+
entry;
|
|
8
|
+
constructor(entry) {
|
|
9
|
+
this.entry = entry;
|
|
10
|
+
}
|
|
11
|
+
static load(callback) {
|
|
12
|
+
google_1.google.script.url.getLocation((location) => {
|
|
13
|
+
callback(new AppsScriptContainer(NavigationEntry_1.NavigationEntry.fromGoogle(location)));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
observe(handler) {
|
|
17
|
+
google_1.google.script.history.setChangeHandler((event) => {
|
|
18
|
+
const entry = NavigationEntry_1.NavigationEntry.fromGoogle(event);
|
|
19
|
+
this.entry = entry;
|
|
20
|
+
handler(entry);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
sync(entry) {
|
|
24
|
+
if (this.entry.equals(entry)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
this.entry = entry;
|
|
28
|
+
const parameters = {};
|
|
29
|
+
for (const key of new Set(entry.location.searchParams.keys())) {
|
|
30
|
+
parameters[key] = entry.location.searchParams.getAll(key);
|
|
31
|
+
}
|
|
32
|
+
google_1.google.script.history.push(entry.state, parameters, entry.location.hash.normalize());
|
|
33
|
+
}
|
|
34
|
+
current() {
|
|
35
|
+
return this.entry;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.AppsScriptContainer = AppsScriptContainer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AppsScriptContainer } from "./AppsScriptContainer";
|
|
2
|
+
import { AppsScriptIframe } from "./AppsScriptIframe";
|
|
3
|
+
export declare class AppsScriptHistoryPipeline {
|
|
4
|
+
private readonly container;
|
|
5
|
+
private readonly iframe;
|
|
6
|
+
constructor(container: AppsScriptContainer, iframe: AppsScriptIframe);
|
|
7
|
+
static create(callback: (pipeline: AppsScriptHistoryPipeline) => void): void;
|
|
8
|
+
sync(): () => void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppsScriptHistoryPipeline = void 0;
|
|
4
|
+
const AppsScriptContainer_1 = require("./AppsScriptContainer");
|
|
5
|
+
const AppsScriptIframe_1 = require("./AppsScriptIframe");
|
|
6
|
+
class AppsScriptHistoryPipeline {
|
|
7
|
+
container;
|
|
8
|
+
iframe;
|
|
9
|
+
constructor(container, iframe) {
|
|
10
|
+
this.container = container;
|
|
11
|
+
this.iframe = iframe;
|
|
12
|
+
}
|
|
13
|
+
static create(callback) {
|
|
14
|
+
AppsScriptContainer_1.AppsScriptContainer.load((container) => {
|
|
15
|
+
callback(new AppsScriptHistoryPipeline(container, new AppsScriptIframe_1.AppsScriptIframe()));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
sync() {
|
|
19
|
+
this.iframe.sync(this.container.current());
|
|
20
|
+
this.container.observe((entry) => {
|
|
21
|
+
this.iframe.sync(entry);
|
|
22
|
+
});
|
|
23
|
+
return this.iframe.observe((entry) => {
|
|
24
|
+
this.container.sync(entry);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.AppsScriptHistoryPipeline = AppsScriptHistoryPipeline;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NavigationEntry } from "./NavigationEntry";
|
|
2
|
+
export declare class AppsScriptIframe {
|
|
3
|
+
private entry;
|
|
4
|
+
constructor();
|
|
5
|
+
observe(handler: (entry: NavigationEntry) => void): () => void;
|
|
6
|
+
sync(entry: NavigationEntry): void;
|
|
7
|
+
current(): NavigationEntry;
|
|
8
|
+
private read;
|
|
9
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppsScriptIframe = void 0;
|
|
4
|
+
const HashProperty_1 = require("./HashProperty");
|
|
5
|
+
const NavigationEntry_1 = require("./NavigationEntry");
|
|
6
|
+
const NavigationLocation_1 = require("./NavigationLocation");
|
|
7
|
+
class AppsScriptIframe {
|
|
8
|
+
entry;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.entry = this.read();
|
|
11
|
+
}
|
|
12
|
+
observe(handler) {
|
|
13
|
+
const listener = () => {
|
|
14
|
+
const entry = this.read();
|
|
15
|
+
this.entry = entry;
|
|
16
|
+
handler(entry);
|
|
17
|
+
};
|
|
18
|
+
window.addEventListener("hashchange", listener);
|
|
19
|
+
window.addEventListener("popstate", listener);
|
|
20
|
+
return () => {
|
|
21
|
+
window.removeEventListener("hashchange", listener);
|
|
22
|
+
window.removeEventListener("popstate", listener);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
sync(entry) {
|
|
26
|
+
if (this.entry.equals(entry)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const hash = entry.location.hash.normalize();
|
|
30
|
+
const search = entry.location.searchParams.toString();
|
|
31
|
+
const nextHash = search ? `${hash}?${search}` : hash;
|
|
32
|
+
history.replaceState(entry.state, "", nextHash);
|
|
33
|
+
this.entry = entry;
|
|
34
|
+
}
|
|
35
|
+
current() {
|
|
36
|
+
return this.entry;
|
|
37
|
+
}
|
|
38
|
+
read() {
|
|
39
|
+
const hash = new HashProperty_1.HashProperty(window.location.hash);
|
|
40
|
+
const normalized = hash.toString().replace(/^#/, "");
|
|
41
|
+
const [, query = ""] = normalized.split("?");
|
|
42
|
+
return new NavigationEntry_1.NavigationEntry(history.state ?? {}, new NavigationLocation_1.NavigationLocation(hash, new URLSearchParams(query)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.AppsScriptIframe = AppsScriptIframe;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HashProperty = void 0;
|
|
4
|
+
class HashProperty {
|
|
5
|
+
value;
|
|
6
|
+
constructor(value) {
|
|
7
|
+
this.value = value;
|
|
8
|
+
}
|
|
9
|
+
normalize() {
|
|
10
|
+
if (!this.value)
|
|
11
|
+
return "#/";
|
|
12
|
+
if (this.value.startsWith("#/"))
|
|
13
|
+
return this.value;
|
|
14
|
+
if (this.value.startsWith("#"))
|
|
15
|
+
return `#/${this.value.slice(1)}`;
|
|
16
|
+
if (this.value.startsWith("/"))
|
|
17
|
+
return `#${this.value}`;
|
|
18
|
+
return `#/${this.value}`;
|
|
19
|
+
}
|
|
20
|
+
equals(other) {
|
|
21
|
+
return this.normalize() === other.normalize();
|
|
22
|
+
}
|
|
23
|
+
toString() {
|
|
24
|
+
return this.normalize();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.HashProperty = HashProperty;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GoogleScriptHistoryEvent, GoogleScriptUrlLocation } from "../google";
|
|
2
|
+
import { NavigationLocation } from "./NavigationLocation";
|
|
3
|
+
export declare class NavigationEntry {
|
|
4
|
+
readonly state: Record<string, unknown>;
|
|
5
|
+
readonly location: NavigationLocation;
|
|
6
|
+
constructor(state: Record<string, unknown>, location: NavigationLocation);
|
|
7
|
+
static fromGoogle(source: GoogleScriptHistoryEvent | GoogleScriptUrlLocation): NavigationEntry;
|
|
8
|
+
equals(other: NavigationEntry): boolean;
|
|
9
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NavigationEntry = void 0;
|
|
4
|
+
const HashProperty_1 = require("./HashProperty");
|
|
5
|
+
const NavigationLocation_1 = require("./NavigationLocation");
|
|
6
|
+
class NavigationEntry {
|
|
7
|
+
state;
|
|
8
|
+
location;
|
|
9
|
+
constructor(state, location) {
|
|
10
|
+
this.state = state;
|
|
11
|
+
this.location = location;
|
|
12
|
+
}
|
|
13
|
+
static fromGoogle(source) {
|
|
14
|
+
const location = "location" in source ? source.location : source;
|
|
15
|
+
const state = "state" in source ? source.state : {};
|
|
16
|
+
const searchParams = new URLSearchParams();
|
|
17
|
+
for (const [key, values] of Object.entries(location.parameters)) {
|
|
18
|
+
for (const value of values) {
|
|
19
|
+
searchParams.append(key, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return new NavigationEntry(state, new NavigationLocation_1.NavigationLocation(new HashProperty_1.HashProperty(location.hash), searchParams));
|
|
23
|
+
}
|
|
24
|
+
equals(other) {
|
|
25
|
+
const equalsState = (left, right) => {
|
|
26
|
+
if (Object.is(left, right)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (typeof left !== "object" ||
|
|
30
|
+
left === null ||
|
|
31
|
+
typeof right !== "object" ||
|
|
32
|
+
right === null) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
36
|
+
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return (left.length === right.length &&
|
|
40
|
+
left.every((value, index) => equalsState(value, right[index])));
|
|
41
|
+
}
|
|
42
|
+
const leftObject = left;
|
|
43
|
+
const rightObject = right;
|
|
44
|
+
const leftKeys = Object.keys(leftObject);
|
|
45
|
+
const rightKeys = Object.keys(rightObject);
|
|
46
|
+
return (leftKeys.length === rightKeys.length &&
|
|
47
|
+
leftKeys.every((key) => Object.hasOwn(rightObject, key) &&
|
|
48
|
+
equalsState(leftObject[key], rightObject[key])));
|
|
49
|
+
};
|
|
50
|
+
return (equalsState(this.state, other.state) &&
|
|
51
|
+
this.location.equals(other.location));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.NavigationEntry = NavigationEntry;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { HashProperty } from "./HashProperty";
|
|
2
|
+
export declare class NavigationLocation {
|
|
3
|
+
readonly hash: HashProperty;
|
|
4
|
+
readonly searchParams: URLSearchParams;
|
|
5
|
+
constructor(hash: HashProperty, searchParams: URLSearchParams);
|
|
6
|
+
equals(other: NavigationLocation): boolean;
|
|
7
|
+
private normalizeSearchParams;
|
|
8
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NavigationLocation = void 0;
|
|
4
|
+
class NavigationLocation {
|
|
5
|
+
hash;
|
|
6
|
+
searchParams;
|
|
7
|
+
constructor(hash, searchParams) {
|
|
8
|
+
this.hash = hash;
|
|
9
|
+
this.searchParams = searchParams;
|
|
10
|
+
}
|
|
11
|
+
equals(other) {
|
|
12
|
+
const thisHash = this.hash.normalize().split("?")[0];
|
|
13
|
+
const otherHash = other.hash.normalize().split("?")[0];
|
|
14
|
+
if (thisHash !== otherHash) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const thisKeys = [...new Set(this.searchParams.keys())].sort();
|
|
18
|
+
const otherKeys = [...new Set(other.searchParams.keys())].sort();
|
|
19
|
+
if (thisKeys.length !== otherKeys.length) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return thisKeys.every((key, index) => {
|
|
23
|
+
if (key !== otherKeys[index]) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const thisValues = this.searchParams.getAll(key);
|
|
27
|
+
const otherValues = other.searchParams.getAll(key);
|
|
28
|
+
return (thisValues.length === otherValues.length &&
|
|
29
|
+
thisValues.every((value, valueIndex) => value === otherValues[valueIndex]));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
normalizeSearchParams(searchParams) {
|
|
33
|
+
const keys = [...new Set(searchParams.keys())].sort();
|
|
34
|
+
const normalized = new URLSearchParams();
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
for (const value of searchParams.getAll(key)) {
|
|
37
|
+
normalized.append(key, value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return normalized.toString();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.NavigationLocation = NavigationLocation;
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gasboost/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"jsdom": "^30.0.1"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
|
+
"test": "vitest run --config vitest.config.mts"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { google } from "./google";
|
|
2
|
+
import { AppsScriptJob } from "./job/AppsScriptJob";
|
|
3
|
+
import { AppsScriptJobQueue } from "./job/AppsScriptJobQueue";
|
|
4
|
+
import { AppsScriptJobRunner } from "./job/AppsScriptJobRunner";
|
|
5
|
+
import { appsScriptJobStore } from "./job/AppsScriptJobStore";
|
|
6
|
+
|
|
7
|
+
type RpcResponse = {
|
|
8
|
+
contents: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type RpcDefinition = {
|
|
12
|
+
args: unknown[];
|
|
13
|
+
result: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type RpcApp = Record<string, RpcDefinition>;
|
|
17
|
+
|
|
18
|
+
type AppsScriptClient<TApp extends RpcApp> = {
|
|
19
|
+
[K in keyof TApp]: TApp[K] extends {
|
|
20
|
+
args: infer TArgs extends unknown[];
|
|
21
|
+
result: infer TResult;
|
|
22
|
+
}
|
|
23
|
+
? (...args: TArgs) => Promise<TResult>
|
|
24
|
+
: never;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const appsScriptTransport = {
|
|
28
|
+
call(name: string, args: unknown[]): Promise<RpcResponse> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
google.script.run
|
|
31
|
+
.withSuccessHandler(resolve)
|
|
32
|
+
.withFailureHandler(reject)
|
|
33
|
+
[name](...args);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface AppsScriptJobs {
|
|
39
|
+
start: <T>(label: string, execute: () => Promise<T>) => Promise<T>;
|
|
40
|
+
cancel: (jobId: string) => void;
|
|
41
|
+
retry: (jobId: string) => void;
|
|
42
|
+
subscribe: (listener: () => void) => () => void;
|
|
43
|
+
getSnapshot: () => AppsScriptJob<any>[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function appsScriptClient<TApp extends RpcApp>(): {
|
|
47
|
+
client: AppsScriptClient<TApp>;
|
|
48
|
+
jobs: AppsScriptJobs;
|
|
49
|
+
} {
|
|
50
|
+
const jobQueue = new AppsScriptJobQueue();
|
|
51
|
+
const jobRunner = new AppsScriptJobRunner(jobQueue);
|
|
52
|
+
const jobStore = appsScriptJobStore(jobRunner);
|
|
53
|
+
|
|
54
|
+
const client = new Proxy(
|
|
55
|
+
{},
|
|
56
|
+
{
|
|
57
|
+
get(_target, property) {
|
|
58
|
+
if (typeof property !== "string") {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (...args: unknown[]) => {
|
|
63
|
+
return jobQueue.enqueue(property, async () => {
|
|
64
|
+
const res = await appsScriptTransport.call(property, args);
|
|
65
|
+
return JSON.parse(res.contents);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
) as AppsScriptClient<TApp>;
|
|
71
|
+
|
|
72
|
+
const jobs = {
|
|
73
|
+
start: jobQueue.enqueue.bind(jobQueue),
|
|
74
|
+
cancel: jobRunner.cancel.bind(jobRunner),
|
|
75
|
+
retry: jobRunner.retry.bind(jobRunner),
|
|
76
|
+
subscribe: jobStore.subscribe.bind(jobStore),
|
|
77
|
+
getSnapshot: jobStore.getSnapshot.bind(jobStore),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return { client, jobs };
|
|
81
|
+
}
|
package/src/google.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type GoogleScriptRun = {
|
|
2
|
+
withSuccessHandler<T = unknown>(
|
|
3
|
+
callback: (result: T) => void,
|
|
4
|
+
): GoogleScriptRun;
|
|
5
|
+
withFailureHandler(callback: (error: Error) => void): GoogleScriptRun;
|
|
6
|
+
// GAS は run.任意のサーバー関数名(...) を呼ぶので index signature を持たせる
|
|
7
|
+
[serverFunctionName: string]: any;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type GoogleScriptHistoryEvent = {
|
|
11
|
+
state: Record<string, unknown>;
|
|
12
|
+
location: {
|
|
13
|
+
hash: string;
|
|
14
|
+
parameter: Record<string, string>;
|
|
15
|
+
parameters: Record<string, string[]>;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type GoogleScriptUrlLocation = {
|
|
20
|
+
hash: string;
|
|
21
|
+
parameter: Record<string, string>;
|
|
22
|
+
parameters: Record<string, string[]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type GoogleApi = {
|
|
26
|
+
script: {
|
|
27
|
+
run: GoogleScriptRun;
|
|
28
|
+
history: {
|
|
29
|
+
push: (
|
|
30
|
+
stateObject: Record<string, unknown>,
|
|
31
|
+
params: Record<string, unknown>,
|
|
32
|
+
hash: string,
|
|
33
|
+
) => void;
|
|
34
|
+
replace: (
|
|
35
|
+
stateObject: Record<string, unknown>,
|
|
36
|
+
params: Record<string, unknown>,
|
|
37
|
+
hash: string,
|
|
38
|
+
) => void;
|
|
39
|
+
setChangeHandler: (
|
|
40
|
+
callback: (event: GoogleScriptHistoryEvent) => void,
|
|
41
|
+
) => void;
|
|
42
|
+
};
|
|
43
|
+
url: {
|
|
44
|
+
getLocation: (
|
|
45
|
+
callback: (location: GoogleScriptUrlLocation) => void,
|
|
46
|
+
) => void;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* google.script.* は Apps Script のランタイムが注入するグローバル。
|
|
53
|
+
* ここでは「型付きで参照」だけ提供する。
|
|
54
|
+
*/
|
|
55
|
+
export const google = (globalThis as any).google as GoogleApi;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export class AppsScriptJob<T> {
|
|
2
|
+
private _startedAt: Date | null = null;
|
|
3
|
+
private _endedAt: Date | null = null;
|
|
4
|
+
private _result: T | null = null;
|
|
5
|
+
private _error: unknown | null = null;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
public readonly label: string,
|
|
9
|
+
public readonly execute: () => Promise<T>,
|
|
10
|
+
private resolve: (value: T) => void,
|
|
11
|
+
private reject: (reason?: any) => void,
|
|
12
|
+
public readonly id: string = crypto.randomUUID(),
|
|
13
|
+
public readonly createdAt: Date = new Date(),
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
start() {
|
|
17
|
+
this._startedAt = new Date();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
success(result: T) {
|
|
21
|
+
this._endedAt = new Date();
|
|
22
|
+
this._result = result;
|
|
23
|
+
this.resolve(result);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fail(error: unknown) {
|
|
27
|
+
this._endedAt = new Date();
|
|
28
|
+
this._error = error;
|
|
29
|
+
this.reject(error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public get endedAt() {
|
|
33
|
+
return this._endedAt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public get result() {
|
|
37
|
+
return this._result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public get error() {
|
|
41
|
+
return this._error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public isPending() {
|
|
45
|
+
return this._startedAt === null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public isRunning() {
|
|
49
|
+
return this._startedAt !== null && this._endedAt === null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public isSuccess() {
|
|
53
|
+
return this._endedAt !== null && this._error === null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public isFailed() {
|
|
57
|
+
return this._endedAt !== null && this._error !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public get status() {
|
|
61
|
+
if (this.isPending()) return "pending";
|
|
62
|
+
if (this.isRunning()) return "running";
|
|
63
|
+
if (this.isSuccess()) return "success";
|
|
64
|
+
if (this.isFailed()) return "failed";
|
|
65
|
+
return "unknown";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public cancel(): boolean {
|
|
69
|
+
if (!this.isPending()) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const error = new AppsScriptJobCancelledError();
|
|
74
|
+
this.fail(error);
|
|
75
|
+
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class AppsScriptJobCancelledError extends Error {
|
|
81
|
+
constructor() {
|
|
82
|
+
super("Job cancelled");
|
|
83
|
+
this.name = "AppsScriptJobCancelledError";
|
|
84
|
+
}
|
|
85
|
+
}
|