rip-lang 3.16.0 → 3.16.2
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 +3 -4
- package/bin/rip +201 -18
- package/bin/rip-schema +175 -0
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +200 -19
- package/docs/RIP-DUCKDB.md +64 -1
- package/docs/RIP-INTRO.md +4 -4
- package/docs/RIP-LANG.md +36 -38
- package/docs/RIP-SCHEMA.md +1204 -364
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +4195 -966
- package/docs/dist/rip.min.js +1161 -284
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +2 -1
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
- package/docs/index.html +1 -1
- package/docs/ui/bundle.json +55 -55
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +1 -1
- package/docs/ui/index.html +1 -1
- package/package.json +15 -7
- package/rip-loader.js +59 -2
- package/src/AGENTS.md +43 -12
- package/src/browser.js +52 -11
- package/src/compiler.js +538 -80
- package/src/components.js +488 -48
- package/src/dts.js +80 -50
- package/src/grammar/README.md +29 -170
- package/src/grammar/grammar.rip +17 -12
- package/src/grammar/solar.rip +4 -17
- package/src/lexer.js +82 -32
- package/src/parser.js +229 -229
- package/src/schema/dts.js +328 -54
- package/src/schema/loader-server.js +2 -1
- package/src/schema/runtime-browser-stubs.js +20 -9
- package/src/schema/runtime-ddl.js +161 -44
- package/src/schema/runtime-migrate.js +681 -0
- package/src/schema/runtime-orm.js +698 -54
- package/src/schema/runtime-validate.js +808 -24
- package/src/schema/runtime.generated.js +2395 -135
- package/src/schema/schema.js +1054 -94
- package/src/typecheck.js +1610 -127
- package/src/types.js +90 -6
- package/src/grammar/lunar.rip +0 -2412
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
package/docs/RIP-APP.md
CHANGED
|
@@ -24,21 +24,39 @@ shaped the way it is, and how to use it well.
|
|
|
24
24
|
|
|
25
25
|
# Contents
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
- [Rip App — Application Framework](#rip-app--application-framework)
|
|
28
|
+
- [Contents](#contents)
|
|
29
|
+
- [1. The four-layer architecture](#1-the-four-layer-architecture)
|
|
30
|
+
- [2. Quick start](#2-quick-start)
|
|
31
|
+
- [Real apps: bundles + file-based routing](#real-apps-bundles--file-based-routing)
|
|
32
|
+
- [3. The subsystems](#3-the-subsystems)
|
|
33
|
+
- [Stash](#stash)
|
|
34
|
+
- [Sources and render gates](#sources-and-render-gates)
|
|
35
|
+
- [createMutation](#createmutation)
|
|
36
|
+
- [createResource](#createresource)
|
|
37
|
+
- [Timing helpers](#timing-helpers)
|
|
38
|
+
- [Components store](#components-store)
|
|
39
|
+
- [createRouter](#createrouter)
|
|
40
|
+
- [createRenderer](#createrenderer)
|
|
41
|
+
- [launch](#launch)
|
|
42
|
+
- [ARIA helpers](#aria-helpers)
|
|
43
|
+
- [4. Lifecycle invariants](#4-lifecycle-invariants)
|
|
44
|
+
- [Component lifecycle order](#component-lifecycle-order)
|
|
45
|
+
- [User hooks](#user-hooks)
|
|
46
|
+
- [Effect ownership](#effect-ownership)
|
|
47
|
+
- [Effect cleanup-on-rerun](#effect-cleanup-on-rerun)
|
|
48
|
+
- [Parent chain (for context)](#parent-chain-for-context)
|
|
49
|
+
- [Layout and page parentage](#layout-and-page-parentage)
|
|
50
|
+
- [Factory blocks (for/if in render)](#factory-blocks-forif-in-render)
|
|
51
|
+
- [Keyed list reconciliation](#keyed-list-reconciliation)
|
|
52
|
+
- [5. Async effects](#5-async-effects)
|
|
53
|
+
- [6. Gotchas](#6-gotchas)
|
|
54
|
+
- [The bundle boundary matters](#the-bundle-boundary-matters)
|
|
55
|
+
- [Render-template name shadowing (fixed)](#render-template-name-shadowing-fixed)
|
|
56
|
+
- [Nested `for` loops can both name `i` (fixed)](#nested-for-loops-can-both-name-i-fixed)
|
|
57
|
+
- [Snapshot tests are brittle](#snapshot-tests-are-brittle)
|
|
58
|
+
- [No browser e2e tests](#no-browser-e2e-tests)
|
|
59
|
+
- [7. When NOT to use Rip App](#7-when-not-to-use-rip-app)
|
|
42
60
|
|
|
43
61
|
---
|
|
44
62
|
|
|
@@ -185,7 +203,7 @@ A deep reactive proxy with path navigation. Single-app state,
|
|
|
185
203
|
JSON-persistable, fine-grained signal subscription per key.
|
|
186
204
|
|
|
187
205
|
```rip
|
|
188
|
-
app =
|
|
206
|
+
app = createStash
|
|
189
207
|
user:
|
|
190
208
|
name: "Alice"
|
|
191
209
|
prefs:
|
|
@@ -205,7 +223,7 @@ app.keys 'user' # → ['name', 'prefs']
|
|
|
205
223
|
app.join 'user', email: "..." # shallow merge
|
|
206
224
|
|
|
207
225
|
# Use raw object underneath
|
|
208
|
-
plain =
|
|
226
|
+
plain = unwrapStash(app) # back to a plain JS object
|
|
209
227
|
```
|
|
210
228
|
|
|
211
229
|
**Single-stash policy**: Rip App assumes one stash per page (the one
|
|
@@ -214,10 +232,97 @@ beforeunload-flush mechanism both rely on this. Apps needing
|
|
|
214
232
|
isolated state silos should use plain `__state(...)` signals or
|
|
215
233
|
namespace under different keys on `app.data`.
|
|
216
234
|
|
|
235
|
+
### Sources and render gates
|
|
236
|
+
|
|
237
|
+
Server data lives in the stash as **source** keys — cells that know how
|
|
238
|
+
to load themselves. `app/stash.rip` becomes the app's data manifest:
|
|
239
|
+
|
|
240
|
+
```rip
|
|
241
|
+
import { source } from '@rip-lang/app'
|
|
242
|
+
|
|
243
|
+
export stash =
|
|
244
|
+
cart: { items: [] } # plain client state
|
|
245
|
+
user: source { fetch: -> User.parse(api.get!('user').json!()) }
|
|
246
|
+
products: source # five minutes of freshness is plenty
|
|
247
|
+
fetch: -> Product.array.parse(api.get!('products').json!()) # validated Product[]
|
|
248
|
+
staleTime: '5 min'
|
|
249
|
+
order: source { fetch: (id:: string) -> Order.parse(api.get!("orders/#{id}").json!()) }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
A source is **lazy** (loads on first touch, not at startup) and **not a
|
|
253
|
+
promise** — it owns its in-flight load internally; its value is part of
|
|
254
|
+
the stash's reactive fabric like any other key:
|
|
255
|
+
|
|
256
|
+
- **Reading is reading.** An ungated read (`@app.data.user`) is honest —
|
|
257
|
+
`User | null` — and kicks the load without blocking; the null branch is
|
|
258
|
+
the skeleton branch. A gated binding (`user <~ @app.data.user` in a
|
|
259
|
+
route or layout) is loaded before the component constructs and types
|
|
260
|
+
non-null.
|
|
261
|
+
- **Writing is assigning.** `@app.data.user = u` writes through to the
|
|
262
|
+
cell; every consumer updates in place. A write bumps the cell's
|
|
263
|
+
generation and aborts any in-flight load — an old fetch can never
|
|
264
|
+
clobber a newer write. `@app.data.user = null` means *invalidate*.
|
|
265
|
+
- **Keyed families.** A `fetch` with a required first param is one cell
|
|
266
|
+
per key: `@app.data.order(id)` is an ungated keyed read; a route gates
|
|
267
|
+
with `order <~ @app.data.order(params.id)`. Entries live under an LRU
|
|
268
|
+
cap and revalidate like singletons.
|
|
269
|
+
- **Freshness.** `staleTime` (ms, a duration string like `'5 min'`, or
|
|
270
|
+
`'forever'`) controls revalidation. The default `0` is
|
|
271
|
+
stale-while-revalidate: every gate serves the cached value instantly
|
|
272
|
+
and refetches in the background. A gate only ever *blocks* on unloaded
|
|
273
|
+
cells.
|
|
274
|
+
- **Errors are never cached.** A failed initial load returns the cell to
|
|
275
|
+
unloaded (the next gate retries) and rejects the navigation into the
|
|
276
|
+
nearest `onError` boundary as `{ status, message, error, path }`. A
|
|
277
|
+
failed refetch keeps the last-good value and records the error on the
|
|
278
|
+
handle — mounted gates are never yanked (the live-binding invariant).
|
|
279
|
+
- **The handle.** `app.data.source(path, key?)` returns reactive
|
|
280
|
+
`{ value, loading, error, refetch(), reset() }` for screens that render
|
|
281
|
+
*through* loading/error instead of gating.
|
|
282
|
+
- **One reset.** `app.data.reset()` restores plain keys to their declared
|
|
283
|
+
defaults, unloads every source (aborting in-flight loads, clearing
|
|
284
|
+
keyed caches), and purges the persisted snapshot — signout in one call.
|
|
285
|
+
- **Serialization is a projection.** `persistStash` persists plain keys
|
|
286
|
+
and skips source keys (server data is refetchable; the gate reloads on
|
|
287
|
+
restore). `app.data.peek(path)` is the non-triggering read for code
|
|
288
|
+
*about* the stash (serializers, devtools).
|
|
289
|
+
- **Preloading.** Off by default; opt in with `serve({ preload: 'intent' })`
|
|
290
|
+
in the root entry. Once on, resting the pointer on (or focusing) a
|
|
291
|
+
router-owned link for ~50ms warms the gates the destination would *newly*
|
|
292
|
+
mount — a layout already mounted under the current route is skipped, so
|
|
293
|
+
hovering siblings never re-fetches shared shell data. Navigation joins
|
|
294
|
+
loads already in flight (one in-flight load per source). Sweeping past a
|
|
295
|
+
link cancels before it fires, and preload failures never surface — the
|
|
296
|
+
cell records them and navigation's own gate retries.
|
|
297
|
+
|
|
298
|
+
Testing gated components needs no mocks: a write marks a cell loaded, so
|
|
299
|
+
seed and construct — `app.data.user = fixtureUser`, then `new Profile(...)`.
|
|
300
|
+
|
|
301
|
+
### createMutation
|
|
302
|
+
|
|
303
|
+
The write-side primitive. Reads are per-key (`source`); writes are
|
|
304
|
+
per-action — a mutation wraps the action and exposes reactive
|
|
305
|
+
`pending` / `succeeded` / `error`. Invoke it directly with the payload
|
|
306
|
+
(it *is* the action; there is no `.mutate`):
|
|
307
|
+
|
|
308
|
+
```rip
|
|
309
|
+
updateUser = createMutation ((data) -> User.parse(api.patch!('user', { json: data }).json!())),
|
|
310
|
+
onSuccess: (u) => @app.data.user = u # write-back — every reader updates, no refetch
|
|
311
|
+
onError: (e) => errors = parseApiError!(e)
|
|
312
|
+
|
|
313
|
+
submit: (e) -> e.preventDefault(); updateUser(form)
|
|
314
|
+
# render: the submit button reads updateUser.pending; banners read succeeded/error
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Callbacks (and a source's `fetch:`) are plain options on a runtime call —
|
|
318
|
+
nothing binds their `this`. Use `=>` for anything touching component
|
|
319
|
+
members.
|
|
320
|
+
|
|
217
321
|
### createResource
|
|
218
322
|
|
|
219
|
-
|
|
220
|
-
state
|
|
323
|
+
The standalone packaging of the same async cell, for **instance-local**
|
|
324
|
+
async (a search box's per-instance results are component state, not app
|
|
325
|
+
data). Same race protection, abort support, and reactive loading state.
|
|
221
326
|
|
|
222
327
|
```rip
|
|
223
328
|
user = createResource ->
|
|
@@ -291,6 +396,7 @@ router = createRouter components,
|
|
|
291
396
|
|
|
292
397
|
router.push '/users/42?tab=settings'
|
|
293
398
|
router.replace '/login'
|
|
399
|
+
router.push '/cart', noScroll: true # don't reset scroll on this nav
|
|
294
400
|
router.back()
|
|
295
401
|
router.forward()
|
|
296
402
|
|
|
@@ -316,6 +422,65 @@ The renderer uses `router.current` to drive its mount effect. Each
|
|
|
316
422
|
field is also a separate signal so subscribers can track only what
|
|
317
423
|
they care about.
|
|
318
424
|
|
|
425
|
+
#### Anchor opt-outs and active-link styling
|
|
426
|
+
|
|
427
|
+
The router intercepts plain `<a>` clicks at the document level. Two
|
|
428
|
+
per-anchor attributes adjust that behavior:
|
|
429
|
+
|
|
430
|
+
| Attribute | Effect |
|
|
431
|
+
| ----------------------- | ------------------------------------------------------------------------------------- |
|
|
432
|
+
| `data-router-ignore` | Skip SPA interception entirely. The browser performs a full navigation. |
|
|
433
|
+
| `data-router-noscroll` | Take the SPA navigation, but don't reset scroll to `(0, 0)`. |
|
|
434
|
+
|
|
435
|
+
Anchors with `target="_blank"`, `[download]`, cross-origin hrefs, or
|
|
436
|
+
hrefs outside `base` are also skipped automatically.
|
|
437
|
+
|
|
438
|
+
**Active-link highlighting.** On every navigation the router walks
|
|
439
|
+
in-document anchors and sets `aria-current` on those that match the
|
|
440
|
+
current path:
|
|
441
|
+
|
|
442
|
+
- exact match → `aria-current="page"`
|
|
443
|
+
- prefix match (`/blog` on `/blog/123`) → `aria-current="true"`
|
|
444
|
+
- otherwise → attribute removed (only if the router set it)
|
|
445
|
+
|
|
446
|
+
Style it with attribute selectors — no per-link boilerplate needed:
|
|
447
|
+
|
|
448
|
+
```css
|
|
449
|
+
nav a[aria-current="page"] { color: red; font-weight: bold; }
|
|
450
|
+
nav a[aria-current="true"] { color: red; }
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Setting `aria-current` manually on an anchor wins — the router only
|
|
454
|
+
touches values it set itself.
|
|
455
|
+
|
|
456
|
+
**Scroll restoration.** New navigations (`push` or a link click)
|
|
457
|
+
reset scroll to `(0, 0)`. Back/forward (`popstate`) restores the
|
|
458
|
+
scroll position the page had when you left it. Same-document fragment
|
|
459
|
+
links (`#section`) defer to the browser.
|
|
460
|
+
|
|
461
|
+
**Typed routes (compile-time).** In a typed project (one with
|
|
462
|
+
`rip.strict: true` or `::` annotations), `rip check` synthesizes a
|
|
463
|
+
`__RipRoutes` union from the file tree under `app/routes/` and
|
|
464
|
+
threads it through three places:
|
|
465
|
+
|
|
466
|
+
| Place | Type | Catches |
|
|
467
|
+
| ---------------------------------- | --------------------------------------------------------------------- | ---------------------------------- |
|
|
468
|
+
| `<a href: "...">` in render blocks | `__RipRoutes` for `/`-prefixed literals; any string otherwise | Typos in known routes |
|
|
469
|
+
| `router.push url, opts?` | `__RipRoutes` (replaces base `string`) | Typos in programmatic navigation |
|
|
470
|
+
| `@params` in `routes/[id].rip` | `{ id: string }` (replaces `Record<string, string>`) | Typos like `@params.bogus` |
|
|
471
|
+
|
|
472
|
+
Anchor `href` uses a `const`-generic conditional: a literal starting
|
|
473
|
+
with `/` must satisfy `__RipRoutes`, while external schemes
|
|
474
|
+
(`https://`, `mailto:`, `tel:`), fragments (`#anchor`), and dynamic
|
|
475
|
+
`string` values fall through unchecked. Typos like `<a href: "/crat">`
|
|
476
|
+
produce a single-line error naming the valid routes.
|
|
477
|
+
`router.replace` is deliberately left at `string` — it's commonly
|
|
478
|
+
used to mutate the current URL with query strings, where the built
|
|
479
|
+
value can't satisfy a literal-route union. Catch-all routes
|
|
480
|
+
(`[...rest].rip`) are excluded from `__RipRoutes` — they're runtime
|
|
481
|
+
404 fallbacks, not navigation targets, so including them as
|
|
482
|
+
`/${string}` would defeat typo-catching for every other route.
|
|
483
|
+
|
|
319
484
|
### createRenderer
|
|
320
485
|
|
|
321
486
|
The render loop. Subscribes to `router.current`, mounts/unmounts
|
|
@@ -422,6 +587,8 @@ mount(target) ← only the renderer calls this directly
|
|
|
422
587
|
Per-child push wrap: each child's _create runs with
|
|
423
588
|
child as current, so the child's reactive bindings
|
|
424
589
|
register on child._disposers, not parent's.
|
|
590
|
+
beforeMount() ← user hook; signals/state ready, DOM not yet in tree
|
|
591
|
+
effects created here auto-register on this component
|
|
425
592
|
_setup() ← post-creation effects (rare; most go in _init)
|
|
426
593
|
mounted() ← user hook; DOM is in the tree now
|
|
427
594
|
__popComponent
|
|
@@ -436,6 +603,20 @@ unmount({ removeDOM = true }) ← idempotent (_unmounted flag short-circuits se
|
|
|
436
603
|
DOM removal (if requested)
|
|
437
604
|
```
|
|
438
605
|
|
|
606
|
+
### User hooks
|
|
607
|
+
|
|
608
|
+
The framework recognizes these hook names on any component. All are
|
|
609
|
+
optional; the runtime calls each only if defined.
|
|
610
|
+
|
|
611
|
+
| Hook | When it fires | Notes |
|
|
612
|
+
| --------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
613
|
+
| `beforeMount` | After `_create`, before DOM is attached | Effects created here auto-register on the component |
|
|
614
|
+
| `mounted` | After DOM attached | Runs once per visit |
|
|
615
|
+
| `beforeUnmount` | Before children unmount and disposers fire | Signals/effects still live |
|
|
616
|
+
| `unmounted` | After disposers fire and DOM is removed | Final notification; runs once per visit |
|
|
617
|
+
| `onError` | A throw escapes any component method (render, hook, event) | Receives `{ status?, message?, error?, path? }`; the renderer walks the layout chain to find the nearest defining component |
|
|
618
|
+
|
|
619
|
+
|
|
439
620
|
### Effect ownership
|
|
440
621
|
|
|
441
622
|
Every `__effect(fn)` call automatically registers its disposer with
|
package/docs/RIP-DUCKDB.md
CHANGED
|
@@ -31,6 +31,7 @@ you're in the right place.
|
|
|
31
31
|
6. [When you genuinely need to mutate a referenced indexed column](#6-when-you-genuinely-need-to-mutate)
|
|
32
32
|
7. [Escape hatches](#7-escape-hatches)
|
|
33
33
|
8. [Should I use DuckDB for OLTP at all?](#8-should-i-use-duckdb-for-oltp-at-all)
|
|
34
|
+
9. [Transactions, sequences, and migration edges](#9-transactions-sequences-and-migration-edges)
|
|
34
35
|
|
|
35
36
|
---
|
|
36
37
|
|
|
@@ -184,7 +185,7 @@ was originally documented from:
|
|
|
184
185
|
```coffee
|
|
185
186
|
Partner = schema :model
|
|
186
187
|
name! string
|
|
187
|
-
slug
|
|
188
|
+
slug! string @unique
|
|
188
189
|
|
|
189
190
|
Patient = schema :model
|
|
190
191
|
mrn? string
|
|
@@ -461,12 +462,74 @@ DuckDB is a fine fit when:
|
|
|
461
462
|
- You want analytics queries to live in the same engine as the
|
|
462
463
|
transactional data
|
|
463
464
|
|
|
465
|
+
### Turning a mutation-shaped workload into an append-shaped one
|
|
466
|
+
|
|
467
|
+
A workload that looks write-heavy on paper is often append-heavy in
|
|
468
|
+
disguise. The standard move — shared by change feeds, event logs,
|
|
469
|
+
audit trails, and sync protocols (Replicache / Zero-style) — is an
|
|
470
|
+
**append-only log with a monotonic version column** beside the entity
|
|
471
|
+
tables: a write appends a new row instead of mutating an indexed
|
|
472
|
+
parent in place, and readers pull `WHERE version > :cursor`. This
|
|
473
|
+
plays directly to DuckDB's strengths (append-heavy writes, fast
|
|
474
|
+
monotonic range scans) and routes around the one real weak spot from
|
|
475
|
+
§1 — in-place updates to indexed / FK rows, which an append never
|
|
476
|
+
performs. The trade is the usual one for log-structured designs: the
|
|
477
|
+
log grows and wants periodic compaction (prune history below the
|
|
478
|
+
lowest live reader cursor, `CHECKPOINT` to flush), and "current state"
|
|
479
|
+
becomes a view or materialization over the log rather than the rows
|
|
480
|
+
themselves. When a write-heavy feature can be expressed this way, the
|
|
481
|
+
"poor fit" case above (routinely mutating indexed identifiers) often
|
|
482
|
+
stops applying — and harbor's single-port `/sql` with NDJSON streaming
|
|
483
|
+
is a natural transport for serving the resulting feed.
|
|
484
|
+
|
|
464
485
|
For a project where you're not sure: prototype with DuckDB, watch
|
|
465
486
|
the FK behavior, and switch to PostgreSQL or SQLite if you find
|
|
466
487
|
yourself reaching for the escape hatches more than once or twice.
|
|
467
488
|
|
|
468
489
|
---
|
|
469
490
|
|
|
491
|
+
## 9. Transactions, sequences, and migration edges
|
|
492
|
+
|
|
493
|
+
Findings from running the Rip Schema transaction and migration
|
|
494
|
+
machinery against live DuckDB (via duckdb-harbor) — each one shapes
|
|
495
|
+
runtime or differ behavior:
|
|
496
|
+
|
|
497
|
+
**FK-referenced tables are frozen for DDL too.** The §1 rule isn't
|
|
498
|
+
just about UPDATE/DELETE — most `ALTER TABLE` operations (add column,
|
|
499
|
+
drop column, type changes) on a table referenced by another table's FK
|
|
500
|
+
fail with `Dependency Error: Cannot alter entry "users" because there
|
|
501
|
+
are entries that depend on it`. Changing such a table means recreating
|
|
502
|
+
the referencing tables around it. The migration differ classifies
|
|
503
|
+
these steps as **`blocked`** in `rip schema status` and refuses to
|
|
504
|
+
write them into a migration file — the rebuild is a human decision.
|
|
505
|
+
Unreferenced tables alter normally.
|
|
506
|
+
|
|
507
|
+
**No `SAVEPOINT`.** DuckDB supports flat transactions only. A nested
|
|
508
|
+
`schema.transaction!` therefore *joins* the outer transaction rather
|
|
509
|
+
than creating an independently-rollbackable unit — one rollback undoes
|
|
510
|
+
the whole nest. Don't design flows that need partial rollback.
|
|
511
|
+
|
|
512
|
+
**Sequences are non-transactional.** `nextval()` consumed inside a
|
|
513
|
+
rolled-back transaction is not returned — a failed `create!` leaves a
|
|
514
|
+
gap in the `id` sequence. Gaps are normal and harmless; never write
|
|
515
|
+
code that assumes ids are contiguous, and never predict "the next id"
|
|
516
|
+
from the last one you saw.
|
|
517
|
+
|
|
518
|
+
**Harbor sessions work in every auth mode.** Transactions ride
|
|
519
|
+
duckdb-harbor's session protocol (`POST /sql/sessions/new`, then
|
|
520
|
+
per-statement `session_id`). Own-session lifecycle is scoped as
|
|
521
|
+
`__HARBOR_SELF__:sessions:create` / `:delete` — allowed by default for
|
|
522
|
+
any caller, including unauthenticated local-dev mode
|
|
523
|
+
(`harbor_serve(..., token := NULL)`), where sessions are owned by the
|
|
524
|
+
synthetic `harbor.local-dev` principal. A 403 from
|
|
525
|
+
`schema.transaction!` means a custom `harbor_authorization_function`
|
|
526
|
+
explicitly denies the `__HARBOR_SELF__:sessions:` scope — add a branch
|
|
527
|
+
matching it. (Earlier harbor versions misfiled session creation as an
|
|
528
|
+
admin action; that required `RIP_DB_TOKEN` plus an admin grant and is
|
|
529
|
+
the reason older notes here said transactions need a token.)
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
470
533
|
## See also
|
|
471
534
|
|
|
472
535
|
- [`docs/RIP-SCHEMA.md`](./RIP-SCHEMA.md) — the schema/ORM documentation,
|
package/docs/RIP-INTRO.md
CHANGED
|
@@ -78,7 +78,7 @@ Read as families, not atoms:
|
|
|
78
78
|
|---|---|---|
|
|
79
79
|
| Existence / safety | `x?` · `x ?? y` · `a?.b` · `a?.[0]` · `a?.()` · `a?[0]` · `a?(x)` · `el?.prop = v` · `?!` (presence / Houdini) · `?? throw` | Nothing-safe access and guards |
|
|
80
80
|
| Dammit / await | `fetch! url` · `user.save!` · `User.find! 1` | One glyph: "call it and await" |
|
|
81
|
-
| Void / required | `def process!` (suppresses implicit return) · `name! string` (required field) · `email
|
|
81
|
+
| Void / required | `def process!` (suppresses implicit return) · `name! string` (required field) · `email! email @unique` (required + unique) | Same `!` glyph, context-disambiguated |
|
|
82
82
|
| Math | `//` floor div · `%%` true mod · `1 < x < 10` chained compare · `arr[-1]` negative index · `"-" * 40` string repeat | Math you can read |
|
|
83
83
|
| Regex | `str =~ /re/` with `_[1]` captures · `str[/re/, 1]` · `///...///` heregex | Pattern matching as an expression |
|
|
84
84
|
| Assignment sugar | `.=` method-assign (`x .= trim()`) · `?.=` optional-chain assign · `*>obj = {a:1}` merge-assign | "Mutate this thing" |
|
|
@@ -188,7 +188,7 @@ Status = schema
|
|
|
188
188
|
# DB-backed model
|
|
189
189
|
User = schema :model
|
|
190
190
|
name! string
|
|
191
|
-
email
|
|
191
|
+
email! email @unique
|
|
192
192
|
role? "admin" | "user"
|
|
193
193
|
@timestamps
|
|
194
194
|
@has_many Order
|
|
@@ -363,7 +363,7 @@ A close second is **non-reactive reads due to aliasing / stash access patterns**
|
|
|
363
363
|
| `fetch!` | dammit — call + await |
|
|
364
364
|
| `def fn!` | void — suppress implicit return |
|
|
365
365
|
| `name! string` | required field (in `schema` body) |
|
|
366
|
-
| `email
|
|
366
|
+
| `email! email @unique` | required + unique (in `schema :model` body) |
|
|
367
367
|
| `MAX =! 100` | readonly const |
|
|
368
368
|
|
|
369
369
|
### The `?` family
|
|
@@ -383,7 +383,7 @@ A close second is **non-reactive reads due to aliasing / stash access patterns**
|
|
|
383
383
|
| Line form | Example |
|
|
384
384
|
|---|---|
|
|
385
385
|
| Field (type implicit string) | `name! 1..50` |
|
|
386
|
-
| Field + modifiers | `email
|
|
386
|
+
| Field + modifiers | `email! email @unique` (required + unique) |
|
|
387
387
|
| Field + range | `password! 8..100` |
|
|
388
388
|
| Field + literal union | `sex? "M" \| "F" \| "U"` |
|
|
389
389
|
| Inline field transform | `email!, -> it.email.toLowerCase()` |
|
package/docs/RIP-LANG.md
CHANGED
|
@@ -346,12 +346,12 @@ Multiple lines
|
|
|
346
346
|
| `[-n]` | Negative index | `arr[-1]` | `arr.at(-1)` |
|
|
347
347
|
| `*` | String repeat | `"-" * 40` | `"-".repeat(40)` |
|
|
348
348
|
| `<` `<=` | Chained comparison | `1 < x < 10` | `(1 < x) && (x < 10)` |
|
|
349
|
-
| `\|>` | Pipe | `x \|> fn` or `x \|> fn(y)` | `fn(x)` or `fn(x, y)` |
|
|
350
349
|
| `.=` | Method assign | `x .= trim()` | `x = x.trim()` |
|
|
351
350
|
| `*>` | Merge assign | `*>obj = {a: 1}` | `Object.assign(obj, {a: 1})` |
|
|
352
351
|
| `not in` | Not in | `x not in arr` | Negated membership test |
|
|
353
352
|
| `not of` | Not of | `k not of obj` | Negated key existence |
|
|
354
353
|
| `<=>` | Two-way bind | `value <=> name` | Bidirectional reactive binding (render blocks) |
|
|
354
|
+
| `<~` | Render-ready | `user <~ @app.data.user` | Load-before-render binding (component bodies) |
|
|
355
355
|
| `*{ }` | Map literal | `*{/pat/: val}` | `new Map([[/pat/, val]])` |
|
|
356
356
|
| `:name` | Symbol literal | `:redo` | `Symbol.for("redo")` — Ruby-style interned symbol |
|
|
357
357
|
|
|
@@ -700,32 +700,6 @@ arr[i] # → arr[i] — variable index
|
|
|
700
700
|
|
|
701
701
|
Only literal negative numbers trigger the `.at()` transform. Variable indexes pass through as-is.
|
|
702
702
|
|
|
703
|
-
## Pipe Operator (`|>`)
|
|
704
|
-
|
|
705
|
-
Pipes a value into a function as its first argument. Chains left-to-right:
|
|
706
|
-
|
|
707
|
-
```coffee
|
|
708
|
-
# Simple reference — value becomes the sole argument
|
|
709
|
-
5 |> double # → double(5)
|
|
710
|
-
10 |> Math.sqrt # → Math.sqrt(10)
|
|
711
|
-
"hello" |> console.log # → console.log("hello")
|
|
712
|
-
|
|
713
|
-
# Multi-arg — value is inserted as the FIRST argument
|
|
714
|
-
5 |> add(3) # → add(5, 3)
|
|
715
|
-
data |> filter(isActive) # → filter(data, isActive)
|
|
716
|
-
"World" |> greet("!") # → greet("World", "!")
|
|
717
|
-
|
|
718
|
-
# Chaining — reads left-to-right like a pipeline
|
|
719
|
-
5 |> double |> add(1) |> console.log
|
|
720
|
-
# → console.log(add(double(5), 1))
|
|
721
|
-
|
|
722
|
-
# Works with dotted methods
|
|
723
|
-
users |> Array.from # → Array.from(users)
|
|
724
|
-
data |> JSON.stringify(null, 2) # → JSON.stringify(data, null, 2)
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
This is the **Elixir-style** pipe — strictly better than F#'s (which only supports bare function references) and cleaner than Hack's (which requires a `%` placeholder). No special syntax to learn; if the right side is a call, the left value goes first.
|
|
728
|
-
|
|
729
703
|
---
|
|
730
704
|
|
|
731
705
|
# 4. Functions
|
|
@@ -907,6 +881,7 @@ Rip's reactive features are **language-level operators**, not library imports.
|
|
|
907
881
|
| `:=` | State | "gets state" | Reactive state variable |
|
|
908
882
|
| `~=` | Computed | "always equals" | Computed value (auto-updates) |
|
|
909
883
|
| `~>` | Effect | "always calls" | Side effect on dependency change |
|
|
884
|
+
| `<~` | Render-ready | "loads from" | Server-backed state, loaded before render (component bodies) |
|
|
910
885
|
| `=!` | Readonly | "equals, dammit!" | Constant (`const`) |
|
|
911
886
|
|
|
912
887
|
## Reactive Behavior
|
|
@@ -968,6 +943,35 @@ ticker ~>
|
|
|
968
943
|
-> clearInterval interval # Cleanup function
|
|
969
944
|
```
|
|
970
945
|
|
|
946
|
+
## Render-Ready State (`<~`)
|
|
947
|
+
|
|
948
|
+
The fourth creation form completes the reactivity grid: `<~` binds a
|
|
949
|
+
component member to a server-backed stash key (a `source` — see the
|
|
950
|
+
App framework docs) and declares that the key must be **loaded before
|
|
951
|
+
the component renders**. The binding is therefore non-null — no `if user`
|
|
952
|
+
guards, no loading flags:
|
|
953
|
+
|
|
954
|
+
```coffee
|
|
955
|
+
export Profile = component
|
|
956
|
+
user <~ @app.data.user # loaded before render → non-null
|
|
957
|
+
form := { ...user } # synchronous — the value is present
|
|
958
|
+
order <~ @app.data.order(params.id) # keyed source: one cell per id
|
|
959
|
+
theme <~ @app.data.settings.theme # subpath: loads the nearest source
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
Rules, all enforced at compile time or deterministically at mount:
|
|
963
|
+
|
|
964
|
+
- `<~` is only valid at the top of a **component body**, and only in
|
|
965
|
+
routes and layouts (a reusable child takes gated values as props).
|
|
966
|
+
- The right-hand side must be a literal `@app.data.…` path — the compiler
|
|
967
|
+
hoists it into a static gate-set the renderer reads before construction.
|
|
968
|
+
- A keyed gate's key expression may only reference `params` / `query`.
|
|
969
|
+
- The path must resolve to a `source` key — gating a plain key is an error.
|
|
970
|
+
|
|
971
|
+
An **ungated** read of the same key (`user ~= @app.data.user`) is the
|
|
972
|
+
progressive-rendering form: it types as `T | null`, kicks the load without
|
|
973
|
+
blocking, and the null branch is the skeleton branch.
|
|
974
|
+
|
|
971
975
|
## Auto-Unwrapping
|
|
972
976
|
|
|
973
977
|
Reactive variables automatically unwrap in most contexts:
|
|
@@ -1593,12 +1597,11 @@ App = component
|
|
|
1593
1597
|
|
|
1594
1598
|
```coffee
|
|
1595
1599
|
App = component
|
|
1596
|
-
beforeMount
|
|
1597
|
-
mounted
|
|
1598
|
-
updated = -> p "updated"
|
|
1600
|
+
beforeMount = -> p "about to mount"
|
|
1601
|
+
mounted = -> p "mounted"
|
|
1599
1602
|
beforeUnmount = -> p "about to unmount"
|
|
1600
|
-
unmounted
|
|
1601
|
-
onError
|
|
1603
|
+
unmounted = -> p "unmounted"
|
|
1604
|
+
onError = (err) -> p "caught: #{err.message}"
|
|
1602
1605
|
```
|
|
1603
1606
|
|
|
1604
1607
|
**Effects:**
|
|
@@ -1880,7 +1883,7 @@ Status = schema
|
|
|
1880
1883
|
# DB-backed model with ORM and migrations
|
|
1881
1884
|
User = schema :model
|
|
1882
1885
|
name! string
|
|
1883
|
-
email
|
|
1886
|
+
email! email @unique
|
|
1884
1887
|
@timestamps
|
|
1885
1888
|
@has_many Order
|
|
1886
1889
|
beforeValidation: -> @email = @email.toLowerCase()
|
|
@@ -2369,11 +2372,6 @@ Each would need design discussion before building.
|
|
|
2369
2372
|
`user ~>? fetch!("/api/users/#{id}").json!` gives `user.loading`,
|
|
2370
2373
|
`user.error`, `user.data`. Park until real-world usage shows demand.
|
|
2371
2374
|
|
|
2372
|
-
- **Pipe operator (`|>`) — Hack-style placeholder** — Currently Rip uses
|
|
2373
|
-
Elixir-style first-arg insertion. A `%` placeholder for arbitrary position
|
|
2374
|
-
(`data |> fn(1, %, 3)`) could be added later if needed. Current design
|
|
2375
|
-
covers 95%+ of cases.
|
|
2376
|
-
|
|
2377
2375
|
---
|
|
2378
2376
|
|
|
2379
2377
|
## Resources
|