@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.
Files changed (63) hide show
  1. package/dist/AppsScriptClient.d.ts +30 -0
  2. package/dist/AppsScriptClient.js +43 -0
  3. package/dist/AppsScriptJob.d.ts +28 -0
  4. package/dist/AppsScriptJob.js +84 -0
  5. package/dist/AppsScriptJobQueue.d.ts +14 -0
  6. package/dist/AppsScriptJobQueue.js +40 -0
  7. package/dist/AppsScriptJobRunner.d.ts +20 -0
  8. package/dist/AppsScriptJobRunner.js +93 -0
  9. package/dist/AppsScriptJobStore.d.ts +7 -0
  10. package/dist/AppsScriptJobStore.js +9 -0
  11. package/dist/google.d.ts +36 -0
  12. package/dist/google.js +8 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +7 -0
  15. package/dist/job/AppsScriptJob.d.ts +28 -0
  16. package/dist/job/AppsScriptJob.js +84 -0
  17. package/dist/job/AppsScriptJobQueue.d.ts +14 -0
  18. package/dist/job/AppsScriptJobQueue.js +40 -0
  19. package/dist/job/AppsScriptJobRunner.d.ts +20 -0
  20. package/dist/job/AppsScriptJobRunner.js +93 -0
  21. package/dist/job/AppsScriptJobStore.d.ts +7 -0
  22. package/dist/job/AppsScriptJobStore.js +9 -0
  23. package/dist/navigation/AppsScriptContainer.d.ts +9 -0
  24. package/dist/navigation/AppsScriptContainer.js +38 -0
  25. package/dist/navigation/AppsScriptHistoryPipeline.d.ts +9 -0
  26. package/dist/navigation/AppsScriptHistoryPipeline.js +28 -0
  27. package/dist/navigation/AppsScriptIframe.d.ts +9 -0
  28. package/dist/navigation/AppsScriptIframe.js +45 -0
  29. package/dist/navigation/HashProperty.d.ts +7 -0
  30. package/dist/navigation/HashProperty.js +27 -0
  31. package/dist/navigation/NavigationEntry.d.ts +9 -0
  32. package/dist/navigation/NavigationEntry.js +54 -0
  33. package/dist/navigation/NavigationLocation.d.ts +8 -0
  34. package/dist/navigation/NavigationLocation.js +43 -0
  35. package/package.json +20 -0
  36. package/src/AppsScriptClient.ts +81 -0
  37. package/src/google.ts +55 -0
  38. package/src/index.ts +4 -0
  39. package/src/job/AppsScriptJob.ts +85 -0
  40. package/src/job/AppsScriptJobQueue.ts +48 -0
  41. package/src/job/AppsScriptJobRunner.ts +107 -0
  42. package/src/job/AppsScriptJobStore.ts +16 -0
  43. package/src/navigation/AppsScriptContainer.ts +47 -0
  44. package/src/navigation/AppsScriptHistoryPipeline.ts +29 -0
  45. package/src/navigation/AppsScriptIframe.ts +63 -0
  46. package/src/navigation/HashProperty.ts +20 -0
  47. package/src/navigation/NavigationEntry.ts +82 -0
  48. package/src/navigation/NavigationLocation.ts +54 -0
  49. package/tests/AppsScriptClient.spec.ts +322 -0
  50. package/tests/AppsScriptClient.type.spec.ts +52 -0
  51. package/tests/AppsScriptContainer.spec.ts +502 -0
  52. package/tests/AppsScriptHistoryPipeline.spec.ts +303 -0
  53. package/tests/AppsScriptIframe.spec.ts +452 -0
  54. package/tests/AppsScriptJob.spec.ts +83 -0
  55. package/tests/AppsScriptJobQueue.spec.ts +125 -0
  56. package/tests/AppsScriptJobRunner.spec.ts +361 -0
  57. package/tests/HashProperty.spec.ts +29 -0
  58. package/tests/NavigationEntry.spec.ts +507 -0
  59. package/tests/NavigationLocation.spec.ts +189 -0
  60. package/tests/setup.ts +15 -0
  61. package/tsconfig.build.json +14 -0
  62. package/tsconfig.json +6 -0
  63. package/vitest.config.mts +8 -0
@@ -0,0 +1,361 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { AppsScriptJobCancelledError } from "../src/job/AppsScriptJob";
3
+ import { AppsScriptJobQueue } from "../src/job/AppsScriptJobQueue";
4
+ import { AppsScriptJobRunner } from "../src/job/AppsScriptJobRunner";
5
+
6
+ function deferred<T>() {
7
+ let resolve!: (value: T) => void;
8
+ let reject!: (reason?: unknown) => void;
9
+
10
+ const promise = new Promise<T>((res, rej) => {
11
+ resolve = res;
12
+ reject = rej;
13
+ });
14
+
15
+ return {
16
+ promise,
17
+ resolve,
18
+ reject,
19
+ };
20
+ }
21
+
22
+ describe("AppsScriptJobRunner", () => {
23
+ it("enqueueされたJobを実行する", async () => {
24
+ const queue = new AppsScriptJobQueue();
25
+ new AppsScriptJobRunner(queue);
26
+
27
+ const execute = vi.fn(async () => "result");
28
+
29
+ await expect(queue.enqueue("test", execute)).resolves.toBe("result");
30
+
31
+ expect(execute).toHaveBeenCalledTimes(1);
32
+ });
33
+
34
+ it("成功したJobは一覧から削除される", async () => {
35
+ const queue = new AppsScriptJobQueue();
36
+ const runner = new AppsScriptJobRunner(queue);
37
+
38
+ await queue.enqueue("test", async () => "result");
39
+
40
+ await vi.waitFor(() => {
41
+ expect(runner.getJobs()).toHaveLength(0);
42
+ });
43
+ });
44
+
45
+ it("失敗したJobはfailedとして一覧に残る", async () => {
46
+ const queue = new AppsScriptJobQueue();
47
+ const runner = new AppsScriptJobRunner(queue);
48
+
49
+ const error = new Error("failed");
50
+
51
+ await expect(
52
+ queue.enqueue("test", async () => {
53
+ throw error;
54
+ }),
55
+ ).rejects.toBe(error);
56
+
57
+ await vi.waitFor(() => {
58
+ expect(runner.getJobs()).toHaveLength(1);
59
+ });
60
+
61
+ const [job] = runner.getJobs();
62
+
63
+ expect(job.status).toBe("failed");
64
+ expect(job.error).toBe(error);
65
+ });
66
+
67
+ it("最大30Jobまで同時実行する", async () => {
68
+ const queue = new AppsScriptJobQueue();
69
+ new AppsScriptJobRunner(queue);
70
+
71
+ let running = 0;
72
+ let maxRunning = 0;
73
+
74
+ const jobs = Array.from({ length: 31 }, () => {
75
+ const control = deferred<void>();
76
+
77
+ const promise = queue.enqueue("test", async () => {
78
+ running++;
79
+ maxRunning = Math.max(maxRunning, running);
80
+
81
+ await control.promise;
82
+
83
+ running--;
84
+ });
85
+
86
+ return {
87
+ control,
88
+ promise,
89
+ };
90
+ });
91
+
92
+ await vi.waitFor(() => {
93
+ expect(maxRunning).toBe(30);
94
+ });
95
+
96
+ expect(running).toBe(30);
97
+
98
+ jobs[0].control.resolve();
99
+
100
+ await vi.waitFor(() => {
101
+ expect(running).toBe(30);
102
+ });
103
+
104
+ for (const job of jobs) {
105
+ job.control.resolve();
106
+ }
107
+
108
+ await Promise.all(jobs.map((job) => job.promise));
109
+
110
+ expect(maxRunning).toBe(30);
111
+ });
112
+
113
+ it("retryすると同じlabelとexecuteで新しいJobを実行する", async () => {
114
+ const queue = new AppsScriptJobQueue();
115
+ const runner = new AppsScriptJobRunner(queue);
116
+
117
+ const execute = vi
118
+ .fn<() => Promise<string>>()
119
+ .mockRejectedValueOnce(new Error("failed"))
120
+ .mockResolvedValueOnce("success");
121
+
122
+ await expect(queue.enqueue("test", execute)).rejects.toThrow("failed");
123
+
124
+ await vi.waitFor(() => {
125
+ expect(runner.getJobs()).toHaveLength(1);
126
+ });
127
+
128
+ const failedJob = runner.getJobs()[0];
129
+ const failedJobId = failedJob.id;
130
+
131
+ runner.retry(failedJobId);
132
+
133
+ await vi.waitFor(() => {
134
+ expect(execute).toHaveBeenCalledTimes(2);
135
+ });
136
+
137
+ await vi.waitFor(() => {
138
+ expect(runner.getJobs()).toHaveLength(0);
139
+ });
140
+
141
+ expect(failedJob.label).toBe("test");
142
+ });
143
+
144
+ it("removeするとJob一覧から削除される", async () => {
145
+ const queue = new AppsScriptJobQueue();
146
+ const runner = new AppsScriptJobRunner(queue);
147
+
148
+ await expect(
149
+ queue.enqueue("test", async () => {
150
+ throw new Error("failed");
151
+ }),
152
+ ).rejects.toThrow();
153
+
154
+ const [job] = runner.getJobs();
155
+
156
+ runner.remove(job.id);
157
+
158
+ expect(runner.getJobs()).toEqual([]);
159
+ });
160
+
161
+ it("存在しないJobをremoveしても何も起きない", () => {
162
+ const queue = new AppsScriptJobQueue();
163
+ const runner = new AppsScriptJobRunner(queue);
164
+
165
+ expect(() => runner.remove("missing")).not.toThrow();
166
+ });
167
+
168
+ it("存在しないJobをretryしても何も起きない", () => {
169
+ const queue = new AppsScriptJobQueue();
170
+ const runner = new AppsScriptJobRunner(queue);
171
+
172
+ expect(() => runner.retry("missing")).not.toThrow();
173
+ });
174
+
175
+ it("変更がない場合getJobsは同じsnapshotを返す", () => {
176
+ const queue = new AppsScriptJobQueue();
177
+ const runner = new AppsScriptJobRunner(queue);
178
+
179
+ const first = runner.getJobs();
180
+ const second = runner.getJobs();
181
+
182
+ expect(second).toBe(first);
183
+ });
184
+
185
+ it("Job追加後は新しいsnapshotを返す", () => {
186
+ const queue = new AppsScriptJobQueue();
187
+ const runner = new AppsScriptJobRunner(queue);
188
+
189
+ const first = runner.getJobs();
190
+
191
+ void queue.enqueue("test", () => new Promise(() => {}));
192
+
193
+ const second = runner.getJobs();
194
+
195
+ expect(second).not.toBe(first);
196
+ expect(second).toHaveLength(1);
197
+ });
198
+
199
+ it("subscribeしたlistenerに状態変更を通知する", () => {
200
+ const queue = new AppsScriptJobQueue();
201
+ const runner = new AppsScriptJobRunner(queue);
202
+ const listener = vi.fn();
203
+
204
+ runner.subscribe(listener);
205
+
206
+ void queue.enqueue("test", () => new Promise(() => {}));
207
+
208
+ expect(listener).toHaveBeenCalled();
209
+ });
210
+
211
+ it("unsubscribe後はlistenerを呼ばない", () => {
212
+ const queue = new AppsScriptJobQueue();
213
+ const runner = new AppsScriptJobRunner(queue);
214
+ const listener = vi.fn();
215
+
216
+ const unsubscribe = runner.subscribe(listener);
217
+ unsubscribe();
218
+
219
+ void queue.enqueue("test", () => new Promise(() => {}));
220
+
221
+ expect(listener).not.toHaveBeenCalled();
222
+ });
223
+
224
+ it("pending JobをcancelするとQueueからも削除され実行されない", async () => {
225
+ const queue = new AppsScriptJobQueue();
226
+ const runner = new AppsScriptJobRunner(queue);
227
+
228
+ const controls = Array.from({ length: 30 }, () => deferred<void>());
229
+
230
+ const runningPromises = controls.map((control, index) =>
231
+ queue.enqueue(`running-${index}`, async () => {
232
+ await control.promise;
233
+ }),
234
+ );
235
+
236
+ const pendingExecute = vi.fn(async () => "pending");
237
+
238
+ const pendingPromise = queue.enqueue("pending", pendingExecute);
239
+
240
+ await vi.waitFor(() => {
241
+ expect(runner.getJobs().filter((job) => job.isRunning())).toHaveLength(
242
+ 30,
243
+ );
244
+ });
245
+
246
+ const pendingJob = runner.getJobs().find((job) => job.label === "pending");
247
+
248
+ expect(pendingJob).toBeDefined();
249
+ expect(pendingJob?.status).toBe("pending");
250
+
251
+ runner.cancel(pendingJob!.id);
252
+
253
+ await expect(pendingPromise).rejects.toBeInstanceOf(
254
+ AppsScriptJobCancelledError,
255
+ );
256
+
257
+ expect(
258
+ runner.getJobs().find((job) => job.id === pendingJob!.id),
259
+ ).toBeUndefined();
260
+
261
+ controls[0].resolve();
262
+ await runningPromises[0];
263
+
264
+ await Promise.resolve();
265
+ await Promise.resolve();
266
+
267
+ expect(pendingExecute).not.toHaveBeenCalled();
268
+
269
+ for (const control of controls.slice(1)) {
270
+ control.resolve();
271
+ }
272
+
273
+ await Promise.all(runningPromises);
274
+ });
275
+
276
+ it("running JobをcancelしてもRunnerから削除されない", async () => {
277
+ const queue = new AppsScriptJobQueue();
278
+ const runner = new AppsScriptJobRunner(queue);
279
+
280
+ const control = deferred<void>();
281
+
282
+ const promise = queue.enqueue("running", async () => {
283
+ await control.promise;
284
+ });
285
+
286
+ await vi.waitFor(() => {
287
+ expect(runner.getJobs()).toHaveLength(1);
288
+ expect(runner.getJobs()[0]?.isRunning()).toBe(true);
289
+ });
290
+
291
+ const runningJob = runner.getJobs()[0];
292
+
293
+ runner.cancel(runningJob!.id);
294
+
295
+ expect(runner.getJobs()).toContain(runningJob);
296
+ expect(runningJob?.isRunning()).toBe(true);
297
+
298
+ control.resolve();
299
+ await promise;
300
+ });
301
+
302
+ it("running Jobをcancelしても同時実行数を超えない", async () => {
303
+ const queue = new AppsScriptJobQueue();
304
+ const runner = new AppsScriptJobRunner(queue);
305
+
306
+ const controls = Array.from({ length: 30 }, () => deferred<void>());
307
+
308
+ const runningPromises = controls.map((control, index) =>
309
+ queue.enqueue(`running-${index}`, async () => {
310
+ await control.promise;
311
+ }),
312
+ );
313
+
314
+ const pendingExecute = vi.fn(async () => "pending");
315
+
316
+ const pendingPromise = queue.enqueue("pending", pendingExecute);
317
+
318
+ await vi.waitFor(() => {
319
+ expect(runner.getJobs().filter((job) => job.isRunning())).toHaveLength(
320
+ 30,
321
+ );
322
+ });
323
+
324
+ const runningJob = runner
325
+ .getJobs()
326
+ .find((job) => job.label === "running-0");
327
+
328
+ expect(runningJob).toBeDefined();
329
+
330
+ runner.cancel(runningJob!.id);
331
+
332
+ // running中なのでcancelされず、Runnerにも残る
333
+ expect(
334
+ runner.getJobs().find((job) => job.id === runningJob!.id),
335
+ ).toBeDefined();
336
+
337
+ expect(runningJob!.isRunning()).toBe(true);
338
+
339
+ // 枠は空いていないのでpendingは開始されない
340
+ await Promise.resolve();
341
+ await Promise.resolve();
342
+
343
+ expect(pendingExecute).not.toHaveBeenCalled();
344
+
345
+ // 1件が本当に完了した時点で初めて枠が空く
346
+ controls[0].resolve();
347
+ await runningPromises[0];
348
+
349
+ await vi.waitFor(() => {
350
+ expect(pendingExecute).toHaveBeenCalledOnce();
351
+ });
352
+
353
+ await expect(pendingPromise).resolves.toBe("pending");
354
+
355
+ for (const control of controls.slice(1)) {
356
+ control.resolve();
357
+ }
358
+
359
+ await Promise.all(runningPromises);
360
+ });
361
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { HashProperty } from "../src/navigation/HashProperty";
3
+
4
+ describe("HashProperty", () => {
5
+ it.each([
6
+ ["", "#/"],
7
+ ["#/users", "#/users"],
8
+ ["#users", "#/users"],
9
+ ["/users", "#/users"],
10
+ ["users", "#/users"],
11
+ ["/users?page=2", "#/users?page=2"],
12
+ ])("%s を %s に正規化できる", (input, expected) => {
13
+ expect(new HashProperty(input).normalize()).toBe(expected);
14
+ });
15
+
16
+ it.each([
17
+ ["#/users", "/users"],
18
+ ["#/users", "users"],
19
+ ["#/users?page=2", "/users?page=2"],
20
+ ])("同じ意味の hash を同一と判定できる", (left, right) => {
21
+ expect(new HashProperty(left).equals(new HashProperty(right))).toBe(true);
22
+ });
23
+
24
+ it("異なる hash を異なる値と判定できる", () => {
25
+ expect(
26
+ new HashProperty("#/users").equals(new HashProperty("#/settings")),
27
+ ).toBe(false);
28
+ });
29
+ });