snapback2 0.0.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.
- package/README.md +127 -0
- package/bin/snapback2.mjs +36 -0
- package/dist/api.d.ts +20 -0
- package/dist/assets.d.ts +34 -0
- package/dist/client-assets.d.ts +26 -0
- package/dist/client-offline.d.ts +94 -0
- package/dist/client-outbox.d.ts +58 -0
- package/dist/client-upload.d.ts +41 -0
- package/dist/client-wire.d.ts +41 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.mjs +5 -0
- package/dist/client.mjs.map +7 -0
- package/dist/compiler-core.mjs +60 -0
- package/dist/compiler-core.mjs.map +7 -0
- package/dist/compiler-lib.d.ts +71 -0
- package/dist/compiler.mjs +5 -0
- package/dist/compiler.mjs.map +7 -0
- package/dist/drain.d.ts +33 -0
- package/dist/expo/files.d.ts +3 -0
- package/dist/expo/index.d.ts +16 -0
- package/dist/expo/token-store.d.ts +7 -0
- package/dist/expo/witness.d.ts +28 -0
- package/dist/expo/witness.mjs +2 -0
- package/dist/expo/witness.mjs.map +7 -0
- package/dist/expo.d.ts +4 -0
- package/dist/expo.mjs +9 -0
- package/dist/expo.mjs.map +7 -0
- package/dist/guide/auth.md +89 -0
- package/dist/guide/effects.md +52 -0
- package/dist/guide/families.json +446 -0
- package/dist/guide/grammar.md +52 -0
- package/dist/guide/live-query.md +27 -0
- package/dist/guide/offline.md +75 -0
- package/dist/guide/personas.md +15 -0
- package/dist/guide/quarry.md +297 -0
- package/dist/guide/testing.md +57 -0
- package/dist/index.d.ts +362 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +7 -0
- package/dist/offline-protocol.d.ts +180 -0
- package/dist/offline-schema.d.ts +1 -0
- package/dist/offline.d.ts +177 -0
- package/dist/react-core.d.ts +91 -0
- package/dist/react-core.mjs +2 -0
- package/dist/react-core.mjs.map +7 -0
- package/dist/react-native/components.d.ts +23 -0
- package/dist/react-native/files.d.ts +43 -0
- package/dist/react-native/source.d.ts +16 -0
- package/dist/react-native.d.ts +4 -0
- package/dist/react-native.mjs +2 -0
- package/dist/react-native.mjs.map +7 -0
- package/dist/react.d.ts +19 -0
- package/dist/react.mjs +2 -0
- package/dist/react.mjs.map +7 -0
- package/dist/sqlite-test.mjs +1720 -0
- package/dist/sqlite-test.mjs.map +7 -0
- package/dist/sse.d.ts +44 -0
- package/dist/store/byte-cache.d.ts +162 -0
- package/dist/store/canonical.d.ts +2 -0
- package/dist/store/indexeddb.d.ts +23 -0
- package/dist/store/locks.d.ts +11 -0
- package/dist/store/outbox.d.ts +113 -0
- package/dist/store/overlay-retirement.d.ts +17 -0
- package/dist/store/projection.d.ts +11 -0
- package/dist/store/range-store.d.ts +388 -0
- package/dist/store/sqlite-driver.conformance.d.ts +7 -0
- package/dist/store/sqlite-driver.d.ts +16 -0
- package/dist/store/sqlite-expo.d.ts +3 -0
- package/dist/store/sqlite.d.ts +31 -0
- package/dist/templates/chat/expo/App.tsx +29 -0
- package/dist/templates/chat/expo/app.json +11 -0
- package/dist/templates/chat/expo/index.js +3 -0
- package/dist/templates/chat/expo/shared/log.js +92 -0
- package/dist/templates/chat/expo/shared/log.ts +130 -0
- package/dist/templates/chat/expo/shared/offline.js +8 -0
- package/dist/templates/chat/expo/shared/offline.ts +9 -0
- package/dist/templates/chat/expo/src/Chat.tsx +23 -0
- package/dist/templates/chat/expo/src/Composer.tsx +23 -0
- package/dist/templates/chat/expo/src/MediaView.tsx +29 -0
- package/dist/templates/chat/expo/src/Witness.tsx +131 -0
- package/dist/templates/chat/expo/src/screens/Inbox.tsx +9 -0
- package/dist/templates/chat/expo/src/screens/Search.tsx +10 -0
- package/dist/templates/chat/expo/src/screens/Thread.tsx +22 -0
- package/dist/templates/chat/expo/src/witness-state.ts +68 -0
- package/dist/templates/chat/expo/tsconfig.json +5 -0
- package/dist/templates/chat/react/index.html +5 -0
- package/dist/templates/chat/react/shared/log.js +92 -0
- package/dist/templates/chat/react/shared/log.ts +130 -0
- package/dist/templates/chat/react/shared/offline.js +8 -0
- package/dist/templates/chat/react/shared/offline.ts +9 -0
- package/dist/templates/chat/react/src/App.tsx +36 -0
- package/dist/templates/chat/react/src/Composer.tsx +45 -0
- package/dist/templates/chat/react/src/Inbox.tsx +24 -0
- package/dist/templates/chat/react/src/Search.tsx +26 -0
- package/dist/templates/chat/react/src/Thread.tsx +126 -0
- package/dist/templates/chat/react/src/env.d.ts +1 -0
- package/dist/templates/chat/react/src/main.tsx +20 -0
- package/dist/templates/chat/shared/log.js +92 -0
- package/dist/templates/chat/shared/log.ts +130 -0
- package/dist/templates/chat/shared/offline.js +8 -0
- package/dist/templates/chat/shared/offline.ts +9 -0
- package/dist/templates/chat/snapback/deliveries.q +33 -0
- package/dist/templates/chat/snapback/delivery.ts +30 -0
- package/dist/templates/chat/snapback/feeds.q +14 -0
- package/dist/templates/chat/snapback/follows.q +11 -0
- package/dist/templates/chat/snapback/groups.q +40 -0
- package/dist/templates/chat/snapback/messages.q +32 -0
- package/dist/templates/chat/snapback/notifications.q +7 -0
- package/dist/templates/chat/snapback/posts.q +4 -0
- package/dist/templates/chat/snapback/profiles.q +17 -0
- package/dist/templates/chat/snapback/schema.q +136 -0
- package/dist/templates/chat/snapback/seed.ts +45 -0
- package/dist/templates/chat/snapback/tests/chat.test.ts +759 -0
- package/dist/templates/react/index.html +5 -0
- package/dist/templates/react/src/App.tsx +5 -0
- package/dist/templates/react/src/main.tsx +18 -0
- package/dist/templates/todos/snapback/schema.q +11 -0
- package/dist/templates/todos/snapback/seed.ts +10 -0
- package/dist/templates/todos/snapback/tests/todos.test.ts +13 -0
- package/dist/templates/todos/snapback/todos.q +6 -0
- package/dist/test-runner.mjs +8541 -0
- package/dist/test-runner.mjs.map +7 -0
- package/dist/test.d.ts +28 -0
- package/dist/test.mjs +8541 -0
- package/dist/test.mjs.map +7 -0
- package/dist/token-store.d.ts +3 -0
- package/dist/twin-hydrate.d.ts +43 -0
- package/dist/twin.d.ts +70 -0
- package/dist/types.d.ts +410 -0
- package/dist/witness-test.mjs +2 -0
- package/dist/witness-test.mjs.map +7 -0
- package/package.json +117 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"family": "bound",
|
|
4
|
+
"code": "E_BOUND_TAKE_REQUIRED",
|
|
5
|
+
"wrong": "db.scan(todos.byOwner, [ctx.viewer])",
|
|
6
|
+
"right": "db.scan(todos.byOwner, [ctx.viewer], { take: 50 })"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"family": "opaque",
|
|
10
|
+
"code": "E_PROVENANCE_AMBIGUOUS",
|
|
11
|
+
"wrong": "let key = args.id; key = args.otherId; db.scan(items.byKey, [key], { take: 50 })",
|
|
12
|
+
"right": "db.scan(items.byKey, [args.id], { take: 50 })"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"family": "schema",
|
|
16
|
+
"code": "E_SCHEMA_TABLE_UNKNOWN",
|
|
17
|
+
"wrong": "db.get(missing, args.id)",
|
|
18
|
+
"right": "db.get(todos, args.id)"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"family": "rule",
|
|
22
|
+
"code": "E_RULE",
|
|
23
|
+
"wrong": "insert: rule.deny()",
|
|
24
|
+
"right": "insert: rule.viewerIs(\"ownerId\")"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"family": "auth",
|
|
28
|
+
"code": "E_AUTH_REQUIRED",
|
|
29
|
+
"wrong": "client.query(api.todos.list, {})",
|
|
30
|
+
"right": "createClient({ url, as: \"alice\" }).query(api.todos.list, {})"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"family": "args",
|
|
34
|
+
"code": "E_ARGS_TYPE",
|
|
35
|
+
"wrong": "client.query(api.messages.thread, { conversationId: 1 })",
|
|
36
|
+
"right": "client.query(api.messages.thread, { conversationId: \"conversation-1\", c: null })"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"family": "op",
|
|
40
|
+
"code": "E_OP_UNKNOWN",
|
|
41
|
+
"wrong": "client.query(api.messages.threads, {})",
|
|
42
|
+
"right": "client.query(api.messages.thread, { conversationId: \"conversation-1\", c: null })"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"family": "successor",
|
|
46
|
+
"code": "E_SUCCESSOR_DESTRUCTIVE",
|
|
47
|
+
"wrong": "remove a populated column and save",
|
|
48
|
+
"right": "apply the emitted .snapback/migration-<generation>.json plan"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"family": "effect",
|
|
52
|
+
"code": "E_EFFECT_GRANT",
|
|
53
|
+
"wrong": "grants: [] // then fetch an external origin",
|
|
54
|
+
"right": "grants: [\"https://api.example.com\"]"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"family": "input",
|
|
58
|
+
"code": "E_INPUT_TRACE_UNKNOWN",
|
|
59
|
+
"wrong": "snapback2 trace 999999",
|
|
60
|
+
"right": "snapback2 trace latest"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"family": "owner",
|
|
64
|
+
"code": "E_OWNER_NOT_RUNNING",
|
|
65
|
+
"wrong": "snapback2 why todos.list --as alice",
|
|
66
|
+
"right": "snapback2 dev && snapback2 why todos.list --as alice"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"family": "bound",
|
|
70
|
+
"code": "E_UNBOUND_SCAN",
|
|
71
|
+
"wrong": "db.scan(todos.byOwner, [ctx.viewer])",
|
|
72
|
+
"right": "db.scan(todos.byOwner, [ctx.viewer], { take: 50 })"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"family": "bound",
|
|
76
|
+
"code": "E_UNBOUND_LOOP",
|
|
77
|
+
"wrong": "for (const item of unboundedItems) visit(item)",
|
|
78
|
+
"right": "for (const item of args.items) visit(item) // args.items declares max"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"family": "bound",
|
|
82
|
+
"code": "E_UNBOUND_COUNT",
|
|
83
|
+
"wrong": "db.scan(todos.byOwner, [ctx.viewer], { take: 50 }).count()",
|
|
84
|
+
"right": "db.scan(todos.byOwner, [ctx.viewer], { take: 50 }).countCapped()"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"family": "bound",
|
|
88
|
+
"code": "E_STRUCTURAL_COUNT",
|
|
89
|
+
"wrong": "return hiddenRows.countCapped()",
|
|
90
|
+
"right": "return maintainedVisibleCount"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"family": "opaque",
|
|
94
|
+
"code": "E_AMBIENT_TIME",
|
|
95
|
+
"wrong": "const now = Date.now()",
|
|
96
|
+
"right": "const now = ctx.now()"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"family": "opaque",
|
|
100
|
+
"code": "E_OPAQUE",
|
|
101
|
+
"wrong": "return unknownLibraryCall(args.id)",
|
|
102
|
+
"right": "return db.get(todos, args.id)"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"family": "bound",
|
|
106
|
+
"code": "E_BOUND",
|
|
107
|
+
"wrong": "return a program whose certified work exceeds the ceiling",
|
|
108
|
+
"right": "reduce take/max values until the program fits the certified ceiling"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"family": "args",
|
|
112
|
+
"code": "E_ARGS_SPREAD",
|
|
113
|
+
"wrong": "query({ ...makeArgs() }, handler)",
|
|
114
|
+
"right": "query({ id: t.id() }, handler)"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"family": "args",
|
|
118
|
+
"code": "E_ARGS_DESCRIPTOR",
|
|
119
|
+
"wrong": "query({ id: String }, handler)",
|
|
120
|
+
"right": "query({ id: t.id() }, handler)"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"family": "op",
|
|
124
|
+
"code": "E_OP_INVALID",
|
|
125
|
+
"wrong": "export const broken = query({}, () => missingName)",
|
|
126
|
+
"right": "export const valid = query({}, () => null)"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"family": "op",
|
|
130
|
+
"code": "E_OP_NONE",
|
|
131
|
+
"wrong": "const helper = () => null",
|
|
132
|
+
"right": "export const operation = query({}, () => null)"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"family": "op",
|
|
136
|
+
"code": "E_OP_RESERVED",
|
|
137
|
+
"wrong": "export const then = query({}, () => null)",
|
|
138
|
+
"right": "export const next = query({}, () => null)"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"family": "op",
|
|
142
|
+
"code": "E_OP_MODULE_BASENAME",
|
|
143
|
+
"wrong": "snapback/activity.ts and snapback/admin/activity.ts",
|
|
144
|
+
"right": "snapback/activity.ts and snapback/admin-activity.ts"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"family": "opaque",
|
|
148
|
+
"code": "E_DIAG_TRUNCATED",
|
|
149
|
+
"wrong": "leave more refusals than one check can report",
|
|
150
|
+
"right": "fix the reported refusals and run snapback2 check again"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"family": "schema",
|
|
154
|
+
"code": "E_SCHEMA_RESERVED",
|
|
155
|
+
"wrong": "export const _meta = table({ columns: {} })",
|
|
156
|
+
"right": "export const metadata = table({ columns: {} })"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"family": "schema",
|
|
160
|
+
"code": "E_SCHEMA_ID_RESERVED",
|
|
161
|
+
"wrong": "table({ columns: { id: t.id() } })",
|
|
162
|
+
"right": "table({ columns: {} }) // id is implicit"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"family": "schema",
|
|
166
|
+
"code": "E_SCHEMA_UNKNOWN_TABLE",
|
|
167
|
+
"wrong": "schema({ tables: { missing } })",
|
|
168
|
+
"right": "schema({ tables: { todos } })"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"family": "schema",
|
|
172
|
+
"code": "E_SCHEMA_INDEX_COLUMN",
|
|
173
|
+
"wrong": "indexes: { byMissing: { columns: [\"missing\"] } }",
|
|
174
|
+
"right": "indexes: { byTitle: { columns: [\"title\"] } }"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"family": "schema",
|
|
178
|
+
"code": "E_SCHEMA_INDEX_OPTION",
|
|
179
|
+
"wrong": "indexes: { byTitle: { columns: [\"title\"], unknown: true } }",
|
|
180
|
+
"right": "indexes: { byTitle: { columns: [\"title\"], unique: true } }"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"family": "schema",
|
|
184
|
+
"code": "E_SCHEMA_CONSTRAINT",
|
|
185
|
+
"wrong": "constraints: [{ unique: \"missing\" }]",
|
|
186
|
+
"right": "constraints: [{ unique: \"byTitle\" }]"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"family": "schema",
|
|
190
|
+
"code": "E_SCHEMA_MAINTAIN",
|
|
191
|
+
"wrong": "maintains: [{ count: \"missing\", by: [\"ownerId\"], into: \"counts\" }]",
|
|
192
|
+
"right": "maintains: [{ count: \"todos\", by: [\"ownerId\"], into: \"counts\" }]"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"family": "schema",
|
|
196
|
+
"code": "E_SCHEMA_ASSET_MAINTAIN",
|
|
197
|
+
"wrong": "maintains: [{ count: \"posts\", by: [\"photo\"], into: \"counts\" }]",
|
|
198
|
+
"right": "maintains: [{ count: \"posts\", by: [\"ownerId\"], into: \"counts\" }]"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"family": "schema",
|
|
202
|
+
"code": "E_SCHEMA_ASSET_SEED",
|
|
203
|
+
"wrong": "rows: { profiles: [{ id: \"profile-alice\", avatar: { id: \"asset-1\", width: 100, height: 100 } }] }",
|
|
204
|
+
"right": "rows: { profiles: [{ id: \"profile-alice\", avatar: null }] }"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"family": "schema",
|
|
208
|
+
"code": "E_SCHEMA_DEFAULT_EXPORT",
|
|
209
|
+
"wrong": "export const app = schema({ tables: {} })",
|
|
210
|
+
"right": "export default schema({ tables: {} })"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"family": "schema",
|
|
214
|
+
"code": "E_SCHEMA_COLUMN",
|
|
215
|
+
"wrong": "columns: { title: t.text() }",
|
|
216
|
+
"right": "columns: { title: t.text(100) }"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"family": "schema",
|
|
220
|
+
"code": "E_SCHEMA_INVALID",
|
|
221
|
+
"wrong": "export default schema(makeSchema())",
|
|
222
|
+
"right": "export default schema({ tables: { todos } })"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"family": "rule",
|
|
226
|
+
"code": "E_RULE_INVALID",
|
|
227
|
+
"wrong": "read: customRule()",
|
|
228
|
+
"right": "read: rule.viewerIs(\"ownerId\")"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"family": "args",
|
|
232
|
+
"code": "E_DECIMAL_SCALE",
|
|
233
|
+
"wrong": "```q\nmutation add():\n insert prices { amount: 1.234 }\n```",
|
|
234
|
+
"right": "```q\nmutation add():\n insert prices { amount: 1.23 }\n```"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"family": "args",
|
|
238
|
+
"code": "E_ENUM_VARIANT",
|
|
239
|
+
"wrong": "status: \"unknown\"",
|
|
240
|
+
"right": "status: \"open\""
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"family": "input",
|
|
244
|
+
"code": "E_SEED_INVALID",
|
|
245
|
+
"wrong": "export default makeSeed()",
|
|
246
|
+
"right": "export default seed({ personas: [], rows: {} })"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"family": "input",
|
|
250
|
+
"code": "E_SEED_PERSONA",
|
|
251
|
+
"wrong": "personas: [1]",
|
|
252
|
+
"right": "personas: [\"alice\"]"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"family": "input",
|
|
256
|
+
"code": "E_SEED_TABLE",
|
|
257
|
+
"wrong": "rows: { missing: [] }",
|
|
258
|
+
"right": "rows: { todos: [] }"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"family": "input",
|
|
262
|
+
"code": "E_SEED_ROW",
|
|
263
|
+
"wrong": "rows: { todos: [null] }",
|
|
264
|
+
"right": "rows: { todos: [{ id: \"todo-1\", title: \"Ship\" }] }"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"family": "input",
|
|
268
|
+
"code": "E_SEED_SHAPE",
|
|
269
|
+
"wrong": "rows: { todos: [{ id: \"todo-1\" }] }",
|
|
270
|
+
"right": "rows: { todos: [{ id: \"todo-1\", title: \"Ship\" }] }"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"family": "input",
|
|
274
|
+
"code": "E_SEED_VALUE",
|
|
275
|
+
"wrong": "rows: { todos: [{ id: \"todo-1\", title: 7 }] }",
|
|
276
|
+
"right": "rows: { todos: [{ id: \"todo-1\", title: \"Ship\" }] }"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"family": "effect",
|
|
280
|
+
"code": "E_EFFECT_CLOSURE",
|
|
281
|
+
"wrong": "const token = secret; effect({ run: () => token })",
|
|
282
|
+
"right": "effect({ run: (payload) => payload })"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"family": "effect",
|
|
286
|
+
"code": "E_EFFECT_TYPE",
|
|
287
|
+
"wrong": "effect({ grants: [], run: 1 })",
|
|
288
|
+
"right": "effect({ grants: [], run: async () => null })"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"family": "effect",
|
|
292
|
+
"code": "E_EFFECT_IMPORT",
|
|
293
|
+
"wrong": "import helper from \"unapproved-runtime\"",
|
|
294
|
+
"right": "use the payload and granted effect tools"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"family": "effect",
|
|
298
|
+
"code": "E_EFFECT_THEN_ARGS",
|
|
299
|
+
"wrong": "then: mutation({ id: t.id() }, handler)",
|
|
300
|
+
"right": "then: mutation({ outboxId: t.id(), effect: t.text(100), result: t.json(4096) }, handler)"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"family": "effect",
|
|
304
|
+
"code": "E_EFFECT_UNKNOWN",
|
|
305
|
+
"wrong": "emit(missingEffect, payload)",
|
|
306
|
+
"right": "emit(declaredEffect, payload)"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"family": "effect",
|
|
310
|
+
"code": "E_EMIT_IN_QUERY",
|
|
311
|
+
"wrong": "query({}, () => { emit(notify, null); return null })",
|
|
312
|
+
"right": "mutation({}, () => { emit(notify, null); return null })"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"family": "quarry",
|
|
316
|
+
"code": "E_QUARRY_SYNTAX",
|
|
317
|
+
"wrong": "```q\ntable items\n name text <=100\n```",
|
|
318
|
+
"right": "```q\ntable items:\n name: text <=100\n public\n```"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"family": "quarry",
|
|
322
|
+
"code": "E_QUARRY_UNKNOWN",
|
|
323
|
+
"wrong": "```q\nquery one(id: items):\n return missing[id]\n```",
|
|
324
|
+
"right": "```q\nquery one(id: items):\n return items[id]\n```"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"family": "quarry",
|
|
328
|
+
"code": "E_QUARRY_AMBIGUOUS",
|
|
329
|
+
"wrong": "```q\nquery recent(owner: principal):\n return posts[owner] first 20\n```",
|
|
330
|
+
"right": "```q\nquery recent(owner: principal):\n return posts[owner] first 20 by byAuthorTime\n```"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"family": "quarry",
|
|
334
|
+
"code": "E_QUARRY_UNSUPPORTED",
|
|
335
|
+
"wrong": "```q\nquery visible(author: principal):\n return posts[author] first 20 by byAuthorAt where .visible = true\n```",
|
|
336
|
+
"right": "```q\n-- schema.q: by byAuthorVisibleAt: author, visible, at, id\nquery visible(author: principal):\n return posts[author, true] first 20 by byAuthorVisibleAt\n```"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"family": "quarry",
|
|
340
|
+
"code": "E_QUARRY_PLACEMENT",
|
|
341
|
+
"wrong": "```q\n-- snapback/items.q\ntable items:\n name: text <=100\n```",
|
|
342
|
+
"right": "```q\n-- snapback/schema.q\ntable items:\n name: text <=100\n public\n```"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"family": "quarry",
|
|
346
|
+
"code": "E_QUARRY_TYPE",
|
|
347
|
+
"wrong": "```q\nquery present(x: time ?):\n if x:\n return true\n return false\n```",
|
|
348
|
+
"right": "```q\nquery present(x: time ?):\n return x != null\n```"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"family": "quarry",
|
|
352
|
+
"code": "E_QUARRY_RETAIN",
|
|
353
|
+
"wrong": "```q\ntable messages:\n body: text <=4000\n retain forever\n```",
|
|
354
|
+
"right": "```q\ntable messages:\n body: text <=4000\n retain delivered-history\n```"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"family": "quarry",
|
|
358
|
+
"code": "E_QUARRY_ONLY",
|
|
359
|
+
"wrong": "```ts\nexport const list = query({}, () => null);\n```",
|
|
360
|
+
"right": "```q\nquery list():\n return null\n```"
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"family": "args",
|
|
364
|
+
"code": "E_ARGS_CURSOR_INDEX",
|
|
365
|
+
"wrong": "indexes: { byConvTime: { columns: [\"conversationId\", \"at\"] } }",
|
|
366
|
+
"right": "indexes: { byConvTime: { columns: [\"conversationId\", \"at\", \"id\"] } }"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"family": "input",
|
|
370
|
+
"code": "E_INPUT",
|
|
371
|
+
"wrong": "pass a cursor minted for another index or order",
|
|
372
|
+
"right": "pass the previous page's next token back as the cursor argument"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"family": "offline",
|
|
376
|
+
"code": "E_OFFLINE_CARRIER",
|
|
377
|
+
"wrong": "createClient({ offline: { store: unavailableCarrier } })",
|
|
378
|
+
"right": "configure the IndexedDB web carrier or Node file test carrier"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"family": "offline",
|
|
382
|
+
"code": "E_OFFLINE_LOCK",
|
|
383
|
+
"wrong": "open the same account in two writing browser tabs",
|
|
384
|
+
"right": "close the other tab before opening this account"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"family": "args",
|
|
388
|
+
"code": "E_ARGS_ID_SCALAR",
|
|
389
|
+
"wrong": "db.insert(todos, { id: \"\", title })",
|
|
390
|
+
"right": "db.insert(todos, { id: ctx.newId(), title })"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"family": "args",
|
|
394
|
+
"code": "E_ARGS_ID_TEXT",
|
|
395
|
+
"wrong": "const pair = leftId + \":\" + rightId",
|
|
396
|
+
"right": "db.get(dmPairs.byPair, [leftId, rightId])"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"family": "offline",
|
|
400
|
+
"code": "E_OFFLINE_RESET",
|
|
401
|
+
"wrong": "client.resume({ generation: heldGeneration }) // after the program changed",
|
|
402
|
+
"right": "on 409 drop held coverage, keep the outbox, fetch /api/program?generation=<returned>, cold-drain"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"family": "offline",
|
|
406
|
+
"code": "E_OFFLINE_RESUME",
|
|
407
|
+
"wrong": "client.resume({ mounted: fortyQueries })",
|
|
408
|
+
"right": "client.resume({ mounted: first32 }) // re-subscribe the rest as ordinary reads"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"family": "offline",
|
|
412
|
+
"code": "E_OFFLINE_INTENT_EXPIRED",
|
|
413
|
+
"wrong": "replay: { grant: thirtyOneDayOldGrant, offset: 0, count: 2, intent }",
|
|
414
|
+
"right": "resume first; replay under the grant it returns"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"family": "offline",
|
|
418
|
+
"code": "E_OFFLINE_INTENT_REUSE",
|
|
419
|
+
"wrong": "replay: { intent: settledIntent, ... } // with different args",
|
|
420
|
+
"right": "new bytes take a new slice and a new intent; retry only the original bytes"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"family": "offline",
|
|
424
|
+
"code": "E_ASSET_VIEWED",
|
|
425
|
+
"wrong": "client.assetBlob(url) // for a view-once row this device already viewed",
|
|
426
|
+
"right": "render a view-once row once; a viewed row answers null offline and fetches nothing"
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"family": "offline",
|
|
430
|
+
"code": "E_OFFLINE_ID_BLOCK",
|
|
431
|
+
"wrong": "replay: { grant, offset: 8191, count: 2, intent: `${grant}:8191` }",
|
|
432
|
+
"right": "replay: { grant, offset: unusedOffset, count: 1 + maxNewIds, intent: `${grant}:${unusedOffset}` }"
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"family": "offline",
|
|
436
|
+
"code": "E_OFFLINE_AUTHORITY",
|
|
437
|
+
"wrong": "client.query(api.messages.thread, args) // a delivery for dev:bob while dev:alice's partition is open",
|
|
438
|
+
"right": "keep the held coverage: open the store as the delivery's principal (sign in as it) before committing it; never a reset"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"family": "offline",
|
|
442
|
+
"code": "E_OFFLINE_CONFLICT",
|
|
443
|
+
"wrong": "replay a queued edit whose traced row moved and read the refusal as a session error",
|
|
444
|
+
"right": "the intent settles rejected with reason: \"conflict\" and site; read the current row and send a new intent"
|
|
445
|
+
}
|
|
446
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Quarry grammar
|
|
2
|
+
|
|
3
|
+
Use `snapback/schema.q` and `snapback/<module>.q`. Seeds, tests, and effect
|
|
4
|
+
bodies are TypeScript; effect `then` is Quarry.
|
|
5
|
+
|
|
6
|
+
```q
|
|
7
|
+
use identity
|
|
8
|
+
|
|
9
|
+
table todos:
|
|
10
|
+
title: text <=200
|
|
11
|
+
done: bool
|
|
12
|
+
ownerId: principal
|
|
13
|
+
by ownerId
|
|
14
|
+
read <- .ownerId = viewer
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Schema uses `use identity [password|guests]`, `table`, `maintain`, and `channel`.
|
|
18
|
+
Tables hold columns, indexes, invariants, rules, `after insert <-
|
|
19
|
+
exists`, and optionally `retain until-revoked` or `retain delivered-history`
|
|
20
|
+
(default: the former). Absences, uniform-range holes, and tombstones evict under
|
|
21
|
+
both. Only hidden rows differ: the default evicts; `delivered-history` keeps.
|
|
22
|
+
Put the word on the table whose rows can hide. A per-recipient log with immutable
|
|
23
|
+
`.recipient = viewer` never hides, so needs no word. `.` is the row. Maintains
|
|
24
|
+
count/sum by fields. Channels declare `publish`, `subscribe`, and `ttl Ns`.
|
|
25
|
+
|
|
26
|
+
Types: `principal`, `id`, table references, `int`, `time`, `bool`,
|
|
27
|
+
`decimal <scale>`, `money <CUR> [scale]`, `text <=N`, `text M..N`, `json <=N`,
|
|
28
|
+
`bytes <=N`, `image <=N`, `video <=N`, `enum(...)`, `[type] <=N`, and
|
|
29
|
+
operation-only `cursor`. Text may add a format; `?` makes any type optional.
|
|
30
|
+
|
|
31
|
+
```q
|
|
32
|
+
query list():
|
|
33
|
+
return todos[viewer] first 50
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Programs are `effect name from module`, `query`, or `mutation`.
|
|
37
|
+
`shape name = { fields }` applies to a row/page/read or splices as `...name`.
|
|
38
|
+
Statements include bindings, `return`, `require`, `if/else`, bounded `for`,
|
|
39
|
+
`match`, and `emit`; expressions include
|
|
40
|
+
objects, arrays, reads/shapes, `exists`, `#maintain[key]`, four word writes,
|
|
41
|
+
`! [else CODE]`, `??`, `merge`, and bounded built-ins.
|
|
42
|
+
|
|
43
|
+
Reads use `table[value, …]` or `table[column = value, …]` plus `[after cursor]
|
|
44
|
+
first|last N [by index]`; point reads may use `by index`.
|
|
45
|
+
`merge table [after cursor] first|last N by index:` has indented lanes. Declare
|
|
46
|
+
`c: cursor ?`; absent starts page one. Pass opaque `next` back unchanged. A
|
|
47
|
+
pageable merge index ends in `id`.
|
|
48
|
+
Equality is total; ordered comparisons need nonoptional operands or direct
|
|
49
|
+
`if x != null:` narrowing. `where` refuses with index/read rewrites. Upserts
|
|
50
|
+
cannot spell `id`. Decimal/money seeds are
|
|
51
|
+
whole units; wire arguments remain minor units. `sorted` is
|
|
52
|
+
`(lo, hi) = sorted(a, b)`. `shape`, `fn`, and `cursor` are reserved bindings.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Live query
|
|
2
|
+
|
|
3
|
+
A query is live because its authoritative read trace intersects a commit, not
|
|
4
|
+
because the author declares an invalidation list. Bound every scan:
|
|
5
|
+
|
|
6
|
+
For paging, every cursor index ends in intrinsic `id`. Declare a `cursor ?`
|
|
7
|
+
argument and pass it to Quarry's forward-only `after` clause; `null` means the
|
|
8
|
+
first page.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
export const thread = query({ conversationId: t.ref(conversations) }, ({ db, ctx, args }) => {
|
|
12
|
+
db.require(
|
|
13
|
+
db.exists(memberships.byConversationPrincipal, [args.conversationId, ctx.viewer]),
|
|
14
|
+
"MEMBERSHIP_REQUIRED",
|
|
15
|
+
);
|
|
16
|
+
return db.scan(messages.byConvTime, [args.conversationId], { order: "desc", take: 50 });
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`complete` and `capped` receipts carry `next: Cursor | null`. Pass that opaque
|
|
21
|
+
token back as the cursor argument for the next page. It represents the last
|
|
22
|
+
returned row, is bound to the table/index/order, and must not be inspected or
|
|
23
|
+
constructed by application code.
|
|
24
|
+
|
|
25
|
+
Subscribe through `client.subscribe(api.messages.thread, args, listener)`.
|
|
26
|
+
Each SSE data frame is a query response or refusal. Connection `live`/`stale`
|
|
27
|
+
facts belong to the client and never replace `complete`/`capped`/`denied`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Offline
|
|
2
|
+
|
|
3
|
+
`offline.store`: IndexedDB or test files; `E_OFFLINE_CARRIER` if unavailable.
|
|
4
|
+
Session expiry seals; sign-out or replacement wipes;
|
|
5
|
+
one writer (`E_OFFLINE_LOCK`). `offline.fenceDays` (1..30) bounds trust, not
|
|
6
|
+
reading: past it no predicting or replay until a resume succeeds; held
|
|
7
|
+
answers read `stale`. `useQuery(op, args, { drain: true })` drains to `null`
|
|
8
|
+
by its `Cursor`, then watches the tail. Offline `run()` resolves
|
|
9
|
+
`{ state: "queued", intent, ids }`, replayed FIFO after resume.
|
|
10
|
+
`useOutbox()` exposes `entries`, `dismiss`, `ready` (current grant or no partition),
|
|
11
|
+
and `fence: "inside" | "past"`. `client.outbox.drain()` replays;
|
|
12
|
+
it and `settled()` return `{ stopped: "fence" }` when resume is needed.
|
|
13
|
+
The twin evaluates held rows: exact if covered, else `partial` with `reason`.
|
|
14
|
+
Asset bytes share their row's lease, keyed `(assetId, width)`;
|
|
15
|
+
`offline.maxAssetBytes` (256 MiB) caps an LRU. `offline.viewOnce(table, row)`
|
|
16
|
+
marks an image at fetch, a clip at play (`assetCache.markViewed`): then the
|
|
17
|
+
row reads `null`, `useAssetUrl` mints no URL, `assetBlob` refuses `E_ASSET_VIEWED`.
|
|
18
|
+
|
|
19
|
+
Answers: `receipt: true` on `POST /api/query` or
|
|
20
|
+
`receipt=1` on `/api/subscribe` returns `{ protocol: 1, generation,
|
|
21
|
+
schemaHash, planHash, authority, state, data, next, seq, rows, receipt,
|
|
22
|
+
validator }`; `rows` pools each row once, `data` and `receipt` cite
|
|
23
|
+
`{ "$row": { table, id } }`, an equal `validator` gets `{ state: "unchanged",
|
|
24
|
+
seq, validator }`. `GET /api/program?generation=`: IR, certificates, schema;
|
|
25
|
+
`/api/manifest` has `tables[<name>].retain`.
|
|
26
|
+
|
|
27
|
+
## Resume
|
|
28
|
+
|
|
29
|
+
`POST /api/resume { protocol: 1, generation, schemaHash, authEpoch, remaining?,
|
|
30
|
+
log?: { op, args, watermark, validator }, mounted: [{ op, args, validator }] }`
|
|
31
|
+
is one owner action: the log tail (`{ unchanged: true }` or an envelope
|
|
32
|
+
and `watermark`), then mounted queries, to 1 MiB (`more` past it).
|
|
33
|
+
The log op pages ascending, `after c
|
|
34
|
+
first <=50`; mounted caps at 32 (`E_OFFLINE_RESUME` 413, `which`). New
|
|
35
|
+
program or schema: `E_OFFLINE_RESET` (409) with the live `{ generation,
|
|
36
|
+
schemaHash }`; drop held coverage, keep the outbox, fetch `/api/program`,
|
|
37
|
+
re-resume. Stale `authEpoch`: `E_AUTH_SESSION`. Another partition's delivery:
|
|
38
|
+
`E_OFFLINE_AUTHORITY`, coverage kept.
|
|
39
|
+
No facts: query for a receipt, then logless resume. No query:
|
|
40
|
+
`E_OFFLINE_RESUME`; keep the queue.
|
|
41
|
+
|
|
42
|
+
## Grants
|
|
43
|
+
|
|
44
|
+
Login, guest, resume return `grant: { id, from, count: 8192, mintedAt,
|
|
45
|
+
expiresAt }` (principal-bound, 30 days) if `remaining`, the client's unused
|
|
46
|
+
ids, is absent or under 1024. `POST /api/mutation { op, args, replay: {
|
|
47
|
+
grant, offset, count, intent: "<grant>:<offset>", trace } }` injects ids
|
|
48
|
+
`from + offset …` as the `new` envelope. Refusals:
|
|
49
|
+
`E_OFFLINE_INTENT_EXPIRED` (unknown, foreign, or expired grant),
|
|
50
|
+
`E_OFFLINE_ID_BLOCK` (slice outside the grant or settled),
|
|
51
|
+
`E_OFFLINE_INTENT_REUSE` (same intent, other bytes). Same bytes twice get the
|
|
52
|
+
stored response; a 503 never double-sends.
|
|
53
|
+
|
|
54
|
+
## Conflict
|
|
55
|
+
|
|
56
|
+
`trace: { points: [{ table, index, key, digest }], verdicts: [{ ruleHash,
|
|
57
|
+
inputsDigest, result }] }` is the read set at queue time: each point's row
|
|
58
|
+
digest (SHA-256 of the canonical wire row, `null` if absent), each verdict's
|
|
59
|
+
`inputsDigest` (SHA-256 of wire `[table, index, key]`). At commit the server
|
|
60
|
+
executes on current rows and compares; a moved point or verdict rolls back:
|
|
61
|
+
`{ state: "rejected", reason: "conflict", site, intent }`
|
|
62
|
+
(`E_OFFLINE_CONFLICT`). Ranges re-execute unvalidated: a fan-out uses members
|
|
63
|
+
at commit. Past-fence entries have `predicted: false, reason: "fence"` and
|
|
64
|
+
an empty trace: only the program checks conflicts. The compare-and-set edit:
|
|
65
|
+
|
|
66
|
+
```q
|
|
67
|
+
mutation edit(messageId: messages, body: text <=4000, baseBody: text <=4000):
|
|
68
|
+
msg = messages[messageId] ! else NOT_FOUND
|
|
69
|
+
require msg.authorId = viewer else NOT_AUTHOR
|
|
70
|
+
require msg.body = baseBody else CONFLICT
|
|
71
|
+
update messages[msg] { body }
|
|
72
|
+
for m in memberships[msg.conversationId] first 50 by byConversationPrincipal:
|
|
73
|
+
insert deliveries { recipient: m.principal, conversationId: msg.conversationId, messageId, kind: 'edit', at: now }
|
|
74
|
+
return null
|
|
75
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Personas
|
|
2
|
+
|
|
3
|
+
Seed a small, named cast for development and tests:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
personas: ["alice", "bob", "charlie"],
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Use `snapback2 run <op> '{}' --as alice`, `snapback2 why <query> --as alice`,
|
|
10
|
+
or `createClient({ url, as: "alice" })`. Persona ids are stable (`dev:alice`)
|
|
11
|
+
and remain distinct across clients. The loopback-only `--as`/header shortcut
|
|
12
|
+
does not mint a stored session. `snapback2 persona create alice` instead
|
|
13
|
+
issues a fresh, finite ordinary session for the same principal each time, so
|
|
14
|
+
logout and expiry use the production session path. A `serve` owner refuses
|
|
15
|
+
persona headers.
|