@lucent-lang/lucent 0.0.4 → 0.1.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.
Files changed (152) hide show
  1. package/README.md +97 -0
  2. package/app.plugin.js +46 -10
  3. package/dist/{bench-CR3E7e2e.js → bench-B555m0wU.js} +10 -6
  4. package/dist/{build-CAIrRSYA.js → build-UZ5Zm5m6.js} +1 -1
  5. package/dist/{build-check-QozuNJm5.js → build-check-Dmf8_yiw.js} +28 -13
  6. package/dist/{check-1SCmYlqA.js → check-D-H9WLG4.js} +1 -1
  7. package/dist/cli.js +62 -13
  8. package/dist/{codes-CIuwXt1S.js → codes-D-6LxUqD.js} +163 -7
  9. package/dist/compiler.js +4 -4
  10. package/dist/{dashboard-B-NzjjaN.js → dashboard-C8iF5-6T.js} +2 -2
  11. package/dist/{dev-BYWMP7oC.js → dev-B3lSx5CF.js} +62 -23
  12. package/dist/{diagnostic-BM9sXmwl.js → diagnostic-BvwLZQS1.js} +3 -1
  13. package/dist/{doctor-C0PrfWT6.js → doctor-CTAX3euc.js} +1 -1
  14. package/dist/{doctor-DdR4JnVc.js → doctor-CrgOAV5L.js} +1 -1
  15. package/dist/{explain--vV1oaRl.js → explain-Byit07F-.js} +1 -1
  16. package/dist/{packages-HyskKIsY.js → packages-CSmKeaAh.js} +2 -33
  17. package/dist/pipeline-BBY0df_2.js +700 -0
  18. package/dist/project-BLhWwnns.js +342 -0
  19. package/dist/sdk-coverage-DFGiDOzR.js +113 -0
  20. package/dist/sdk-diff-DpPYWzrl.js +109 -0
  21. package/dist/sdk-lock-QOxEFD1o.js +38 -0
  22. package/dist/{sdk-prefetch-mo04JDZV.js → sdk-prefetch-DX6ztlPN.js} +2 -2
  23. package/dist/{sdk-search-Iv4K3-8a.js → sdk-search-BShBL1LL.js} +2 -2
  24. package/dist/{sdk-show-CkrdlqlP.js → sdk-show-B3bI-0M7.js} +5 -4
  25. package/dist/sdk-usage-ljX5bTuZ.js +95 -0
  26. package/dist/src-DlICbK91.js +39682 -0
  27. package/dist/trace-Bi4hkmPx.js +139 -0
  28. package/lib/core.cjs +368 -1
  29. package/lib/sdk/android.d.ts +54 -1
  30. package/lib/sdk/compose.d.ts +94 -0
  31. package/lib/sdk/compose.schemas.json.gz +0 -0
  32. package/lib/sdk/core.d.ts +201 -0
  33. package/lib/sdk/ios.d.ts +75 -2
  34. package/lib/sdk/ui.d.ts +129 -0
  35. package/metro/transformer.cjs +11 -2
  36. package/package.json +2 -1
  37. package/runtime/cpp/lucent/abort.cpp +59 -11
  38. package/runtime/cpp/lucent/abort.h +22 -6
  39. package/runtime/cpp/lucent/array.h +12 -2
  40. package/runtime/cpp/lucent/async.h +101 -21
  41. package/runtime/cpp/lucent/bigint.cpp +898 -0
  42. package/runtime/cpp/lucent/bigint.h +302 -0
  43. package/runtime/cpp/lucent/buffer.cpp +228 -0
  44. package/runtime/cpp/lucent/buffer.h +328 -0
  45. package/runtime/cpp/lucent/builtins.cpp +4 -1
  46. package/runtime/cpp/lucent/bytes.h +21 -2
  47. package/runtime/cpp/lucent/callback.cpp +29 -0
  48. package/runtime/cpp/lucent/callback.h +230 -0
  49. package/runtime/cpp/lucent/compute.cpp +370 -0
  50. package/runtime/cpp/lucent/compute.h +455 -0
  51. package/runtime/cpp/lucent/conversions.h +111 -0
  52. package/runtime/cpp/lucent/core.h +16 -5
  53. package/runtime/cpp/lucent/equality.h +107 -28
  54. package/runtime/cpp/lucent/execution.cpp +416 -0
  55. package/runtime/cpp/lucent/execution.h +270 -0
  56. package/runtime/cpp/lucent/extension.cpp +97 -0
  57. package/runtime/cpp/lucent/extension.h +210 -0
  58. package/runtime/cpp/lucent/helpers.h +12 -5
  59. package/runtime/cpp/lucent/items.h +52 -0
  60. package/runtime/cpp/lucent/jserror.cpp +9 -0
  61. package/runtime/cpp/lucent/jserror.h +11 -0
  62. package/runtime/cpp/lucent/jsi/convert.cpp +138 -0
  63. package/runtime/cpp/lucent/jsi/convert.h +128 -59
  64. package/runtime/cpp/lucent/jsi/host.cpp +213 -35
  65. package/runtime/cpp/lucent/jsi/host.h +116 -16
  66. package/runtime/cpp/lucent/json.h +19 -1
  67. package/runtime/cpp/lucent/lifecycle.cpp +260 -0
  68. package/runtime/cpp/lucent/lifecycle.h +124 -0
  69. package/runtime/cpp/lucent/lucent.h +7 -0
  70. package/runtime/cpp/lucent/map.h +41 -7
  71. package/runtime/cpp/lucent/native.cpp +35 -47
  72. package/runtime/cpp/lucent/native.h +116 -26
  73. package/runtime/cpp/lucent/number.cpp +38 -38
  74. package/runtime/cpp/lucent/number.h +33 -0
  75. package/runtime/cpp/lucent/operation.h +65 -0
  76. package/runtime/cpp/lucent/ops.h +14 -6
  77. package/runtime/cpp/lucent/platform/android.cpp +223 -10
  78. package/runtime/cpp/lucent/platform/android.h +227 -4
  79. package/runtime/cpp/lucent/platform/android_activity.cpp +258 -0
  80. package/runtime/cpp/lucent/platform/android_requests.h +140 -0
  81. package/runtime/cpp/lucent/platform/compose.h +256 -0
  82. package/runtime/cpp/lucent/platform/ios.h +325 -25
  83. package/runtime/cpp/lucent/platform/ios.mm +1 -14
  84. package/runtime/cpp/lucent/platform/ios_ui.h +192 -0
  85. package/runtime/cpp/lucent/platform/ios_ui.mm +454 -0
  86. package/runtime/cpp/lucent/platform/swiftui.h +190 -0
  87. package/runtime/cpp/lucent/presentation.cpp +11 -0
  88. package/runtime/cpp/lucent/presentation.h +109 -0
  89. package/runtime/cpp/lucent/reactive.cpp +406 -0
  90. package/runtime/cpp/lucent/reactive.h +566 -0
  91. package/runtime/cpp/lucent/report.cpp +40 -0
  92. package/runtime/cpp/lucent/report.h +16 -0
  93. package/runtime/cpp/lucent/resource.cpp +83 -0
  94. package/runtime/cpp/lucent/resource.h +75 -0
  95. package/runtime/cpp/lucent/scheduler.cpp +38 -124
  96. package/runtime/cpp/lucent/scheduler.h +102 -83
  97. package/runtime/cpp/lucent/scope.cpp +308 -0
  98. package/runtime/cpp/lucent/scope.h +452 -0
  99. package/runtime/cpp/lucent/sizing.h +263 -0
  100. package/runtime/cpp/lucent/slots.h +231 -0
  101. package/runtime/cpp/lucent/trace.cpp +539 -0
  102. package/runtime/cpp/lucent/trace.h +251 -0
  103. package/runtime/cpp/lucent/transport.cpp +9 -0
  104. package/runtime/cpp/lucent/transport.h +341 -0
  105. package/runtime/cpp/lucent/view.cpp +123 -0
  106. package/runtime/cpp/lucent/view.h +157 -0
  107. package/runtime/cpp/rn/LucentComponentView.h +121 -0
  108. package/runtime/cpp/rn/LucentComponentView.mm +479 -0
  109. package/runtime/cpp/rn/LucentModule.cpp +37 -7
  110. package/runtime/cpp/rn/LucentModule.h +3 -0
  111. package/runtime/cpp/rn/LucentViewChildren.h +80 -0
  112. package/runtime/cpp/rn/LucentViewChildren.mm +211 -0
  113. package/runtime/cpp/rn/LucentViewRegistry.h +62 -0
  114. package/runtime/cpp/rn/LucentViewRequests.cpp +91 -0
  115. package/runtime/cpp/rn/LucentViewRequests.h +104 -0
  116. package/runtime/cpp/rn/LucentViewSizing.h +345 -0
  117. package/runtime/cpp/rn/LucentViewSlots.h +242 -0
  118. package/runtime/cpp/rn/LucentViewValues.h +125 -0
  119. package/runtime/cpp/rn/LucentViews.h +440 -0
  120. package/runtime/cpp/rn/LucentViewsAndroid.cpp +439 -0
  121. package/runtime/cpp/rn/LucentViewsAndroid.h +138 -0
  122. package/runtime/cpp/third_party/dragonbox/LICENSE-Boost +23 -0
  123. package/runtime/cpp/third_party/dragonbox/README.md +16 -0
  124. package/runtime/cpp/third_party/dragonbox/dragonbox.h +4440 -0
  125. package/runtime/js/index.d.ts +11 -1
  126. package/runtime/js/index.js +84 -8
  127. package/runtime/js/views.d.ts +38 -0
  128. package/runtime/js/views.js +226 -0
  129. package/runtime/native/android/CMakeLists.txt +4 -1
  130. package/runtime/native/android/src/compose/java/dev/lucent/compose/CompositionRules.kt +44 -0
  131. package/runtime/native/android/src/compose/java/dev/lucent/compose/LucentComposition.kt +161 -0
  132. package/runtime/native/android/src/main/java/dev/lucent/LucentActivities.java +292 -0
  133. package/runtime/native/android/src/main/java/dev/lucent/LucentChildren.java +174 -0
  134. package/runtime/native/android/src/main/java/dev/lucent/LucentHostView.java +232 -0
  135. package/runtime/native/android/src/main/java/dev/lucent/LucentInitializer.java +46 -0
  136. package/runtime/native/android/src/main/java/dev/lucent/LucentPackage.java +4 -3
  137. package/runtime/native/android/src/main/java/dev/lucent/LucentRequestActivity.java +92 -0
  138. package/runtime/native/android/src/main/java/dev/lucent/LucentSlotView.java +102 -0
  139. package/runtime/native/android/src/main/java/dev/lucent/LucentViewManager.java +149 -0
  140. package/runtime/native/android/src/main/java/dev/lucent/LucentViewManagers.java +16 -0
  141. package/runtime/native/android/src/main/java/dev/lucent/LucentViews.java +44 -0
  142. package/runtime/test/jsi/harness.cpp +9 -0
  143. package/schemas/build.schema.json +96 -1
  144. package/schemas/check.schema.json +56 -1
  145. package/schemas/lucent.schema.json +495 -9
  146. package/schemas/sdk-diff.schema.json +70 -0
  147. package/schemas/sdk-lock.schema.json +67 -0
  148. package/ts-plugin/index.js +77 -4
  149. package/dist/pipeline-CH7-2cxu.js +0 -232
  150. package/dist/project-D8tEet0k.js +0 -150
  151. package/dist/sdk-coverage-BoW_3Efd.js +0 -64
  152. package/dist/src-BD7f1_aZ.js +0 -11493
@@ -0,0 +1,452 @@
1
+ // Lucent runtime — lifetime scopes and one-shot operations.
2
+ //
3
+ // A Scope owns work that must end when its owner does (a module instance,
4
+ // a view mount, a subscription, a task): child scopes, pending operations
5
+ // and cleanups. Disposing it ends all of them, deterministically and once.
6
+ //
7
+ // An Operation is one pending native request under a scope: it settles
8
+ // exactly once, from any thread, and whatever loses the race is released,
9
+ // never delivered. Its token names it without a pointer, so a late native
10
+ // callback can be checked against the scope's current state.
11
+ //
12
+ // Neither knows about JavaScript, the scheduler or the Lucent lock: callbacks
13
+ // run on the thread that settles or disposes, except that a scope with an
14
+ // owner (Scope::Owner, an execution context) is disposed there.
15
+ #pragma once
16
+
17
+ #include <atomic>
18
+ #include <cstdint>
19
+ #include <exception>
20
+ #include <functional>
21
+ #include <map>
22
+ #include <memory>
23
+ #include <mutex>
24
+ #include <optional>
25
+ #include <stdexcept>
26
+ #include <type_traits>
27
+ #include <utility>
28
+ #include <vector>
29
+
30
+ #include "core.h"
31
+ #include "jserror.h"
32
+
33
+ namespace lucent {
34
+
35
+ /// 0: no JS runtime (headless or native-only work).
36
+ using RuntimeId = uint32_t;
37
+ /// Process-unique and never reused; 0 is none.
38
+ using ScopeId = uint64_t;
39
+ /// Per scope; changes when the scope is disposed.
40
+ using Generation = uint32_t;
41
+ /// Process-unique and never reused; 0 is none.
42
+ using OperationId = uint64_t;
43
+
44
+ /// Valid only while all four fields match a live scope and the operation is
45
+ /// still pending there (Scope::validates).
46
+ struct OperationToken {
47
+ RuntimeId runtime = 0;
48
+ ScopeId scope = 0;
49
+ Generation generation = 0;
50
+ OperationId operation = 0;
51
+
52
+ friend bool operator==(const OperationToken&, const OperationToken&) = default;
53
+ };
54
+
55
+ enum class OperationState : uint8_t { Pending, Succeeded, Failed, Cancelled };
56
+
57
+ /// The AbortError a disposing scope cancels its pending operations with.
58
+ Error scopeDisposedError();
59
+
60
+ class Scope;
61
+
62
+ namespace detail {
63
+
64
+ /// Aggregates like `using`: `next`, thrown while `pending` was, becomes a
65
+ /// SuppressedError of both. Either may be null.
66
+ std::exception_ptr aggregateErrors(std::exception_ptr pending, std::exception_ptr next);
67
+
68
+ /// The part of an operation its scope uses, whatever the value type.
69
+ class OperationBase : public std::enable_shared_from_this<OperationBase> {
70
+ public:
71
+ OperationBase(const OperationBase&) = delete;
72
+ OperationBase& operator=(const OperationBase&) = delete;
73
+ virtual ~OperationBase() = default;
74
+
75
+ const OperationToken& token() const { return token_; }
76
+
77
+ OperationState state() const { return state_.load(std::memory_order_acquire); }
78
+
79
+ /// Outcomes that arrived after settlement and were dropped: repeated or
80
+ /// racing native callbacks.
81
+ uint64_t lateOutcomes() const { return late_.load(std::memory_order_relaxed); }
82
+
83
+ /// The registration's cleanup (unregistering the native listener, say).
84
+ /// It runs once, after settlement: then, or now if the operation already
85
+ /// settled (it completed during registration). An empty function removes
86
+ /// a cleanup that has not run; setting a second one is a caller bug.
87
+ void setCleanup(std::function<void()> cleanup);
88
+
89
+ protected:
90
+ OperationBase() = default;
91
+
92
+ /// Takes a token from `scope`, and joins its pending operations if
93
+ /// `pending` and the scope is active; false otherwise.
94
+ bool attach(const std::shared_ptr<Scope>& scope, bool pending);
95
+
96
+ /// Leaves the scope's pending operations, once settled.
97
+ void detach();
98
+
99
+ /// Runs the cleanup if the operation settled and it has not run.
100
+ std::exception_ptr runCleanup();
101
+
102
+ void countLate() { late_.fetch_add(1, std::memory_order_relaxed); }
103
+
104
+ /// Reports what an operation's callback threw (reportUncaught).
105
+ static void report(std::exception_ptr e);
106
+
107
+ /// Cancels for Scope::dispose, which runs the cleanups afterwards. Returns
108
+ /// what the listeners threw.
109
+ virtual std::exception_ptr cancelOwned(const Error& reason) = 0;
110
+
111
+ /// Guards the state transition and every callback slot.
112
+ mutable std::mutex m_;
113
+ std::atomic<OperationState> state_{OperationState::Pending};
114
+
115
+ private:
116
+ friend class lucent::Scope;
117
+
118
+ OperationToken token_;
119
+ std::weak_ptr<Scope> scope_;
120
+ std::function<void()> cleanup_;
121
+ std::atomic<uint64_t> late_{0};
122
+ };
123
+
124
+ } // namespace detail
125
+
126
+ /// States only move forward: active, disposing, disposed.
127
+ ///
128
+ /// Every method is safe from any thread. A scope may have an owner, the
129
+ /// execution context its work belongs to: then dispose() from any other
130
+ /// thread, and the last reference dropped there, post the disposal to the
131
+ /// owner and return at once. Otherwise, or once the owner takes no more
132
+ /// work, cleanups run on the thread that disposes. A dispose() that finds
133
+ /// the scope already disposing (reentrant, or on another thread) returns at
134
+ /// once without waiting for it.
135
+ class Scope : public std::enable_shared_from_this<Scope> {
136
+ public:
137
+ enum class State : uint8_t { Active, Disposing, Disposed };
138
+ using CleanupId = uint64_t;
139
+
140
+ /// Where a scope's disposal runs: an execution context (execution.h).
141
+ class Owner {
142
+ public:
143
+ virtual bool isCurrent() const = 0;
144
+
145
+ /// Runs `job` there later; false if it takes no more work.
146
+ virtual bool post(std::function<void()> job) = 0;
147
+
148
+ protected:
149
+ ~Owner() = default;
150
+ };
151
+
152
+ /// A child has its parent's owner and belongs to its parent's runtime:
153
+ /// under a parent with a runtime, another `runtime` (0 included) throws
154
+ /// std::invalid_argument. A child of an inactive parent is created
155
+ /// disposed.
156
+ static std::shared_ptr<Scope> create(RuntimeId runtime, const std::shared_ptr<Scope>& parent = nullptr);
157
+
158
+ /// A scope without a parent, owned by `owner`.
159
+ static std::shared_ptr<Scope> createRoot(RuntimeId runtime, std::weak_ptr<Owner> owner);
160
+
161
+ /// The last reference to an active scope disposes it; errors go to
162
+ /// reportUncaught.
163
+ ~Scope();
164
+
165
+ Scope(const Scope&) = delete;
166
+ Scope& operator=(const Scope&) = delete;
167
+
168
+ ScopeId id() const { return id_; }
169
+
170
+ RuntimeId runtime() const { return runtime_; }
171
+
172
+ Generation generation() const { return generation_.load(std::memory_order_acquire); }
173
+
174
+ State state() const { return state_.load(std::memory_order_acquire); }
175
+
176
+ /// Registers a cleanup. If the scope is not active, runs it now, on the
177
+ /// calling thread (its error reaches the caller), and returns 0.
178
+ CleanupId onDispose(std::function<void()> cleanup);
179
+
180
+ /// Removes a cleanup that has not run; false if it ran, was removed, or
181
+ /// disposal has begun.
182
+ bool remove(CleanupId id);
183
+
184
+ /// Idempotent. Changes the generation (invalidating tokens) and rejects
185
+ /// new registrations, then disposes the children (most recently created
186
+ /// first), cancels the pending operations (most recent first) and runs
187
+ /// their cleanups, then runs this scope's cleanups in reverse order.
188
+ /// Everything runs even if something throws; the errors come back
189
+ /// aggregated like `using`: the last one, with the earlier ones as
190
+ /// SuppressedError. Posted to the owner, the scope stays active until the
191
+ /// disposal runs there, and its errors go to reportUncaught.
192
+ std::exception_ptr dispose();
193
+
194
+ bool validates(const OperationToken& token) const;
195
+
196
+ /// What the scope holds now: live children, pending operations and
197
+ /// cleanups not yet run (for debug ownership reports).
198
+ size_t registrations() const;
199
+
200
+ private:
201
+ friend class detail::OperationBase;
202
+
203
+ Scope(RuntimeId runtime, const std::shared_ptr<Scope>& parent, std::weak_ptr<Owner> owner);
204
+
205
+ /// What disposal runs once the scope has stopped taking registrations.
206
+ struct Teardown {
207
+ std::map<ScopeId, std::shared_ptr<Scope>> children;
208
+ std::map<OperationId, std::shared_ptr<detail::OperationBase>> operations;
209
+ std::map<CleanupId, std::function<void()>> cleanups;
210
+ };
211
+
212
+ /// Moves the scope to disposing and takes its teardown; empty if it was
213
+ /// not active.
214
+ std::optional<Teardown> begin();
215
+
216
+ static std::exception_ptr finish(Teardown& teardown);
217
+
218
+ /// Disposes on the calling thread.
219
+ std::exception_ptr disposeHere();
220
+
221
+ bool adopt(const std::shared_ptr<detail::OperationBase>& op, bool pending);
222
+ void forget(OperationId id);
223
+ bool adoptChild(const std::shared_ptr<Scope>& child);
224
+ void forgetChild(ScopeId id);
225
+
226
+ const RuntimeId runtime_;
227
+ const ScopeId id_;
228
+ const std::weak_ptr<Scope> parent_;
229
+ const std::weak_ptr<Owner> owner_;
230
+
231
+ std::atomic<Generation> generation_{1};
232
+ std::atomic<State> state_{State::Active};
233
+
234
+ /// Guards the containers below. Never held while user code runs or while
235
+ /// a user callback is destroyed.
236
+ mutable std::mutex m_;
237
+ CleanupId nextCleanup_ = 1;
238
+ // Keyed by id, so iteration order is registration or creation order.
239
+ std::map<CleanupId, std::function<void()>> cleanups_;
240
+ std::map<ScopeId, std::shared_ptr<Scope>> children_;
241
+ std::map<OperationId, std::shared_ptr<detail::OperationBase>> operations_;
242
+ };
243
+
244
+ /// One pending request with exactly one outcome: a value (none for void),
245
+ /// an error, or a cancellation. Settle it from any thread; only the first
246
+ /// call wins. Errors thrown by its listeners and cleanup are reported with
247
+ /// reportUncaught, except during Scope::dispose, which returns them.
248
+ template <class T>
249
+ class Operation final : public detail::OperationBase {
250
+ public:
251
+ using Value = std::conditional_t<std::is_void_v<T>, Undefined, T>;
252
+
253
+ struct Outcome {
254
+ OperationState state = OperationState::Pending;
255
+ /// Set when Succeeded.
256
+ std::optional<Value> value;
257
+ /// Set when Failed or Cancelled.
258
+ Error error;
259
+ };
260
+
261
+ using Listener = std::function<void(const Outcome&)>;
262
+
263
+ /// Begins the native work and returns the cleanup that ends it (or an
264
+ /// empty function). It may settle the operation before returning.
265
+ using Registration = std::function<std::function<void()>(const std::shared_ptr<Operation>&)>;
266
+
267
+ /// Pending under `scope`; Cancelled if the scope is not active.
268
+ static std::shared_ptr<Operation> start(const std::shared_ptr<Scope>& scope) { return create(scope, nullptr); }
269
+
270
+ /// Starts an operation and runs `registration`. Already aborted (an
271
+ /// aborted signal gives `abortedBy`) or under an inactive scope, the
272
+ /// operation starts Cancelled and registration is not called. If
273
+ /// registration throws, the operation fails with that error.
274
+ static std::shared_ptr<Operation> start(const std::shared_ptr<Scope>& scope, const Registration& registration,
275
+ Opt<Error> abortedBy = {}) {
276
+ if (!registration) throw std::invalid_argument("An operation's registration must be a function");
277
+
278
+ auto op = create(scope, abortedBy.has() ? abortedBy.get() : nullptr);
279
+
280
+ if (op->state() != OperationState::Pending) return op;
281
+
282
+ std::function<void()> cleanup;
283
+ try {
284
+ cleanup = registration(op);
285
+ } catch (...) {
286
+ op->fail(currentError(std::current_exception()));
287
+ return op;
288
+ }
289
+
290
+ if (cleanup) op->setCleanup(std::move(cleanup));
291
+
292
+ return op;
293
+ }
294
+
295
+ /// True only for the call that settles the operation.
296
+ bool succeed(Value value)
297
+ requires(!std::is_void_v<T>)
298
+ {
299
+ return complete(OperationState::Succeeded, std::optional<Value>(std::move(value)), nullptr);
300
+ }
301
+
302
+ bool succeed()
303
+ requires std::is_void_v<T>
304
+ {
305
+ return complete(OperationState::Succeeded, std::optional<Value>(Value{}), nullptr);
306
+ }
307
+
308
+ bool fail(Error error) { return complete(OperationState::Failed, std::nullopt, std::move(error)); }
309
+
310
+ bool cancel(Error reason) { return complete(OperationState::Cancelled, std::nullopt, std::move(reason)); }
311
+
312
+ /// Runs `listener` once with the outcome: at settlement, on the settling
313
+ /// thread, or now if already settled. Listeners run in the order added.
314
+ void onSettled(Listener listener) {
315
+ if (!listener) throw std::invalid_argument("An operation listener must be a function");
316
+
317
+ {
318
+ std::lock_guard<std::mutex> g(m_);
319
+
320
+ if (state_.load(std::memory_order_relaxed) == OperationState::Pending) {
321
+ listeners_.push_back(std::move(listener));
322
+ return;
323
+ }
324
+ }
325
+
326
+ try {
327
+ listener(outcome_);
328
+ } catch (...) {
329
+ report(std::current_exception());
330
+ }
331
+ }
332
+
333
+ /// Receives the value of a success that lost (to a cancellation, say), so
334
+ /// its resources are released rather than delivered. Without a hook it is
335
+ /// destroyed. Runs on the losing caller's thread.
336
+ void onLateOutcome(std::function<void(Value&&)> release)
337
+ requires(!std::is_void_v<T>)
338
+ {
339
+ std::lock_guard<std::mutex> g(m_);
340
+
341
+ std::swap(lateRelease_, release);
342
+ }
343
+
344
+ private:
345
+ Operation() = default;
346
+
347
+ static std::shared_ptr<Operation> create(const std::shared_ptr<Scope>& scope, Error cancelledBy) {
348
+ if (!scope) throw std::invalid_argument("An operation needs a scope");
349
+
350
+ std::shared_ptr<Operation> op(new Operation());
351
+
352
+ if (!op->attach(scope, cancelledBy == nullptr)) {
353
+ std::optional<Value> none;
354
+ Error reason = cancelledBy ? std::move(cancelledBy) : scopeDisposedError();
355
+ std::vector<Listener> nobody;
356
+
357
+ op->transition(OperationState::Cancelled, none, reason, nobody);
358
+ }
359
+
360
+ return op;
361
+ }
362
+
363
+ /// The winning transition records the outcome and takes the listeners;
364
+ /// false if already settled.
365
+ bool transition(OperationState to, std::optional<Value>& value, Error& error, std::vector<Listener>& listeners) {
366
+ if (state_.load(std::memory_order_acquire) != OperationState::Pending) return false;
367
+
368
+ std::lock_guard<std::mutex> g(m_);
369
+
370
+ OperationState expected = OperationState::Pending;
371
+ if (!state_.compare_exchange_strong(expected, to, std::memory_order_acq_rel)) return false;
372
+
373
+ outcome_.state = to;
374
+ outcome_.value = std::move(value);
375
+ outcome_.error = std::move(error);
376
+ listeners.swap(listeners_);
377
+ return true;
378
+ }
379
+
380
+ /// A settlement by a caller. A loser is counted and its value released.
381
+ bool complete(OperationState to, std::optional<Value> value, Error error) {
382
+ // Leaving the scope may drop its reference, the last but this one.
383
+ auto self = shared_from_this();
384
+ std::vector<Listener> listeners;
385
+
386
+ if (!transition(to, value, error, listeners)) {
387
+ countLate();
388
+ releaseLate(std::move(value));
389
+ return false;
390
+ }
391
+
392
+ detach();
393
+
394
+ for (auto& listener : listeners) {
395
+ try {
396
+ listener(outcome_);
397
+ } catch (...) {
398
+ report(std::current_exception());
399
+ }
400
+ }
401
+
402
+ report(runCleanup());
403
+
404
+ return true;
405
+ }
406
+
407
+ std::exception_ptr cancelOwned(const Error& reason) override {
408
+ std::optional<Value> none;
409
+ Error error = reason;
410
+ std::vector<Listener> listeners;
411
+
412
+ if (!transition(OperationState::Cancelled, none, error, listeners)) return nullptr;
413
+
414
+ std::exception_ptr errors;
415
+ for (auto& listener : listeners) {
416
+ try {
417
+ listener(outcome_);
418
+ } catch (...) {
419
+ errors = detail::aggregateErrors(errors, std::current_exception());
420
+ }
421
+ }
422
+
423
+ return errors;
424
+ }
425
+
426
+ void releaseLate(std::optional<Value>&& value) {
427
+ if constexpr (!std::is_void_v<T>) {
428
+ if (!value) return;
429
+
430
+ std::function<void(Value&&)> release;
431
+ {
432
+ std::lock_guard<std::mutex> g(m_);
433
+ release = lateRelease_;
434
+ }
435
+
436
+ if (!release) return;
437
+
438
+ try {
439
+ release(std::move(*value));
440
+ } catch (...) {
441
+ report(std::current_exception());
442
+ }
443
+ }
444
+ }
445
+
446
+ /// Written once, by the winning transition under m_; read-only after.
447
+ Outcome outcome_;
448
+ std::vector<Listener> listeners_;
449
+ std::function<void(Value&&)> lateRelease_;
450
+ };
451
+
452
+ } // namespace lucent