@uzuhq/code-sdk 0.3.8
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/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/dev-globals.d.ts +28 -0
- package/dist/dev-globals.js +16 -0
- package/dist/dev-hooks.d.ts +233 -0
- package/dist/dev-hooks.js +130 -0
- package/dist/dev-hooks.test.d.ts +1 -0
- package/dist/dev-hooks.test.js +294 -0
- package/dist/dev-state-patch.d.ts +81 -0
- package/dist/dev-state-patch.js +295 -0
- package/dist/dev-state-patch.test.d.ts +1 -0
- package/dist/dev-state-patch.test.js +333 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +440 -0
- package/dist/json-patch.d.ts +7 -0
- package/dist/json-patch.js +78 -0
- package/dist/random.d.ts +11 -0
- package/dist/random.js +34 -0
- package/dist/reconnectable-ws.d.ts +60 -0
- package/dist/reconnectable-ws.js +229 -0
- package/dist/room.d.ts +23 -0
- package/dist/room.js +36 -0
- package/dist/run/local-server-action.d.ts +15 -0
- package/dist/run/local-server-action.js +97 -0
- package/dist/run/optimistic-action-client.d.ts +57 -0
- package/dist/run/optimistic-action-client.js +119 -0
- package/dist/run/server-action.d.ts +16 -0
- package/dist/run/server-action.js +170 -0
- package/dist/server-only.d.ts +26 -0
- package/dist/server-only.js +20 -0
- package/dist/sync/local.d.ts +8 -0
- package/dist/sync/local.js +51 -0
- package/dist/sync/online.d.ts +5 -0
- package/dist/sync/online.js +165 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.js +8 -0
- package/package.json +34 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docs
|
|
3
|
+
* - 外部 automation API: docs/docs/play_screen_v3/sdk-guide/dev-hooks.md
|
|
4
|
+
*
|
|
5
|
+
* `__uzu_dev.mergeRawState` / `__uzu_dev.patchRawState` の中身。
|
|
6
|
+
* scenario state を「object 階層の部分更新 (RFC 7396)」と「array 要素単体の書換
|
|
7
|
+
* 含む path-based ops (RFC 6902)」で書き換えるための utility。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* RFC 7396 風 Merge Patch。
|
|
11
|
+
*
|
|
12
|
+
* RFC 7396 strict との差分:
|
|
13
|
+
* - `null` は target 側の **削除ではなく** null をセットする (`timerEndsAt: null`
|
|
14
|
+
* のような nullable field を狙い撃ちするユースケースを優先)
|
|
15
|
+
* - 値を削除したいときは {@link JsonPatchOp} の `remove` を使う
|
|
16
|
+
*
|
|
17
|
+
* Generic は使い手側の type を残すための shape ヒントで、ランタイム上は
|
|
18
|
+
* `Record<string, unknown>` と等価。
|
|
19
|
+
*/
|
|
20
|
+
export type JsonMergePatch<S = unknown> = Partial<{
|
|
21
|
+
[K in keyof S]: S[K] extends unknown[] ? S[K] : S[K] extends object ? JsonMergePatch<S[K]> : S[K] | null;
|
|
22
|
+
}> & Record<string, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* RFC 6902 JSON Patch operation。
|
|
25
|
+
*
|
|
26
|
+
* - `add` / `replace` / `test`: `value` 必須
|
|
27
|
+
* - `remove`: `value` / `from` 不要
|
|
28
|
+
* - `move` / `copy`: `from` 必須 (JSON Pointer)
|
|
29
|
+
* - `path` は JSON Pointer (例: `/board/1/4`、root は空文字)
|
|
30
|
+
*/
|
|
31
|
+
export type JsonPatchOp = {
|
|
32
|
+
op: 'add';
|
|
33
|
+
path: string;
|
|
34
|
+
value: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
op: 'remove';
|
|
37
|
+
path: string;
|
|
38
|
+
} | {
|
|
39
|
+
op: 'replace';
|
|
40
|
+
path: string;
|
|
41
|
+
value: unknown;
|
|
42
|
+
} | {
|
|
43
|
+
op: 'move';
|
|
44
|
+
path: string;
|
|
45
|
+
from: string;
|
|
46
|
+
} | {
|
|
47
|
+
op: 'copy';
|
|
48
|
+
path: string;
|
|
49
|
+
from: string;
|
|
50
|
+
} | {
|
|
51
|
+
op: 'test';
|
|
52
|
+
path: string;
|
|
53
|
+
value: unknown;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* RFC 7396 (Merge Patch) を target に in-place 適用する。
|
|
57
|
+
*
|
|
58
|
+
* 規則:
|
|
59
|
+
* - `undefined` は no-op (key 自体を patch から落としたのと同じ)
|
|
60
|
+
* - `null` は **null をセット** (RFC 7396 strict と異なる、deletion はしない)
|
|
61
|
+
* - patch が object かつ target も object → 再帰 merge
|
|
62
|
+
* - patch が array → atomic replace (要素単位 merge なし、RFC 7396 spec 通り)
|
|
63
|
+
* - patch が primitive → overwrite
|
|
64
|
+
* - **target が array で patch が non-array object** → throw (silent な
|
|
65
|
+
* `[..., ...]` → `{ "0": ..., "1": ... }` 化を防ぐ)
|
|
66
|
+
*
|
|
67
|
+
* 型 mismatch (例: object → primitive) は overwrite で許可する。
|
|
68
|
+
*/
|
|
69
|
+
export declare function applyJsonMergePatch(target: Record<string, unknown>, patch: JsonMergePatch): void;
|
|
70
|
+
/**
|
|
71
|
+
* RFC 6902 (JSON Patch) operations を target に in-place 適用する。
|
|
72
|
+
*
|
|
73
|
+
* 1 op でも失敗したら **その時点で throw** する (RFC 6902 spec: operations は
|
|
74
|
+
* sequentially evaluate、失敗時の rollback は規定なし)。caller が atomic に
|
|
75
|
+
* したいなら state を事前に snapshot しておく。
|
|
76
|
+
*
|
|
77
|
+
* root pointer (`''`) は本実装では `add` / `replace` のみ許可し、
|
|
78
|
+
* doc のフィールドを丸ごと差し替える形で動く (= 結局 `setRawState` で十分な
|
|
79
|
+
* ケースなので、わざわざ `patchRawState` で使うことはほぼない)。
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyJsonPatch(target: Record<string, unknown>, ops: JsonPatchOp[]): void;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docs
|
|
3
|
+
* - 外部 automation API: docs/docs/play_screen_v3/sdk-guide/dev-hooks.md
|
|
4
|
+
*
|
|
5
|
+
* `__uzu_dev.mergeRawState` / `__uzu_dev.patchRawState` の中身。
|
|
6
|
+
* scenario state を「object 階層の部分更新 (RFC 7396)」と「array 要素単体の書換
|
|
7
|
+
* 含む path-based ops (RFC 6902)」で書き換えるための utility。
|
|
8
|
+
*/
|
|
9
|
+
// ─── RFC 7396 Merge Patch ────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* RFC 7396 (Merge Patch) を target に in-place 適用する。
|
|
12
|
+
*
|
|
13
|
+
* 規則:
|
|
14
|
+
* - `undefined` は no-op (key 自体を patch から落としたのと同じ)
|
|
15
|
+
* - `null` は **null をセット** (RFC 7396 strict と異なる、deletion はしない)
|
|
16
|
+
* - patch が object かつ target も object → 再帰 merge
|
|
17
|
+
* - patch が array → atomic replace (要素単位 merge なし、RFC 7396 spec 通り)
|
|
18
|
+
* - patch が primitive → overwrite
|
|
19
|
+
* - **target が array で patch が non-array object** → throw (silent な
|
|
20
|
+
* `[..., ...]` → `{ "0": ..., "1": ... }` 化を防ぐ)
|
|
21
|
+
*
|
|
22
|
+
* 型 mismatch (例: object → primitive) は overwrite で許可する。
|
|
23
|
+
*/
|
|
24
|
+
export function applyJsonMergePatch(target, patch) {
|
|
25
|
+
if (Array.isArray(target)) {
|
|
26
|
+
// 入口でガード済みのはずだが、再帰内で array 階層に踏み込むこともあるので
|
|
27
|
+
// defense in depth で再度ガード。
|
|
28
|
+
throw new TypeError('[uzu_dev] mergeRawState: cannot deep-merge into an array field. ' +
|
|
29
|
+
'Use patchRawState ({ op: "replace", path: "/.../<index>" }) for element-level writes, ' +
|
|
30
|
+
'or pass a full array to atomic-replace it.');
|
|
31
|
+
}
|
|
32
|
+
for (const key of Object.keys(patch)) {
|
|
33
|
+
const value = patch[key];
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
if (value === null) {
|
|
37
|
+
target[key] = null;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const current = target[key];
|
|
41
|
+
if (typeof value === 'object' &&
|
|
42
|
+
!Array.isArray(value) &&
|
|
43
|
+
current !== null &&
|
|
44
|
+
typeof current === 'object') {
|
|
45
|
+
if (Array.isArray(current)) {
|
|
46
|
+
// 既存値が array なのに patch 側は plain object → 明示的に止める
|
|
47
|
+
// (silent に array が dict 化する破壊を防ぐ)。
|
|
48
|
+
throw new TypeError(`[uzu_dev] mergeRawState: refusing to merge a plain object into array field "${key}". ` +
|
|
49
|
+
`Use patchRawState ({ op: "replace", path: "/${key}/<index>" }) for element-level writes, ` +
|
|
50
|
+
`or pass a full array to atomic-replace it.`);
|
|
51
|
+
}
|
|
52
|
+
applyJsonMergePatch(current, value);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
target[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ─── RFC 6902 JSON Patch ─────────────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* JSON Pointer (RFC 6901) を tokens に分解する。
|
|
62
|
+
* `/board/1/~04` → `['board', '1', '~4']`
|
|
63
|
+
*
|
|
64
|
+
* RFC 6901 のエスケープ:
|
|
65
|
+
* - `~1` → `/`
|
|
66
|
+
* - `~0` → `~`
|
|
67
|
+
*
|
|
68
|
+
* 復号順は ~1 → ~0。逆順で解くと `~01` が `/` に化けるので注意。
|
|
69
|
+
*/
|
|
70
|
+
function parseJsonPointer(pointer) {
|
|
71
|
+
if (pointer === '')
|
|
72
|
+
return [];
|
|
73
|
+
if (!pointer.startsWith('/')) {
|
|
74
|
+
throw new TypeError(`[uzu_dev] invalid JSON Pointer: ${pointer} (must start with "/")`);
|
|
75
|
+
}
|
|
76
|
+
return pointer
|
|
77
|
+
.slice(1)
|
|
78
|
+
.split('/')
|
|
79
|
+
.map((token) => token.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* JSON Pointer の **親 container** と **末端 key** に解決する。
|
|
83
|
+
* root pointer (`''`) は対応外: 呼び出し側で別途扱う。
|
|
84
|
+
*/
|
|
85
|
+
function resolveParent(doc, tokens) {
|
|
86
|
+
if (tokens.length === 0) {
|
|
87
|
+
throw new TypeError('[uzu_dev] root pointer ("") is not addressable by this helper');
|
|
88
|
+
}
|
|
89
|
+
let current = doc;
|
|
90
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
91
|
+
const token = tokens[i];
|
|
92
|
+
if (current === null || typeof current !== 'object') {
|
|
93
|
+
throw new TypeError(`[uzu_dev] JSON Pointer path "${tokens.slice(0, i + 1).join('/')}" goes through a non-container`);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(current)) {
|
|
96
|
+
const idx = parseArrayIndex(token, current.length, /* allowAppend */ false);
|
|
97
|
+
current = current[idx];
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
current = current[token];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (current === null || typeof current !== 'object') {
|
|
104
|
+
throw new TypeError(`[uzu_dev] JSON Pointer parent "${tokens.slice(0, -1).join('/')}" is not a container`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
parent: current,
|
|
108
|
+
key: tokens[tokens.length - 1],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* JSON Pointer から値を取得する。root pointer は doc 自体。
|
|
113
|
+
* 存在しない path は throw する。
|
|
114
|
+
*/
|
|
115
|
+
function getByPointer(doc, pointer) {
|
|
116
|
+
const tokens = parseJsonPointer(pointer);
|
|
117
|
+
let current = doc;
|
|
118
|
+
for (const token of tokens) {
|
|
119
|
+
if (current === null || typeof current !== 'object') {
|
|
120
|
+
throw new TypeError(`[uzu_dev] JSON Pointer path "${pointer}" goes through a non-container`);
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(current)) {
|
|
123
|
+
const idx = parseArrayIndex(token, current.length, /* allowAppend */ false);
|
|
124
|
+
current = current[idx];
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const obj = current;
|
|
128
|
+
if (!(token in obj)) {
|
|
129
|
+
throw new TypeError(`[uzu_dev] JSON Pointer path "${pointer}" does not exist`);
|
|
130
|
+
}
|
|
131
|
+
current = obj[token];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return current;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Array index token を numeric index に解決する。
|
|
138
|
+
* `'-'` は append (RFC 6902 仕様、`add` 操作のみ許可) で `length` を返す。
|
|
139
|
+
*/
|
|
140
|
+
function parseArrayIndex(token, length, allowAppend) {
|
|
141
|
+
if (token === '-') {
|
|
142
|
+
if (!allowAppend) {
|
|
143
|
+
throw new TypeError('[uzu_dev] array append ("-") is only valid for `add` operations');
|
|
144
|
+
}
|
|
145
|
+
return length;
|
|
146
|
+
}
|
|
147
|
+
// RFC 6901: array index は ASCII 数字のみ。先頭 0 は禁止 (ただし "0" 単独は OK)。
|
|
148
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(token)) {
|
|
149
|
+
throw new TypeError(`[uzu_dev] invalid array index "${token}"`);
|
|
150
|
+
}
|
|
151
|
+
const idx = Number(token);
|
|
152
|
+
if (idx < 0 || idx > length) {
|
|
153
|
+
throw new RangeError(`[uzu_dev] array index ${idx} out of range (length=${length})`);
|
|
154
|
+
}
|
|
155
|
+
return idx;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 値の深い等価判定 (RFC 6902 `test` 用)。`JSON.stringify` ベース。
|
|
159
|
+
* `undefined` を含む値や key 順序差まで真面目に見るほどには使わないので十分。
|
|
160
|
+
*/
|
|
161
|
+
function deepEqual(a, b) {
|
|
162
|
+
if (a === b)
|
|
163
|
+
return true;
|
|
164
|
+
try {
|
|
165
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* RFC 6902 (JSON Patch) operations を target に in-place 適用する。
|
|
173
|
+
*
|
|
174
|
+
* 1 op でも失敗したら **その時点で throw** する (RFC 6902 spec: operations は
|
|
175
|
+
* sequentially evaluate、失敗時の rollback は規定なし)。caller が atomic に
|
|
176
|
+
* したいなら state を事前に snapshot しておく。
|
|
177
|
+
*
|
|
178
|
+
* root pointer (`''`) は本実装では `add` / `replace` のみ許可し、
|
|
179
|
+
* doc のフィールドを丸ごと差し替える形で動く (= 結局 `setRawState` で十分な
|
|
180
|
+
* ケースなので、わざわざ `patchRawState` で使うことはほぼない)。
|
|
181
|
+
*/
|
|
182
|
+
export function applyJsonPatch(target, ops) {
|
|
183
|
+
for (const op of ops) {
|
|
184
|
+
applyOne(target, op);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function applyOne(doc, op) {
|
|
188
|
+
switch (op.op) {
|
|
189
|
+
case 'add': {
|
|
190
|
+
const tokens = parseJsonPointer(op.path);
|
|
191
|
+
if (tokens.length === 0) {
|
|
192
|
+
// root への add は doc 全体の差し替え。doc は in-place mutate しか
|
|
193
|
+
// できないので、ここでは現実的に全 key を入れ替える形で実現する。
|
|
194
|
+
replaceRoot(doc, op.value);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const { parent, key } = resolveParent(doc, tokens);
|
|
198
|
+
if (Array.isArray(parent)) {
|
|
199
|
+
const idx = parseArrayIndex(key, parent.length, /* allowAppend */ true);
|
|
200
|
+
parent.splice(idx, 0, op.value);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
parent[key] = op.value;
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
case 'remove': {
|
|
208
|
+
const tokens = parseJsonPointer(op.path);
|
|
209
|
+
if (tokens.length === 0) {
|
|
210
|
+
throw new TypeError('[uzu_dev] cannot remove the root document');
|
|
211
|
+
}
|
|
212
|
+
const { parent, key } = resolveParent(doc, tokens);
|
|
213
|
+
if (Array.isArray(parent)) {
|
|
214
|
+
const idx = parseArrayIndex(key, parent.length - 1, /* allowAppend */ false);
|
|
215
|
+
parent.splice(idx, 1);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
if (!(key in parent)) {
|
|
219
|
+
throw new TypeError(`[uzu_dev] cannot remove non-existent path "${op.path}"`);
|
|
220
|
+
}
|
|
221
|
+
delete parent[key];
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
case 'replace': {
|
|
226
|
+
const tokens = parseJsonPointer(op.path);
|
|
227
|
+
if (tokens.length === 0) {
|
|
228
|
+
replaceRoot(doc, op.value);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const { parent, key } = resolveParent(doc, tokens);
|
|
232
|
+
if (Array.isArray(parent)) {
|
|
233
|
+
const idx = parseArrayIndex(key, parent.length - 1, /* allowAppend */ false);
|
|
234
|
+
parent[idx] = op.value;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
if (!(key in parent)) {
|
|
238
|
+
throw new TypeError(`[uzu_dev] cannot replace non-existent path "${op.path}"`);
|
|
239
|
+
}
|
|
240
|
+
parent[key] = op.value;
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
case 'move': {
|
|
245
|
+
if (op.from === op.path)
|
|
246
|
+
return;
|
|
247
|
+
// RFC 6902: from が path の真の prefix だと不正 (循環移動)
|
|
248
|
+
if (isProperPrefixPointer(op.from, op.path)) {
|
|
249
|
+
throw new TypeError(`[uzu_dev] cannot move into own descendant: from="${op.from}" path="${op.path}"`);
|
|
250
|
+
}
|
|
251
|
+
const value = getByPointer(doc, op.from);
|
|
252
|
+
applyOne(doc, { op: 'remove', path: op.from });
|
|
253
|
+
applyOne(doc, { op: 'add', path: op.path, value });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case 'copy': {
|
|
257
|
+
const value = getByPointer(doc, op.from);
|
|
258
|
+
// RFC 6902: copy は値を deep clone することを要求する。共有参照が後段の
|
|
259
|
+
// remove/replace で破壊されるのを防ぐため `structuredClone` を使う。
|
|
260
|
+
const cloned = structuredClone(value);
|
|
261
|
+
applyOne(doc, { op: 'add', path: op.path, value: cloned });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case 'test': {
|
|
265
|
+
const value = getByPointer(doc, op.path);
|
|
266
|
+
if (!deepEqual(value, op.value)) {
|
|
267
|
+
throw new Error(`[uzu_dev] test failed at "${op.path}"`);
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
default: {
|
|
272
|
+
const exhaustive = op;
|
|
273
|
+
throw new TypeError(`[uzu_dev] unknown JSON Patch op: ${JSON.stringify(exhaustive)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function isProperPrefixPointer(prefix, path) {
|
|
278
|
+
if (prefix === '')
|
|
279
|
+
return path !== '';
|
|
280
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* root pointer (`''`) への add / replace 用。doc を in-place で空にして
|
|
284
|
+
* value の中身を流し込む。value が object でない場合は Record の形に
|
|
285
|
+
* 入らないので throw する (root を primitive / array に置換はサポート対象外、
|
|
286
|
+
* setRawState を使うべき)。
|
|
287
|
+
*/
|
|
288
|
+
function replaceRoot(doc, value) {
|
|
289
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
290
|
+
throw new TypeError('[uzu_dev] root replace requires a plain object value; use setRawState for full state replacement');
|
|
291
|
+
}
|
|
292
|
+
for (const key of Object.keys(doc))
|
|
293
|
+
delete doc[key];
|
|
294
|
+
Object.assign(doc, value);
|
|
295
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|