@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,322 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AppsScriptJobCancelledError } from "../src/job/AppsScriptJob";
|
|
3
|
+
|
|
4
|
+
type SuccessHandler = (result: unknown) => void;
|
|
5
|
+
type FailureHandler = (error: Error) => void;
|
|
6
|
+
|
|
7
|
+
let successHandler: SuccessHandler;
|
|
8
|
+
let failureHandler: FailureHandler;
|
|
9
|
+
let serverFunctions: Record<string, ReturnType<typeof vi.fn>>;
|
|
10
|
+
|
|
11
|
+
const run = new Proxy(
|
|
12
|
+
{
|
|
13
|
+
withSuccessHandler(callback: SuccessHandler) {
|
|
14
|
+
successHandler = callback;
|
|
15
|
+
return run;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
withFailureHandler(callback: FailureHandler) {
|
|
19
|
+
failureHandler = callback;
|
|
20
|
+
return run;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
get(target, property, receiver) {
|
|
25
|
+
if (property in target) {
|
|
26
|
+
return Reflect.get(target, property, receiver);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const name = String(property);
|
|
30
|
+
|
|
31
|
+
serverFunctions[name] ??= vi.fn();
|
|
32
|
+
|
|
33
|
+
return serverFunctions[name];
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
vi.stubGlobal("google", {
|
|
39
|
+
script: {
|
|
40
|
+
run,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const { appsScriptClient, appsScriptTransport } =
|
|
45
|
+
await import("../src/AppsScriptClient");
|
|
46
|
+
|
|
47
|
+
type TestApp = {
|
|
48
|
+
sum: {
|
|
49
|
+
args: [a: number, b: number];
|
|
50
|
+
result: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
getUser: {
|
|
54
|
+
args: [id: string];
|
|
55
|
+
result: {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
fail: {
|
|
62
|
+
args: [];
|
|
63
|
+
result: never;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function deferred<T>() {
|
|
68
|
+
let resolve!: (value: T) => void;
|
|
69
|
+
let reject!: (reason?: unknown) => void;
|
|
70
|
+
|
|
71
|
+
const promise = new Promise<T>((res, rej) => {
|
|
72
|
+
resolve = res;
|
|
73
|
+
reject = rej;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
promise,
|
|
78
|
+
resolve,
|
|
79
|
+
reject,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("appsScriptTransport", () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
serverFunctions = {};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("指定したGAS関数を引数付きで呼び出す", () => {
|
|
89
|
+
const promise = appsScriptTransport.call("sum", [1, 2]);
|
|
90
|
+
|
|
91
|
+
expect(serverFunctions.sum).toHaveBeenCalledWith(1, 2);
|
|
92
|
+
|
|
93
|
+
successHandler({
|
|
94
|
+
contents: "3",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return expect(promise).resolves.toEqual({
|
|
98
|
+
contents: "3",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("failure handlerのErrorをrejectする", async () => {
|
|
103
|
+
const promise = appsScriptTransport.call("fail", []);
|
|
104
|
+
|
|
105
|
+
const error = new Error("failed");
|
|
106
|
+
|
|
107
|
+
failureHandler(error);
|
|
108
|
+
|
|
109
|
+
await expect(promise).rejects.toBe(error);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("appsScriptClient", () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
serverFunctions = {};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("RPC名と引数をTransportへ渡してJSONをparseする", async () => {
|
|
119
|
+
const { client } = appsScriptClient<TestApp>();
|
|
120
|
+
|
|
121
|
+
const promise = client.sum(1, 2);
|
|
122
|
+
|
|
123
|
+
expect(serverFunctions.sum).toHaveBeenCalledWith(1, 2);
|
|
124
|
+
|
|
125
|
+
successHandler({
|
|
126
|
+
contents: "3",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await expect(promise).resolves.toBe(3);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("objectをJSON.parseして返す", async () => {
|
|
133
|
+
const { client } = appsScriptClient<TestApp>();
|
|
134
|
+
|
|
135
|
+
const promise = client.getUser("user-1");
|
|
136
|
+
|
|
137
|
+
successHandler({
|
|
138
|
+
contents: JSON.stringify({
|
|
139
|
+
id: "user-1",
|
|
140
|
+
name: "Taro",
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await expect(promise).resolves.toEqual({
|
|
145
|
+
id: "user-1",
|
|
146
|
+
name: "Taro",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("DateはDateへ復元せず文字列として返す", async () => {
|
|
151
|
+
type DateApp = {
|
|
152
|
+
get: {
|
|
153
|
+
args: [];
|
|
154
|
+
result: {
|
|
155
|
+
createdAt: Date;
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const { client } = appsScriptClient<DateApp>();
|
|
161
|
+
|
|
162
|
+
const promise = client.get();
|
|
163
|
+
|
|
164
|
+
successHandler({
|
|
165
|
+
contents: JSON.stringify({
|
|
166
|
+
createdAt: new Date("2026-09-04T00:00:00.000Z"),
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = await promise;
|
|
171
|
+
|
|
172
|
+
expect(result.createdAt).toBe("2026-09-04T00:00:00.000Z");
|
|
173
|
+
expect(result.createdAt).not.toBeInstanceOf(Date);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("GAS側の失敗をそのままrejectする", async () => {
|
|
177
|
+
const { client } = appsScriptClient<TestApp>();
|
|
178
|
+
|
|
179
|
+
const promise = client.fail();
|
|
180
|
+
|
|
181
|
+
const error = new Error("failed");
|
|
182
|
+
failureHandler(error);
|
|
183
|
+
|
|
184
|
+
await expect(promise).rejects.toBe(error);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("不正なJSONならJSON.parseエラーになる", async () => {
|
|
188
|
+
const { client } = appsScriptClient<TestApp>();
|
|
189
|
+
|
|
190
|
+
const promise = client.sum(1, 2);
|
|
191
|
+
|
|
192
|
+
successHandler({
|
|
193
|
+
contents: "{invalid-json",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await expect(promise).rejects.toBeInstanceOf(SyntaxError);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("client経由のRPC Jobをjobsから購読できる", async () => {
|
|
200
|
+
const { client, jobs } = appsScriptClient<TestApp>();
|
|
201
|
+
const listener = vi.fn();
|
|
202
|
+
|
|
203
|
+
jobs.subscribe(listener);
|
|
204
|
+
|
|
205
|
+
const promise = client.sum(1, 2);
|
|
206
|
+
|
|
207
|
+
const snapshot = jobs.getSnapshot();
|
|
208
|
+
|
|
209
|
+
expect(snapshot).toHaveLength(1);
|
|
210
|
+
expect(snapshot[0].label).toBe("sum");
|
|
211
|
+
expect(snapshot[0].status).toBe("running");
|
|
212
|
+
|
|
213
|
+
successHandler({
|
|
214
|
+
contents: "3",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await promise;
|
|
218
|
+
|
|
219
|
+
await vi.waitFor(() => {
|
|
220
|
+
expect(jobs.getSnapshot()).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(listener).toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("失敗したRPC Jobはjobsにfailedとして残る", async () => {
|
|
227
|
+
const { client, jobs } = appsScriptClient<TestApp>();
|
|
228
|
+
|
|
229
|
+
const promise = client.fail();
|
|
230
|
+
|
|
231
|
+
failureHandler(new Error("failed"));
|
|
232
|
+
|
|
233
|
+
await expect(promise).rejects.toThrow("failed");
|
|
234
|
+
|
|
235
|
+
await vi.waitFor(() => {
|
|
236
|
+
expect(jobs.getSnapshot()).toHaveLength(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(jobs.getSnapshot()[0].status).toBe("failed");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("jobs.startとclientが同じQueueを共有する", async () => {
|
|
243
|
+
const { client, jobs } = appsScriptClient<TestApp>();
|
|
244
|
+
|
|
245
|
+
const manual = new Promise<void>(() => {});
|
|
246
|
+
|
|
247
|
+
void jobs.start("manual", () => manual);
|
|
248
|
+
|
|
249
|
+
const rpcPromise = client.sum(1, 2);
|
|
250
|
+
|
|
251
|
+
const snapshot = jobs.getSnapshot();
|
|
252
|
+
|
|
253
|
+
expect(snapshot.map((job) => job.label)).toEqual(["manual", "sum"]);
|
|
254
|
+
|
|
255
|
+
successHandler({
|
|
256
|
+
contents: "3",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await rpcPromise;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("Symbol propertyへのアクセスはundefined", () => {
|
|
263
|
+
const { client } = appsScriptClient<TestApp>();
|
|
264
|
+
|
|
265
|
+
expect(
|
|
266
|
+
(client as unknown as Record<PropertyKey, unknown>)[Symbol.toStringTag],
|
|
267
|
+
).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("jobs.cancelでpending Jobをキャンセルすると実行されずPromiseがrejectする", async () => {
|
|
271
|
+
const { jobs } = appsScriptClient<TestApp>();
|
|
272
|
+
|
|
273
|
+
const controls = Array.from({ length: 30 }, () => deferred<void>());
|
|
274
|
+
|
|
275
|
+
const runningPromises = controls.map((control, index) =>
|
|
276
|
+
jobs.start(`running-${index}`, async () => {
|
|
277
|
+
await control.promise;
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const pendingExecute = vi.fn(async () => "pending");
|
|
282
|
+
|
|
283
|
+
const pendingPromise = jobs.start("pending", pendingExecute);
|
|
284
|
+
|
|
285
|
+
await vi.waitFor(() => {
|
|
286
|
+
expect(jobs.getSnapshot().filter((job) => job.isRunning())).toHaveLength(
|
|
287
|
+
30,
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const pendingJob = jobs
|
|
292
|
+
.getSnapshot()
|
|
293
|
+
.find((job) => job.label === "pending");
|
|
294
|
+
|
|
295
|
+
expect(pendingJob).toBeDefined();
|
|
296
|
+
expect(pendingJob?.status).toBe("pending");
|
|
297
|
+
|
|
298
|
+
jobs.cancel(pendingJob!.id);
|
|
299
|
+
|
|
300
|
+
await expect(pendingPromise).rejects.toBeInstanceOf(
|
|
301
|
+
AppsScriptJobCancelledError,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(
|
|
305
|
+
jobs.getSnapshot().find((job) => job.id === pendingJob!.id),
|
|
306
|
+
).toBeUndefined();
|
|
307
|
+
|
|
308
|
+
controls[0].resolve();
|
|
309
|
+
await runningPromises[0];
|
|
310
|
+
|
|
311
|
+
await Promise.resolve();
|
|
312
|
+
await Promise.resolve();
|
|
313
|
+
|
|
314
|
+
expect(pendingExecute).not.toHaveBeenCalled();
|
|
315
|
+
|
|
316
|
+
for (const control of controls.slice(1)) {
|
|
317
|
+
control.resolve();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await Promise.all(runningPromises);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from "vitest";
|
|
2
|
+
import { appsScriptClient } from "../src/AppsScriptClient";
|
|
3
|
+
|
|
4
|
+
type App = {
|
|
5
|
+
sum: {
|
|
6
|
+
args: [a: number, b: number];
|
|
7
|
+
result: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
findUser: {
|
|
11
|
+
args: [id: string];
|
|
12
|
+
result: {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("AppsScriptClient types", () => {
|
|
20
|
+
it("RPCの引数と戻り値を保持する", () => {
|
|
21
|
+
const { client } = appsScriptClient<App>();
|
|
22
|
+
|
|
23
|
+
expectTypeOf(client.sum).parameter(0).toEqualTypeOf<number>();
|
|
24
|
+
expectTypeOf(client.sum).parameter(1).toEqualTypeOf<number>();
|
|
25
|
+
expectTypeOf(client.sum).returns.toEqualTypeOf<Promise<number>>();
|
|
26
|
+
expectTypeOf(client.findUser).returns.toEqualTypeOf<
|
|
27
|
+
Promise<{
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
}>
|
|
31
|
+
>();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("RPCの戻り値型を保持する", () => {
|
|
35
|
+
const { client } = appsScriptClient<App>();
|
|
36
|
+
|
|
37
|
+
expectTypeOf(client.sum).returns.toEqualTypeOf<Promise<number>>();
|
|
38
|
+
|
|
39
|
+
expectTypeOf(client.findUser).returns.toEqualTypeOf<
|
|
40
|
+
Promise<{
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}>
|
|
44
|
+
>();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("存在しないRPCを公開しない", () => {
|
|
48
|
+
const { client } = appsScriptClient<App>();
|
|
49
|
+
|
|
50
|
+
expectTypeOf(client).not.toHaveProperty("unknown");
|
|
51
|
+
});
|
|
52
|
+
});
|