@gasboost/client 0.1.0 → 0.1.1

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/dist/navigation/AppsScriptIframe.js +5 -4
  4. package/package.json +21 -1
  5. package/dist/AppsScriptJob.d.ts +0 -28
  6. package/dist/AppsScriptJob.js +0 -84
  7. package/dist/AppsScriptJobQueue.d.ts +0 -14
  8. package/dist/AppsScriptJobQueue.js +0 -40
  9. package/dist/AppsScriptJobRunner.d.ts +0 -20
  10. package/dist/AppsScriptJobRunner.js +0 -93
  11. package/dist/AppsScriptJobStore.d.ts +0 -7
  12. package/dist/AppsScriptJobStore.js +0 -9
  13. package/src/AppsScriptClient.ts +0 -81
  14. package/src/google.ts +0 -55
  15. package/src/index.ts +0 -4
  16. package/src/job/AppsScriptJob.ts +0 -85
  17. package/src/job/AppsScriptJobQueue.ts +0 -48
  18. package/src/job/AppsScriptJobRunner.ts +0 -107
  19. package/src/job/AppsScriptJobStore.ts +0 -16
  20. package/src/navigation/AppsScriptContainer.ts +0 -47
  21. package/src/navigation/AppsScriptHistoryPipeline.ts +0 -29
  22. package/src/navigation/AppsScriptIframe.ts +0 -63
  23. package/src/navigation/HashProperty.ts +0 -20
  24. package/src/navigation/NavigationEntry.ts +0 -82
  25. package/src/navigation/NavigationLocation.ts +0 -54
  26. package/tests/AppsScriptClient.spec.ts +0 -322
  27. package/tests/AppsScriptClient.type.spec.ts +0 -52
  28. package/tests/AppsScriptContainer.spec.ts +0 -502
  29. package/tests/AppsScriptHistoryPipeline.spec.ts +0 -303
  30. package/tests/AppsScriptIframe.spec.ts +0 -452
  31. package/tests/AppsScriptJob.spec.ts +0 -83
  32. package/tests/AppsScriptJobQueue.spec.ts +0 -125
  33. package/tests/AppsScriptJobRunner.spec.ts +0 -361
  34. package/tests/HashProperty.spec.ts +0 -29
  35. package/tests/NavigationEntry.spec.ts +0 -507
  36. package/tests/NavigationLocation.spec.ts +0 -189
  37. package/tests/setup.ts +0 -15
  38. package/tsconfig.build.json +0 -14
  39. package/tsconfig.json +0 -6
  40. package/vitest.config.mts +0 -8
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gasboost
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,386 @@
1
+ # @gasboost/client
2
+
3
+ Google Apps Script Web アプリケーション向けの、フレームワーク非依存クライアントライブラリです。
4
+
5
+ `@gasboost/app` で定義した RPC 契約を利用して、フロントエンドから型安全に Google Apps Script のサーバー関数を呼び出せます。
6
+
7
+ また、RPC や任意の非同期処理を Job として管理する仕組みと、Google Apps Script Web アプリケーションの History 同期機能を提供します。
8
+
9
+ ## インストール
10
+
11
+ ```bash
12
+ pnpm add @gasboost/client
13
+ ```
14
+
15
+ npm:
16
+
17
+ ```bash
18
+ npm install @gasboost/client
19
+ ```
20
+
21
+ ## 型安全な RPC
22
+
23
+ バックエンドで `@gasboost/app` を使って RPC を定義します。
24
+
25
+ ```ts
26
+ // backend/main.ts
27
+
28
+ import { AppsScript, type InferAppsScript } from "@gasboost/app";
29
+
30
+ const app = new AppsScript()
31
+ .call("sum", (a: number, b: number) => a + b)
32
+ .call("getUser", async (id: string) => ({
33
+ id,
34
+ name: "Taro",
35
+ }));
36
+
37
+ export default app;
38
+
39
+ export type AppType = InferAppsScript<typeof app>;
40
+ ```
41
+
42
+ フロントエンドでは `AppType` を型として共有します。
43
+
44
+ ```ts
45
+ import { appsScriptClient } from "@gasboost/client";
46
+ import type { AppType } from "../backend/main";
47
+
48
+ const { client } = appsScriptClient<AppType>();
49
+ ```
50
+
51
+ これにより、登録された RPC が型安全な関数として利用できます。
52
+
53
+ ```ts
54
+ const total = await client.sum(1, 2);
55
+
56
+ const user = await client.getUser("user-1");
57
+ ```
58
+
59
+ 引数型と戻り値型は `AppType` から推論されます。
60
+
61
+ 存在しない RPC や不正な引数は TypeScript 上で検出できます。
62
+
63
+ ## RPC Transport
64
+
65
+ RPC は Google Apps Script が提供する `google.script.run` を利用して実行されます。
66
+
67
+ 例えば、
68
+
69
+ ```ts
70
+ await client.sum(1, 2);
71
+ ```
72
+
73
+ は内部的には対応する GAS のサーバー関数を、
74
+
75
+ ```text
76
+ google.script.run.sum(1, 2)
77
+ ```
78
+
79
+ のように呼び出します。
80
+
81
+ 成功時には `AppsScriptResponse.contents` を JSON として parse し、その結果を返します。
82
+
83
+ GAS 側の失敗は Promise の reject としてそのまま伝播します。
84
+
85
+ ## JSON Response
86
+
87
+ RPC のレスポンスは JSON として扱われます。
88
+
89
+ ```ts
90
+ const user = await client.getUser("user-1");
91
+ ```
92
+
93
+ 例えばバックエンドが、
94
+
95
+ ```ts
96
+ {
97
+ id: "user-1",
98
+ name: "Taro",
99
+ }
100
+ ```
101
+
102
+ を返した場合、フロントエンドでも同じ構造の object として取得できます。
103
+
104
+ 不正な JSON が返された場合は `JSON.parse` のエラーになります。
105
+
106
+ ### Date
107
+
108
+ `Date` は JavaScript の `Date` instance には復元されません。
109
+
110
+ 例えばバックエンドが、
111
+
112
+ ```ts
113
+ {
114
+ createdAt: new Date("2026-09-04T00:00:00.000Z"),
115
+ }
116
+ ```
117
+
118
+ を返した場合、クライアントでは、
119
+
120
+ ```ts
121
+ {
122
+ createdAt: "2026-09-04T00:00:00.000Z",
123
+ }
124
+ ```
125
+
126
+ のように string として取得されます。
127
+
128
+ `@gasboost/app` の `InferAppsScript` も、この JSON シリアライズ後の型に合わせて `Date` を `string` として推論します。
129
+
130
+ ## appsScriptClient
131
+
132
+ ```ts
133
+ const { client, jobs } = appsScriptClient<AppType>();
134
+ ```
135
+
136
+ `appsScriptClient()` は次の2つを返します。
137
+
138
+ ```ts
139
+ {
140
+ client,
141
+ jobs,
142
+ }
143
+ ```
144
+
145
+ ### `client`
146
+
147
+ `AppType` から生成される型安全な RPC client です。
148
+
149
+ ### `jobs`
150
+
151
+ RPC と非同期処理の Job を管理します。
152
+
153
+ 現在以下の API を提供します。
154
+
155
+ ```ts
156
+ jobs.start(label, execute);
157
+ jobs.cancel(jobId);
158
+ jobs.retry(jobId);
159
+ jobs.subscribe(listener);
160
+ jobs.getSnapshot();
161
+ ```
162
+
163
+ ## Job
164
+
165
+ `client` 経由で実行した RPC は自動的に Job として管理されます。
166
+
167
+ ```ts
168
+ const { client, jobs } = appsScriptClient<AppType>();
169
+
170
+ const promise = client.getUser("user-1");
171
+
172
+ const snapshot = jobs.getSnapshot();
173
+ ```
174
+
175
+ RPC 名が Job の `label` になります。
176
+
177
+ ```ts
178
+ job.label === "getUser";
179
+ ```
180
+
181
+ Job には一意な `id` が割り当てられます。
182
+
183
+ ## Job Status
184
+
185
+ Job は次の状態を持ちます。
186
+
187
+ ```text
188
+ pending
189
+ running
190
+ success
191
+ failed
192
+ ```
193
+
194
+ 状態は `status` から取得できます。
195
+
196
+ ```ts
197
+ job.status;
198
+ ```
199
+
200
+ 個別の判定メソッドも利用できます。
201
+
202
+ ```ts
203
+ job.isPending();
204
+ job.isRunning();
205
+ job.isSuccess();
206
+ job.isFailed();
207
+ ```
208
+
209
+ Job は次の情報も保持します。
210
+
211
+ ```ts
212
+ job.id;
213
+ job.label;
214
+ job.createdAt;
215
+ job.endedAt;
216
+ job.result;
217
+ job.error;
218
+ ```
219
+
220
+ ## Job の実行
221
+
222
+ RPC 以外の任意の非同期処理も、同じ Job Queue で管理できます。
223
+
224
+ ```ts
225
+ const { jobs } = appsScriptClient<AppType>();
226
+
227
+ const result = await jobs.start("load-data", async () => {
228
+ return await loadData();
229
+ });
230
+ ```
231
+
232
+ `client` から実行された RPC と `jobs.start()` は同じ Queue / Runner を共有します。
233
+
234
+ ## 並列実行
235
+
236
+ Job Runner は複数 Job の並列実行に対応しています。
237
+
238
+ 現在の最大同時実行数は `30` です。
239
+
240
+ 上限を超えた Job は `pending` として Queue に残り、実行中の Job が完了すると順次実行されます。
241
+
242
+ ## 成功した Job
243
+
244
+ 成功した Job は完了後に Job 一覧から削除されます。
245
+
246
+ ```ts
247
+ jobs.getSnapshot();
248
+ ```
249
+
250
+ には、実行中・待機中・失敗した Job が主に残ります。
251
+
252
+ ## 失敗した Job
253
+
254
+ 失敗した Job は一覧に `failed` 状態で残ります。
255
+
256
+ ```ts
257
+ const failedJob = jobs.getSnapshot().find((job) => job.status === "failed");
258
+ ```
259
+
260
+ エラーは `job.error` から取得できます。
261
+
262
+ ## Retry
263
+
264
+ 失敗した Job は ID を指定して再実行できます。
265
+
266
+ ```ts
267
+ jobs.retry(job.id);
268
+ ```
269
+
270
+ 元の Job は一覧から削除され、同じ `label` と `execute` を利用した新しい Job が Queue に追加されます。
271
+
272
+ そのため、retry 後の Job は新しい ID を持ちます。
273
+
274
+ ## Cancel
275
+
276
+ `pending` 状態の Job はキャンセルできます。
277
+
278
+ ```ts
279
+ jobs.cancel(job.id);
280
+ ```
281
+
282
+ キャンセルされた Job は Queue と Job 一覧から削除され、対応する Promise は `AppsScriptJobCancelledError` で reject されます。
283
+
284
+ すでに `running` になった Job はキャンセルされません。
285
+
286
+ 現在の cancel は、実行開始前の Job を Queue から取り除くための機能です。
287
+
288
+ ## Job の購読
289
+
290
+ Job 一覧の変更を購読できます。
291
+
292
+ ```ts
293
+ const unsubscribe = jobs.subscribe(() => {
294
+ console.log(jobs.getSnapshot());
295
+ });
296
+ ```
297
+
298
+ 購読を解除する場合:
299
+
300
+ ```ts
301
+ unsubscribe();
302
+ ```
303
+
304
+ `getSnapshot()` は現在の Job 一覧を返します。
305
+
306
+ ```ts
307
+ const jobsSnapshot = jobs.getSnapshot();
308
+ ```
309
+
310
+ このインターフェースは React の `useSyncExternalStore` から直接利用できる形になっています。
311
+
312
+ React から利用する場合は `@gasboost/react` の `useAppsScriptJob` を利用できます。
313
+
314
+ ## History
315
+
316
+ `AppsScriptHistoryPipeline` は Google Apps Script Web アプリケーションの navigation state を同期するための仕組みです。
317
+
318
+ ```ts
319
+ import { AppsScriptHistoryPipeline } from "@gasboost/client";
320
+ ```
321
+
322
+ Google Apps Script が提供する、
323
+
324
+ - `google.script.history`
325
+ - `google.script.url`
326
+
327
+ と iframe 側の History を同期します。
328
+
329
+ ```text
330
+ GAS Container History
331
+
332
+ AppsScriptHistoryPipeline
333
+
334
+ iframe History
335
+ ```
336
+
337
+ 通常 React アプリケーションでは、直接利用する代わりに `@gasboost/react` の `AppsScriptRouter` を利用します。
338
+
339
+ ## Export
340
+
341
+ 現在 `@gasboost/client` から公開されている API:
342
+
343
+ ```ts
344
+ appsScriptClient;
345
+ AppsScriptJob;
346
+ AppsScriptJobStore;
347
+ AppsScriptHistoryPipeline;
348
+ ```
349
+
350
+ ## ローカル RPC について
351
+
352
+ `@gasboost/vite` の `dev` plugin は、Vite Dev Server 上に Local RPC endpoint を提供します。
353
+
354
+ ```text
355
+ POST /__gasboost/{rpcName}
356
+ ```
357
+
358
+ ただし現在の `@gasboost/client` の RPC transport は `google.script.run` を利用します。
359
+
360
+ そのため、`@gasboost/vite` の Local RPC endpoint へ自動的に transport を切り替える機能は、現在の `@gasboost/client` には含まれていません。
361
+
362
+ ## 責務
363
+
364
+ `@gasboost/client` が担当するもの:
365
+
366
+ - `AppType` に基づく型安全 RPC client
367
+ - `google.script.run` を利用した RPC transport
368
+ - JSON response の parse
369
+ - RPC / 非同期処理の Job Queue
370
+ - Job の状態管理
371
+ - pending Job の cancel
372
+ - Job の retry
373
+ - Job Store の購読
374
+ - GAS Container と iframe の History 同期
375
+
376
+ React 固有の処理は `@gasboost/react` が担当します。
377
+
378
+ ## 関連パッケージ
379
+
380
+ - `@gasboost/app` — バックエンドと RPC 契約の定義
381
+ - `@gasboost/vite` — GAS build と Local RPC
382
+ - `@gasboost/react` — React integration
383
+
384
+ ## License
385
+
386
+ MIT
@@ -36,10 +36,11 @@ class AppsScriptIframe {
36
36
  return this.entry;
37
37
  }
38
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)));
39
+ const normalized = new HashProperty_1.HashProperty(window.location.hash)
40
+ .toString()
41
+ .replace(/^#/, "");
42
+ const [path = "/", query = ""] = normalized.split("?");
43
+ return new NavigationEntry_1.NavigationEntry(history.state ?? {}, new NavigationLocation_1.NavigationLocation(new HashProperty_1.HashProperty(path), new URLSearchParams(query)));
43
44
  }
44
45
  }
45
46
  exports.AppsScriptIframe = AppsScriptIframe;
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "@gasboost/client",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
+ "description": "Type-safe Google Apps Script RPC client with jobs and navigation.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/gasboost/app.git",
8
+ "directory": "packages/client"
9
+ },
10
+ "license": "MIT",
11
+ "author": "tiger-oshima",
12
+ "keywords": [
13
+ "google-apps-script",
14
+ "gas",
15
+ "rpc",
16
+ "typescript",
17
+ "web-app",
18
+ "gasboost"
19
+ ],
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
4
24
  "main": "./dist/index.js",
5
25
  "types": "./dist/index.d.ts",
6
26
  "exports": {
@@ -1,28 +0,0 @@
1
- export declare class AppsScriptJob<T> {
2
- readonly label: string;
3
- readonly execute: () => Promise<T>;
4
- private resolve;
5
- private reject;
6
- readonly id: string;
7
- readonly createdAt: Date;
8
- private _startedAt;
9
- private _endedAt;
10
- private _result;
11
- private _error;
12
- constructor(label: string, execute: () => Promise<T>, resolve: (value: T) => void, reject: (reason?: any) => void, id?: string, createdAt?: Date);
13
- start(): void;
14
- success(result: T): void;
15
- fail(error: unknown): void;
16
- get endedAt(): Date | null;
17
- get result(): T | null;
18
- get error(): unknown;
19
- isPending(): boolean;
20
- isRunning(): boolean;
21
- isSuccess(): boolean;
22
- isFailed(): boolean;
23
- get status(): "failed" | "pending" | "running" | "success" | "unknown";
24
- cancel(): boolean;
25
- }
26
- export declare class AppsScriptJobCancelledError extends Error {
27
- constructor();
28
- }
@@ -1,84 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppsScriptJobCancelledError = exports.AppsScriptJob = void 0;
4
- class AppsScriptJob {
5
- label;
6
- execute;
7
- resolve;
8
- reject;
9
- id;
10
- createdAt;
11
- _startedAt = null;
12
- _endedAt = null;
13
- _result = null;
14
- _error = null;
15
- constructor(label, execute, resolve, reject, id = crypto.randomUUID(), createdAt = new Date()) {
16
- this.label = label;
17
- this.execute = execute;
18
- this.resolve = resolve;
19
- this.reject = reject;
20
- this.id = id;
21
- this.createdAt = createdAt;
22
- }
23
- start() {
24
- this._startedAt = new Date();
25
- }
26
- success(result) {
27
- this._endedAt = new Date();
28
- this._result = result;
29
- this.resolve(result);
30
- }
31
- fail(error) {
32
- this._endedAt = new Date();
33
- this._error = error;
34
- this.reject(error);
35
- }
36
- get endedAt() {
37
- return this._endedAt;
38
- }
39
- get result() {
40
- return this._result;
41
- }
42
- get error() {
43
- return this._error;
44
- }
45
- isPending() {
46
- return this._startedAt === null;
47
- }
48
- isRunning() {
49
- return this._startedAt !== null && this._endedAt === null;
50
- }
51
- isSuccess() {
52
- return this._endedAt !== null && this._error === null;
53
- }
54
- isFailed() {
55
- return this._endedAt !== null && this._error !== null;
56
- }
57
- get status() {
58
- if (this.isPending())
59
- return "pending";
60
- if (this.isRunning())
61
- return "running";
62
- if (this.isSuccess())
63
- return "success";
64
- if (this.isFailed())
65
- return "failed";
66
- return "unknown";
67
- }
68
- cancel() {
69
- if (!this.isPending()) {
70
- return false;
71
- }
72
- const error = new AppsScriptJobCancelledError();
73
- this.fail(error);
74
- return true;
75
- }
76
- }
77
- exports.AppsScriptJob = AppsScriptJob;
78
- class AppsScriptJobCancelledError extends Error {
79
- constructor() {
80
- super("Job cancelled");
81
- this.name = "AppsScriptJobCancelledError";
82
- }
83
- }
84
- exports.AppsScriptJobCancelledError = AppsScriptJobCancelledError;
@@ -1,14 +0,0 @@
1
- import { AppsScriptJob } from "./AppsScriptJob";
2
- export interface RunnableJob {
3
- run(): Promise<void>;
4
- addJob<T>(job: AppsScriptJob<T>): void;
5
- }
6
- export declare class AppsScriptJobQueue {
7
- private jobs;
8
- private listeners;
9
- enqueue<T>(label: string, execute: () => Promise<T>): Promise<T>;
10
- dequeue(): AppsScriptJob<any> | undefined;
11
- private notify;
12
- subscribe(listener: RunnableJob): () => boolean;
13
- remove(jobId: string): void;
14
- }
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppsScriptJobQueue = void 0;
4
- const AppsScriptJob_1 = require("./AppsScriptJob");
5
- class AppsScriptJobQueue {
6
- jobs = [];
7
- listeners = new Set();
8
- enqueue(label, execute) {
9
- const promise = new Promise((resolve, reject) => {
10
- // キューにジョブを追加
11
- const job = new AppsScriptJob_1.AppsScriptJob(label, execute, resolve, reject);
12
- this.jobs.push(job);
13
- this.notify(job);
14
- });
15
- return promise;
16
- }
17
- dequeue() {
18
- const job = this.jobs.shift();
19
- if (!job)
20
- return undefined;
21
- return job;
22
- }
23
- notify(job) {
24
- this.listeners.forEach((listener) => {
25
- listener.addJob(job);
26
- listener.run();
27
- });
28
- }
29
- subscribe(listener) {
30
- this.listeners.add(listener);
31
- return () => this.listeners.delete(listener);
32
- }
33
- remove(jobId) {
34
- const index = this.jobs.findIndex((job) => job.id === jobId);
35
- if (index !== -1) {
36
- this.jobs.splice(index, 1);
37
- }
38
- }
39
- }
40
- exports.AppsScriptJobQueue = AppsScriptJobQueue;
@@ -1,20 +0,0 @@
1
- import { AppsScriptJob } from "./AppsScriptJob";
2
- import { AppsScriptJobQueue, RunnableJob } from "./AppsScriptJobQueue";
3
- export declare class AppsScriptJobRunner implements RunnableJob {
4
- readonly queue: AppsScriptJobQueue;
5
- private jobs;
6
- private listeners;
7
- private MAX_CONCURRENT;
8
- private running;
9
- private snapshot;
10
- private dirty;
11
- constructor(queue: AppsScriptJobQueue);
12
- addJob<T>(job: AppsScriptJob<T>): void;
13
- getJobs(): AppsScriptJob<any>[];
14
- run(): Promise<void>;
15
- remove(jobId: string): void;
16
- cancel(jobId: string): void;
17
- retry(jobId: AppsScriptJob<any>["id"]): void;
18
- subscribe(listener: () => void): () => boolean;
19
- private notify;
20
- }