@atom.io/template-spa-astro-ssr 0.0.4 → 0.0.6

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/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(`drafts.sqlite`)
19
- const initialBody = `A note from SQLite for document 1.`
20
- database.prepare(`UPDATE drafts SET body = ? WHERE id = 1`).run(initialBody)
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(`A note from SQLite for document 1.`))
32
- assert.ok(html.indexOf(`data-resumable`) < html.indexOf(`<astro-island`))
33
- assert.equal((html.match(/data-resumable/g) ?? []).length, 1)
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 changedBody = `Changed after the first server render`
36
- database.prepare(`UPDATE drafts SET body = ? WHERE id = 1`).run(changedBody)
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(changedBody))
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 sidecar together`,
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`) errors.push(message.text())
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(`textarea`)
78
- assert.equal(await page.locator(`textarea`).count(), 4)
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(`textarea`).first().inputValue(),
81
- development ? changedBody : initialBody,
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(new Set(seeds).size, 2)
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.__draftNodes = [...document.querySelectorAll(`textarea`)]
87
- window.__draftHost = document.querySelector(`astro-island`)
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(`.draft`)
97
- await cards.nth(0).locator(`textarea`).fill(`A browser edit`)
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"] .draft`)
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(`textarea`)].every((element, index) =>
106
- index < 2
107
- ? element === window.__draftNodes[index]
108
- : element !== window.__draftNodes[index],
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(`textarea`)
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(`textarea`).fill(`A browser edit`)
166
+ await cards.nth(0).locator(`input[type="number"]`).fill(`31`)
126
167
  await page.waitForFunction(
127
168
  () =>
128
- document.querySelector(`[data-preview]`).textContent === `A browser edit`,
169
+ document.querySelector(`[data-preview]`).textContent ===
170
+ `31 units available`,
129
171
  )
130
172
  assert.equal(
131
- await cards.nth(2).locator(`textarea`).inputValue(),
132
- `A browser edit`,
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 sidecar without RPC`,
179
+ `Hydration must consume the island seed without RPC`,
138
180
  )
139
- const refresh = async (index, body) => {
140
- database.prepare(`UPDATE drafts SET body = ? WHERE id = 1`).run(body)
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.nth(index).locator(`button`).click()
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(await cards.nth(index).locator(`textarea`).inputValue(), body)
201
+ assert.equal(
202
+ await cards.nth(index).locator(`input[type="number"]`).inputValue(),
203
+ String(stock),
204
+ )
153
205
  }
154
- await refresh(0, `Updated in SQLite after SSR`)
155
- await refresh(0, `Updated in SQLite again`)
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, `Another database revision`)
215
+ await refresh(2, 35)
164
216
  assert.equal(rpcRequests.length, 3)
165
217
  assert.equal(
166
- await cards.nth(0).locator(`textarea`).inputValue(),
167
- `Another database revision`,
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(`textarea`).fill(`Keep this edit across navigation`)
226
+ await cards.nth(0).locator(`input[type="number"]`).fill(`41`)
175
227
  database
176
- .prepare(`UPDATE drafts SET body = ? WHERE id = 1`)
177
- .run(`A newer server value`)
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 = [...document.querySelectorAll(`textarea`)]
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.__draftHost.childNodes.length === 0)
187
- await page.getByRole(`link`, { name: `Back to drafts` }).click()
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"] .draft`)
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(`textarea`).inputValue(),
205
- `Keep this edit across navigation`,
260
+ await cards.nth(0).locator(`input[type="number"]`).inputValue(),
261
+ `41`,
206
262
  )
207
263
  assert.equal(
208
- await cards.nth(2).locator(`textarea`).inputValue(),
209
- `Keep this edit across navigation`,
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(`textarea`)].every(
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, `Refreshed after returning`)
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(`textarea`)
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.__pristineNodes = [...document.querySelectorAll(`textarea`)]
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"] .draft`)
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(`textarea`).first().inputValue(),
243
- development ? `Refreshed after returning` : initialBody,
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(`textarea`)].every(
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, `Fresh reset from a cached snapshot`)
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, 5)
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(`Fresh reset from a cached snapshot`))
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\/(drafts|router)|read-draft\.sql/)
402
+ assert.doesNotMatch(url, /server\/(inventory|router)|read-product\.sql/)
289
403
  assert.doesNotMatch(
290
404
  code,
291
- /node:sqlite|read-draft\.sql|SELECT id, title|A note from SQLite/,
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 resets, server data, client:load and client:visible, initial DOM hydration, shared state, delayed fresh mounts, RPC resets, navigation without lost edits, and server-code removal passed.`,
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.prepare(`UPDATE drafts SET body = ? WHERE id = 1`).run(initialBody)
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 Draft } from "../src/api.ts"
2
- import { readDraft } from "../src/server/drafts.ts"
1
+ import { api, type Product } from "../src/api.ts"
2
+ import { readProduct } from "../src/server/inventory.ts"
3
3
 
4
- const direct = readDraft(1)
5
- const revision: string = direct.revision
6
- const dto: Draft = direct
7
- const dtoRevision: string = dto.revision
8
- const remote = await api.drafts.get.query({ id: 1 })
9
- const remoteRevision: string = remote.revision
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.drafts.get.query({ id: `one` })
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
- readDraft(`one`)
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
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "include": [
24
24
  "src",
25
+ "exhibits",
25
26
  "astro.config.ts",
26
27
  "scripts",
27
28
  "tests/types.ts"
@@ -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
- })
@@ -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
- }
@@ -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;