funicular 0.3.0 → 0.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +486 -1
- data/demo/local_notes.html +207 -0
- data/demo/test_chartjs.html +9 -9
- data/demo/test_component.html +8 -8
- data/demo/test_error_boundary.html +44 -41
- data/demo/test_router.html +48 -48
- data/demo/tic-tac-toe.html +25 -25
- data/docs/architecture.md +227 -12
- data/docs/local_database.md +1035 -0
- data/lib/funicular/assets/funicular.rb +14 -0
- data/lib/funicular/configuration.rb +65 -0
- data/lib/funicular/epoch_header.rb +69 -0
- data/lib/funicular/epoch_stamping.rb +66 -0
- data/lib/funicular/helpers/picoruby_helper.rb +96 -1
- data/lib/funicular/railtie.rb +30 -0
- data/lib/funicular/schema.rb +45 -12
- data/lib/funicular/session_epoch.rb +110 -0
- data/lib/funicular/ssr/runtime.rb +58 -12
- data/lib/funicular/ssr.rb +25 -0
- data/lib/funicular/testing/node_runner.mjs +19 -0
- data/lib/funicular/testing.rb +47 -0
- data/lib/funicular/vendor/mrbc/VERSION +1 -1
- data/lib/funicular/vendor/mrbc/mrbc.js +655 -574
- data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -1
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +800 -530
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -2
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
- data/lib/funicular/vendor/picoruby-test-node/picoruby.js +2 -6909
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/lib/funicular/version.rb +1 -1
- data/lib/funicular.rb +1 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +37 -38
- data/lib/tasks/funicular.rake +10 -2
- data/minitest/callback_error_visibility_test.rb +48 -0
- data/minitest/configuration_test.rb +78 -0
- data/minitest/dsl_test.rb +264 -0
- data/minitest/epoch_header_test.rb +149 -0
- data/minitest/epoch_stamping_test.rb +225 -0
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +5 -5
- data/minitest/fixtures/funicular_app/components/probe_component.rb +15 -0
- data/minitest/form_for_test.rb +2 -2
- data/minitest/hydration_test.rb +2 -2
- data/minitest/navigation_guard_test.rb +65 -0
- data/minitest/picoruby_helper_test.rb +236 -0
- data/minitest/schema_test.rb +47 -0
- data/minitest/session_epoch_test.rb +122 -0
- data/minitest/sig_tags_test.rb +30 -0
- data/minitest/ssr_database_test.rb +78 -0
- data/minitest/ssr_reload_test.rb +106 -0
- data/minitest/ssr_test.rb +41 -0
- data/minitest/testing_ensure_compiled_test.rb +52 -0
- data/minitest/validations_test.rb +35 -5
- data/minitest/view_context_test.rb +15 -15
- data/mrbgem.rake +2 -0
- data/mrblib/0_tags.rb +62 -0
- data/mrblib/cable.rb +1 -1
- data/mrblib/component.rb +226 -24
- data/mrblib/db.rb +3116 -0
- data/mrblib/error_boundary.rb +25 -19
- data/mrblib/file_upload.rb +17 -7
- data/mrblib/form_builder.rb +10 -10
- data/mrblib/funicular.rb +136 -17
- data/mrblib/http.rb +84 -107
- data/mrblib/model.rb +1178 -23
- data/mrblib/relation.rb +342 -0
- data/mrblib/router.rb +45 -4
- data/mrblib/styles.rb +122 -12
- data/mrblib/view_context.rb +3 -32
- data/sig/component.rbs +25 -4
- data/sig/db.rbs +328 -0
- data/sig/error_boundary.rbs +4 -4
- data/sig/funicular.rbs +5 -0
- data/sig/http.rbs +8 -21
- data/sig/model.rbs +101 -7
- data/sig/relation.rbs +44 -0
- data/sig/router.rbs +1 -0
- data/sig/styles.rbs +19 -5
- data/sig/tags.rbs +54 -0
- data/sig/view_context.rbs +47 -34
- metadata +23 -2
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +0 -1
data/docs/architecture.md
CHANGED
|
@@ -30,40 +30,238 @@ stay free of browser-only calls on any server code path
|
|
|
30
30
|
|---------------------------------------------------------|-------------------------------------------------------------------------------------------------|
|
|
31
31
|
| `funicular.rb` | Top-level module: `start`, `router`, `server?`, `debug_color` export |
|
|
32
32
|
| `runtime.rb` | Per-app runtime context propagated through render/SSR/hydration |
|
|
33
|
-
| `
|
|
33
|
+
| `0_tags.rb` | Bareword tag DSL mixed into `Component`; reserved-name list and collision errors |
|
|
34
|
+
| `view_context.rb` | Internal element factory shared by the tag DSL, FormBuilder, and framework helpers |
|
|
34
35
|
| `component.rb` | `Funicular::Component` base: state, props, lifecycle, suspense loading, refs |
|
|
35
36
|
| `vdom.rb` | Virtual DOM nodes, including component vnodes with ordinary `children` |
|
|
36
37
|
| `differ.rb` | `Differ.diff(old, new)` -- minimal patch set, key-based list reconciliation |
|
|
37
38
|
| `patcher.rb` | `Patcher.apply(dom, patches)` -- apply patches to the real DOM |
|
|
38
39
|
| `html_serializer.rb` | `VDOM::HTMLSerializer` -- VDOM to HTML string (used by SSR) |
|
|
39
40
|
| `router.rb` | Client-side router, route DSL, per-runtime route helper object, History API |
|
|
40
|
-
| `model.rb` | Object-REST Mapper (`all`/`find`/`create`/`update`/`destroy`)
|
|
41
|
-
| `
|
|
41
|
+
| `model.rb` | Object-REST Mapper (`all`/`find`/`create`/`update`/`destroy`) + local query API (`storage`/`refresh`, associations, `migrate` blocks) |
|
|
42
|
+
| `db.rb` | `Funicular::DB`: local SQLite databases, DDL derivation, snapshot persistence, change events, `wipe` |
|
|
43
|
+
| `relation.rb` | Lazy chainable Relation and SQL builder for local queries |
|
|
44
|
+
| `http.rb` | Low-level fetch wrapper, CSRF |
|
|
42
45
|
| `cable.rb` | ActionCable-compatible consumer/subscription client |
|
|
43
|
-
| `store.rb`, `store_singleton.rb`, `store_collection.rb` | IndexedDB-backed stores
|
|
44
|
-
| `form_builder.rb` | `
|
|
46
|
+
| `store.rb`, `store_singleton.rb`, `store_collection.rb` | IndexedDB-backed stores (superseded by the local database; see below) |
|
|
47
|
+
| `form_builder.rb` | `form_for` field helpers with inline error rendering |
|
|
45
48
|
| `0_validations.rb`, `1_validators.rb` | ActiveModel-style validators and `errors` |
|
|
46
|
-
| `styles.rb` | CSS-in-Ruby `styles
|
|
49
|
+
| `styles.rb` | CSS-in-Ruby bareword `styles do ... end` builder and generated `styles.name` accessors |
|
|
47
50
|
| `error_boundary.rb` | `ErrorBoundary` component |
|
|
48
51
|
| `file_upload.rb` | File / FormData upload helper |
|
|
49
52
|
| `debug.rb` | Development-only component/error registry for the DevTools extension |
|
|
50
53
|
| `environment_inquirer.rb` | Environment detection (`server?`, `development?`) |
|
|
51
54
|
|
|
52
55
|
The render cycle: a state change calls `patch()`, which rebuilds the component's
|
|
53
|
-
VDOM by calling `render
|
|
56
|
+
VDOM by calling `render`, diffs it against the previous VDOM with `Differ`,
|
|
54
57
|
and applies the result with `Patcher`. Event handlers are native DOM listeners,
|
|
55
58
|
re-bound on each render.
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`
|
|
60
|
-
`
|
|
60
|
+
Inside `render` (zero-arity as of 0.4.0), `self` is the component, so the
|
|
61
|
+
DSL is bareword: HTML is authored as `div`, custom elements as
|
|
62
|
+
`tag(:custom_element)`, child components as `component`, forms as `form_for`,
|
|
63
|
+
styles as `styles.name(variant)` or `styles[:name]`, resources as
|
|
64
|
+
`resources[:name]`, and routes as `routes.user_path(id)`. Component state is
|
|
61
65
|
explicitly read with `state[:name]` or `state.fetch(:name)`.
|
|
62
66
|
|
|
67
|
+
Tag and helper names (~46 words) are reserved inside component classes:
|
|
68
|
+
defining one raises `DSLCollisionError` at class-definition time
|
|
69
|
+
(`method_added`) or at first mount (`validate_dsl_conflicts!`, which also
|
|
70
|
+
covers `attr_*` on mruby and included modules). `allow_dsl_override :name`
|
|
71
|
+
opts out per class; the shadowed element stays reachable via `tag(:name)`.
|
|
72
|
+
Two caveats are inherent to barewords: `p` builds a `<p>` element (use
|
|
73
|
+
`puts x.inspect` for debugging; non-Hash arguments raise with a hint), and
|
|
74
|
+
a local variable named after a tag shadows the zero-paren call form (write
|
|
75
|
+
`option()` or rename the local). Procs handed to ANOTHER component --
|
|
76
|
+
ErrorBoundary's `fallback:` -- run under that component's cursor and
|
|
77
|
+
therefore receive an explicit view context instead of barewords.
|
|
78
|
+
|
|
79
|
+
Style definitions are bareword too: the class-level `styles do ... end`
|
|
80
|
+
block runs on a BasicObject cleanroom builder, so any name (including
|
|
81
|
+
`display`, `hash`, ...) defines a style identically on mruby and CRuby.
|
|
82
|
+
Computed values need the explicit form `styles { |css| css.define(...) }`.
|
|
83
|
+
Unknown style lookups raise instead of returning an empty class string.
|
|
84
|
+
|
|
63
85
|
Component children are ordinary VDOM children stored on
|
|
64
86
|
`VDOM::Component#children`; there is no delayed `children_block` prop. This keeps
|
|
65
87
|
SSR, diffing, ErrorBoundary rendering, and hydration on the same data model.
|
|
66
88
|
|
|
89
|
+
## Local database (sqlite3)
|
|
90
|
+
|
|
91
|
+
`Funicular::Model` is backed by an in-browser SQLite database
|
|
92
|
+
(picoruby-sqlite3 on wasm: `:memory:` database + IndexedDB snapshot
|
|
93
|
+
persistence). The user-facing contract is documented in
|
|
94
|
+
[local_database.md](local_database.md); the contributor-relevant invariants
|
|
95
|
+
are:
|
|
96
|
+
|
|
97
|
+
- The entire subsystem is optional and defaults off. Rails emits an explicit
|
|
98
|
+
page opt-in only for `config.local_database = true`; without it schema and
|
|
99
|
+
REST traffic continue normally, while DB boot, SQLite, IndexedDB, Web Locks,
|
|
100
|
+
replica write-through, and session-epoch handling do not run. Disabled
|
|
101
|
+
`storage :local` declarations fail at the pre-DOM start gate, and runtime
|
|
102
|
+
local APIs raise `UnavailableError`. Disabling does not delete snapshots.
|
|
103
|
+
|
|
104
|
+
- Two databases split by durability class: `funicular_replica`
|
|
105
|
+
(server-recoverable, dropped and rebuilt on schema mismatch) and
|
|
106
|
+
`funicular_local` (client-only data, evolved via numbered `migrate` blocks
|
|
107
|
+
with per-table versions in a meta table; never dropped in production --
|
|
108
|
+
dev auto-resets on migration failure, `reset: true` baselines and
|
|
109
|
+
`Model.reset_local` are the explicit reset paths). Both auto-persist,
|
|
110
|
+
in the `persistent_writer` state only, via debounce plus a
|
|
111
|
+
`visibilitychange` backstop.
|
|
112
|
+
- One apply entry point: every FRAMEWORK-CONTROLLED replica write
|
|
113
|
+
(fetch-through, write-through, and, in the future, Cable-pushed
|
|
114
|
+
replication) funnels through the same upsert/delete path in
|
|
115
|
+
`Funicular::DB`, which is also where per-table change events fire. The
|
|
116
|
+
sanctioned exception is app-level raw SQL through the guarded handles,
|
|
117
|
+
whose contract is to call `notify_changed` afterwards. Relation#delete_all
|
|
118
|
+
exists only for `storage :local`; on replica Relations it raises
|
|
119
|
+
ReplicaWriteError even on the writer tab.
|
|
120
|
+
- The source-of-truth contract on `Funicular::Model`: the bare class targets
|
|
121
|
+
the model's source of truth -- REST verbs with optional callbacks of ONE
|
|
122
|
+
shape, `(result, error)`, for replica/ephemeral models (BREAKING vs <=0.4:
|
|
123
|
+
update/destroy were boolean-first), the local table for `storage :local`
|
|
124
|
+
models (there the `.local` prefix is an optional alias). `.local` is the
|
|
125
|
+
explicitly-marked local/cache view: it returns immediate values and raises
|
|
126
|
+
on genuine bugs; on ephemeral models it raises
|
|
127
|
+
`Funicular::DB::NoTableError`.
|
|
128
|
+
- Boot barrier: all schemas are collected before replica DDL/fingerprint
|
|
129
|
+
work runs (exactly once per boot); any schema failure fails startup with
|
|
130
|
+
aggregated errors. The fingerprint covers DDL-affecting data only and IS
|
|
131
|
+
the canonical schema JSON, stored in a replica metadata table and
|
|
132
|
+
compared by string equality (no digest dependency; never user_version).
|
|
133
|
+
- Isolation: namespace identity is a typed, versioned tuple
|
|
134
|
+
(["v1", app_id, "anonymous"] / ["v1", app_id, "user", key]) encoded as
|
|
135
|
+
canonical JSON -- never naive concatenation (collision-free even for a
|
|
136
|
+
literal "anonymous" user key or delimiter-containing values); the same
|
|
137
|
+
encoded identity is used for snapshot keys, the Web Lock name, the
|
|
138
|
+
previous-identity value in the Rails session, and epoch rotation.
|
|
139
|
+
(Configuration contract for user_key/anonymous_only: see the dedicated
|
|
140
|
+
bullet below.) When the subsystem is enabled, a separate SESSION EPOCH is
|
|
141
|
+
managed by the Railtie with no app code: kept in the Rails session, rotated (SecureRandom)
|
|
142
|
+
whenever the computed user_key changes, stamped on all REST/schema
|
|
143
|
+
responses (X-Funicular-Epoch). A mismatching OR MISSING epoch on an
|
|
144
|
+
apply-path response moves the page to a TERMINAL invalid-session state:
|
|
145
|
+
the response is discarded and replica applies, local writes, raw writes,
|
|
146
|
+
and persistence are refused for the life of the page (a non-reloading
|
|
147
|
+
on_session_change hook cannot re-enable them; default hook reloads). The
|
|
148
|
+
terminal flag is an irreversible latch independent of durability state;
|
|
149
|
+
a terminal WRITER steps down -- debounce cancelled, in-flight persist
|
|
150
|
+
serialized, lock-holding promise resolved so the lock releases,
|
|
151
|
+
connections kept only as a non-persistent read view.
|
|
152
|
+
- Durability is a three-state machine per page: persistent_writer (holds
|
|
153
|
+
the Web Lock; restores, persists, writes), persistent_reader (replica
|
|
154
|
+
fully functional incl. in-memory revalidation writes, never persisted;
|
|
155
|
+
LOCAL connection PRAGMA query_only=ON; local writes and
|
|
156
|
+
flush/wipe/reset_local raise ReadOnlyTabError; persist/close are not
|
|
157
|
+
on the proxy surface at all, on any tab), and
|
|
158
|
+
volatile (everything works including local writes, nothing persists;
|
|
159
|
+
entered when Web Locks or IndexedDB are unavailable; named distinctly
|
|
160
|
+
from the `storage :ephemeral` model declaration). Lock protocol: boot
|
|
161
|
+
decides instantly via `ifAvailable: true`; a reader stays reader for the
|
|
162
|
+
life of the page (no promotion in v1 -- reload to write); the writer
|
|
163
|
+
parks the lock on a promise resolved at teardown. DB handles
|
|
164
|
+
are guarded proxies with a closed allowlist, never raw SQLite3::Database:
|
|
165
|
+
proxy `transaction` yields the proxy (the gem's own transaction yields
|
|
166
|
+
the raw db), `prepare` returns a guarded statement, batch/deserialize/
|
|
167
|
+
commit/rollback are classified; pending notify_changed state is tied to
|
|
168
|
+
raw-transaction commit/rollback. Read-only states are enforced per
|
|
169
|
+
STATEMENT at execution time via sqlite3_stmt_readonly
|
|
170
|
+
(Statement#readonly?, new sqlite3 API): write statements -> framework
|
|
171
|
+
exception; PRAGMA query_only/ATTACH/DETACH rejected separately (SQLite
|
|
172
|
+
classifies them read-only; query_only alone is not a guarantee -- it can
|
|
173
|
+
be switched OFF); execute_batch refused outright in read-only states;
|
|
174
|
+
EVERY execution entry point re-checks the current latch/state when run
|
|
175
|
+
(execute, step, ResultSet#next, get_first_* alike).
|
|
176
|
+
- `wipe` (writer-only) advances the MUTATION GENERATION (deliberately not
|
|
177
|
+
called "epoch" -- the session epoch is a different mechanism): stale
|
|
178
|
+
in-flight applies are rejected, debounce timers cancelled, watchers
|
|
179
|
+
notified only after DBs are queryable again. If ANY local table's stored
|
|
180
|
+
migration version exceeds the declared maximum, the WHOLE local DB fails
|
|
181
|
+
loud: every local-model operation (read or write, any table) raises
|
|
182
|
+
SchemaTooNewError and the DB sits at query_only=ON; raw SELECT export and
|
|
183
|
+
reset_local (which internally lifts query_only for the rebuild; still
|
|
184
|
+
ReadOnlyTabError on non-writer tabs) are the only doors. No per-table
|
|
185
|
+
nuance in v1.
|
|
186
|
+
- When opted in, boot is one state machine (`Funicular::DB.boot`, driven by
|
|
187
|
+
Funicular.start, independent of load_schemas usage): declarations ->
|
|
188
|
+
namespace+epoch -> writer election -> local restore+migrations -> replica
|
|
189
|
+
restore+fingerprint (after ALL schemas collected) -> components mount.
|
|
190
|
+
Schema failure: completion block not invoked, console.error always,
|
|
191
|
+
on_boot_error(errors) when set. The HTTP layer settles every schema
|
|
192
|
+
request exactly once (success / HTTP error / parse error / Promise
|
|
193
|
+
rejection), so the barrier cannot hang; an empty schema set is valid only
|
|
194
|
+
when no replica models are declared.
|
|
195
|
+
- picoruby-indexeddb error classification cannot ride the generic JS
|
|
196
|
+
Promise bridge (it carries only error.message; JS::Promise#await raises a
|
|
197
|
+
string): the gem's own EM_JS promises resolve a TAGGED result -- success
|
|
198
|
+
as { ok:, value: }, failure as { ok:, name:, message: } -- converted to
|
|
199
|
+
values/typed Ruby exceptions; the same shape applies to the request and
|
|
200
|
+
transaction helpers, not just open. Only listed availability errors
|
|
201
|
+
(SecurityError, InvalidStateError, missing global) fall back to the
|
|
202
|
+
in-memory store (-> volatile state); quota and data errors surface;
|
|
203
|
+
onblocked is not availability failure -- it waits with a fixed timeout
|
|
204
|
+
(default 5000ms), raises a typed BlockedError on expiry, and a late open
|
|
205
|
+
success after the timeout closes that connection immediately; open cleans
|
|
206
|
+
up its callback registry in ensure.
|
|
207
|
+
- picoruby-sqlite3 gains official primitives instead of Funicular poking
|
|
208
|
+
internals: open a memory DB without snapshot binding (no restore/no
|
|
209
|
+
persist -- plain `close` on an unbound DB persists nothing, so no
|
|
210
|
+
`close(persist: false)` variant is needed) and `Statement#readonly?`. (No
|
|
211
|
+
named-snapshot-deletion API: it would target the gem's built-in store,
|
|
212
|
+
which Funicular does not use -- `wipe` deletes the two namespaced keys
|
|
213
|
+
directly from Funicular's own KVS.) Snapshots are stored Base64-encoded
|
|
214
|
+
(binary Strings do not survive the JS bridge). The proxy exposes NO
|
|
215
|
+
`persist`/`close` on any tab: the framework's DBs are unbound memory DBs
|
|
216
|
+
(SQLite-level persist has no valid target; close would destroy a
|
|
217
|
+
framework-owned connection). Snapshot I/O itself is OWNED BY FUNICULAR:
|
|
218
|
+
`Funicular::DB` opens its IndexedDB store with `fallback: false` and does
|
|
219
|
+
its own serialize/deserialize round trips -- the sqlite3 gem's built-in
|
|
220
|
+
STORE_NAME persistence (whose KVS defaults to fallback: true and would
|
|
221
|
+
hide unavailability) is not used by Funicular. Storage failure at boot,
|
|
222
|
+
v1: availability errors on open -> volatile (backend absent by design);
|
|
223
|
+
ANY other storage failure -- open errors
|
|
224
|
+
(QuotaExceeded/Unknown/Version/Blocked-timeout) or snapshot GET errors
|
|
225
|
+
(quota/data) -- FAILS THE BOOT: components do not mount, console.error +
|
|
226
|
+
on_boot_error. Recovery: corrupt snapshot (GET failure, handle exists) ->
|
|
227
|
+
the hook may call wipe then reload; open failure -> fix browser state,
|
|
228
|
+
reload. No latches, no partial boot, no in-page recovery (deferred).
|
|
229
|
+
- watch, v1: the block must return a Relation (else a helpful raise); the
|
|
230
|
+
framework materializes it and subscribes to that Relation's table,
|
|
231
|
+
re-subscribing each evaluation. Hashes/scalars/raw SQL: use Model.on_change
|
|
232
|
+
+ patch. (The dependency collector, tables: option, and refresh :auto's
|
|
233
|
+
barrier-time all-endpoint validation are deferred with :auto itself.)
|
|
234
|
+
- Every opted-in application requires `user_key`, or `anonymous_only = true`
|
|
235
|
+
as the explicit choice for an auth-less app (both set = config error), even
|
|
236
|
+
when it has only replica models. Rails validates after initialization and
|
|
237
|
+
before helper output; DB.boot validates the emitted contract defensively.
|
|
238
|
+
The opt-in flag plus namespace and epoch metadata are emitted by
|
|
239
|
+
`picoruby_include_tag` as HTML-escaped data attributes (no new helper;
|
|
240
|
+
CSR-only apps covered). No metadata means disabled, never an implicit
|
|
241
|
+
anonymous `"funicular"` namespace.
|
|
242
|
+
Session epoch state is stored PER application_id
|
|
243
|
+
(session[:funicular_epochs][app_id] = { identity:, epoch: }) so multiple
|
|
244
|
+
Funicular apps sharing one Rails session cannot rotate each other's
|
|
245
|
+
epochs.
|
|
246
|
+
- notify_changed takes a model class or (role, table) pair -- change
|
|
247
|
+
identity is [database_role, table_name]; inside a raw transaction the
|
|
248
|
+
event and persist scheduling defer to commit and vanish on rollback.
|
|
249
|
+
- Event bus: change events fire post-commit, coalesced to one per table per
|
|
250
|
+
transaction; watcher delivery is queued (never nested inside a component
|
|
251
|
+
update) and subscriber exceptions are isolated.
|
|
252
|
+
- SSR: all SQLite3 access is deferred inside methods so class bodies still
|
|
253
|
+
evaluate under CRuby; materializing a local query during SSR raises
|
|
254
|
+
`Funicular::DB::UnavailableError` (fail loud, never an empty result).
|
|
255
|
+
|
|
256
|
+
The Store layer (`store.rb`, `store_singleton.rb`, `store_collection.rb`) is
|
|
257
|
+
superseded by this: every Store feature has a local-database counterpart
|
|
258
|
+
(scopes -> tables/where, `expires_in` -> `refresh :auto`, `on_change` ->
|
|
259
|
+
table change events + `watch`, `subscribes_to` -> future `refresh :live`,
|
|
260
|
+
`cleared_on`/`dispatch` -> `Funicular::DB.wipe`). Store stays in the tree
|
|
261
|
+
untouched for now -- its `subscribes_to` implementation is the design
|
|
262
|
+
reference for `refresh :live` -- but nothing new builds on it, and it will be
|
|
263
|
+
deprecated and removed once `refresh :live` ships.
|
|
264
|
+
|
|
67
265
|
## `lib/` Rails integration
|
|
68
266
|
|
|
69
267
|
- `compiler.rb` -- runs the vendored `mrbc` (WebAssembly, via Node.js) to
|
|
@@ -96,6 +294,23 @@ SSR, diffing, ErrorBoundary rendering, and hydration on the same data model.
|
|
|
96
294
|
Because `copy_wasm` reads sibling directories inside the picoruby repository, it
|
|
97
295
|
only works from within that checkout -- see Development below.
|
|
98
296
|
|
|
297
|
+
## JavaScript interop contract
|
|
298
|
+
|
|
299
|
+
As of picoruby commit 9e69333f, `JS::Object` inherits `BasicObject` instead of
|
|
300
|
+
`Object`. Consequences for framework code:
|
|
301
|
+
|
|
302
|
+
- Dot access on JS values is reliable for names Kernel used to shadow
|
|
303
|
+
(`hash`, `send`, `open`, `class`, `method`, ...): they now reach the JS side
|
|
304
|
+
via `method_missing`.
|
|
305
|
+
- The Ruby protocol predicates `nil?`, `is_a?`, `kind_of?`, `instance_of?`, and
|
|
306
|
+
`respond_to?` are defined in C on `JS::Object` (a `?` suffix is illegal in a
|
|
307
|
+
JS identifier, so they can never shadow a JS property). `respond_to?` does a
|
|
308
|
+
real method-table lookup only; it does not report JS properties.
|
|
309
|
+
- Any other name ending in `?` or `!` raises `NoMethodError` instead of being
|
|
310
|
+
forwarded to JS, so typos fail loudly rather than silently returning nil.
|
|
311
|
+
- `==`, `to_s`, `inspect`, `[]`, `[]=`, `to_a`, and `typeof` are defined
|
|
312
|
+
directly on `JS::Object` and behave as before.
|
|
313
|
+
|
|
99
314
|
## Server-side rendering, briefly
|
|
100
315
|
|
|
101
316
|
For SSR the `mrblib/` framework is loaded into the Rails process under CRuby.
|
|
@@ -103,7 +318,7 @@ For SSR the `mrblib/` framework is loaded into the Rails process under CRuby.
|
|
|
103
318
|
`app/funicular/initializer.rb`, builds a `Runtime` around that router, builds the
|
|
104
319
|
component's VDOM, and serializes it with `HTMLSerializer`. The state is also
|
|
105
320
|
embedded as `window.__FUNICULAR_STATE__` so the browser can hydrate the markup
|
|
106
|
-
rather than rebuild it. Keep `render
|
|
321
|
+
rather than rebuild it. Keep `render` deterministic and free of browser-only
|
|
107
322
|
calls so the same code is safe on both sides.
|
|
108
323
|
|
|
109
324
|
## Development
|