hikoutei 0.4.2 → 0.4.3
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/application/sync/outbound/SheetsEffectDispatcher.d.ts +64 -0
- package/dist/application/sync/outbound/SheetsEffectDispatcher.d.ts.map +1 -0
- package/dist/application/sync/outbound/SheetsEffectDispatcher.js +440 -0
- package/dist/application/sync/outbound/SheetsEffectDispatcher.js.map +1 -0
- package/dist/application/sync/service/SyncServiceBootstrap.d.ts +3 -4
- package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
- package/dist/application/sync/service/SyncServiceBootstrap.js +7 -4
- package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
- package/dist/application/sync/telemetry/syncTiming.d.ts +20 -33
- package/dist/application/sync/telemetry/syncTiming.d.ts.map +1 -1
- package/dist/application/sync/telemetry/syncTiming.js +14 -12
- package/dist/application/sync/telemetry/syncTiming.js.map +1 -1
- package/dist/infrastructure/storage/errors.d.ts +10 -5
- package/dist/infrastructure/storage/errors.d.ts.map +1 -1
- package/dist/infrastructure/storage/errors.js +10 -7
- package/dist/infrastructure/storage/errors.js.map +1 -1
- package/dist/infrastructure/storage/index.d.ts +6 -4
- package/dist/infrastructure/storage/index.d.ts.map +1 -1
- package/dist/infrastructure/storage/index.js +3 -2
- package/dist/infrastructure/storage/index.js.map +1 -1
- package/dist/infrastructure/storage/sqlite/schema.d.ts +12 -11
- package/dist/infrastructure/storage/sqlite/schema.d.ts.map +1 -1
- package/dist/infrastructure/storage/sqlite/schema.js +17 -109
- package/dist/infrastructure/storage/sqlite/schema.js.map +1 -1
- package/dist/infrastructure/storage/state/canonical/canonicalCommit.d.ts +2 -2
- package/dist/infrastructure/storage/state/canonical/canonicalCommit.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/canonical/canonicalCommit.js +2 -2
- package/dist/infrastructure/storage/state/canonical/canonicalCommit.js.map +1 -1
- package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.d.ts +1 -2
- package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.js +1 -2
- package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.js.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationCanonical.d.ts +1 -1
- package/dist/infrastructure/storage/state/observation/observationCanonical.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationQuarantine.d.ts +1 -1
- package/dist/infrastructure/storage/state/observation/observationQuarantine.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationQuarantine.js +1 -1
- package/dist/infrastructure/storage/state/observation/observationQuarantine.js.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationTypes.d.ts +1 -1
- package/dist/infrastructure/storage/state/observation/observationTypes.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationValidation.d.ts +1 -1
- package/dist/infrastructure/storage/state/observation/observationValidation.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationWriter.d.ts +1 -1
- package/dist/infrastructure/storage/state/observation/observationWriter.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/observation/observationWriter.js +1 -2
- package/dist/infrastructure/storage/state/observation/observationWriter.js.map +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts +1 -2
- package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriter.js +1 -2
- package/dist/infrastructure/storage/state/resolution/resolutionWriter.js.map +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.d.ts +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.js +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.js.map +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts.map +1 -1
- package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js +2 -2
- package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js.map +1 -1
- package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts +1 -1
- package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts.map +1 -1
- package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js +1 -1
- package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js.map +1 -1
- package/dist/infrastructure/storage/sync/shared/syncRegistry.d.ts +1 -1
- package/dist/infrastructure/storage/sync/shared/syncRegistry.js +1 -1
- package/package.json +13 -7
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts +0 -66
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js +0 -123
- package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts +0 -111
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js +0 -369
- package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts +0 -127
- package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorker.js +0 -552
- package/dist/application/sync/outbound/effects/SyncEffectWorker.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts +0 -85
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js +0 -74
- package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts +0 -31
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js +0 -222
- package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.d.ts +0 -14
- package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js +0 -25
- package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts +0 -61
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js +0 -296
- package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts +0 -17
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js +0 -80
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts +0 -13
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts.map +0 -1
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js +0 -248
- package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts +0 -141
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.js +0 -318
- package/dist/infrastructure/storage/sync/outbound/effectOutbox.js.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts +0 -143
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js +0 -16
- package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts +0 -33
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js +0 -259
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts +0 -27
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts.map +0 -1
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js +0 -316
- package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js.map +0 -1
- package/dist/infrastructure/storage/sync/shared/writerLease.d.ts +0 -76
- package/dist/infrastructure/storage/sync/shared/writerLease.d.ts.map +0 -1
- package/dist/infrastructure/storage/sync/shared/writerLease.js +0 -189
- package/dist/infrastructure/storage/sync/shared/writerLease.js.map +0 -1
- package/docs/advanced-sheets-gateway-concurrency-problem.md +0 -434
- package/docs/architecture.md +0 -224
- package/docs/ci.md +0 -293
- package/docs/code-guidelines.md +0 -248
- package/docs/development.md +0 -74
- package/docs/gateway-removal-inventory.md +0 -147
- package/docs/git-workflow.md +0 -224
- package/docs/google-sheets-sync-scaling-strategy.md +0 -459
- package/docs/mikro-orm-adapter-spike.md +0 -89
- package/docs/quick-start.md +0 -137
- package/docs/sql-layer-plan.md +0 -59
- package/docs/sync-bulk-write-benchmark.md +0 -2117
- package/docs/sync-observability.md +0 -100
- package/docs/task-queue-write-model.md +0 -640
- package/docs/typed-sheets-mvp-scope-2026-06-29.md +0 -490
- package/docs/typed-sheets-plan.md +0 -417
- package/docs/write-and-synchronization-flow.md +0 -179
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
# typed-sheets Plan
|
|
2
|
-
|
|
3
|
-
## 1. Project Positioning
|
|
4
|
-
|
|
5
|
-
`typed-sheets`는 Google Sheets를 초기 MVP, 내부 운영툴, 저트래픽 어드민, 프로토타입의 lightweight database처럼 사용할 때 필요한 TypeScript repository layer다.
|
|
6
|
-
|
|
7
|
-
핵심 포지션은 다음이다.
|
|
8
|
-
|
|
9
|
-
> Typed repository and safe write layer for Google Sheets-backed MVPs.
|
|
10
|
-
|
|
11
|
-
이 프로젝트는 Google Sheets를 MySQL/Postgres 대체재로 주장하지 않는다. 또한 `google-spreadsheet`나 `@googleapis/sheets` 같은 API client를 대체하려는 것도 아니다.
|
|
12
|
-
|
|
13
|
-
핵심 차별점은 Google Sheets를 DB처럼 쓸 때 조용히 깨지는 상태를 성공으로 처리하지 않는 것이다.
|
|
14
|
-
|
|
15
|
-
- Sheet header와 TypeScript schema의 drift 감지
|
|
16
|
-
- row 데이터를 typed object로 변환
|
|
17
|
-
- key column 기반 repository API 제공
|
|
18
|
-
- `_version` column 기반 optimistic locking
|
|
19
|
-
- Google Sheets API quota를 고려한 확장 가능한 구조
|
|
20
|
-
- 향후 Apps Script `LockService` write gateway 확장 가능성
|
|
21
|
-
|
|
22
|
-
## 2. Existing Ecosystem Comparison
|
|
23
|
-
|
|
24
|
-
### `google-spreadsheet`
|
|
25
|
-
|
|
26
|
-
- Google Sheets API를 편하게 다루기 위한 wrapper다.
|
|
27
|
-
- row/cell 조작은 편하지만 repository abstraction은 아니다.
|
|
28
|
-
- schema drift, key uniqueness, optimistic locking을 라이브러리의 중심 책임으로 보지 않는다.
|
|
29
|
-
|
|
30
|
-
`typed-sheets`에서는 이 라이브러리를 내부 adapter 구현체로 사용할 수 있다.
|
|
31
|
-
|
|
32
|
-
### `@googleapis/sheets`
|
|
33
|
-
|
|
34
|
-
- Google 공식 Sheets API client다.
|
|
35
|
-
- 저수준 API 호출, 인증, range read/write에 적합하다.
|
|
36
|
-
- typed repository, schema validation, stale write protection은 직접 구현해야 한다.
|
|
37
|
-
|
|
38
|
-
`typed-sheets`에서는 production adapter의 가장 보수적인 기반이 될 수 있다.
|
|
39
|
-
|
|
40
|
-
### `spreadsheet-orm`
|
|
41
|
-
|
|
42
|
-
- Google Spreadsheet를 ORM처럼 다루려는 비교적 가까운 시도다.
|
|
43
|
-
- query builder, schema management 등 ORM 방향의 기능을 포함한다.
|
|
44
|
-
- `typed-sheets`는 더 좁게 가는 것이 좋다.
|
|
45
|
-
|
|
46
|
-
`typed-sheets`의 MVP는 ORM 전체가 아니라 다음만 강하게 잡는다.
|
|
47
|
-
|
|
48
|
-
- header drift detection
|
|
49
|
-
- typed row parsing
|
|
50
|
-
- key-based repository
|
|
51
|
-
- `_version` optimistic locking
|
|
52
|
-
|
|
53
|
-
## 3. MVP API Review
|
|
54
|
-
|
|
55
|
-
제안된 MVP API는 크지 않다. 오히려 프로젝트의 차별점을 증명하기에 적절한 최소 범위다.
|
|
56
|
-
|
|
57
|
-
초기 API:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import { createSheetRepository, text, number, boolean } from "typed-sheets";
|
|
61
|
-
|
|
62
|
-
const users = createSheetRepository({
|
|
63
|
-
sheetName: "Users",
|
|
64
|
-
key: "id",
|
|
65
|
-
columns: {
|
|
66
|
-
id: text(),
|
|
67
|
-
email: text(),
|
|
68
|
-
age: number().optional(),
|
|
69
|
-
active: boolean(),
|
|
70
|
-
_version: number(),
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
await users.assertSchema();
|
|
75
|
-
|
|
76
|
-
const all = await users.findAll();
|
|
77
|
-
const user = await users.findById("u1");
|
|
78
|
-
|
|
79
|
-
await users.insert({
|
|
80
|
-
id: "u1",
|
|
81
|
-
email: "a@test.com",
|
|
82
|
-
age: 20,
|
|
83
|
-
active: true,
|
|
84
|
-
_version: 1,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
await users.update("u1", current => ({
|
|
88
|
-
...current,
|
|
89
|
-
age: current.age + 1,
|
|
90
|
-
}));
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
단, 실제 구현에서는 `createSheetRepository`에 adapter를 명시적으로 주입하는 형태가 더 좋다.
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
const users = createSheetRepository({
|
|
97
|
-
adapter,
|
|
98
|
-
sheetName: "Users",
|
|
99
|
-
key: "id",
|
|
100
|
-
columns: {
|
|
101
|
-
id: text(),
|
|
102
|
-
email: text(),
|
|
103
|
-
age: number().optional(),
|
|
104
|
-
active: boolean(),
|
|
105
|
-
_version: number(),
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
이렇게 하면 core repository logic을 Google API, OAuth, network와 분리해 fake adapter로 테스트할 수 있다.
|
|
111
|
-
|
|
112
|
-
## 4. MVP Scope
|
|
113
|
-
|
|
114
|
-
MVP에 포함할 기능:
|
|
115
|
-
|
|
116
|
-
- schema definition API
|
|
117
|
-
- header 검증
|
|
118
|
-
- 필수 column 누락 감지
|
|
119
|
-
- 중복 header 감지
|
|
120
|
-
- key column 누락 감지
|
|
121
|
-
- row parsing
|
|
122
|
-
- string, number, boolean parser
|
|
123
|
-
- optional column
|
|
124
|
-
- `findAll`
|
|
125
|
-
- `findById`
|
|
126
|
-
- `insert`
|
|
127
|
-
- `update`
|
|
128
|
-
- `_version` 기반 optimistic locking
|
|
129
|
-
- `SchemaDriftError`
|
|
130
|
-
- `ConflictError`
|
|
131
|
-
- `ParseError`
|
|
132
|
-
- fake adapter 기반 테스트
|
|
133
|
-
|
|
134
|
-
MVP에서 제외할 기능:
|
|
135
|
-
|
|
136
|
-
- relation / join
|
|
137
|
-
- lazy loading
|
|
138
|
-
- SQL-like query language
|
|
139
|
-
- migration engine
|
|
140
|
-
- transaction manager
|
|
141
|
-
- multi-row atomic transaction
|
|
142
|
-
- Apps Script 자동 설치
|
|
143
|
-
- Apps Script Web App gateway
|
|
144
|
-
- cache
|
|
145
|
-
- request collapse
|
|
146
|
-
- retry/backoff
|
|
147
|
-
- browser support
|
|
148
|
-
- dashboard UI
|
|
149
|
-
|
|
150
|
-
## 5. Google Sheets를 DB처럼 쓸 때의 한계
|
|
151
|
-
|
|
152
|
-
Google Sheets는 spreadsheet이지 database가 아니다. 따라서 다음 한계를 명확히 문서화해야 한다.
|
|
153
|
-
|
|
154
|
-
- storage layer에 primary key / unique constraint가 없다.
|
|
155
|
-
- multi-row transaction이 없다.
|
|
156
|
-
- 여러 사용자가 동시에 수정할 때 lost update가 발생할 수 있다.
|
|
157
|
-
- 비개발자가 header를 변경하거나 삭제할 수 있다.
|
|
158
|
-
- 값 타입이 느슨해서 string, number, boolean, date가 섞일 수 있다.
|
|
159
|
-
- formula, formatted value, raw value가 다를 수 있다.
|
|
160
|
-
- 전체 row scan이 커질수록 느려지고 quota를 많이 쓴다.
|
|
161
|
-
- Google Sheets API quota와 timeout에 영향을 받는다.
|
|
162
|
-
- Google OAuth, service account, 공유 권한 설정이 운영 변수가 된다.
|
|
163
|
-
- Apps Script를 붙이면 OAuth 승인, Workspace 정책, script quota, 배포 문제가 추가된다.
|
|
164
|
-
|
|
165
|
-
따라서 이 프로젝트는 다음 사용처에만 적합하다고 명확히 제한해야 한다.
|
|
166
|
-
|
|
167
|
-
- 초기 MVP
|
|
168
|
-
- 내부 운영툴
|
|
169
|
-
- 저트래픽 어드민
|
|
170
|
-
- 비개발자가 직접 수정하는 운영 데이터
|
|
171
|
-
- 프로토타입
|
|
172
|
-
|
|
173
|
-
## 6. Domain / Application / Adapter 분리 구조
|
|
174
|
-
|
|
175
|
-
권장 구조:
|
|
176
|
-
|
|
177
|
-
```txt
|
|
178
|
-
src/
|
|
179
|
-
index.ts
|
|
180
|
-
domain/ # 정규화, 평가, 충돌, 상태 규칙
|
|
181
|
-
shared/ # 공통 상수·인코딩·상태 계약
|
|
182
|
-
application/
|
|
183
|
-
orm/ # 사용자 ORM facade와 mapping/flush
|
|
184
|
-
sync/ # gateway, worker, reconciliation
|
|
185
|
-
adapter/ # persistence와 Sheets provider 계약/구현
|
|
186
|
-
infrastructure/
|
|
187
|
-
storage/ # SQLite schema, canonical state, outbox
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Domain 책임
|
|
191
|
-
|
|
192
|
-
`domain`은 Google API와 SQLite 구현을 몰라야 한다.
|
|
193
|
-
|
|
194
|
-
책임:
|
|
195
|
-
|
|
196
|
-
- schema definition 해석
|
|
197
|
-
- header validation
|
|
198
|
-
- duplicate header detection
|
|
199
|
-
- required column validation
|
|
200
|
-
- key column validation
|
|
201
|
-
- row parsing
|
|
202
|
-
- duplicate key detection
|
|
203
|
-
- repository method 구현
|
|
204
|
-
- `_version` optimistic locking
|
|
205
|
-
- typed error throw
|
|
206
|
-
|
|
207
|
-
### Application 책임
|
|
208
|
-
|
|
209
|
-
application은 domain 규칙과 adapter/infrastructure를 조율한다.
|
|
210
|
-
|
|
211
|
-
책임:
|
|
212
|
-
|
|
213
|
-
- ORM entity lifecycle과 flush 계획
|
|
214
|
-
- effect worker와 reconciliation 실행
|
|
215
|
-
- Apps Script gateway operation 조합
|
|
216
|
-
|
|
217
|
-
### Adapter 책임
|
|
218
|
-
|
|
219
|
-
adapter는 Google Sheets와 통신하는 부분만 담당한다.
|
|
220
|
-
|
|
221
|
-
책임:
|
|
222
|
-
|
|
223
|
-
- 인증
|
|
224
|
-
- spreadsheet 접근
|
|
225
|
-
- sheet 조회
|
|
226
|
-
- range read/write
|
|
227
|
-
- append row
|
|
228
|
-
- update row
|
|
229
|
-
- batch API 사용 여부
|
|
230
|
-
- retry/backoff
|
|
231
|
-
- cache/request collapse
|
|
232
|
-
- Apps Script gateway 호출
|
|
233
|
-
|
|
234
|
-
초기 adapter port 예시:
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
export type SheetCell = string | number | boolean | null;
|
|
238
|
-
|
|
239
|
-
export interface SheetSnapshot {
|
|
240
|
-
headers: string[];
|
|
241
|
-
rows: SheetCell[][];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export interface SheetAdapter {
|
|
245
|
-
readSheet(sheetName: string): Promise<SheetSnapshot>;
|
|
246
|
-
appendRow(sheetName: string, row: SheetCell[]): Promise<void>;
|
|
247
|
-
updateRow(sheetName: string, rowNumber: number, row: SheetCell[]): Promise<void>;
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
`rowNumber`는 Google Sheet 기준 1-based row number로 둔다. header가 1행이므로 data row는 2행부터 시작한다.
|
|
252
|
-
|
|
253
|
-
## 7. Optimistic Locking 설계
|
|
254
|
-
|
|
255
|
-
`_version` column은 MVP에서 필수로 두는 것이 좋다.
|
|
256
|
-
|
|
257
|
-
기본 흐름:
|
|
258
|
-
|
|
259
|
-
1. `update(id, updater)` 호출
|
|
260
|
-
2. sheet를 읽는다.
|
|
261
|
-
3. key column으로 row를 찾는다.
|
|
262
|
-
4. 현재 `_version`을 기억한다.
|
|
263
|
-
5. updater를 적용한다.
|
|
264
|
-
6. write 직전 같은 row를 다시 읽거나 adapter-level compare-and-set을 수행한다.
|
|
265
|
-
7. version이 바뀌었으면 `ConflictError`를 던진다.
|
|
266
|
-
8. version이 같으면 `_version + 1`로 write한다.
|
|
267
|
-
|
|
268
|
-
MVP fake adapter에서는 read-between-write 시나리오를 테스트할 수 있게 만들어 stale write를 증명한다.
|
|
269
|
-
|
|
270
|
-
실제 Google Sheets API만으로는 완전한 atomic compare-and-set이 어렵다. 따라서 MVP에서는 stale write risk를 줄이는 repository-level fencing으로 시작하고, 2차 확장에서 Apps Script `LockService` 기반 serialized write gateway로 강화한다.
|
|
271
|
-
|
|
272
|
-
## 8. Error 설계
|
|
273
|
-
|
|
274
|
-
### `SchemaDriftError`
|
|
275
|
-
|
|
276
|
-
다음 상황에서 발생:
|
|
277
|
-
|
|
278
|
-
- header 중복
|
|
279
|
-
- required column 누락
|
|
280
|
-
- key column 누락
|
|
281
|
-
- `_version` column 누락
|
|
282
|
-
- duplicate key 발견
|
|
283
|
-
|
|
284
|
-
### `ParseError`
|
|
285
|
-
|
|
286
|
-
다음 상황에서 발생:
|
|
287
|
-
|
|
288
|
-
- required value가 비어 있음
|
|
289
|
-
- number parser 실패
|
|
290
|
-
- boolean parser 실패
|
|
291
|
-
- `_version` 값이 number가 아님
|
|
292
|
-
|
|
293
|
-
### `ConflictError`
|
|
294
|
-
|
|
295
|
-
다음 상황에서 발생:
|
|
296
|
-
|
|
297
|
-
- update 중 `_version`이 변경됨
|
|
298
|
-
- stale row write가 감지됨
|
|
299
|
-
|
|
300
|
-
## 9. Test Scenarios
|
|
301
|
-
|
|
302
|
-
테스트는 실제 Google API 없이 fake adapter로 먼저 작성한다.
|
|
303
|
-
|
|
304
|
-
### Schema tests
|
|
305
|
-
|
|
306
|
-
- `assertSchema`는 모든 column이 있으면 성공한다.
|
|
307
|
-
- required column이 없으면 `SchemaDriftError`를 던진다.
|
|
308
|
-
- key column이 없으면 `SchemaDriftError`를 던진다.
|
|
309
|
-
- `_version` column이 없으면 `SchemaDriftError`를 던진다.
|
|
310
|
-
- duplicate header가 있으면 `SchemaDriftError`를 던진다.
|
|
311
|
-
|
|
312
|
-
### Parsing tests
|
|
313
|
-
|
|
314
|
-
- text column을 string으로 파싱한다.
|
|
315
|
-
- number column을 number로 파싱한다.
|
|
316
|
-
- boolean column을 boolean으로 파싱한다.
|
|
317
|
-
- optional column은 empty value를 `undefined`로 파싱한다.
|
|
318
|
-
- required column이 비어 있으면 `ParseError`를 던진다.
|
|
319
|
-
- number 변환에 실패하면 `ParseError`를 던진다.
|
|
320
|
-
- boolean 변환에 실패하면 `ParseError`를 던진다.
|
|
321
|
-
|
|
322
|
-
### Repository read tests
|
|
323
|
-
|
|
324
|
-
- `findAll`은 typed object 배열을 반환한다.
|
|
325
|
-
- `findById`는 key가 일치하는 row를 반환한다.
|
|
326
|
-
- `findById`는 없으면 `null`을 반환한다.
|
|
327
|
-
- duplicate key가 있으면 `SchemaDriftError`를 던진다.
|
|
328
|
-
|
|
329
|
-
### Insert tests
|
|
330
|
-
|
|
331
|
-
- `insert`는 header 순서대로 row를 append한다.
|
|
332
|
-
- duplicate key insert는 `SchemaDriftError` 또는 별도 duplicate key error를 던진다.
|
|
333
|
-
- insert row parse/serialization 실패를 테스트한다.
|
|
334
|
-
|
|
335
|
-
### Update tests
|
|
336
|
-
|
|
337
|
-
- `update`는 현재 row를 updater에 넘긴다.
|
|
338
|
-
- `update`는 `_version`을 1 증가시킨다.
|
|
339
|
-
- `update`는 header 순서대로 row를 write한다.
|
|
340
|
-
- target row가 없으면 `null`을 반환한다.
|
|
341
|
-
- write 전 version이 바뀌면 `ConflictError`를 던진다.
|
|
342
|
-
|
|
343
|
-
## 10. Implementation Order
|
|
344
|
-
|
|
345
|
-
권장 구현 순서:
|
|
346
|
-
|
|
347
|
-
1. `package.json`, `tsconfig`, test runner 설정
|
|
348
|
-
2. `SheetAdapter` interface 정의
|
|
349
|
-
3. error class 정의
|
|
350
|
-
4. column schema primitive 구현
|
|
351
|
-
5. schema validation 구현
|
|
352
|
-
6. row parsing 구현
|
|
353
|
-
7. `findAll`, `findById` 구현
|
|
354
|
-
8. `insert` 구현
|
|
355
|
-
9. `_version` 기반 `update` 구현
|
|
356
|
-
10. fake adapter 테스트 작성
|
|
357
|
-
11. README 정리
|
|
358
|
-
12. 실제 Google Sheets adapter는 MVP core 검증 후 별도 단계로 진행
|
|
359
|
-
|
|
360
|
-
## 11. Expected MVP Size
|
|
361
|
-
|
|
362
|
-
목표 규모:
|
|
363
|
-
|
|
364
|
-
- production TypeScript: 500~900 lines
|
|
365
|
-
- tests: 700~1,200 lines
|
|
366
|
-
- README/docs/config: 400~700 lines
|
|
367
|
-
- total MVP: 1,500~2,800 lines
|
|
368
|
-
|
|
369
|
-
핵심은 기능 수가 아니라 다음을 테스트로 증명하는 것이다.
|
|
370
|
-
|
|
371
|
-
- schema drift를 조용히 통과시키지 않는다.
|
|
372
|
-
- parse failure를 조용히 통과시키지 않는다.
|
|
373
|
-
- duplicate key를 조용히 통과시키지 않는다.
|
|
374
|
-
- stale write를 조용히 통과시키지 않는다.
|
|
375
|
-
|
|
376
|
-
## 12. Later Extensions
|
|
377
|
-
|
|
378
|
-
MVP 이후 후보:
|
|
379
|
-
|
|
380
|
-
1. read cache / request collapse
|
|
381
|
-
2. timeout / retry / exponential backoff
|
|
382
|
-
3. `@googleapis/sheets` adapter
|
|
383
|
-
4. `google-spreadsheet` adapter
|
|
384
|
-
5. Apps Script 자동 설치 CLI
|
|
385
|
-
6. Apps Script `LockService` 기반 serialized write gateway
|
|
386
|
-
7. schema drift report
|
|
387
|
-
8. GitHub Action
|
|
388
|
-
9. Google Sheet template generator
|
|
389
|
-
10. `_createdAt`, `_updatedAt` system columns
|
|
390
|
-
11. soft delete
|
|
391
|
-
12. audit log sheet
|
|
392
|
-
|
|
393
|
-
## 13. Apps Script 방향
|
|
394
|
-
|
|
395
|
-
향후 CLI 예시:
|
|
396
|
-
|
|
397
|
-
```sh
|
|
398
|
-
npx typed-sheets init --spreadsheet-id xxx
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
목표:
|
|
402
|
-
|
|
403
|
-
- Google OAuth 로그인
|
|
404
|
-
- 대상 spreadsheet 확인
|
|
405
|
-
- bound Apps Script project 생성
|
|
406
|
-
- Apps Script 코드 업로드
|
|
407
|
-
- `LockService` 기반 write function 설치
|
|
408
|
-
- 설치 검증
|
|
409
|
-
|
|
410
|
-
가능한 공식 API 흐름:
|
|
411
|
-
|
|
412
|
-
- `projects.create`
|
|
413
|
-
- `projects.updateContent`
|
|
414
|
-
- spreadsheet file id를 parent로 사용해 bound script 생성
|
|
415
|
-
- 필요한 경우 installable trigger 생성
|
|
416
|
-
|
|
417
|
-
MVP에는 포함하지 않는다. OAuth, Workspace 정책, 권한 승인, Apps Script API 활성화 문제 때문에 core 안정성을 먼저 증명한 뒤 진행한다.
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
# Write and Synchronization Flow
|
|
2
|
-
|
|
3
|
-
Hikoutei commits local state first and materializes Google Sheets changes
|
|
4
|
-
asynchronously. The root ORM API is SQLite-only; the internal sync service owns
|
|
5
|
-
all Sheet communication.
|
|
6
|
-
|
|
7
|
-
## Public ORM flow
|
|
8
|
-
|
|
9
|
-
```text
|
|
10
|
-
em.persist(entity) / em.remove(entity)
|
|
11
|
-
│
|
|
12
|
-
▼
|
|
13
|
-
SQLite transaction
|
|
14
|
-
└─ entity table
|
|
15
|
-
│
|
|
16
|
-
▼
|
|
17
|
-
return to application
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
`createTypedSheets()` never contacts Google Sheets and does not require a Sheet
|
|
21
|
-
route, provider client, or provisioner. Reads always come from SQLite.
|
|
22
|
-
|
|
23
|
-
## Internal sync service flow
|
|
24
|
-
|
|
25
|
-
The service-side bootstrap uses the same SQLite adapter and adds the canonical
|
|
26
|
-
sync state and durable outbox to the flush transaction:
|
|
27
|
-
|
|
28
|
-
```text
|
|
29
|
-
EntityManager.flush()
|
|
30
|
-
│
|
|
31
|
-
▼
|
|
32
|
-
SQLite transaction
|
|
33
|
-
├─ entity table
|
|
34
|
-
├─ canonical sync state
|
|
35
|
-
├─ projection registry/state
|
|
36
|
-
└─ durable Sheet effect outbox
|
|
37
|
-
│
|
|
38
|
-
▼
|
|
39
|
-
internal effect supervisor
|
|
40
|
-
├─ claim with a lease
|
|
41
|
-
├─ send a bounded direct REST batch to the sync provider
|
|
42
|
-
├─ persist uncertain delivery and schedule a durable postcondition probe
|
|
43
|
-
└─ mark the effect applied, terminally failed, or recoverably pending
|
|
44
|
-
│
|
|
45
|
-
▼
|
|
46
|
-
sync provider (googleSheetsApi) ──▶ Google Sheets
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
A successful `flush()` means that SQLite accepted the local change and queued
|
|
50
|
-
its projection effect. It does not mean that the remote Sheet write has
|
|
51
|
-
completed.
|
|
52
|
-
|
|
53
|
-
## Fast append, update, and delete
|
|
54
|
-
|
|
55
|
-
New System_State rows use the bounded fast append operation where possible.
|
|
56
|
-
Updates and deletes use guarded effects with expected visible revision/hash
|
|
57
|
-
evidence. The same internal worker handles retries, response-loss recovery, and
|
|
58
|
-
reconciliation. Each physical route's effects are split into bounded sub-batches
|
|
59
|
-
so one provider call returns a complete result set instead of a partial prefix.
|
|
60
|
-
These operation types are implementation details and are not methods on the
|
|
61
|
-
public EntityManager.
|
|
62
|
-
|
|
63
|
-
### Outbound provider
|
|
64
|
-
|
|
65
|
-
The outbound half of the worker runs through one Google Sheets API provider
|
|
66
|
-
(`googleSheetsApi`): a single service-account provider owns provisioning,
|
|
67
|
-
outbound effects, table reads, row anchors, and User_Input observation with
|
|
68
|
-
no Apps Script at all. The legacy Apps Script gateway and the
|
|
69
|
-
`appsScript`/`googleApiWorker` options were removed; existing deployments
|
|
70
|
-
should switch to `googleSheetsApi`.
|
|
71
|
-
|
|
72
|
-
The direct provider implements the established sync semantics in TypeScript: bulk
|
|
73
|
-
preflight with fail-closed validation (headers, anchors, identities,
|
|
74
|
-
receipts), receipt replay/idempotency, visible/candidate/repair guards,
|
|
75
|
-
full-row deletion guards, one atomic `spreadsheets.batchUpdate` per
|
|
76
|
-
applicable target mutation + receipt write, and receipt-backed postcondition
|
|
77
|
-
recovery. It disables SDK auto-retry (the durable worker owns retries),
|
|
78
|
-
spaces request starts at 1,100 ms per class (reads and writes separately),
|
|
79
|
-
and trims a serialized batch over ~2 MB to an order-preserving prefix with
|
|
80
|
-
`hasMore`. Transport outcomes are classified by the shared
|
|
81
|
-
`classifyTransportOutcome` boundary: pre-mutation 4xx rejections are explicit
|
|
82
|
-
remote failures; timeouts, network errors, 408/429/5xx, and malformed 2xx
|
|
83
|
-
replies stay delivery-uncertain and are probed like any provider response loss.
|
|
84
|
-
This mode is not a performance claim; the raw-transport experiments under
|
|
85
|
-
`scripts/bench/` measure the unguarded API path without receipts or
|
|
86
|
-
compare-and-set.
|
|
87
|
-
|
|
88
|
-
## Inbound User_Input flow
|
|
89
|
-
|
|
90
|
-
The internal service polls registered `User_Input` projections on a bounded
|
|
91
|
-
interval. By default each pass runs an adaptive values-only preflight: it reads
|
|
92
|
-
the cheap value surface, compares it with canonical SQLite state, and only
|
|
93
|
-
escalates a table to the metadata-preserving snapshot read when the preflight
|
|
94
|
-
cannot certify it as unchanged:
|
|
95
|
-
|
|
96
|
-
```text
|
|
97
|
-
User_Input polling
|
|
98
|
-
├─ values-only preflight over registered projections
|
|
99
|
-
├─ escalate changed/ambiguous/schema tables to full metadata
|
|
100
|
-
├─ normalize literal/blank cells and retain formula/merge/error metadata
|
|
101
|
-
├─ resolve business-key row binding
|
|
102
|
-
├─ validate ownership and field revisions
|
|
103
|
-
├─ classify accepted, conflict, stale, or quarantine
|
|
104
|
-
├─ record Sync_Conflicts evidence
|
|
105
|
-
├─ CAS-resolve conflicts in favor of canonical SQLite state
|
|
106
|
-
└─ persist accepted observation and entity mutation in SQLite
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
The preflight never accepts or persists edits. It escalates a table whenever a
|
|
110
|
-
row changed, a business key is unknown or duplicated, an expected active entity
|
|
111
|
-
is missing from the projection, or any cell fails its literal/blank type check,
|
|
112
|
-
so the existing quarantine and conflict rules stay authoritative. Because the
|
|
113
|
-
values-only read drops formula, merged, and error metadata, the bootstrap also
|
|
114
|
-
forces a periodic metadata-preserving safety full scan (default one minute,
|
|
115
|
-
configurable) so invalid cells are rechecked and quarantined rather than
|
|
116
|
-
mistaken for literal user edits. The coordinator records any overdue safety-scan
|
|
117
|
-
lag, including a scan delayed by writer-lease contention, through internal
|
|
118
|
-
polling telemetry without changing the SQLite write boundary.
|
|
119
|
-
|
|
120
|
-
Accepted observation writes update canonical state and the application entity in
|
|
121
|
-
the same SQLite transaction. They do not enqueue a duplicate User_Input effect;
|
|
122
|
-
only the required system projection repair/materialization is considered.
|
|
123
|
-
Conflicts, stale writes, duplicate keys, and malformed cells remain visible in
|
|
124
|
-
SQLite evidence tables. A conflict is not left open indefinitely: the internal
|
|
125
|
-
resolver submits a fenced `acknowledge_system` command using the current
|
|
126
|
-
canonical revision, active candidate hash, and candidate epoch. When it applies,
|
|
127
|
-
it clears the candidate pointer and appends both the guarded canonical
|
|
128
|
-
User_Input rewrite and the `Sync_Conflicts` audit effect in the same SQLite
|
|
129
|
-
transaction. If the CAS is stale, the newer candidate remains authoritative for
|
|
130
|
-
the next observation pass rather than being overwritten.
|
|
131
|
-
|
|
132
|
-
## Provisioning and provider boundary
|
|
133
|
-
|
|
134
|
-
Projection provisioning is an internal service-start operation. The bootstrap
|
|
135
|
-
generates route registrations and headers for the required System_State,
|
|
136
|
-
User_Input, and Sync_Conflicts projections, verifies remote schema drift, and
|
|
137
|
-
starts workers only after provisioning and unresolved-conflict backfill succeed.
|
|
138
|
-
The full direct provider provisions the tabs itself — creating missing tabs,
|
|
139
|
-
initializing truly-empty tabs, and failing closed on header drift — with no
|
|
140
|
-
Apps Script involved. The legacy Apps Script gateway mode was removed; the
|
|
141
|
-
service-account `googleSheetsApi` provider is the only sync path.
|
|
142
|
-
|
|
143
|
-
Sheet consistency is not provided by cross-request Sheet transactions; Google
|
|
144
|
-
Sheets offers no serializable isolation between separate API calls. It comes
|
|
145
|
-
from the hidden effect-receipt tab (each effect id and payload hash is
|
|
146
|
-
recorded with its visible evidence), effect-id/payload-hash dedupe (a
|
|
147
|
-
replayed or concurrent effect is recognized as already applied and never
|
|
148
|
-
double-materialized), the SQLite durable outbox (effects survive restarts and
|
|
149
|
-
are delivered at-least-once), fencing (spreadsheet authority epoch/token and
|
|
150
|
-
worker/effect leases reject stale writers), and postcondition recovery
|
|
151
|
-
(uncertain deliveries are probed until receipt-backed visible evidence
|
|
152
|
-
matches).
|
|
153
|
-
|
|
154
|
-
Applications do not import or call the provider client, protocol, operation
|
|
155
|
-
builders, polling functions, provisioning interfaces, or the direct Sheets
|
|
156
|
-
API provider.
|
|
157
|
-
|
|
158
|
-
In direct mode the same spreadsheet must be reachable by the service account
|
|
159
|
-
(the full provider owns provisioning, outbound writes, and observation): one
|
|
160
|
-
SQLite runtime is the single authoritative writer for the whole spreadsheet.
|
|
161
|
-
The tracked live scenario
|
|
162
|
-
(`scripts/ci/run-api-scenario.mjs --backend live --outbound direct`) verifies
|
|
163
|
-
provisioning from an empty spreadsheet, append/update/delete delivery, mapped
|
|
164
|
-
User_Input polling, stale-edit CAS guards, anchor evidence, and cleanup
|
|
165
|
-
without any Apps Script deployment.
|
|
166
|
-
|
|
167
|
-
## Failure model
|
|
168
|
-
|
|
169
|
-
An HTTP timeout, non-JSON response, 404, or lost connection does not prove that
|
|
170
|
-
a Sheet write failed. The worker persists `delivery_uncertain` with a durable
|
|
171
|
-
probe schedule and dispatch identity, then promotes the effect only after
|
|
172
|
-
receipt-backed visible evidence is read. An explicit structured remote failure
|
|
173
|
-
uses the terminal failure path. SQLite also records a per-spreadsheet authority
|
|
174
|
-
epoch/token; provider mutations carrying an older token are rejected. A pass that only
|
|
175
|
-
requeued uncertain work (a response-loss or postcondition-unapplied loop) backs
|
|
176
|
-
off with a bounded, jittered delay that resets as soon as forward progress
|
|
177
|
-
resumes; lease expiry and recovery keep effects live during the backoff. The
|
|
178
|
-
SQLite commit is the application success boundary; remote delivery is
|
|
179
|
-
at-least-once and asynchronous.
|