@lucent-lang/lucent 0.0.4 → 0.1.1
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 +97 -0
- package/app.plugin.js +46 -10
- package/dist/{bench-CR3E7e2e.js → bench-eEez4jhQ.js} +10 -6
- package/dist/{build-CAIrRSYA.js → build-CpUjeHFz.js} +1 -1
- package/dist/{build-check-QozuNJm5.js → build-check-BA1ZEp9s.js} +28 -13
- package/dist/{check-1SCmYlqA.js → check-BZx3FgI6.js} +1 -1
- package/dist/cli.js +62 -13
- package/dist/{codes-CIuwXt1S.js → codes-D-6LxUqD.js} +163 -7
- package/dist/compiler.js +4 -4
- package/dist/{dashboard-B-NzjjaN.js → dashboard-CC-HXhcd.js} +2 -2
- package/dist/{dev-BYWMP7oC.js → dev-DI5yL8zx.js} +62 -23
- package/dist/{diagnostic-BM9sXmwl.js → diagnostic-BvwLZQS1.js} +3 -1
- package/dist/{doctor-C0PrfWT6.js → doctor-CTAX3euc.js} +1 -1
- package/dist/{doctor-DdR4JnVc.js → doctor-CrgOAV5L.js} +1 -1
- package/dist/{explain--vV1oaRl.js → explain-Byit07F-.js} +1 -1
- package/dist/{packages-HyskKIsY.js → packages-CSmKeaAh.js} +2 -33
- package/dist/pipeline-BM_VM2X_.js +700 -0
- package/dist/project-BKNoTEEp.js +342 -0
- package/dist/sdk-coverage-iiv3JqMp.js +113 -0
- package/dist/sdk-diff-ByJ1Gw-F.js +109 -0
- package/dist/sdk-lock-BQn7IyRs.js +38 -0
- package/dist/{sdk-prefetch-mo04JDZV.js → sdk-prefetch-Bv2pNega.js} +2 -2
- package/dist/{sdk-search-Iv4K3-8a.js → sdk-search-CPGJm_Ne.js} +2 -2
- package/dist/{sdk-show-CkrdlqlP.js → sdk-show-BUkLqRnA.js} +5 -4
- package/dist/sdk-usage-0ZFMAYg1.js +95 -0
- package/dist/src-ChnmWLwn.js +40851 -0
- package/dist/trace-Bi4hkmPx.js +139 -0
- package/lib/core.cjs +368 -1
- package/lib/sdk/android.d.ts +54 -1
- package/lib/sdk/compose.d.ts +94 -0
- package/lib/sdk/compose.schemas.json.gz +0 -0
- package/lib/sdk/core.d.ts +201 -0
- package/lib/sdk/ios.d.ts +75 -2
- package/lib/sdk/ui.d.ts +129 -0
- package/metro/transformer.cjs +11 -2
- package/package.json +4 -3
- package/runtime/cpp/lucent/abort.cpp +59 -11
- package/runtime/cpp/lucent/abort.h +22 -6
- package/runtime/cpp/lucent/array.h +12 -2
- package/runtime/cpp/lucent/async.h +101 -21
- package/runtime/cpp/lucent/bigint.cpp +898 -0
- package/runtime/cpp/lucent/bigint.h +302 -0
- package/runtime/cpp/lucent/buffer.cpp +228 -0
- package/runtime/cpp/lucent/buffer.h +328 -0
- package/runtime/cpp/lucent/builtins.cpp +4 -1
- package/runtime/cpp/lucent/bytes.h +21 -2
- package/runtime/cpp/lucent/callback.cpp +29 -0
- package/runtime/cpp/lucent/callback.h +230 -0
- package/runtime/cpp/lucent/compute.cpp +370 -0
- package/runtime/cpp/lucent/compute.h +455 -0
- package/runtime/cpp/lucent/conversions.h +111 -0
- package/runtime/cpp/lucent/core.h +16 -5
- package/runtime/cpp/lucent/equality.h +107 -28
- package/runtime/cpp/lucent/execution.cpp +416 -0
- package/runtime/cpp/lucent/execution.h +270 -0
- package/runtime/cpp/lucent/extension.cpp +97 -0
- package/runtime/cpp/lucent/extension.h +210 -0
- package/runtime/cpp/lucent/helpers.h +12 -5
- package/runtime/cpp/lucent/items.h +52 -0
- package/runtime/cpp/lucent/jserror.cpp +9 -0
- package/runtime/cpp/lucent/jserror.h +11 -0
- package/runtime/cpp/lucent/jsi/convert.cpp +138 -0
- package/runtime/cpp/lucent/jsi/convert.h +128 -59
- package/runtime/cpp/lucent/jsi/host.cpp +213 -35
- package/runtime/cpp/lucent/jsi/host.h +116 -16
- package/runtime/cpp/lucent/json.h +19 -1
- package/runtime/cpp/lucent/lifecycle.cpp +260 -0
- package/runtime/cpp/lucent/lifecycle.h +124 -0
- package/runtime/cpp/lucent/lucent.h +7 -0
- package/runtime/cpp/lucent/map.h +41 -7
- package/runtime/cpp/lucent/native.cpp +35 -47
- package/runtime/cpp/lucent/native.h +116 -26
- package/runtime/cpp/lucent/number.cpp +38 -38
- package/runtime/cpp/lucent/number.h +33 -0
- package/runtime/cpp/lucent/operation.h +65 -0
- package/runtime/cpp/lucent/ops.h +14 -6
- package/runtime/cpp/lucent/platform/android.cpp +223 -10
- package/runtime/cpp/lucent/platform/android.h +227 -4
- package/runtime/cpp/lucent/platform/android_activity.cpp +258 -0
- package/runtime/cpp/lucent/platform/android_requests.h +140 -0
- package/runtime/cpp/lucent/platform/compose.h +256 -0
- package/runtime/cpp/lucent/platform/ios.h +325 -25
- package/runtime/cpp/lucent/platform/ios.mm +1 -14
- package/runtime/cpp/lucent/platform/ios_ui.h +192 -0
- package/runtime/cpp/lucent/platform/ios_ui.mm +454 -0
- package/runtime/cpp/lucent/platform/swiftui.h +190 -0
- package/runtime/cpp/lucent/presentation.cpp +11 -0
- package/runtime/cpp/lucent/presentation.h +109 -0
- package/runtime/cpp/lucent/reactive.cpp +406 -0
- package/runtime/cpp/lucent/reactive.h +566 -0
- package/runtime/cpp/lucent/report.cpp +40 -0
- package/runtime/cpp/lucent/report.h +16 -0
- package/runtime/cpp/lucent/resource.cpp +83 -0
- package/runtime/cpp/lucent/resource.h +75 -0
- package/runtime/cpp/lucent/scheduler.cpp +38 -124
- package/runtime/cpp/lucent/scheduler.h +102 -83
- package/runtime/cpp/lucent/scope.cpp +308 -0
- package/runtime/cpp/lucent/scope.h +452 -0
- package/runtime/cpp/lucent/sizing.h +263 -0
- package/runtime/cpp/lucent/slots.h +231 -0
- package/runtime/cpp/lucent/trace.cpp +539 -0
- package/runtime/cpp/lucent/trace.h +251 -0
- package/runtime/cpp/lucent/transport.cpp +9 -0
- package/runtime/cpp/lucent/transport.h +341 -0
- package/runtime/cpp/lucent/view.cpp +123 -0
- package/runtime/cpp/lucent/view.h +157 -0
- package/runtime/cpp/rn/LucentComponentView.h +121 -0
- package/runtime/cpp/rn/LucentComponentView.mm +479 -0
- package/runtime/cpp/rn/LucentModule.cpp +37 -7
- package/runtime/cpp/rn/LucentModule.h +3 -0
- package/runtime/cpp/rn/LucentViewChildren.h +80 -0
- package/runtime/cpp/rn/LucentViewChildren.mm +211 -0
- package/runtime/cpp/rn/LucentViewRegistry.h +62 -0
- package/runtime/cpp/rn/LucentViewRequests.cpp +91 -0
- package/runtime/cpp/rn/LucentViewRequests.h +104 -0
- package/runtime/cpp/rn/LucentViewSizing.h +345 -0
- package/runtime/cpp/rn/LucentViewSlots.h +242 -0
- package/runtime/cpp/rn/LucentViewValues.h +125 -0
- package/runtime/cpp/rn/LucentViews.h +440 -0
- package/runtime/cpp/rn/LucentViewsAndroid.cpp +439 -0
- package/runtime/cpp/rn/LucentViewsAndroid.h +138 -0
- package/runtime/cpp/third_party/dragonbox/LICENSE-Boost +23 -0
- package/runtime/cpp/third_party/dragonbox/README.md +16 -0
- package/runtime/cpp/third_party/dragonbox/dragonbox.h +4440 -0
- package/runtime/js/index.d.ts +11 -1
- package/runtime/js/index.js +84 -8
- package/runtime/js/views.d.ts +38 -0
- package/runtime/js/views.js +226 -0
- package/runtime/native/android/CMakeLists.txt +4 -1
- package/runtime/native/android/src/compose/java/dev/lucent/compose/CompositionRules.kt +44 -0
- package/runtime/native/android/src/compose/java/dev/lucent/compose/LucentComposition.kt +161 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentActivities.java +292 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentChildren.java +174 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentHostView.java +232 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentInitializer.java +46 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentPackage.java +4 -3
- package/runtime/native/android/src/main/java/dev/lucent/LucentRequestActivity.java +92 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentSlotView.java +102 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentViewManager.java +149 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentViewManagers.java +16 -0
- package/runtime/native/android/src/main/java/dev/lucent/LucentViews.java +44 -0
- package/runtime/test/jsi/harness.cpp +9 -0
- package/schemas/build.schema.json +96 -1
- package/schemas/check.schema.json +56 -1
- package/schemas/lucent.schema.json +495 -9
- package/schemas/sdk-diff.schema.json +70 -0
- package/schemas/sdk-lock.schema.json +67 -0
- package/ts-plugin/index.js +85 -4
- package/dist/pipeline-CH7-2cxu.js +0 -232
- package/dist/project-D8tEet0k.js +0 -150
- package/dist/sdk-coverage-BoW_3Efd.js +0 -64
- package/dist/src-BD7f1_aZ.js +0 -11493
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
// Lucent runtime — the UI's reactive graph: signals, computed values and
|
|
2
|
+
// effects, owned by one execution context (a view's: the main context).
|
|
3
|
+
//
|
|
4
|
+
// Everything runs on that context and nothing else: every call from another
|
|
5
|
+
// thread throws std::logic_error (handles may be copied anywhere, but the
|
|
6
|
+
// last copy of a computed value or an effect must go there too), and
|
|
7
|
+
// nothing waits for the Lucent lock or the JavaScript thread. The rules
|
|
8
|
+
// (docs/architecture.md, under reactive.h; test/reactive/reference.ts is
|
|
9
|
+
// their executable statement):
|
|
10
|
+
//
|
|
11
|
+
// - A signal notifies when a write changes its value by Object.is (NaN is
|
|
12
|
+
// itself, 0 and -0 differ; objects by identity: mutating one in place
|
|
13
|
+
// notifies nothing). A signal may take another equality.
|
|
14
|
+
// - A computed value is lazy: evaluated when read, again only when a source
|
|
15
|
+
// it read last time changed since. An unchanged result changes nothing
|
|
16
|
+
// downstream; an error is kept and rethrown to readers until a source
|
|
17
|
+
// changes. It cannot write a signal, nor read itself.
|
|
18
|
+
// - An effect runs once when made, then again after something it read
|
|
19
|
+
// changed. Each run replaces what the last one read. Before it reruns, and
|
|
20
|
+
// when it is disposed, its last run's scope is disposed, untracked: tasks
|
|
21
|
+
// started in it cancelled, then its cleanups and nested effects in
|
|
22
|
+
// reverse order.
|
|
23
|
+
// - Writes inside a transaction, an effect or a cleanup wait for it to end;
|
|
24
|
+
// the outermost end runs the pending effects at once, in the order they
|
|
25
|
+
// were made, so no effect sees half of a transaction. An effect about to
|
|
26
|
+
// run more than `loopLimit` times in one update stops it and is reported
|
|
27
|
+
// with the effects that triggered each other.
|
|
28
|
+
// - Only the synchronous run tracks: cleanups, untracked(), and whatever runs
|
|
29
|
+
// later (a continuation after an await) read without subscribing.
|
|
30
|
+
#pragma once
|
|
31
|
+
|
|
32
|
+
#include <cmath>
|
|
33
|
+
#include <cstdint>
|
|
34
|
+
#include <exception>
|
|
35
|
+
#include <functional>
|
|
36
|
+
#include <map>
|
|
37
|
+
#include <memory>
|
|
38
|
+
#include <mutex>
|
|
39
|
+
#include <optional>
|
|
40
|
+
#include <stdexcept>
|
|
41
|
+
#include <string>
|
|
42
|
+
#include <type_traits>
|
|
43
|
+
#include <unordered_set>
|
|
44
|
+
#include <utility>
|
|
45
|
+
#include <vector>
|
|
46
|
+
|
|
47
|
+
#include "core.h"
|
|
48
|
+
#include "equality.h"
|
|
49
|
+
#include "execution.h"
|
|
50
|
+
#include "jserror.h"
|
|
51
|
+
#include "scope.h"
|
|
52
|
+
|
|
53
|
+
namespace lucent::ui {
|
|
54
|
+
|
|
55
|
+
/// Object.is: NaN is itself, 0 and -0 differ.
|
|
56
|
+
inline bool sameValue(double a, double b) {
|
|
57
|
+
if (std::isnan(a)) return std::isnan(b);
|
|
58
|
+
return a == b && std::signbit(a) == std::signbit(b);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Missing, null and a value differ; values by Object.is.
|
|
62
|
+
template <class T>
|
|
63
|
+
bool sameValue(const Opt<T>& a, const Opt<T>& b);
|
|
64
|
+
|
|
65
|
+
/// Otherwise `===`: identity for objects, never their content.
|
|
66
|
+
template <class T>
|
|
67
|
+
bool sameValue(const T& a, const T& b) {
|
|
68
|
+
return strictEquals(a, b);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
template <class T>
|
|
72
|
+
bool sameValue(const Opt<T>& a, const Opt<T>& b) {
|
|
73
|
+
return a.state() == b.state() && (!a.has() || sameValue(a.get(), b.get()));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// A signal's or computed value's default equality.
|
|
77
|
+
struct SameValue {
|
|
78
|
+
template <class T>
|
|
79
|
+
bool operator()(const T& a, const T& b) const {
|
|
80
|
+
return sameValue(a, b);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
template <class T>
|
|
85
|
+
using Equality = std::function<bool(const T&, const T&)>;
|
|
86
|
+
|
|
87
|
+
struct GraphOptions {
|
|
88
|
+
/// Runs one effect may make in one update before the update stops as a loop.
|
|
89
|
+
uint32_t loopLimit = 100;
|
|
90
|
+
|
|
91
|
+
/// Errors no caller can catch: an effect's ("effect"), its cleanups'
|
|
92
|
+
/// ("effect cleanup"), a loop ("effect loop"). Default: reportUncaught.
|
|
93
|
+
std::function<void(std::exception_ptr error, const char* where)> onError;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
namespace detail {
|
|
97
|
+
class Node;
|
|
98
|
+
class Observer;
|
|
99
|
+
class EffectNode;
|
|
100
|
+
template <class T>
|
|
101
|
+
class ComputedNode;
|
|
102
|
+
} // namespace detail
|
|
103
|
+
|
|
104
|
+
template <class T>
|
|
105
|
+
class Signal;
|
|
106
|
+
template <class T>
|
|
107
|
+
class Computed;
|
|
108
|
+
class Effect;
|
|
109
|
+
class Graph;
|
|
110
|
+
|
|
111
|
+
/// Makes an effect in graph->scope() and runs it at once. `name` names it in
|
|
112
|
+
/// loop reports. Made in a scope that is no longer active, it never runs.
|
|
113
|
+
Effect effect(const std::shared_ptr<Graph>& graph, std::function<void()> fn, std::string name = "effect");
|
|
114
|
+
|
|
115
|
+
class Graph : public std::enable_shared_from_this<Graph> {
|
|
116
|
+
struct Token {};
|
|
117
|
+
|
|
118
|
+
public:
|
|
119
|
+
/// A graph owned by `owner`, used only there.
|
|
120
|
+
static std::shared_ptr<Graph> create(ExecutionContext& owner = ExecutionContext::main(), GraphOptions options = {});
|
|
121
|
+
|
|
122
|
+
Graph(Token, ExecutionContext& owner, GraphOptions options);
|
|
123
|
+
Graph(const Graph&) = delete;
|
|
124
|
+
Graph& operator=(const Graph&) = delete;
|
|
125
|
+
|
|
126
|
+
ExecutionContext& owner() const { return owner_; }
|
|
127
|
+
|
|
128
|
+
/// Where effects made outside any mount belong; dispose() ends it.
|
|
129
|
+
const std::shared_ptr<Scope>& root() const { return root_; }
|
|
130
|
+
|
|
131
|
+
/// Where effects, cleanups and tasks made now belong: the running effect's
|
|
132
|
+
/// run, the scope of within(), else the root.
|
|
133
|
+
const std::shared_ptr<Scope>& scope() const {
|
|
134
|
+
checkOwner();
|
|
135
|
+
return scope_;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Runs `f`; its writes, and the effects they make pending, wait for the
|
|
139
|
+
/// outermost transaction to end, which runs the pending effects before
|
|
140
|
+
/// returning. Not a rollback: what `f` wrote before throwing stays.
|
|
141
|
+
template <class F>
|
|
142
|
+
decltype(auto) transaction(F&& f) {
|
|
143
|
+
checkOwner();
|
|
144
|
+
Batch batch(*this);
|
|
145
|
+
return std::forward<F>(f)();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Runs `f` without subscribing whatever is running to what it reads.
|
|
149
|
+
template <class F>
|
|
150
|
+
decltype(auto) untracked(F&& f) {
|
|
151
|
+
checkOwner();
|
|
152
|
+
Tracking none(*this, nullptr);
|
|
153
|
+
return std::forward<F>(f)();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Runs `f` with `scope` as where effects and cleanups made belong (a
|
|
157
|
+
/// view mount's setup).
|
|
158
|
+
template <class F>
|
|
159
|
+
decltype(auto) within(const std::shared_ptr<Scope>& scope, F&& f) {
|
|
160
|
+
checkOwner();
|
|
161
|
+
if (!scope) throw std::invalid_argument("within() needs a scope");
|
|
162
|
+
ScopeSwap swap(*this, scope);
|
|
163
|
+
return std::forward<F>(f)();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Registers a cleanup with scope(): it runs when that scope is disposed
|
|
167
|
+
/// (before the running effect's next run), untracked.
|
|
168
|
+
void onCleanup(std::function<void()> cleanup);
|
|
169
|
+
|
|
170
|
+
/// Disposes the root: effects made outside any mount end.
|
|
171
|
+
void dispose();
|
|
172
|
+
|
|
173
|
+
/// Throws std::logic_error unless called on the owner context.
|
|
174
|
+
void checkOwner() const;
|
|
175
|
+
|
|
176
|
+
private:
|
|
177
|
+
template <class T>
|
|
178
|
+
friend class Signal;
|
|
179
|
+
template <class T>
|
|
180
|
+
friend class Computed;
|
|
181
|
+
template <class T>
|
|
182
|
+
friend class detail::ComputedNode;
|
|
183
|
+
friend class detail::Observer;
|
|
184
|
+
friend class Effect;
|
|
185
|
+
friend Effect effect(const std::shared_ptr<Graph>& graph, std::function<void()> fn, std::string name);
|
|
186
|
+
|
|
187
|
+
/// Writes wait while one lives; the outermost runs the pending effects.
|
|
188
|
+
struct Batch {
|
|
189
|
+
explicit Batch(Graph& g) : g(g) { g.batch_++; }
|
|
190
|
+
~Batch() {
|
|
191
|
+
if (--g.batch_ == 0) g.flushPending();
|
|
192
|
+
}
|
|
193
|
+
Graph& g;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/// What reads subscribe, while one lives.
|
|
197
|
+
struct Tracking {
|
|
198
|
+
Tracking(Graph& g, detail::Observer* observer) : g(g), outer(g.tracker_) { g.tracker_ = observer; }
|
|
199
|
+
~Tracking() { g.tracker_ = outer; }
|
|
200
|
+
Graph& g;
|
|
201
|
+
detail::Observer* outer;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
struct ScopeSwap {
|
|
205
|
+
ScopeSwap(Graph& g, std::shared_ptr<Scope> scope) : g(g), outer(std::exchange(g.scope_, std::move(scope))) {}
|
|
206
|
+
~ScopeSwap() { g.scope_ = std::move(outer); }
|
|
207
|
+
Graph& g;
|
|
208
|
+
std::shared_ptr<Scope> outer;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/// A read: subscribes the tracking observer.
|
|
212
|
+
void read(detail::Node& node);
|
|
213
|
+
/// A signal's value changed: marks what depends on it, and runs the
|
|
214
|
+
/// pending effects unless writes wait.
|
|
215
|
+
void changed(detail::Node& node);
|
|
216
|
+
void mark(detail::Node& node);
|
|
217
|
+
void enqueue(detail::EffectNode& effect);
|
|
218
|
+
void flushPending();
|
|
219
|
+
void runEffect(detail::EffectNode& effect);
|
|
220
|
+
void disposeEffect(detail::EffectNode& effect);
|
|
221
|
+
void loop(detail::EffectNode& effect);
|
|
222
|
+
void report(std::exception_ptr error, const char* where);
|
|
223
|
+
/// Throws InvalidStateError while a computed value is being evaluated.
|
|
224
|
+
void checkWritable() const;
|
|
225
|
+
|
|
226
|
+
ExecutionContext& owner_;
|
|
227
|
+
std::weak_ptr<Scope::Owner> ownerRef_;
|
|
228
|
+
GraphOptions options_;
|
|
229
|
+
std::shared_ptr<Scope> root_;
|
|
230
|
+
std::shared_ptr<Scope> scope_;
|
|
231
|
+
|
|
232
|
+
detail::Observer* tracker_ = nullptr;
|
|
233
|
+
detail::EffectNode* running_ = nullptr;
|
|
234
|
+
uint32_t batch_ = 0;
|
|
235
|
+
uint32_t computing_ = 0;
|
|
236
|
+
bool flushing_ = false;
|
|
237
|
+
|
|
238
|
+
/// Each write's marks, so a node is visited once per write.
|
|
239
|
+
uint64_t epoch_ = 0;
|
|
240
|
+
/// Each update (flush), for counting an effect's runs in it.
|
|
241
|
+
uint64_t updates_ = 0;
|
|
242
|
+
/// Effects made, which orders them.
|
|
243
|
+
uint64_t made_ = 0;
|
|
244
|
+
|
|
245
|
+
/// Pending effects, in the order they were made.
|
|
246
|
+
std::map<uint64_t, std::shared_ptr<detail::EffectNode>> queue_;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
namespace detail {
|
|
250
|
+
|
|
251
|
+
class Node : public std::enable_shared_from_this<Node> {
|
|
252
|
+
public:
|
|
253
|
+
explicit Node(std::shared_ptr<Graph> graph) : graph_(std::move(graph)) {}
|
|
254
|
+
virtual ~Node() = default;
|
|
255
|
+
Node(const Node&) = delete;
|
|
256
|
+
Node& operator=(const Node&) = delete;
|
|
257
|
+
|
|
258
|
+
/// Brings a computed value up to date; a signal always is.
|
|
259
|
+
virtual void refresh() {}
|
|
260
|
+
|
|
261
|
+
std::shared_ptr<Graph> graph_;
|
|
262
|
+
/// Changes with the value (a computed value's error counts as a change).
|
|
263
|
+
uint64_t version_ = 0;
|
|
264
|
+
std::vector<Observer*> observers_;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/// A computed value or an effect: what it read, with the versions it read.
|
|
268
|
+
class Observer : public Node {
|
|
269
|
+
public:
|
|
270
|
+
Observer(std::shared_ptr<Graph> graph, bool effect) : Node(std::move(graph)), effect_(effect) {}
|
|
271
|
+
~Observer() override;
|
|
272
|
+
|
|
273
|
+
/// Whether a source changed since it was read (computed sources brought
|
|
274
|
+
/// up to date first, in the order read).
|
|
275
|
+
bool stale();
|
|
276
|
+
|
|
277
|
+
/// A read during a run: the first read of a source keeps its version.
|
|
278
|
+
void track(Node& source);
|
|
279
|
+
|
|
280
|
+
void begin() { next_.clear(); }
|
|
281
|
+
/// The run's reads replace the last run's.
|
|
282
|
+
void finish();
|
|
283
|
+
void unlinkAll();
|
|
284
|
+
|
|
285
|
+
struct Edge {
|
|
286
|
+
std::shared_ptr<Node> source;
|
|
287
|
+
uint64_t version;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/// Edges in the order read, with a hashed index once there are more than
|
|
291
|
+
/// a few, so a run reading n sources costs O(n), not O(n²).
|
|
292
|
+
class Edges {
|
|
293
|
+
public:
|
|
294
|
+
bool has(const Node* node) const;
|
|
295
|
+
void add(Edge edge);
|
|
296
|
+
void clear();
|
|
297
|
+
|
|
298
|
+
std::vector<Edge>::iterator begin() { return list_.begin(); }
|
|
299
|
+
std::vector<Edge>::iterator end() { return list_.end(); }
|
|
300
|
+
|
|
301
|
+
private:
|
|
302
|
+
static constexpr size_t kLinear = 8;
|
|
303
|
+
|
|
304
|
+
std::vector<Edge> list_;
|
|
305
|
+
std::unordered_set<const Node*> index_;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const bool effect_;
|
|
309
|
+
Edges sources_;
|
|
310
|
+
Edges next_;
|
|
311
|
+
/// Never run yet.
|
|
312
|
+
bool fresh_ = true;
|
|
313
|
+
/// Something it depends on was written since it was last up to date.
|
|
314
|
+
bool maybeStale_ = true;
|
|
315
|
+
uint64_t mark_ = 0;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
class EffectNode final : public Observer {
|
|
319
|
+
public:
|
|
320
|
+
EffectNode(std::shared_ptr<Graph> graph, std::function<void()> fn, std::string name, uint64_t order)
|
|
321
|
+
: Observer(std::move(graph), true), fn_(std::move(fn)), name_(std::move(name)), order_(order) {}
|
|
322
|
+
|
|
323
|
+
std::function<void()> fn_;
|
|
324
|
+
const std::string name_;
|
|
325
|
+
const uint64_t order_;
|
|
326
|
+
|
|
327
|
+
/// The last run's scope: its tasks, cleanups and nested effects.
|
|
328
|
+
std::shared_ptr<Scope> run_;
|
|
329
|
+
RuntimeId runtime_ = 0;
|
|
330
|
+
std::weak_ptr<Scope> owner_;
|
|
331
|
+
Scope::CleanupId registration_ = 0;
|
|
332
|
+
|
|
333
|
+
bool queued_ = false;
|
|
334
|
+
bool disposed_ = false;
|
|
335
|
+
/// The effect running when this one was made pending (for loop reports).
|
|
336
|
+
std::weak_ptr<EffectNode> cause_;
|
|
337
|
+
uint64_t update_ = 0;
|
|
338
|
+
uint32_t runs_ = 0;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
template <class T>
|
|
342
|
+
class SignalNode final : public Node {
|
|
343
|
+
public:
|
|
344
|
+
SignalNode(std::shared_ptr<Graph> graph, T value, Equality<T> equals)
|
|
345
|
+
: Node(std::move(graph)), value_(std::move(value)), equals_(std::move(equals)) {}
|
|
346
|
+
|
|
347
|
+
T value_;
|
|
348
|
+
Equality<T> equals_;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
template <class T>
|
|
352
|
+
class ComputedNode final : public Observer {
|
|
353
|
+
public:
|
|
354
|
+
ComputedNode(std::shared_ptr<Graph> graph, std::function<T()> fn, Equality<T> equals)
|
|
355
|
+
: Observer(std::move(graph), false), fn_(std::move(fn)), equals_(std::move(equals)) {}
|
|
356
|
+
|
|
357
|
+
void refresh() override {
|
|
358
|
+
if (!maybeStale_) return;
|
|
359
|
+
if (stale()) recompute();
|
|
360
|
+
maybeStale_ = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
void recompute() {
|
|
364
|
+
Graph& g = *graph_;
|
|
365
|
+
std::optional<T> value;
|
|
366
|
+
std::exception_ptr error;
|
|
367
|
+
|
|
368
|
+
begin();
|
|
369
|
+
computing_ = true;
|
|
370
|
+
g.computing_++;
|
|
371
|
+
{
|
|
372
|
+
Graph::Tracking tracking(g, this);
|
|
373
|
+
try {
|
|
374
|
+
value.emplace(fn_());
|
|
375
|
+
} catch (...) {
|
|
376
|
+
error = std::current_exception();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
g.computing_--;
|
|
380
|
+
computing_ = false;
|
|
381
|
+
finish();
|
|
382
|
+
fresh_ = false;
|
|
383
|
+
|
|
384
|
+
if (error) {
|
|
385
|
+
error_ = error;
|
|
386
|
+
version_++;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
bool changed = error_ || !value_ || !equals_(*value_, *value);
|
|
391
|
+
error_ = nullptr;
|
|
392
|
+
|
|
393
|
+
if (changed) {
|
|
394
|
+
value_ = std::move(value);
|
|
395
|
+
version_++;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
std::function<T()> fn_;
|
|
400
|
+
Equality<T> equals_;
|
|
401
|
+
std::optional<T> value_;
|
|
402
|
+
std::exception_ptr error_;
|
|
403
|
+
bool computing_ = false;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
} // namespace detail
|
|
407
|
+
|
|
408
|
+
/// A value the UI tracks. Copies are the same signal.
|
|
409
|
+
template <class T>
|
|
410
|
+
class Signal {
|
|
411
|
+
public:
|
|
412
|
+
/// Empty, until one is assigned (a field of a Lucent object): using it throws.
|
|
413
|
+
Signal() = default;
|
|
414
|
+
explicit Signal(std::shared_ptr<detail::SignalNode<T>> node) : node_(std::move(node)) {}
|
|
415
|
+
|
|
416
|
+
/// The value; the running effect or computed value now depends on it.
|
|
417
|
+
T get() const {
|
|
418
|
+
Graph& g = *node().graph_;
|
|
419
|
+
g.checkOwner();
|
|
420
|
+
g.read(*node_);
|
|
421
|
+
return node_->value_;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/// The value, without depending on it.
|
|
425
|
+
T peek() const {
|
|
426
|
+
node().graph_->checkOwner();
|
|
427
|
+
return node_->value_;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// The same signal: copies share one.
|
|
431
|
+
const void* identity() const { return node_.get(); }
|
|
432
|
+
|
|
433
|
+
/// Notifies what read it, unless `value` equals the current one.
|
|
434
|
+
void set(T value) const {
|
|
435
|
+
Graph& g = *node().graph_;
|
|
436
|
+
g.checkOwner();
|
|
437
|
+
g.checkWritable();
|
|
438
|
+
|
|
439
|
+
if (node_->equals_(node_->value_, value)) return;
|
|
440
|
+
|
|
441
|
+
node_->value_ = std::move(value);
|
|
442
|
+
g.changed(*node_);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private:
|
|
446
|
+
detail::SignalNode<T>& node() const {
|
|
447
|
+
if (!node_) throw std::logic_error("A Signal is used before one was assigned to it");
|
|
448
|
+
return *node_;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
std::shared_ptr<detail::SignalNode<T>> node_;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/// A value derived from others, evaluated lazily. Copies are the same one.
|
|
455
|
+
template <class T>
|
|
456
|
+
class Computed {
|
|
457
|
+
public:
|
|
458
|
+
explicit Computed(std::shared_ptr<detail::ComputedNode<T>> node) : node_(std::move(node)) {}
|
|
459
|
+
|
|
460
|
+
/// The value, up to date; the running effect or computed value now
|
|
461
|
+
/// depends on it. Rethrows the evaluation's error.
|
|
462
|
+
T get() const {
|
|
463
|
+
Graph& g = *node_->graph_;
|
|
464
|
+
g.checkOwner();
|
|
465
|
+
|
|
466
|
+
if (node_->computing_) throwError(String::fromLatin1("RangeError"), String::fromLatin1("A computed value reads itself"));
|
|
467
|
+
|
|
468
|
+
node_->refresh();
|
|
469
|
+
g.read(*node_);
|
|
470
|
+
|
|
471
|
+
if (node_->error_) std::rethrow_exception(node_->error_);
|
|
472
|
+
return *node_->value_;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/// get(), without depending on it.
|
|
476
|
+
T peek() const {
|
|
477
|
+
return node_->graph_->untracked([this] { return get(); });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private:
|
|
481
|
+
std::shared_ptr<detail::ComputedNode<T>> node_;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/// An effect: lives until disposed, or until the scope it was made in is.
|
|
485
|
+
class Effect {
|
|
486
|
+
public:
|
|
487
|
+
Effect() = default;
|
|
488
|
+
explicit Effect(std::shared_ptr<detail::EffectNode> node) : node_(std::move(node)) {}
|
|
489
|
+
|
|
490
|
+
/// Ends it now: its last run's scope is disposed, and it never runs again.
|
|
491
|
+
void dispose() const;
|
|
492
|
+
bool disposed() const { return !node_ || node_->disposed_; }
|
|
493
|
+
|
|
494
|
+
private:
|
|
495
|
+
std::shared_ptr<detail::EffectNode> node_;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/// A mount's prop commits, on their way from React to the UI context. A
|
|
499
|
+
/// commit is the fields that changed since the previous one (a field left
|
|
500
|
+
/// out is unchanged; missing and null are values the write sets), each a
|
|
501
|
+
/// write of an already converted value into the field's signal. Commits
|
|
502
|
+
/// posted from any thread coalesce until the UI applies them, all at once in
|
|
503
|
+
/// one transaction, fields in their order: a field's later write replaces
|
|
504
|
+
/// its earlier one, and effects see one committed state. A job posted with
|
|
505
|
+
/// the first pending commit applies them; code on the UI that must answer
|
|
506
|
+
/// now (a command, a measurement) calls drain() first instead of waiting.
|
|
507
|
+
/// Disposing the mount closes it: pending commits are dropped, new ones
|
|
508
|
+
/// refused.
|
|
509
|
+
class PropInbox : public std::enable_shared_from_this<PropInbox> {
|
|
510
|
+
struct Token {};
|
|
511
|
+
|
|
512
|
+
public:
|
|
513
|
+
struct Change {
|
|
514
|
+
uint32_t field;
|
|
515
|
+
std::function<void()> write;
|
|
516
|
+
};
|
|
517
|
+
using Commit = std::vector<Change>;
|
|
518
|
+
|
|
519
|
+
struct Stats {
|
|
520
|
+
/// Commits posted and taken.
|
|
521
|
+
uint64_t commits = 0;
|
|
522
|
+
/// Applications: transactions that applied pending commits.
|
|
523
|
+
uint64_t applied = 0;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
/// On the graph's context: an inbox for `mount`'s props.
|
|
527
|
+
static std::shared_ptr<PropInbox> create(std::shared_ptr<Graph> graph, const std::shared_ptr<Scope>& mount);
|
|
528
|
+
|
|
529
|
+
PropInbox(Token, std::shared_ptr<Graph> graph, const std::shared_ptr<Scope>& mount);
|
|
530
|
+
|
|
531
|
+
/// Any thread. False, keeping nothing, once closed.
|
|
532
|
+
bool post(Commit commit);
|
|
533
|
+
|
|
534
|
+
/// On the graph's context: applies what is pending now. False if nothing was.
|
|
535
|
+
bool drain();
|
|
536
|
+
|
|
537
|
+
/// Any thread; idempotent. Drops what is pending.
|
|
538
|
+
void close();
|
|
539
|
+
|
|
540
|
+
bool closed() const;
|
|
541
|
+
Stats stats() const;
|
|
542
|
+
|
|
543
|
+
private:
|
|
544
|
+
std::shared_ptr<Graph> graph_;
|
|
545
|
+
std::weak_ptr<Scope> mount_;
|
|
546
|
+
|
|
547
|
+
mutable std::mutex m_;
|
|
548
|
+
std::map<uint32_t, std::function<void()>> pending_;
|
|
549
|
+
bool scheduled_ = false;
|
|
550
|
+
bool closed_ = false;
|
|
551
|
+
Stats stats_;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
template <class T>
|
|
555
|
+
Signal<T> signal(const std::shared_ptr<Graph>& graph, T initial, Equality<T> equals = SameValue{}) {
|
|
556
|
+
graph->checkOwner();
|
|
557
|
+
return Signal<T>(std::make_shared<detail::SignalNode<T>>(graph, std::move(initial), std::move(equals)));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
template <class F, class T = std::decay_t<std::invoke_result_t<F&>>>
|
|
561
|
+
Computed<T> computed(const std::shared_ptr<Graph>& graph, F fn, Equality<T> equals = SameValue{}) {
|
|
562
|
+
graph->checkOwner();
|
|
563
|
+
return Computed<T>(std::make_shared<detail::ComputedNode<T>>(graph, std::function<T()>(std::move(fn)), std::move(equals)));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
} // namespace lucent::ui
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#include "report.h"
|
|
2
|
+
|
|
3
|
+
#include <cstdio>
|
|
4
|
+
#include <exception>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
#if defined(__ANDROID__)
|
|
8
|
+
#include <android/log.h>
|
|
9
|
+
#elif defined(__APPLE__)
|
|
10
|
+
#include <os/log.h>
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
namespace lucent {
|
|
14
|
+
|
|
15
|
+
namespace {
|
|
16
|
+
/// Where each platform shows an app's errors: logcat, the unified log, and
|
|
17
|
+
/// stderr (host runs; Android and release iOS apps drop it).
|
|
18
|
+
void writeError(const std::string& message) {
|
|
19
|
+
#if defined(__ANDROID__)
|
|
20
|
+
__android_log_write(ANDROID_LOG_ERROR, "Lucent", message.c_str());
|
|
21
|
+
#elif defined(__APPLE__)
|
|
22
|
+
os_log_error(OS_LOG_DEFAULT, "%{public}s", message.c_str());
|
|
23
|
+
#endif
|
|
24
|
+
std::fprintf(stderr, "%s\n", message.c_str());
|
|
25
|
+
}
|
|
26
|
+
} // namespace
|
|
27
|
+
|
|
28
|
+
void logError(const char* message) { writeError(message); }
|
|
29
|
+
|
|
30
|
+
void reportUncaught(std::exception_ptr e, const char* where) {
|
|
31
|
+
try {
|
|
32
|
+
std::rethrow_exception(e);
|
|
33
|
+
} catch (const std::exception& x) {
|
|
34
|
+
writeError(std::string("[lucent] uncaught exception in ") + where + ": " + x.what());
|
|
35
|
+
} catch (...) {
|
|
36
|
+
writeError(std::string("[lucent] uncaught exception in ") + where);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
} // namespace lucent
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Lucent runtime — the one place errors nobody can catch are reported.
|
|
2
|
+
#pragma once
|
|
3
|
+
|
|
4
|
+
#include <exception>
|
|
5
|
+
|
|
6
|
+
namespace lucent {
|
|
7
|
+
|
|
8
|
+
/// Reports an error no Lucent code can catch: thrown by a job, a microtask,
|
|
9
|
+
/// a cleanup, or a callback the platform made. `where` names the source.
|
|
10
|
+
void reportUncaught(std::exception_ptr e, const char* where);
|
|
11
|
+
|
|
12
|
+
/// Logs an error where the platform shows an app's errors (logcat, the
|
|
13
|
+
/// unified log) and to stderr.
|
|
14
|
+
void logError(const char* message);
|
|
15
|
+
|
|
16
|
+
} // namespace lucent
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#include "resource.h"
|
|
2
|
+
|
|
3
|
+
#include <stdexcept>
|
|
4
|
+
|
|
5
|
+
#include "report.h"
|
|
6
|
+
|
|
7
|
+
namespace lucent {
|
|
8
|
+
|
|
9
|
+
std::shared_ptr<Resource> Resource::open(String what, std::function<void()> release, ExecutionContext* releaseOn,
|
|
10
|
+
const std::shared_ptr<Scope>& scope) {
|
|
11
|
+
if (!release) throw std::invalid_argument("A resource's release must be a function");
|
|
12
|
+
|
|
13
|
+
std::shared_ptr<Resource> resource(new Resource(std::move(what), std::move(release), releaseOn));
|
|
14
|
+
if (!scope) return resource;
|
|
15
|
+
|
|
16
|
+
resource->scope_ = scope;
|
|
17
|
+
std::weak_ptr<Resource> weak = resource;
|
|
18
|
+
|
|
19
|
+
// Runs now, closing the resource, if the scope is no longer active. A
|
|
20
|
+
// disposal on another thread may run it before the id is stored: the
|
|
21
|
+
// stale id is then never removed, as the scope has taken its cleanups.
|
|
22
|
+
Scope::CleanupId id = scope->onDispose([weak] {
|
|
23
|
+
if (auto r = weak.lock()) r->close();
|
|
24
|
+
});
|
|
25
|
+
resource->cleanup_.store(id);
|
|
26
|
+
|
|
27
|
+
return resource;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Resource::Resource(String what, std::function<void()> release, ExecutionContext* releaseOn)
|
|
31
|
+
: what_(std::move(what)), releaseOn_(releaseOn), release_(std::move(release)) {}
|
|
32
|
+
|
|
33
|
+
Resource::~Resource() {
|
|
34
|
+
// Nothing else refers to it now, so nothing can close it concurrently.
|
|
35
|
+
if (state_.load(std::memory_order_acquire) != State::Open) return;
|
|
36
|
+
|
|
37
|
+
state_.store(State::Closing, std::memory_order_release);
|
|
38
|
+
unlink();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
detail::runOn(releaseOn_, std::move(release_));
|
|
42
|
+
} catch (...) {
|
|
43
|
+
reportUncaught(std::current_exception(), "resource");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void Resource::check() const {
|
|
48
|
+
if (isOpen()) return;
|
|
49
|
+
|
|
50
|
+
throwError(String::fromLatin1("InvalidStateError"), what_ + String::fromLatin1(" is closed"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
bool Resource::close() {
|
|
54
|
+
State expected = State::Open;
|
|
55
|
+
if (!state_.compare_exchange_strong(expected, State::Closing, std::memory_order_acq_rel)) return false;
|
|
56
|
+
|
|
57
|
+
unlink();
|
|
58
|
+
|
|
59
|
+
// Closed once the release has run, wherever it runs, even if it throws.
|
|
60
|
+
auto job = [self = weak_from_this(), release = std::move(release_)] {
|
|
61
|
+
struct Closed {
|
|
62
|
+
const std::weak_ptr<Resource>& self;
|
|
63
|
+
|
|
64
|
+
~Closed() {
|
|
65
|
+
if (auto r = self.lock()) r->state_.store(State::Closed, std::memory_order_release);
|
|
66
|
+
}
|
|
67
|
+
} closed{self};
|
|
68
|
+
|
|
69
|
+
release();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
detail::runOn(releaseOn_, std::move(job));
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void Resource::unlink() {
|
|
77
|
+
Scope::CleanupId id = cleanup_.exchange(0);
|
|
78
|
+
if (id == 0) return;
|
|
79
|
+
|
|
80
|
+
if (auto scope = scope_.lock()) scope->remove(id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
} // namespace lucent
|