@vibes.diy/prompts 14.3.7 → 14.3.9
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/llms/backend.md +38 -8
- package/llms/callai.initial.md +2 -1
- package/llms/callai.md +2 -1
- package/llms/connections.md +59 -5
- package/llms/fireproof.initial.md +21 -2
- package/llms/fireproof.md +24 -5
- package/package.json +4 -4
package/llms/backend.md
CHANGED
|
@@ -675,10 +675,24 @@ upstream that can be down, input that can be malformed — catches it and answer
|
|
|
675
675
|
`{ ok: false, reason }` with a status the app can show, and the platform's 500
|
|
676
676
|
stays the signal for a bug to fix.
|
|
677
677
|
|
|
678
|
+
`/_api` opens to every caller once the app is published. Before its first
|
|
679
|
+
publish, and after the owner unpublishes it, the routes answer the app's own
|
|
680
|
+
people — its owner, its developers, and anyone it is shared with — and
|
|
681
|
+
everyone else gets `404 backend.js _api: not found` without the handler
|
|
682
|
+
running. The page reaches its own routes in every state through `apiFetch`
|
|
683
|
+
(below), which arrives as the signed-in visitor. `call_own_api`, `onChange`
|
|
684
|
+
and `scheduled` keep running, and a connected GitHub webhook runs until the
|
|
685
|
+
owner unpublishes.
|
|
686
|
+
|
|
678
687
|
### Who is calling: `ctx.userInfo` comes from the request, never a cookie
|
|
679
688
|
|
|
680
|
-
Cookies are not read. `ctx.userInfo` is filled in
|
|
689
|
+
Cookies are not read. `ctx.userInfo` is filled in three ways:
|
|
681
690
|
|
|
691
|
+
- **The app's own page, through `apiFetch`** from `use-vibes`. The platform
|
|
692
|
+
makes the call for the signed-in visitor, so the handler runs as them:
|
|
693
|
+
`ctx.userInfo` is that visitor, their `ctx.db` writes pass `access.js` as
|
|
694
|
+
them, and `ctx.connections` spends their connection. A visitor who is not
|
|
695
|
+
signed in arrives as `ctx.userInfo === null`.
|
|
682
696
|
- **A caller outside the app** — a script, another agent, a server — sends the
|
|
683
697
|
person's vibes.diy token as `Authorization: Bearer <token>`. A missing,
|
|
684
698
|
expired or unrecognised token is not an error: the handler runs with
|
|
@@ -686,13 +700,29 @@ Cookies are not read. `ctx.userInfo` is filled in two ways:
|
|
|
686
700
|
- **The builder's own `call_own_api` check** (see the debugging skill) arrives
|
|
687
701
|
already verified, as the app's owner, with no bearer header.
|
|
688
702
|
|
|
689
|
-
A plain `fetch("/_api/...")` from `App.jsx`
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
703
|
+
A plain `fetch("/_api/...")` from `App.jsx` carries no visitor, so the handler
|
|
704
|
+
sees `ctx.userInfo === null`. That suits routes that should not depend on who
|
|
705
|
+
is asking — reading a page, calling a service with a secret. **When a route
|
|
706
|
+
needs to know the visitor — their own records, their connection, their
|
|
707
|
+
permissions — the page calls it with `apiFetch`.** The path is the route under
|
|
708
|
+
`/_api`, and the answer reads like a `Response`:
|
|
709
|
+
|
|
710
|
+
```jsx file=App.jsx partial
|
|
711
|
+
import { apiFetch } from "use-vibes";
|
|
712
|
+
|
|
713
|
+
const res = await apiFetch("/my-rsvp", {
|
|
714
|
+
method: "POST",
|
|
715
|
+
headers: { "content-type": "application/json" },
|
|
716
|
+
body: JSON.stringify({ going: true }),
|
|
717
|
+
});
|
|
718
|
+
const mine = res.ok ? await res.json() : null;
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
A route's own error status resolves with `ok: false`; `apiFetch` rejects only
|
|
722
|
+
when the route could not be reached. Bodies are strings, and answers are for
|
|
723
|
+
JSON-sized results rather than exports. Screens that simply list the visitor's
|
|
724
|
+
own records still read best from the database hooks, which already know who is
|
|
725
|
+
signed in.
|
|
696
726
|
|
|
697
727
|
Writes a handler makes with `ctx.db.put` still pass through `access.js` as the
|
|
698
728
|
caller, so the app's rules hold for API writes too.
|
package/llms/callai.initial.md
CHANGED
|
@@ -88,7 +88,7 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export default function App() {
|
|
91
|
-
const { database, useLiveQuery } = useFireproof("tasks");
|
|
91
|
+
const { database, useLiveQuery, writeRejection, clearWriteRejection } = useFireproof("tasks");
|
|
92
92
|
const { me, can, ready } = useVibe("tasks");
|
|
93
93
|
const { docs } = useLiveQuery("type", { key: "task" });
|
|
94
94
|
const [sentence, setSentence] = useState("");
|
|
@@ -170,6 +170,7 @@ export default function App() {
|
|
|
170
170
|
</form>
|
|
171
171
|
<p>{answer}</p>
|
|
172
172
|
<p role="status">{message}</p>
|
|
173
|
+
{writeRejection && <p role="status">{writeRejection.message} <button type="button" onClick={clearWriteRejection}>Dismiss</button></p>}
|
|
173
174
|
{lastWrite !== undefined && <button type="button" disabled={busy} onClick={undoLast}>Undo</button>}
|
|
174
175
|
{pending !== undefined && !busy && <button type="button" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</button>}
|
|
175
176
|
{docs.map((task) => <TaskCard key={task._id} task={task} database={database} can={can} disabled={busy || pending !== undefined} onSaved={(previous) => setLastWrite({ created: [], replaced: previous })} onCorrect={(doc) => { if (busy || pending !== undefined) return; setCurrent(doc); setSentence(""); }} />)}
|
package/llms/callai.md
CHANGED
|
@@ -88,7 +88,7 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export default function App() {
|
|
91
|
-
const { database, useLiveQuery } = useFireproof("tasks");
|
|
91
|
+
const { database, useLiveQuery, writeRejection, clearWriteRejection } = useFireproof("tasks");
|
|
92
92
|
const { me, can, ready } = useVibe("tasks");
|
|
93
93
|
const { docs } = useLiveQuery("type", { key: "task" });
|
|
94
94
|
const [sentence, setSentence] = useState("");
|
|
@@ -170,6 +170,7 @@ export default function App() {
|
|
|
170
170
|
</form>
|
|
171
171
|
<p>{answer}</p>
|
|
172
172
|
<p role="status">{message}</p>
|
|
173
|
+
{writeRejection && <p role="status">{writeRejection.message} <button type="button" onClick={clearWriteRejection}>Dismiss</button></p>}
|
|
173
174
|
{lastWrite !== undefined && <button type="button" disabled={busy} onClick={undoLast}>Undo</button>}
|
|
174
175
|
{pending !== undefined && !busy && <button type="button" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</button>}
|
|
175
176
|
{docs.map((task) => <TaskCard key={task._id} task={task} database={database} can={can} disabled={busy || pending !== undefined} onSaved={(previous) => setLastWrite({ created: [], replaced: previous })} onCorrect={(doc) => { if (busy || pending !== undefined) return; setCurrent(doc); setSentence(""); }} />)}
|
package/llms/connections.md
CHANGED
|
@@ -27,7 +27,8 @@ a public profile) is an ordinary `ctx.fetch` and needs no connection.
|
|
|
27
27
|
|
|
28
28
|
**Declare the provider in `config.connections`, render the Connect button from
|
|
29
29
|
`useConnection`, and read the account through `@vibes.diy/social` or
|
|
30
|
-
`ctx.connections` in `backend.js`.** All three, every time
|
|
30
|
+
`ctx.connections` in `backend.js`.** All three, every time — and the page
|
|
31
|
+
reaches the third with `apiFetch` (section 4), so it runs as the visitor.
|
|
31
32
|
|
|
32
33
|
### 1. Declare it — `backend.js`
|
|
33
34
|
|
|
@@ -172,10 +173,63 @@ with the credential attached by the platform, and
|
|
|
172
173
|
`await ctx.connections.status(provider)` returns
|
|
173
174
|
`{ state, consented, expiresAt?, accountLabel? }` without spending a call.
|
|
174
175
|
|
|
175
|
-
**Whose account?** In a `fetch` handler it is the signed-in visitor
|
|
176
|
-
the route; in `scheduled` it is the app
|
|
177
|
-
ever read the owner's connection — an app
|
|
178
|
-
does that work in `fetch`, when they are there.
|
|
176
|
+
**Whose account?** In a `fetch` handler it is the signed-in visitor the page
|
|
177
|
+
called the route for with `apiFetch` (section 4); in `scheduled` it is the app
|
|
178
|
+
owner's. A `scheduled` tick can only ever read the owner's connection — an app
|
|
179
|
+
that ranks _each visitor's_ posts does that work in `fetch`, when they are there.
|
|
180
|
+
|
|
181
|
+
### 4. Call it from the page — `apiFetch`
|
|
182
|
+
|
|
183
|
+
**The page calls its connection-backed routes with `apiFetch` from `use-vibes`, so the
|
|
184
|
+
backend runs as the visitor who connected.** `apiFetch` asks the platform to make
|
|
185
|
+
the call for the signed-in visitor: the handler's `ctx.userInfo` is that
|
|
186
|
+
visitor, and `ctx.connections` and `@vibes.diy/social` spend _their_
|
|
187
|
+
connection. The path is the route under `/_api`, so `apiFetch("/top-posts")`
|
|
188
|
+
reaches the `/top-posts` branch above. Wait for the connection before calling,
|
|
189
|
+
so the first answer is the real one.
|
|
190
|
+
|
|
191
|
+
```jsx file=App.jsx app=instagram-connection
|
|
192
|
+
import React, { useEffect, useState } from "react";
|
|
193
|
+
import { useConnection, apiFetch } from "use-vibes";
|
|
194
|
+
|
|
195
|
+
function TopPosts() {
|
|
196
|
+
const { state, consented } = useConnection("instagram");
|
|
197
|
+
const connected = consented && (state === "live" || state === "expiring");
|
|
198
|
+
const [posts, setPosts] = useState([]);
|
|
199
|
+
const [problem, setProblem] = useState(null);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!connected) return;
|
|
202
|
+
apiFetch("/top-posts")
|
|
203
|
+
.then(async (res) => {
|
|
204
|
+
const data = await res.json();
|
|
205
|
+
if (res.ok) setPosts(data.posts);
|
|
206
|
+
else setProblem(`Instagram is ${data.connection ?? "not ready"} — try again in a moment.`);
|
|
207
|
+
})
|
|
208
|
+
.catch(() => setProblem("Could not reach the app just now."));
|
|
209
|
+
}, [connected]);
|
|
210
|
+
if (!connected) return null;
|
|
211
|
+
if (problem) return <p>{problem}</p>;
|
|
212
|
+
return (
|
|
213
|
+
<ol>
|
|
214
|
+
{posts.map((p) => (
|
|
215
|
+
<li key={p.id}>
|
|
216
|
+
<a href={p.permalink}>{p.caption}</a> — {p.likeCount ?? "hidden"} likes
|
|
217
|
+
</li>
|
|
218
|
+
))}
|
|
219
|
+
</ol>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`apiFetch(path, { method, headers, body })` answers like `fetch`: `status`,
|
|
225
|
+
`ok`, `headers`, `text()` and `json()`. The route's own status comes back as
|
|
226
|
+
an answer — a `409` with `{ connection: "needs_consent" }` resolves with
|
|
227
|
+
`ok: false` — and the promise rejects only when the route could not be reached.
|
|
228
|
+
Send JSON as `body: JSON.stringify(data)` with a `content-type:
|
|
229
|
+
application/json` header. A visitor who is not signed in still reaches the
|
|
230
|
+
route, with `ctx.userInfo` set to `null`. The platform keeps the visitor's
|
|
231
|
+
sign-in to itself: `apiFetch` carries who they are, and your code never handles
|
|
232
|
+
a token.
|
|
179
233
|
|
|
180
234
|
## Ticks that have nothing to do
|
|
181
235
|
|
|
@@ -21,7 +21,25 @@ Each document has an `_id`, which can be auto-generated or set explicitly. Auto-
|
|
|
21
21
|
|
|
22
22
|
Use granular documents, e.g. one document per user action, so saving a form or clicking a button should typically create or update a single document, or just a few documents. Avoid patterns that require a single document to grow without bound.
|
|
23
23
|
|
|
24
|
-
`useLiveQuery` populates and refreshes the UI reactively as data arrives, so you usually render empty states rather than loading spinners. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry automatically. If the server rejects a synced write, the local optimistic revision converges **server-wins** (it is overwritten by the server's version) — and the **platform** surfaces the reason in a write-fail toast
|
|
24
|
+
`useLiveQuery` populates and refreshes the UI reactively as data arrives, so you usually render empty states rather than loading spinners. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry automatically. If the server rejects a synced write, the local optimistic revision converges **server-wins** (it is overwritten by the server's version) — and the **platform** surfaces the reason in a write-fail toast whenever it has one: the app's own `forbidden("…")` text, or the platform's message when a signed-in visitor has no write access to the app (both also reach the app as `writeRejection`, below); a reason-less rejection converges silently. So do **not** wrap plain writes in `try/catch` to show denial errors to the user (a save carrying `_files` is the one exception, covered with the image uploader below); keep the UI reactive to the store (`useLiveQuery`/`useDocument`) so the converged state is the feedback, and gate write surfaces up front with `useVibe(dbName).can.create/edit/delete` so a disallowed action never renders in the first place.
|
|
25
|
+
|
|
26
|
+
When the server later refuses a write the visitor can act on — a signed-in visitor saving to a private app, say — the platform shows a notice and `useFireproof` hands the app the same reason as `writeRejection` (`{ docId, message }`, or `null` until a save is refused). Render `writeRejection.message` beside the form it came from and call `clearWriteRejection()` when the visitor dismisses it:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
function NoteForm({ text }) {
|
|
30
|
+
const { database, writeRejection, clearWriteRejection } = useFireproof("notes");
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<button onClick={() => database.put({ type: "note", text })}>Save</button>
|
|
34
|
+
{writeRejection && (
|
|
35
|
+
<p role="status">
|
|
36
|
+
{writeRejection.message} <button onClick={clearWriteRejection}>Dismiss</button>
|
|
37
|
+
</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
25
43
|
|
|
26
44
|
### Basic Example
|
|
27
45
|
|
|
@@ -97,7 +115,7 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
export default function App() {
|
|
100
|
-
const { database, useLiveQuery } = useFireproof("tasks");
|
|
118
|
+
const { database, useLiveQuery, writeRejection, clearWriteRejection } = useFireproof("tasks");
|
|
101
119
|
const { me, can, ready } = useVibe("tasks");
|
|
102
120
|
const { docs } = useLiveQuery("type", { key: "task" });
|
|
103
121
|
const [sentence, setSentence] = useState("");
|
|
@@ -179,6 +197,7 @@ export default function App() {
|
|
|
179
197
|
</form>
|
|
180
198
|
<p>{answer}</p>
|
|
181
199
|
<p role="status">{message}</p>
|
|
200
|
+
{writeRejection && <p role="status">{writeRejection.message} <button type="button" onClick={clearWriteRejection}>Dismiss</button></p>}
|
|
182
201
|
{lastWrite !== undefined && <button type="button" disabled={busy} onClick={undoLast}>Undo</button>}
|
|
183
202
|
{pending !== undefined && !busy && <button type="button" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</button>}
|
|
184
203
|
{docs.map((task) => <TaskCard key={task._id} task={task} database={database} can={can} disabled={busy || pending !== undefined} onSaved={(previous) => setLastWrite({ created: [], replaced: previous })} onCorrect={(doc) => { if (busy || pending !== undefined) return; setCurrent(doc); setSentence(""); }} />)}
|
package/llms/fireproof.md
CHANGED
|
@@ -5,7 +5,7 @@ Fireproof is a document database with live sync, designed to make browser apps e
|
|
|
5
5
|
## Key Features
|
|
6
6
|
|
|
7
7
|
- **Apps run anywhere:** Bundle UI, data, and logic together.
|
|
8
|
-
- **Real-Time, local-first:** Writes land in the local replica instantly and sync in the background; the server validates each on ingest and streams the accepted ones live to every viewer. `useLiveQuery` keeps the UI in sync as data arrives, so you render empty states rather than loading spinners — but a synced write can still be rejected on ingest (access denied, conflicts): the local optimistic revision converges **server-wins** — and the platform surfaces the reason
|
|
8
|
+
- **Real-Time, local-first:** Writes land in the local replica instantly and sync in the background; the server validates each on ingest and streams the accepted ones live to every viewer. `useLiveQuery` keeps the UI in sync as data arrives, so you render empty states rather than loading spinners — but a synced write can still be rejected on ingest (access denied, conflicts): the local optimistic revision converges **server-wins** — and the platform surfaces the reason whenever there is one (an access-rule `forbidden("…")`, or no write access for a signed-in visitor); a reason-less rejection converges silently. Keep the UI reactive to the store rather than depending on a toast; don't assume every write lands, but don't hand-roll denial errors either.
|
|
9
9
|
- **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.
|
|
10
10
|
- **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration. Note: these are NOT top-level exports — they are returned by the `useFireproof()` hook. Always destructure from `const { useLiveQuery, useDocument, database } = useFireproof("dbName")`. A child component that was handed `database` may call `useFireproof(database)` instead of repeating the name; it is the same database.
|
|
11
11
|
|
|
@@ -21,7 +21,25 @@ Each document has an `_id`, which can be auto-generated or set explicitly. Auto-
|
|
|
21
21
|
|
|
22
22
|
Use granular documents, e.g. one document per user action, so saving a form or clicking a button should typically create or update a single document, or just a few documents. Avoid patterns that require a single document to grow without bound.
|
|
23
23
|
|
|
24
|
-
`useLiveQuery` populates and refreshes the UI reactively as data arrives, so you usually render empty states rather than loading spinners. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry automatically. If the server's access rules reject a synced write, the local optimistic revision converges **server-wins** (it is overwritten by the server's version) — and the **platform** surfaces the reason in a write-fail toast
|
|
24
|
+
`useLiveQuery` populates and refreshes the UI reactively as data arrives, so you usually render empty states rather than loading spinners. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry automatically. If the server's access rules reject a synced write, the local optimistic revision converges **server-wins** (it is overwritten by the server's version) — and the **platform** surfaces the reason in a write-fail toast whenever there is one: the app's own `forbidden("…")` text, or the platform's message when a signed-in visitor has no write access to the app (both also reach the app as `writeRejection`, below); a reason-less rejection converges silently. So do **not** wrap plain writes in `try/catch` to show denial errors to the user (a save carrying `_files` is the one exception, covered with the image uploader below); keep the UI reactive to the store (`useLiveQuery`/`useDocument`) so the converged state is the feedback, and gate write surfaces up front with `useVibe(dbName).can.create/edit/delete` so a disallowed action never renders in the first place.
|
|
25
|
+
|
|
26
|
+
When the server later refuses a write the visitor can act on — a signed-in visitor saving to a private app, say — the platform shows a notice and `useFireproof` hands the app the same reason as `writeRejection` (`{ docId, message }`, or `null` until a save is refused). Render `writeRejection.message` beside the form it came from and call `clearWriteRejection()` when the visitor dismisses it:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
function NoteForm({ text }) {
|
|
30
|
+
const { database, writeRejection, clearWriteRejection } = useFireproof("notes");
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<button onClick={() => database.put({ type: "note", text })}>Save</button>
|
|
34
|
+
{writeRejection && (
|
|
35
|
+
<p role="status">
|
|
36
|
+
{writeRejection.message} <button onClick={clearWriteRejection}>Dismiss</button>
|
|
37
|
+
</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
25
43
|
|
|
26
44
|
### Basic Example
|
|
27
45
|
|
|
@@ -97,7 +115,7 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
export default function App() {
|
|
100
|
-
const { database, useLiveQuery } = useFireproof("tasks");
|
|
118
|
+
const { database, useLiveQuery, writeRejection, clearWriteRejection } = useFireproof("tasks");
|
|
101
119
|
const { me, can, ready } = useVibe("tasks");
|
|
102
120
|
const { docs } = useLiveQuery("type", { key: "task" });
|
|
103
121
|
const [sentence, setSentence] = useState("");
|
|
@@ -179,6 +197,7 @@ export default function App() {
|
|
|
179
197
|
</form>
|
|
180
198
|
<p>{answer}</p>
|
|
181
199
|
<p role="status">{message}</p>
|
|
200
|
+
{writeRejection && <p role="status">{writeRejection.message} <button type="button" onClick={clearWriteRejection}>Dismiss</button></p>}
|
|
182
201
|
{lastWrite !== undefined && <button type="button" disabled={busy} onClick={undoLast}>Undo</button>}
|
|
183
202
|
{pending !== undefined && !busy && <button type="button" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</button>}
|
|
184
203
|
{docs.map((task) => <TaskCard key={task._id} task={task} database={database} can={can} disabled={busy || pending !== undefined} onSaved={(previous) => setLastWrite({ created: [], replaced: previous })} onCorrect={(doc) => { if (busy || pending !== undefined) return; setCurrent(doc); setSentence(""); }} />)}
|
|
@@ -506,7 +525,7 @@ App.jsx
|
|
|
506
525
|
|
|
507
526
|
## Offline writes are on by default (`offlineQueue`)
|
|
508
527
|
|
|
509
|
-
For signed-in users, writes are **local-first by default**: a `put`/`del` that fails on the network is durably queued on the device (it resolves, stays visible, and syncs to the cloud when you're back online) instead of rolling back. You don't opt in — every signed-in vibe gets this. An access-denied/validation rejection is **not** queued: it converges **server-wins** (the local optimistic revision is overwritten by the server's version); the platform surfaces the reason
|
|
528
|
+
For signed-in users, writes are **local-first by default**: a `put`/`del` that fails on the network is durably queued on the device (it resolves, stays visible, and syncs to the cloud when you're back online) instead of rolling back. You don't opt in — every signed-in vibe gets this. An access-denied/validation rejection is **not** queued: it converges **server-wins** (the local optimistic revision is overwritten by the server's version); the platform surfaces the reason whenever there is one, and a reason-less rejection converges silently — so keep the UI reactive to the store rather than depending on a toast. Only transport failures queue and retry.
|
|
510
529
|
|
|
511
530
|
Pass `{ offlineQueue: false }` for **server-first, fail-fast** writes: a `put` resolves only when the server accepts it, and a network failure rejects immediately with nothing queued. Choose this for **collaborative multi-writer apps** where two people may edit the same doc — sync is blind last-arrival-wins (Firefly has no `_rev`), so a stale write replayed on reconnect can silently overwrite a newer one. When a lost write is safer than a surprise overwrite, opt out.
|
|
512
531
|
|
|
@@ -532,7 +551,7 @@ The old `{ anonymousLocal: true }` option (and its `migrate` hook) is **deprecat
|
|
|
532
551
|
|
|
533
552
|
## Architecture: Where's My Data?
|
|
534
553
|
|
|
535
|
-
Data lives in a local replica (IndexedDB) that the app reads and writes instantly; that replica syncs to the Firefly server in the background. The server is the authority on acceptance — each synced write is validated by the app's access rules on ingest, persisted, and then synced to all users who have read access. A write that fails validation or hits a conflict is rejected on ingest: the local optimistic revision converges **server-wins**, and the platform surfaces the reason
|
|
554
|
+
Data lives in a local replica (IndexedDB) that the app reads and writes instantly; that replica syncs to the Firefly server in the background. The server is the authority on acceptance — each synced write is validated by the app's access rules on ingest, persisted, and then synced to all users who have read access. A write that fails validation or hits a conflict is rejected on ingest: the local optimistic revision converges **server-wins**, and the platform surfaces the reason whenever there is one — a reason-less rejection converges silently, so keep the UI reactive to the store rather than depending on a toast. Don't assume the write always lands, but don't hand-roll your own denial error UI — let it converge.
|
|
536
555
|
|
|
537
556
|
## Using Fireproof in JavaScript
|
|
538
557
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "14.3.
|
|
3
|
+
"version": "14.3.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"exports": {
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"license": "Apache-2.0",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@adviser/cement": "~0.5.34",
|
|
40
|
-
"@vibes.diy/call-ai-v2": "14.3.
|
|
41
|
-
"@vibes.diy/identity": "14.3.
|
|
42
|
-
"@vibes.diy/use-vibes-types": "14.3.
|
|
40
|
+
"@vibes.diy/call-ai-v2": "14.3.9",
|
|
41
|
+
"@vibes.diy/identity": "14.3.9",
|
|
42
|
+
"@vibes.diy/use-vibes-types": "14.3.9",
|
|
43
43
|
"arktype": "~2.2.3",
|
|
44
44
|
"json-schema-faker": "~0.6.3"
|
|
45
45
|
},
|