@atom.io/template-spa-astro-ssr 0.0.3 → 0.0.5
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/.reylo.yaml +1 -1
- package/README.md +102 -88
- package/README.mdx +20 -25
- package/_gitignore +1 -1
- package/exhibits/PrivateInventory.tsx +8 -0
- package/exhibits/inventory-page.astro +5 -0
- package/exhibits/private-inventory-page.astro +5 -0
- package/package.json +13 -13
- package/schema.sql +5 -3
- package/scripts/seed.ts +4 -4
- package/src/Inventory.tsx +107 -0
- package/src/api.ts +1 -1
- package/src/layouts/Page.astro +2 -3
- package/src/pages/about.astro +9 -7
- package/src/pages/index.astro +21 -24
- package/src/server/inventory.ts +45 -0
- package/src/server/read-product.sql +1 -0
- package/src/server/router.ts +12 -3
- package/src/state.ts +29 -0
- package/src/style.css +22 -13
- package/tests/browser.mjs +185 -66
- package/tests/types.ts +14 -10
- package/tsconfig.json +1 -0
- package/src/Drafts.spa.tsx +0 -62
- package/src/model.ts +0 -21
- package/src/prepare-drafts.ts +0 -22
- package/src/server/drafts.ts +0 -17
- package/src/server/read-draft.sql +0 -1
package/tests/browser.mjs
CHANGED
|
@@ -15,9 +15,18 @@ const server = await (development ? dev : preview)({
|
|
|
15
15
|
})
|
|
16
16
|
const port = development ? server.address.port : server.server.address().port
|
|
17
17
|
const stop = () => server.stop()
|
|
18
|
-
const database = new DatabaseSync(`
|
|
19
|
-
const
|
|
20
|
-
database
|
|
18
|
+
const database = new DatabaseSync(`inventory.sqlite`)
|
|
19
|
+
const initialStock = 24
|
|
20
|
+
database
|
|
21
|
+
.prepare(`UPDATE products SET stock = ?, revision = 1 WHERE id = 1`)
|
|
22
|
+
.run(initialStock)
|
|
23
|
+
const originalProductName = database
|
|
24
|
+
.prepare(`SELECT name FROM products WHERE id = 2`)
|
|
25
|
+
.get().name
|
|
26
|
+
const escapedProductName = `Breakfast tea "Ceylon" & <script>globalThis.__seedExecuted = true</script>`
|
|
27
|
+
database
|
|
28
|
+
.prepare(`UPDATE products SET name = ? WHERE id = 2`)
|
|
29
|
+
.run(escapedProductName)
|
|
21
30
|
let browser
|
|
22
31
|
try {
|
|
23
32
|
const origin = `http://127.0.0.1:${port}`
|
|
@@ -28,24 +37,29 @@ try {
|
|
|
28
37
|
if (!development) {
|
|
29
38
|
assert.equal(response.headers.get(`x-astro-cache`), `MISS`)
|
|
30
39
|
}
|
|
31
|
-
assert.ok(html.includes(`
|
|
32
|
-
assert.ok(html.
|
|
33
|
-
assert.
|
|
40
|
+
assert.ok(html.includes(`House blend`))
|
|
41
|
+
assert.ok(html.includes(`value="24"`))
|
|
42
|
+
assert.doesNotMatch(html, /<astro-resumption|<script[^>]*data-resumable/)
|
|
43
|
+
assert.equal((html.match(/data-resumability=/g) ?? []).length, 2)
|
|
34
44
|
assert.equal((html.match(/aria-busy="true"/g) ?? []).length, 0)
|
|
35
|
-
const
|
|
36
|
-
database
|
|
45
|
+
const changedStock = 27
|
|
46
|
+
database
|
|
47
|
+
.prepare(
|
|
48
|
+
`UPDATE products SET stock = ?, revision = revision + 1 WHERE id = 1`,
|
|
49
|
+
)
|
|
50
|
+
.run(changedStock)
|
|
37
51
|
const repeated = await fetch(origin)
|
|
38
52
|
const repeatedHtml = await repeated.text()
|
|
39
53
|
assert.equal(repeated.status, 200)
|
|
40
54
|
if (development) {
|
|
41
|
-
assert.ok(repeatedHtml.includes(
|
|
55
|
+
assert.ok(repeatedHtml.includes(`value="${changedStock}"`))
|
|
42
56
|
assert.notEqual(repeatedHtml, html)
|
|
43
57
|
} else {
|
|
44
58
|
assert.equal(repeated.headers.get(`x-astro-cache`), `HIT`)
|
|
45
59
|
assert.equal(
|
|
46
60
|
repeatedHtml,
|
|
47
61
|
html,
|
|
48
|
-
`Cache hits preserve HTML and
|
|
62
|
+
`Cache hits preserve HTML and seed attributes together`,
|
|
49
63
|
)
|
|
50
64
|
}
|
|
51
65
|
browser = await chromium.launch()
|
|
@@ -59,7 +73,8 @@ try {
|
|
|
59
73
|
})
|
|
60
74
|
page.on(`pageerror`, (error) => errors.push(error.message))
|
|
61
75
|
page.on(`console`, (message) => {
|
|
62
|
-
if (message.type() === `error`
|
|
76
|
+
if (message.type() === `error` && !message.text().includes(`409 (Conflict)`))
|
|
77
|
+
errors.push(message.text())
|
|
63
78
|
})
|
|
64
79
|
page.on(`response`, (result) => {
|
|
65
80
|
if (result.url().includes(`/api/trpc/`))
|
|
@@ -74,17 +89,42 @@ try {
|
|
|
74
89
|
await route.continue()
|
|
75
90
|
})
|
|
76
91
|
await page.goto(origin, { waitUntil: `commit` })
|
|
77
|
-
await page.waitForSelector(`
|
|
78
|
-
assert.equal(await page.locator(`
|
|
92
|
+
await page.waitForSelector(`input[type="number"]`)
|
|
93
|
+
assert.equal(await page.locator(`input[type="number"]`).count(), 4)
|
|
94
|
+
assert.equal(
|
|
95
|
+
await page.locator(`input[type="number"]`).first().inputValue(),
|
|
96
|
+
String(development ? changedStock : initialStock),
|
|
97
|
+
)
|
|
98
|
+
const payloads = await page
|
|
99
|
+
.locator(`astro-island`)
|
|
100
|
+
.evaluateAll((islands) =>
|
|
101
|
+
islands.map((island) =>
|
|
102
|
+
JSON.parse(island.getAttribute(`data-resumability`)),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
assert.equal(payloads.length, 2)
|
|
106
|
+
assert.equal(payloads[0].values.length, 2)
|
|
107
|
+
assert.equal(
|
|
108
|
+
payloads[0].values.find((entry) => entry[0][1] === 2)[1][1].name,
|
|
109
|
+
escapedProductName,
|
|
110
|
+
)
|
|
79
111
|
assert.equal(
|
|
80
|
-
await page.locator(`
|
|
81
|
-
|
|
112
|
+
await page.locator(`.product h3`).nth(1).textContent(),
|
|
113
|
+
escapedProductName,
|
|
114
|
+
)
|
|
115
|
+
assert.equal(await page.evaluate(() => globalThis.__seedExecuted), undefined)
|
|
116
|
+
assert.deepEqual(
|
|
117
|
+
payloads[1],
|
|
118
|
+
payloads[0],
|
|
119
|
+
`Cached shared reads still attach the complete seed to each island`,
|
|
82
120
|
)
|
|
83
121
|
const seeds = await page.locator(`[data-revision]`).allTextContents()
|
|
84
|
-
assert.equal(
|
|
122
|
+
assert.equal(seeds.length, 4)
|
|
123
|
+
assert.equal(seeds[0], seeds[2])
|
|
124
|
+
assert.equal(seeds[1], seeds[3])
|
|
85
125
|
await page.evaluate(() => {
|
|
86
|
-
window.
|
|
87
|
-
window.
|
|
126
|
+
window.__stockNodes = [...document.querySelectorAll(`input[type="number"]`)]
|
|
127
|
+
window.__stockHost = document.querySelector(`astro-island`)
|
|
88
128
|
})
|
|
89
129
|
gate.resolve()
|
|
90
130
|
await page.waitForSelector(`astro-island[client="load"]:not([ssr])`)
|
|
@@ -93,19 +133,20 @@ try {
|
|
|
93
133
|
1,
|
|
94
134
|
)
|
|
95
135
|
// Edit before the delayed island activates. It must render live shared state.
|
|
96
|
-
const cards = page.locator(`.
|
|
97
|
-
await cards.nth(0).locator(`
|
|
136
|
+
const cards = page.locator(`.product`)
|
|
137
|
+
await cards.nth(0).locator(`input[type="number"]`).fill(`31`)
|
|
98
138
|
await page
|
|
99
|
-
.locator(`astro-island[client="visible"] .
|
|
139
|
+
.locator(`astro-island[client="visible"] .product`)
|
|
100
140
|
.first()
|
|
101
141
|
.scrollIntoViewIfNeeded()
|
|
102
142
|
await page.waitForSelector(`astro-island[client="visible"]:not([ssr])`)
|
|
103
143
|
assert.equal(
|
|
104
144
|
await page.evaluate(() =>
|
|
105
|
-
[...document.querySelectorAll(`
|
|
106
|
-
index
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
[...document.querySelectorAll(`input[type="number"]`)].every(
|
|
146
|
+
(element, index) =>
|
|
147
|
+
index < 2
|
|
148
|
+
? element === window.__stockNodes[index]
|
|
149
|
+
: element !== window.__stockNodes[index],
|
|
109
150
|
),
|
|
110
151
|
),
|
|
111
152
|
true,
|
|
@@ -116,43 +157,54 @@ try {
|
|
|
116
157
|
)
|
|
117
158
|
assert.equal(
|
|
118
159
|
await page
|
|
119
|
-
.locator(`
|
|
160
|
+
.locator(`input[type="number"]`)
|
|
120
161
|
.evaluateAll(
|
|
121
162
|
(elements) => new Set(elements.map((element) => element.id)).size,
|
|
122
163
|
),
|
|
123
164
|
4,
|
|
124
165
|
)
|
|
125
|
-
await cards.nth(0).locator(`
|
|
166
|
+
await cards.nth(0).locator(`input[type="number"]`).fill(`31`)
|
|
126
167
|
await page.waitForFunction(
|
|
127
168
|
() =>
|
|
128
|
-
document.querySelector(`[data-preview]`).textContent ===
|
|
169
|
+
document.querySelector(`[data-preview]`).textContent ===
|
|
170
|
+
`31 units available`,
|
|
129
171
|
)
|
|
130
172
|
assert.equal(
|
|
131
|
-
await cards.nth(2).locator(`
|
|
132
|
-
`
|
|
173
|
+
await cards.nth(2).locator(`input[type="number"]`).inputValue(),
|
|
174
|
+
`31`,
|
|
133
175
|
)
|
|
134
176
|
assert.equal(
|
|
135
177
|
rpcRequests.length,
|
|
136
178
|
0,
|
|
137
|
-
`Hydration must consume the
|
|
179
|
+
`Hydration must consume the island seed without RPC`,
|
|
138
180
|
)
|
|
139
|
-
const refresh = async (index,
|
|
140
|
-
database
|
|
181
|
+
const refresh = async (index, stock) => {
|
|
182
|
+
database
|
|
183
|
+
.prepare(
|
|
184
|
+
`UPDATE products SET stock = ?, revision = revision + 1 WHERE id = 1`,
|
|
185
|
+
)
|
|
186
|
+
.run(stock)
|
|
141
187
|
const before = await cards
|
|
142
188
|
.nth(index)
|
|
143
189
|
.locator(`[data-revision]`)
|
|
144
190
|
.textContent()
|
|
145
|
-
await cards
|
|
191
|
+
await cards
|
|
192
|
+
.nth(index)
|
|
193
|
+
.getByRole(`button`, { name: `Reload from database` })
|
|
194
|
+
.click()
|
|
146
195
|
await page.waitForFunction(
|
|
147
196
|
(previous) =>
|
|
148
197
|
document.querySelectorAll(`[data-revision]`)[previous.index]
|
|
149
198
|
.textContent !== previous.before,
|
|
150
199
|
{ index, before },
|
|
151
200
|
)
|
|
152
|
-
assert.equal(
|
|
201
|
+
assert.equal(
|
|
202
|
+
await cards.nth(index).locator(`input[type="number"]`).inputValue(),
|
|
203
|
+
String(stock),
|
|
204
|
+
)
|
|
153
205
|
}
|
|
154
|
-
await refresh(0,
|
|
155
|
-
await refresh(0,
|
|
206
|
+
await refresh(0, 29)
|
|
207
|
+
await refresh(0, 30)
|
|
156
208
|
assert.equal(rpcRequests.length, 2)
|
|
157
209
|
assert.deepEqual(
|
|
158
210
|
(await page.locator(`[data-revision]`).allTextContents()).filter(
|
|
@@ -160,38 +212,42 @@ try {
|
|
|
160
212
|
),
|
|
161
213
|
[seeds[1], seeds[3]],
|
|
162
214
|
)
|
|
163
|
-
await refresh(2,
|
|
215
|
+
await refresh(2, 35)
|
|
164
216
|
assert.equal(rpcRequests.length, 3)
|
|
165
217
|
assert.equal(
|
|
166
|
-
await cards.nth(0).locator(`
|
|
167
|
-
`
|
|
218
|
+
await cards.nth(0).locator(`input[type="number"]`).inputValue(),
|
|
219
|
+
`35`,
|
|
168
220
|
)
|
|
169
221
|
if (process.env.TEMPLATE_SCREENSHOT)
|
|
170
222
|
await page.screenshot({
|
|
171
223
|
path: process.env.TEMPLATE_SCREENSHOT,
|
|
172
224
|
fullPage: true,
|
|
173
225
|
})
|
|
174
|
-
await cards.nth(0).locator(`
|
|
226
|
+
await cards.nth(0).locator(`input[type="number"]`).fill(`41`)
|
|
175
227
|
database
|
|
176
|
-
.prepare(
|
|
177
|
-
|
|
228
|
+
.prepare(
|
|
229
|
+
`UPDATE products SET stock = ?, revision = revision + 1 WHERE id = 1`,
|
|
230
|
+
)
|
|
231
|
+
.run(43)
|
|
178
232
|
// Inspect incoming DOM before activation; new islands must mount fresh.
|
|
179
233
|
await page.evaluate(() => {
|
|
180
234
|
document.addEventListener(`astro:after-swap`, () => {
|
|
181
|
-
window.__incomingNodes = [
|
|
235
|
+
window.__incomingNodes = [
|
|
236
|
+
...document.querySelectorAll(`input[type="number"]`),
|
|
237
|
+
]
|
|
182
238
|
})
|
|
183
239
|
})
|
|
184
240
|
await page.getByRole(`link`, { name: `How it works` }).click()
|
|
185
241
|
await page.waitForURL(`**/about`)
|
|
186
|
-
await page.waitForFunction(() => window.
|
|
187
|
-
await page.getByRole(`link`, { name: `Back to
|
|
242
|
+
await page.waitForFunction(() => window.__stockHost.childNodes.length === 0)
|
|
243
|
+
await page.getByRole(`link`, { name: `Back to inventory` }).click()
|
|
188
244
|
await page.waitForSelector(`astro-island[client="load"]:not([ssr])`)
|
|
189
245
|
assert.equal(
|
|
190
246
|
await page.locator(`astro-island[client="visible"][ssr]`).count(),
|
|
191
247
|
1,
|
|
192
248
|
)
|
|
193
249
|
await page
|
|
194
|
-
.locator(`astro-island[client="visible"] .
|
|
250
|
+
.locator(`astro-island[client="visible"] .product`)
|
|
195
251
|
.first()
|
|
196
252
|
.scrollIntoViewIfNeeded()
|
|
197
253
|
await page.waitForSelector(`astro-island[client="visible"]:not([ssr])`)
|
|
@@ -201,22 +257,22 @@ try {
|
|
|
201
257
|
`Navigation must retain already initialized values without RPC`,
|
|
202
258
|
)
|
|
203
259
|
assert.equal(
|
|
204
|
-
await cards.nth(0).locator(`
|
|
205
|
-
`
|
|
260
|
+
await cards.nth(0).locator(`input[type="number"]`).inputValue(),
|
|
261
|
+
`41`,
|
|
206
262
|
)
|
|
207
263
|
assert.equal(
|
|
208
|
-
await cards.nth(2).locator(`
|
|
209
|
-
`
|
|
264
|
+
await cards.nth(2).locator(`input[type="number"]`).inputValue(),
|
|
265
|
+
`41`,
|
|
210
266
|
)
|
|
211
267
|
assert.ok(
|
|
212
268
|
await page.evaluate(() =>
|
|
213
|
-
[...document.querySelectorAll(`
|
|
269
|
+
[...document.querySelectorAll(`input[type="number"]`)].every(
|
|
214
270
|
(node, index) => node !== window.__incomingNodes[index],
|
|
215
271
|
),
|
|
216
272
|
),
|
|
217
273
|
)
|
|
218
274
|
|
|
219
|
-
await refresh(0,
|
|
275
|
+
await refresh(0, 45)
|
|
220
276
|
assert.equal(rpcRequests.length, 4)
|
|
221
277
|
// A full load starts a new application; with no edits, both initial roots hydrate.
|
|
222
278
|
const freshGate = Promise.withResolvers()
|
|
@@ -227,24 +283,42 @@ try {
|
|
|
227
283
|
})
|
|
228
284
|
const fullLoad = await page.goto(origin, { waitUntil: `commit` })
|
|
229
285
|
if (!development) assert.equal(fullLoad.headers()[`x-astro-cache`], `HIT`)
|
|
230
|
-
await page.waitForSelector(`
|
|
286
|
+
await page.waitForSelector(`input[type="number"]`)
|
|
287
|
+
await page.evaluate(() => {
|
|
288
|
+
window.__pristineNodes = [
|
|
289
|
+
...document.querySelectorAll(`input[type="number"]`),
|
|
290
|
+
]
|
|
291
|
+
})
|
|
292
|
+
// Hold the earlier island off-DOM so the later, cached SSR reader activates first.
|
|
231
293
|
await page.evaluate(() => {
|
|
232
|
-
window.
|
|
294
|
+
window.__heldIsland = document.querySelector(`astro-island[client="load"]`)
|
|
295
|
+
window.__heldParent = window.__heldIsland.parentElement
|
|
296
|
+
window.__heldIsland.remove()
|
|
233
297
|
})
|
|
298
|
+
await page
|
|
299
|
+
.locator(`astro-island[client="visible"] .product`)
|
|
300
|
+
.first()
|
|
301
|
+
.scrollIntoViewIfNeeded()
|
|
234
302
|
freshGate.resolve()
|
|
303
|
+
await page.waitForSelector(`astro-island[client="visible"]:not([ssr])`)
|
|
304
|
+
assert.equal(
|
|
305
|
+
await page.evaluate(() => window.__heldIsland.hasAttribute(`ssr`)),
|
|
306
|
+
true,
|
|
307
|
+
)
|
|
308
|
+
await page.evaluate(() => window.__heldParent.append(window.__heldIsland))
|
|
235
309
|
await page.waitForSelector(`astro-island[client="load"]:not([ssr])`)
|
|
236
310
|
await page
|
|
237
|
-
.locator(`astro-island[client="visible"] .
|
|
311
|
+
.locator(`astro-island[client="visible"] .product`)
|
|
238
312
|
.first()
|
|
239
313
|
.scrollIntoViewIfNeeded()
|
|
240
314
|
await page.waitForSelector(`astro-island[client="visible"]:not([ssr])`)
|
|
241
315
|
assert.equal(
|
|
242
|
-
await page.locator(`
|
|
243
|
-
development ?
|
|
316
|
+
await page.locator(`input[type="number"]`).first().inputValue(),
|
|
317
|
+
String(development ? 45 : initialStock),
|
|
244
318
|
)
|
|
245
319
|
assert.ok(
|
|
246
320
|
await page.evaluate(() =>
|
|
247
|
-
[...document.querySelectorAll(`
|
|
321
|
+
[...document.querySelectorAll(`input[type="number"]`)].every(
|
|
248
322
|
(node, index) => node === window.__pristineNodes[index],
|
|
249
323
|
),
|
|
250
324
|
),
|
|
@@ -252,10 +326,50 @@ try {
|
|
|
252
326
|
assert.equal(rpcRequests.length, 4)
|
|
253
327
|
|
|
254
328
|
// Even a new browser application resumed from the old cache can reset freshly.
|
|
255
|
-
await refresh(0,
|
|
329
|
+
await refresh(0, 48)
|
|
256
330
|
assert.equal(rpcRequests.length, 5)
|
|
331
|
+
// Saving persists a counted quantity and publishes its real database revision.
|
|
332
|
+
await cards.nth(0).locator(`input[type="number"]`).fill(`32`)
|
|
333
|
+
await cards.nth(0).getByRole(`button`, { name: `Save stock` }).click()
|
|
334
|
+
await page.waitForFunction(
|
|
335
|
+
() =>
|
|
336
|
+
document.querySelector(`[role="status"]`).textContent === `Stock saved`,
|
|
337
|
+
)
|
|
338
|
+
assert.equal(
|
|
339
|
+
database.prepare(`SELECT stock FROM products WHERE id = 1`).get().stock,
|
|
340
|
+
32,
|
|
341
|
+
)
|
|
342
|
+
assert.equal(
|
|
343
|
+
await cards.nth(2).locator(`input[type="number"]`).inputValue(),
|
|
344
|
+
`32`,
|
|
345
|
+
)
|
|
346
|
+
// A competing count must produce a conflict instead of being overwritten.
|
|
347
|
+
database
|
|
348
|
+
.prepare(
|
|
349
|
+
`UPDATE products SET stock = 37, revision = revision + 1 WHERE id = 1`,
|
|
350
|
+
)
|
|
351
|
+
.run()
|
|
352
|
+
await cards.nth(0).locator(`input[type="number"]`).fill(`33`)
|
|
353
|
+
await cards.nth(0).getByRole(`button`, { name: `Save stock` }).click()
|
|
354
|
+
await page.waitForFunction(() =>
|
|
355
|
+
document
|
|
356
|
+
.querySelector(`[role="status"]`)
|
|
357
|
+
.textContent.includes(`Reload before saving`),
|
|
358
|
+
)
|
|
359
|
+
assert.equal(
|
|
360
|
+
database.prepare(`SELECT stock FROM products WHERE id = 1`).get().stock,
|
|
361
|
+
37,
|
|
362
|
+
)
|
|
363
|
+
await cards
|
|
364
|
+
.nth(0)
|
|
365
|
+
.getByRole(`button`, { name: `Reload from database` })
|
|
366
|
+
.click()
|
|
367
|
+
await page.waitForFunction(
|
|
368
|
+
() => document.querySelector(`input[type="number"]`).value === `37`,
|
|
369
|
+
)
|
|
370
|
+
assert.equal(rpcRequests.length, 8)
|
|
257
371
|
const rpcHeaders = await Promise.all(rpcResponses)
|
|
258
|
-
assert.equal(rpcHeaders.length,
|
|
372
|
+
assert.equal(rpcHeaders.length, 8)
|
|
259
373
|
for (const headers of rpcHeaders) {
|
|
260
374
|
assert.equal(headers[`cache-control`], `no-store`)
|
|
261
375
|
assert.equal(headers[`x-astro-cache`], undefined)
|
|
@@ -274,7 +388,7 @@ try {
|
|
|
274
388
|
const expired = await fetch(origin)
|
|
275
389
|
assert.equal(expired.headers.get(`x-astro-cache`), `MISS`)
|
|
276
390
|
const freshHtml = await expired.text()
|
|
277
|
-
assert.ok(freshHtml.includes(`
|
|
391
|
+
assert.ok(freshHtml.includes(`value="37"`))
|
|
278
392
|
assert.notEqual(freshHtml, html)
|
|
279
393
|
const recached = await fetch(origin)
|
|
280
394
|
assert.equal(recached.headers.get(`x-astro-cache`), `HIT`)
|
|
@@ -285,18 +399,23 @@ try {
|
|
|
285
399
|
const executable = await Promise.all(scripts)
|
|
286
400
|
assert.ok(executable.length > 0)
|
|
287
401
|
for (const { code, url } of executable) {
|
|
288
|
-
assert.doesNotMatch(url, /server\/(
|
|
402
|
+
assert.doesNotMatch(url, /server\/(inventory|router)|read-product\.sql/)
|
|
289
403
|
assert.doesNotMatch(
|
|
290
404
|
code,
|
|
291
|
-
/node:sqlite|read-
|
|
405
|
+
/node:sqlite|read-product\.sql|SELECT id, sku|House blend/,
|
|
292
406
|
)
|
|
293
407
|
}
|
|
294
408
|
console.log(
|
|
295
|
-
`${development ? `Development` : `Production`}: ${development ? `uncached SSR` : `cached SSR and 60-second expiry`}, no-store RPC
|
|
409
|
+
`${development ? `Development` : `Production`}: ${development ? `uncached SSR` : `cached SSR and 60-second expiry`}, no-store RPC reads and saves, competing-save conflicts, server data, client:load and client:visible, initial DOM hydration, shared state, delayed fresh mounts, RPC resets, navigation without lost edits, wrapper-free seeds, reverse hydration order, and server-code removal passed.`,
|
|
296
410
|
)
|
|
297
411
|
} finally {
|
|
298
412
|
await browser?.close()
|
|
299
413
|
await stop()
|
|
300
|
-
database
|
|
414
|
+
database
|
|
415
|
+
.prepare(`UPDATE products SET stock = ?, revision = 1 WHERE id = 1`)
|
|
416
|
+
.run(initialStock)
|
|
417
|
+
database
|
|
418
|
+
.prepare(`UPDATE products SET name = ? WHERE id = 2`)
|
|
419
|
+
.run(originalProductName)
|
|
301
420
|
database.close()
|
|
302
421
|
}
|
package/tests/types.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { api, type
|
|
2
|
-
import {
|
|
1
|
+
import { api, type Product } from "../src/api.ts"
|
|
2
|
+
import { readProduct } from "../src/server/inventory.ts"
|
|
3
3
|
|
|
4
|
-
const direct =
|
|
5
|
-
const revision:
|
|
6
|
-
const dto:
|
|
7
|
-
const dtoRevision:
|
|
8
|
-
const remote = await api.
|
|
9
|
-
const remoteRevision:
|
|
4
|
+
const direct = readProduct(1)
|
|
5
|
+
const revision: number = direct.revision
|
|
6
|
+
const dto: Product = direct
|
|
7
|
+
const dtoRevision: number = dto.revision
|
|
8
|
+
const remote = await api.inventory.get.query({ id: 1 })
|
|
9
|
+
const remoteRevision: number = remote.revision
|
|
10
10
|
void [revision, dtoRevision, remoteRevision]
|
|
11
11
|
// @ts-expect-error IDs are numeric throughout the transport contract.
|
|
12
|
-
void api.
|
|
12
|
+
void api.inventory.get.query({ id: `one` })
|
|
13
13
|
|
|
14
14
|
// @ts-expect-error The inferred result does not invent columns absent from SQL.
|
|
15
15
|
void remote.missingColumn
|
|
16
16
|
// @ts-expect-error The shared server function also requires numeric IDs.
|
|
17
|
-
|
|
17
|
+
readProduct(`one`)
|
|
18
|
+
|
|
19
|
+
void api.inventory.setStock.mutate({ id: 1, stock: 12, revision: 1 })
|
|
20
|
+
// @ts-expect-error Stock counts are numeric.
|
|
21
|
+
void api.inventory.setStock.mutate({ id: 1, stock: `twelve`, revision: 1 })
|
package/tsconfig.json
CHANGED
package/src/Drafts.spa.tsx
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { VNode } from "@atom.io/spa"
|
|
2
|
-
import { useId, useLoadable, useSetter, useStore } from "@atom.io/spa"
|
|
3
|
-
import { resetInStore } from "atom.io/internal"
|
|
4
|
-
|
|
5
|
-
import type { Draft } from "./api.ts"
|
|
6
|
-
import { documentIds, draftAtoms } from "./model.ts"
|
|
7
|
-
import { prepareDrafts } from "./prepare-drafts.ts"
|
|
8
|
-
|
|
9
|
-
function DraftCard(props: { id: number }): VNode {
|
|
10
|
-
const store = useStore()
|
|
11
|
-
const label = useId()
|
|
12
|
-
const fallback: Draft = {
|
|
13
|
-
id: props.id,
|
|
14
|
-
title: `Draft ${props.id}`,
|
|
15
|
-
body: ``,
|
|
16
|
-
revision: `00000000-0000-0000-0000-000000000000`,
|
|
17
|
-
}
|
|
18
|
-
const draft = useLoadable(draftAtoms, props.id, fallback)
|
|
19
|
-
const setDraft = useSetter(draftAtoms, props.id)
|
|
20
|
-
return (
|
|
21
|
-
<article class="draft" data-document={props.id} aria-busy={draft().loading}>
|
|
22
|
-
<header>
|
|
23
|
-
<h3>{draft().value.title}</h3>
|
|
24
|
-
<span class="source">{draft().loading ? `Refreshing…` : `Ready`}</span>
|
|
25
|
-
</header>
|
|
26
|
-
<label for={label}>Your note</label>
|
|
27
|
-
<textarea
|
|
28
|
-
id={label}
|
|
29
|
-
value={draft().value.body}
|
|
30
|
-
onInput={(event) => {
|
|
31
|
-
setDraft({ ...draft().value, body: event.currentTarget.value })
|
|
32
|
-
}}
|
|
33
|
-
/>
|
|
34
|
-
<p class="preview" data-preview>
|
|
35
|
-
{draft().value.body}
|
|
36
|
-
</p>
|
|
37
|
-
<p role="status">{draft().error?.message ?? ``}</p>
|
|
38
|
-
<footer>
|
|
39
|
-
<button
|
|
40
|
-
onClick={() => {
|
|
41
|
-
resetInStore(store, draftAtoms, props.id)
|
|
42
|
-
}}
|
|
43
|
-
>
|
|
44
|
-
Refresh from database
|
|
45
|
-
</button>
|
|
46
|
-
<output data-revision>{draft().value.revision}</output>
|
|
47
|
-
</footer>
|
|
48
|
-
</article>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default function Drafts(props: { id: string }): VNode {
|
|
53
|
-
return (
|
|
54
|
-
<div class="draft-grid" data-island={props.id}>
|
|
55
|
-
{documentIds.map((id) => (
|
|
56
|
-
<DraftCard key={id} id={id} />
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
Drafts.prepareStore = prepareDrafts
|
package/src/model.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { atomFamily, type Loadable } from "atom.io"
|
|
2
|
-
import { resumable } from "resumability"
|
|
3
|
-
|
|
4
|
-
import { api, type Draft } from "./api.ts"
|
|
5
|
-
|
|
6
|
-
export const documentIds = [1, 2] as const
|
|
7
|
-
|
|
8
|
-
export const draftAtoms = atomFamily<Loadable<Draft>, number, Error>({
|
|
9
|
-
key: `draft`,
|
|
10
|
-
default: (id) =>
|
|
11
|
-
resumable({
|
|
12
|
-
key: [`draft`, id],
|
|
13
|
-
server: async ({ signal }) => {
|
|
14
|
-
const { readDraft } = await import(`./server/drafts.ts`)
|
|
15
|
-
signal.throwIfAborted()
|
|
16
|
-
return readDraft(id)
|
|
17
|
-
},
|
|
18
|
-
client: ({ signal }) => api.drafts.get.query({ id }, { signal }),
|
|
19
|
-
}),
|
|
20
|
-
catch: [Error],
|
|
21
|
-
})
|
package/src/prepare-drafts.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { AstroStorePreparation } from "@atom.io/spa/astro"
|
|
2
|
-
import { getFromStore } from "atom.io/internal"
|
|
3
|
-
import { createResumption } from "resumability/client"
|
|
4
|
-
import { documentIds, draftAtoms } from "./model.ts"
|
|
5
|
-
|
|
6
|
-
export const prepareDrafts: AstroStorePreparation = async (
|
|
7
|
-
store,
|
|
8
|
-
_props,
|
|
9
|
-
{ element, application },
|
|
10
|
-
) => {
|
|
11
|
-
if (element) {
|
|
12
|
-
const scope = application.get(createResumption, () => {
|
|
13
|
-
const created = createResumption()
|
|
14
|
-
application.onCleanup(created.dispose)
|
|
15
|
-
store.runDefault = (run) =>
|
|
16
|
-
created.run(run, { resume: application.canResume })
|
|
17
|
-
return created
|
|
18
|
-
})
|
|
19
|
-
scope.add(element.closest(`astro-resumption`)!)
|
|
20
|
-
}
|
|
21
|
-
await Promise.all(documentIds.map((id) => getFromStore(store, draftAtoms, id)))
|
|
22
|
-
}
|
package/src/server/drafts.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto"
|
|
2
|
-
import { DatabaseSync } from "node:sqlite"
|
|
3
|
-
|
|
4
|
-
import readDraftQuery from "./read-draft.sql"
|
|
5
|
-
|
|
6
|
-
/** Both SSR and tRPC use this function and its inferred query result. */
|
|
7
|
-
// oxlint-disable-next-line typescript/explicit-module-boundary-types -- Infer the response from Reylo's content-mapped SQL type; tsc verifies the full RPC contract.
|
|
8
|
-
export function readDraft(id: number) {
|
|
9
|
-
const database = new DatabaseSync(`drafts.sqlite`, { readOnly: true })
|
|
10
|
-
try {
|
|
11
|
-
const draft = readDraftQuery.runSync(database, { id })
|
|
12
|
-
if (!draft) throw new Error(`Draft ${id} does not exist`)
|
|
13
|
-
return { ...draft, revision: randomUUID() }
|
|
14
|
-
} finally {
|
|
15
|
-
database.close()
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
SELECT id, title, body FROM drafts WHERE id = :id;
|