round-core 0.1.3 → 0.1.4
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 +68 -18
- package/package.json +1 -1
- package/src/compiler/transformer.js +85 -0
- package/src/index.d.ts +86 -19
- package/src/index.js +4 -12
- package/src/runtime/context-shared.js +63 -0
- package/src/runtime/context.js +27 -58
- package/src/runtime/dom.js +5 -4
- package/src/runtime/error-reporter.js +12 -4
- package/src/runtime/signals.js +96 -3
- package/src/runtime/suspense.js +1 -1
package/README.md
CHANGED
|
@@ -73,9 +73,19 @@ A **signal** is a small reactive container.
|
|
|
73
73
|
Round includes a CLI with a project initializer.
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
+
# Install the CLI
|
|
77
|
+
npm install round
|
|
78
|
+
|
|
79
|
+
# Create a new app
|
|
76
80
|
round init myapp
|
|
81
|
+
|
|
82
|
+
# Navigate to the app directory
|
|
77
83
|
cd myapp
|
|
84
|
+
|
|
85
|
+
# Install dependencies
|
|
78
86
|
npm install
|
|
87
|
+
|
|
88
|
+
# Run the app
|
|
79
89
|
npm run dev
|
|
80
90
|
```
|
|
81
91
|
|
|
@@ -163,6 +173,39 @@ effect(() => {
|
|
|
163
173
|
name('Grace');
|
|
164
174
|
```
|
|
165
175
|
|
|
176
|
+
### `asyncSignal(fetcher)`
|
|
177
|
+
|
|
178
|
+
Create a signal that manages asynchronous data fetching.
|
|
179
|
+
|
|
180
|
+
- It returns a signal that resolves to the data once fetched.
|
|
181
|
+
- **`.pending`**: A reactive signal (boolean) indicating if the fetch is in progress.
|
|
182
|
+
- **`.error`**: A reactive signal containing any error that occurred during fetching.
|
|
183
|
+
- **`.refetch()`**: A method to manually trigger a re-fetch.
|
|
184
|
+
|
|
185
|
+
```jsx
|
|
186
|
+
import { asyncSignal } from 'round-core';
|
|
187
|
+
|
|
188
|
+
const user = asyncSignal(async () => {
|
|
189
|
+
const res = await fetch('/api/user');
|
|
190
|
+
return res.json();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export function UserProfile() {
|
|
194
|
+
return (
|
|
195
|
+
<div>
|
|
196
|
+
{if(user.pending()){
|
|
197
|
+
<div>Loading...</div>
|
|
198
|
+
} else if(user.error()){
|
|
199
|
+
<div>Error: {user.error().message}</div>
|
|
200
|
+
} else {
|
|
201
|
+
<div>Welcome, {user().name}</div>
|
|
202
|
+
}}
|
|
203
|
+
<button onClick={() => user.refetch()}>Reload</button>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
166
209
|
### `untrack(fn)`
|
|
167
210
|
|
|
168
211
|
Run a function without tracking any signals it reads.
|
|
@@ -385,6 +428,25 @@ Notes:
|
|
|
385
428
|
- The `switch` expression is automatically wrapped in a reactive tracker, ensuring that the view updates surgically when the condition (e.g., a signal) changes.
|
|
386
429
|
- Each case handles its own rendering without re-running the parent component.
|
|
387
430
|
|
|
431
|
+
### `try / catch`
|
|
432
|
+
|
|
433
|
+
Round supports both static and **reactive** `try/catch` blocks inside JSX.
|
|
434
|
+
|
|
435
|
+
- **Static**: Just like standard JS, but renders fragments.
|
|
436
|
+
- **Reactive**: By passing a signal to `try(signal)`, the block will **automatically re-run** if the signal (or its dependencies) update. This is perfect for handling transient errors in async data.
|
|
437
|
+
|
|
438
|
+
```jsx
|
|
439
|
+
{try(user()) {
|
|
440
|
+
{if(user() && user().name) {
|
|
441
|
+
<div>Hello {user().name}</div>
|
|
442
|
+
} else if(user.pending()) {
|
|
443
|
+
<div>⏳ Loading...</div>
|
|
444
|
+
}}
|
|
445
|
+
} catch(e) {
|
|
446
|
+
<div className="error"> Failed to load user: {e.message} </div>
|
|
447
|
+
}}
|
|
448
|
+
```
|
|
449
|
+
|
|
388
450
|
## Routing
|
|
389
451
|
|
|
390
452
|
Round includes router primitives intended for SPA navigation. All route paths must start with a forward slash `/`.
|
|
@@ -452,26 +514,14 @@ const LazyWidget = lazy(() => import('./Widget'));
|
|
|
452
514
|
|
|
453
515
|
## Error handling
|
|
454
516
|
|
|
455
|
-
Round
|
|
456
|
-
|
|
457
|
-
- Runtime error reporting with safe boundaries.
|
|
458
|
-
- `ErrorBoundary` to catch render-time errors and show a fallback.
|
|
459
|
-
|
|
460
|
-
### `ErrorBoundary`
|
|
461
|
-
|
|
462
|
-
Wrap components to prevent the whole app from crashing if a child fails to render.
|
|
517
|
+
Round JS favors individual error control and standard browser debugging:
|
|
463
518
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
function DangerousComponent() {
|
|
468
|
-
throw new Error('Boom!');
|
|
469
|
-
}
|
|
519
|
+
1. **Explict `try/catch`**: Use the JSX `try/catch` syntax to handle local component failures gracefully.
|
|
520
|
+
2. **Console-First Reporting**: Unhandled errors in component rendering or reactive effects are logged to the browser console with descriptive metadata (component name, render phase) and then allowed to propagate.
|
|
521
|
+
3. **No Intrusive Overlays**: Round has removed conflicting global error boundaries to ensure that your local handling logic always takes precedence and the developer experience remains clean.
|
|
470
522
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
</ErrorBoundary>
|
|
474
|
-
```
|
|
523
|
+
Example of a descriptive console log:
|
|
524
|
+
`[round] Error in phase "component.render" of component <UserProfile />: TypeError: Cannot read property 'avatar' of undefined`
|
|
475
525
|
|
|
476
526
|
## CLI
|
|
477
527
|
|
package/package.json
CHANGED
|
@@ -262,6 +262,81 @@ export function transform(code, initialDepth = 0) {
|
|
|
262
262
|
return { end: endIdx, replacement };
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
function handleTry(currI, isBare = false) {
|
|
266
|
+
let ptr = currI;
|
|
267
|
+
if (!isBare) ptr = consumeWhitespace(code, currI + 1);
|
|
268
|
+
|
|
269
|
+
if (!code.startsWith('try', ptr)) return null;
|
|
270
|
+
ptr += 3;
|
|
271
|
+
ptr = consumeWhitespace(code, ptr);
|
|
272
|
+
|
|
273
|
+
// Check for reactive try: try(expr) {...}
|
|
274
|
+
let reactiveExpr = null;
|
|
275
|
+
if (code[ptr] === '(') {
|
|
276
|
+
const condRes = extractCondition(code, ptr);
|
|
277
|
+
if (condRes) {
|
|
278
|
+
reactiveExpr = condRes.cond;
|
|
279
|
+
ptr = consumeWhitespace(code, condRes.end);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Must have opening brace for try block
|
|
284
|
+
if (code[ptr] !== '{') return null;
|
|
285
|
+
|
|
286
|
+
const tryBlock = parseBlock(code, ptr);
|
|
287
|
+
if (!tryBlock) return null;
|
|
288
|
+
|
|
289
|
+
const tryContent = code.substring(tryBlock.start + 1, tryBlock.end);
|
|
290
|
+
const transformedTry = transform(tryContent, 1);
|
|
291
|
+
|
|
292
|
+
ptr = tryBlock.end + 1;
|
|
293
|
+
ptr = consumeWhitespace(code, ptr);
|
|
294
|
+
|
|
295
|
+
// Must have catch
|
|
296
|
+
if (!code.startsWith('catch', ptr)) return null;
|
|
297
|
+
ptr += 5;
|
|
298
|
+
ptr = consumeWhitespace(code, ptr);
|
|
299
|
+
|
|
300
|
+
// Extract catch parameter (e) or (err)
|
|
301
|
+
let catchParam = 'e';
|
|
302
|
+
if (code[ptr] === '(') {
|
|
303
|
+
const catchCondRes = extractCondition(code, ptr);
|
|
304
|
+
if (catchCondRes) {
|
|
305
|
+
catchParam = catchCondRes.cond.trim() || 'e';
|
|
306
|
+
ptr = consumeWhitespace(code, catchCondRes.end);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Must have catch block
|
|
311
|
+
if (code[ptr] !== '{') return null;
|
|
312
|
+
|
|
313
|
+
const catchBlock = parseBlock(code, ptr);
|
|
314
|
+
if (!catchBlock) return null;
|
|
315
|
+
|
|
316
|
+
const catchContent = code.substring(catchBlock.start + 1, catchBlock.end);
|
|
317
|
+
const transformedCatch = transform(catchContent, 1);
|
|
318
|
+
|
|
319
|
+
let endIdx = catchBlock.end + 1;
|
|
320
|
+
|
|
321
|
+
// If not bare, consume closing '}'
|
|
322
|
+
if (!isBare) {
|
|
323
|
+
endIdx = consumeWhitespace(code, endIdx);
|
|
324
|
+
if (code[endIdx] !== '}') return null;
|
|
325
|
+
endIdx++;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let replacement;
|
|
329
|
+
if (reactiveExpr) {
|
|
330
|
+
// Reactive try: return a THUNK (function) so dom.js handles it as a reactive child (effect)
|
|
331
|
+
replacement = `{() => { try { ${reactiveExpr}; return (<Fragment>${transformedTry}</Fragment>); } catch(${catchParam}) { return (<Fragment>${transformedCatch}</Fragment>); } }}`;
|
|
332
|
+
} else {
|
|
333
|
+
// Static try: simple IIFE
|
|
334
|
+
replacement = `{(() => { try { return (<Fragment>${transformedTry}</Fragment>); } catch(${catchParam}) { return (<Fragment>${transformedCatch}</Fragment>); } })()}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { end: endIdx, replacement };
|
|
338
|
+
}
|
|
339
|
+
|
|
265
340
|
// --- Main Parser Loop ---
|
|
266
341
|
|
|
267
342
|
let inSingle = false, inDouble = false, inTemplate = false;
|
|
@@ -397,6 +472,9 @@ export function transform(code, initialDepth = 0) {
|
|
|
397
472
|
} else if (code.startsWith('switch', ptr)) {
|
|
398
473
|
const res = handleSwitch(i, false);
|
|
399
474
|
if (res) { result += res.replacement; i = res.end; processed = true; }
|
|
475
|
+
} else if (code.startsWith('try', ptr)) {
|
|
476
|
+
const res = handleTry(i, false);
|
|
477
|
+
if (res) { result += res.replacement; i = res.end; processed = true; }
|
|
400
478
|
}
|
|
401
479
|
}
|
|
402
480
|
|
|
@@ -421,6 +499,13 @@ export function transform(code, initialDepth = 0) {
|
|
|
421
499
|
const res = handleSwitch(i, true);
|
|
422
500
|
if (res) { result += res.replacement; i = res.end; processed = true; }
|
|
423
501
|
}
|
|
502
|
+
} else if (ch === 't' && code.startsWith('try', i)) {
|
|
503
|
+
// Bare try: try { ... } catch { ... } or try(expr) { ... } catch { ... }
|
|
504
|
+
let ptr = consumeWhitespace(code, i + 3);
|
|
505
|
+
if (code[ptr] === '{' || code[ptr] === '(') {
|
|
506
|
+
const res = handleTry(i, true);
|
|
507
|
+
if (res) { result += res.replacement; i = res.end; processed = true; }
|
|
508
|
+
}
|
|
424
509
|
}
|
|
425
510
|
|
|
426
511
|
if (processed) continue;
|
package/src/index.d.ts
CHANGED
|
@@ -101,6 +101,92 @@ export function effect(deps: any[], fn: () => void | (() => void), options?: {
|
|
|
101
101
|
*/
|
|
102
102
|
export function derive<T>(fn: () => T): () => T;
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Async signal that loads data from an async function.
|
|
106
|
+
* Provides reactive pending/error states and refetch capability.
|
|
107
|
+
*/
|
|
108
|
+
export interface AsyncSignal<T> {
|
|
109
|
+
/**
|
|
110
|
+
* Get the current resolved value, or set a new value.
|
|
111
|
+
* Returns undefined while pending or if an error occurred.
|
|
112
|
+
*/
|
|
113
|
+
(newValue?: T): T | undefined;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the current value (reactive).
|
|
117
|
+
*/
|
|
118
|
+
value: T | undefined;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the current value without tracking dependencies.
|
|
122
|
+
*/
|
|
123
|
+
peek(): T | undefined;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Signal indicating whether the async function is currently executing.
|
|
127
|
+
* @example
|
|
128
|
+
* if (user.pending()) {
|
|
129
|
+
* return <Spinner />;
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
pending: RoundSignal<boolean>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Signal containing the error if the async function rejected.
|
|
136
|
+
* Returns null if no error occurred.
|
|
137
|
+
* @example
|
|
138
|
+
* if (user.error()) {
|
|
139
|
+
* return <div>Error: {user.error().message}</div>;
|
|
140
|
+
* }
|
|
141
|
+
*/
|
|
142
|
+
error: RoundSignal<Error | null>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Re-execute the async function to refresh the data.
|
|
146
|
+
* Resets pending to true and clears any previous error.
|
|
147
|
+
* @returns Promise that resolves to the new value
|
|
148
|
+
* @example
|
|
149
|
+
* <button onClick={() => user.refetch()}>Refresh</button>
|
|
150
|
+
*/
|
|
151
|
+
refetch(): Promise<T | undefined>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Options for asyncSignal.
|
|
156
|
+
*/
|
|
157
|
+
export interface AsyncSignalOptions {
|
|
158
|
+
/**
|
|
159
|
+
* If true (default), executes the async function immediately on creation.
|
|
160
|
+
* If false, you must call refetch() to start loading.
|
|
161
|
+
*/
|
|
162
|
+
immediate?: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates an async signal that loads data from an async function.
|
|
167
|
+
* The signal provides reactive pending and error states, plus a refetch method.
|
|
168
|
+
*
|
|
169
|
+
* @param asyncFn - Async function that returns a promise
|
|
170
|
+
* @param options - Configuration options
|
|
171
|
+
* @returns AsyncSignal with pending, error, and refetch properties
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* const user = asyncSignal(() => fetch('/api/user').then(r => r.json()));
|
|
175
|
+
*
|
|
176
|
+
* // In component:
|
|
177
|
+
* {if(user.pending()) {
|
|
178
|
+
* <Spinner />
|
|
179
|
+
* } else if(user.error()) {
|
|
180
|
+
* <Error message={user.error().message} />
|
|
181
|
+
* } else {
|
|
182
|
+
* <Profile user={user()} />
|
|
183
|
+
* }}
|
|
184
|
+
*
|
|
185
|
+
* // Refetch on demand:
|
|
186
|
+
* <button onClick={() => user.refetch()}>Refresh</button>
|
|
187
|
+
*/
|
|
188
|
+
export function asyncSignal<T>(asyncFn: () => Promise<T>, options?: AsyncSignalOptions): AsyncSignal<T>;
|
|
189
|
+
|
|
104
190
|
/**
|
|
105
191
|
* Creates a read/write view of a specific path within a signal object.
|
|
106
192
|
*/
|
|
@@ -319,25 +405,6 @@ export function readContext<T>(ctx: Context<T>): T;
|
|
|
319
405
|
*/
|
|
320
406
|
export function bindContext<T>(ctx: Context<T>): () => T;
|
|
321
407
|
|
|
322
|
-
/**
|
|
323
|
-
* Error Handling
|
|
324
|
-
*/
|
|
325
|
-
|
|
326
|
-
export interface ErrorBoundaryProps {
|
|
327
|
-
/** Content to render if an error occurs. Can be a signal or function. */
|
|
328
|
-
fallback?: any | ((props: { error: any }) => any);
|
|
329
|
-
/** Optional identifier for the boundary. */
|
|
330
|
-
name?: string;
|
|
331
|
-
/** Optional key that, when changed, resets the boundary error state. */
|
|
332
|
-
resetKey?: any;
|
|
333
|
-
/** Content that might throw an error. */
|
|
334
|
-
children?: any;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Component that catches runtime errors in its child tree and displays a fallback UI.
|
|
339
|
-
*/
|
|
340
|
-
export function ErrorBoundary(props: ErrorBoundaryProps): any;
|
|
341
408
|
|
|
342
409
|
/**
|
|
343
410
|
* Async & Code Splitting
|
package/src/index.js
CHANGED
|
@@ -2,9 +2,6 @@ export * from './runtime/signals.js';
|
|
|
2
2
|
export * from './runtime/dom.js';
|
|
3
3
|
export * from './runtime/lifecycle.js';
|
|
4
4
|
export * from './runtime/router.js';
|
|
5
|
-
export * from './runtime/errors.js';
|
|
6
|
-
export * from './runtime/error-store.js';
|
|
7
|
-
export * from './runtime/error-boundary.js';
|
|
8
5
|
export * from './runtime/suspense.js';
|
|
9
6
|
export * from './runtime/context.js';
|
|
10
7
|
export * from './runtime/store.js';
|
|
@@ -13,20 +10,16 @@ import * as Signals from './runtime/signals.js';
|
|
|
13
10
|
import * as DOM from './runtime/dom.js';
|
|
14
11
|
import * as Lifecycle from './runtime/lifecycle.js';
|
|
15
12
|
import * as Router from './runtime/router.js';
|
|
16
|
-
import * as Errors from './runtime/errors.js';
|
|
17
13
|
import * as Suspense from './runtime/suspense.js';
|
|
18
14
|
import * as Context from './runtime/context.js';
|
|
19
15
|
import * as Store from './runtime/store.js';
|
|
20
16
|
|
|
21
17
|
export function render(Component, container) {
|
|
22
18
|
Lifecycle.initLifecycleRoot(container);
|
|
23
|
-
Errors.
|
|
24
|
-
try
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} catch (e) {
|
|
28
|
-
Errors.reportError(e, { phase: 'render', component: Component?.name ?? 'App' });
|
|
29
|
-
}
|
|
19
|
+
// Errors are no longer handled by a global overlay.
|
|
20
|
+
// They propagate to the console or local try/catch.
|
|
21
|
+
const root = DOM.createElement(Component);
|
|
22
|
+
container.appendChild(root);
|
|
30
23
|
}
|
|
31
24
|
|
|
32
25
|
export default {
|
|
@@ -34,7 +27,6 @@ export default {
|
|
|
34
27
|
...DOM,
|
|
35
28
|
...Lifecycle,
|
|
36
29
|
...Router,
|
|
37
|
-
...Errors,
|
|
38
30
|
...Suspense,
|
|
39
31
|
...Context,
|
|
40
32
|
...Store,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
let nextContextId = 1;
|
|
3
|
+
export const contextStack = [];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal helper to push context layers.
|
|
7
|
+
*/
|
|
8
|
+
export function pushContext(values) {
|
|
9
|
+
contextStack.push(values);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal helper to pop context layers.
|
|
14
|
+
*/
|
|
15
|
+
export function popContext() {
|
|
16
|
+
contextStack.pop();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Capture current context stack.
|
|
21
|
+
*/
|
|
22
|
+
export function captureContext() {
|
|
23
|
+
return contextStack.slice();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read context value from the stack.
|
|
28
|
+
*/
|
|
29
|
+
export function readContext(ctx) {
|
|
30
|
+
for (let i = contextStack.length - 1; i >= 0; i--) {
|
|
31
|
+
const layer = contextStack[i];
|
|
32
|
+
if (layer && Object.prototype.hasOwnProperty.call(layer, ctx.id)) {
|
|
33
|
+
return layer[ctx.id];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return ctx.defaultValue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate a new context ID.
|
|
41
|
+
*/
|
|
42
|
+
export function generateContextId() {
|
|
43
|
+
return nextContextId++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export const SuspenseContext = {
|
|
48
|
+
id: generateContextId(),
|
|
49
|
+
defaultValue: null,
|
|
50
|
+
Provider: null
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function runInContext(snapshot, fn) {
|
|
54
|
+
const prev = contextStack.slice();
|
|
55
|
+
contextStack.length = 0;
|
|
56
|
+
contextStack.push(...snapshot);
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} finally {
|
|
60
|
+
contextStack.length = 0;
|
|
61
|
+
contextStack.push(...prev);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/runtime/context.js
CHANGED
|
@@ -1,62 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pushContext, popContext, contextStack, generateContextId, readContext, SuspenseContext, runInContext } from './context-shared.js';
|
|
2
|
+
import { createElement, Fragment } from './dom.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const contextStack = [];
|
|
5
|
-
|
|
6
|
-
function pushContext(values) {
|
|
7
|
-
contextStack.push(values);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function popContext() {
|
|
11
|
-
contextStack.pop();
|
|
12
|
-
}
|
|
4
|
+
export { pushContext, popContext, contextStack, generateContextId, readContext, SuspenseContext, runInContext };
|
|
13
5
|
|
|
14
6
|
/**
|
|
15
|
-
*
|
|
16
|
-
* @template T
|
|
17
|
-
* @param {Context<T>} ctx The context object.
|
|
18
|
-
* @returns {T} The current context value.
|
|
7
|
+
* Internal logic to create a Provider component for a context.
|
|
19
8
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const layer = contextStack[i];
|
|
23
|
-
if (layer && Object.prototype.hasOwnProperty.call(layer, ctx.id)) {
|
|
24
|
-
return layer[ctx.id];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return ctx.defaultValue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Create a new Context object for sharing state between components.
|
|
32
|
-
* @template T
|
|
33
|
-
* @param {T} [defaultValue] The value used when no provider is found.
|
|
34
|
-
* @returns {Context<T>} The context object with a `Provider` component.
|
|
35
|
-
*/
|
|
36
|
-
export function createContext(defaultValue) {
|
|
37
|
-
const ctx = {
|
|
38
|
-
id: nextContextId++,
|
|
39
|
-
defaultValue,
|
|
40
|
-
Provider: null
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function Provider(props = {}) {
|
|
9
|
+
function createProvider(ctx) {
|
|
10
|
+
const Provider = function Provider(props = {}) {
|
|
44
11
|
const children = props.children;
|
|
12
|
+
const value = props.value;
|
|
45
13
|
|
|
46
14
|
// Push context now so that any createElement/appendChild called
|
|
47
15
|
// during the instantiation of this Provider branch picks it up immediately.
|
|
48
|
-
pushContext({ [ctx.id]:
|
|
16
|
+
pushContext({ [ctx.id]: value });
|
|
49
17
|
try {
|
|
50
18
|
// We use a span to handle reactive value updates and dynamic children.
|
|
51
19
|
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
52
20
|
// Read current value (reactive if it's a signal)
|
|
53
|
-
const val = (typeof
|
|
21
|
+
const val = (typeof value === 'function' && value.peek) ? value() : value;
|
|
54
22
|
|
|
55
|
-
// Push it during the effect run too!
|
|
56
|
-
// from this callback (which might trigger more appendChild calls) sees the context.
|
|
23
|
+
// Push it during the effect run too!
|
|
57
24
|
pushContext({ [ctx.id]: val });
|
|
58
25
|
try {
|
|
59
|
-
return children;
|
|
26
|
+
return typeof children === 'function' ? children() : children;
|
|
60
27
|
} finally {
|
|
61
28
|
popContext();
|
|
62
29
|
}
|
|
@@ -64,12 +31,26 @@ export function createContext(defaultValue) {
|
|
|
64
31
|
} finally {
|
|
65
32
|
popContext();
|
|
66
33
|
}
|
|
67
|
-
}
|
|
34
|
+
};
|
|
35
|
+
return Provider;
|
|
36
|
+
}
|
|
68
37
|
|
|
69
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Create a new Context object for sharing state between components.
|
|
40
|
+
*/
|
|
41
|
+
export function createContext(defaultValue) {
|
|
42
|
+
const ctx = {
|
|
43
|
+
id: generateContextId(),
|
|
44
|
+
defaultValue,
|
|
45
|
+
Provider: null
|
|
46
|
+
};
|
|
47
|
+
ctx.Provider = createProvider(ctx);
|
|
70
48
|
return ctx;
|
|
71
49
|
}
|
|
72
50
|
|
|
51
|
+
// Attach providers to built-in shared contexts
|
|
52
|
+
SuspenseContext.Provider = createProvider(SuspenseContext);
|
|
53
|
+
|
|
73
54
|
export function bindContext(ctx) {
|
|
74
55
|
return () => {
|
|
75
56
|
const provided = readContext(ctx);
|
|
@@ -87,15 +68,3 @@ export function bindContext(ctx) {
|
|
|
87
68
|
export function captureContext() {
|
|
88
69
|
return contextStack.slice();
|
|
89
70
|
}
|
|
90
|
-
|
|
91
|
-
export function runInContext(snapshot, fn) {
|
|
92
|
-
const prev = contextStack.slice();
|
|
93
|
-
contextStack.length = 0;
|
|
94
|
-
contextStack.push(...snapshot);
|
|
95
|
-
try {
|
|
96
|
-
return fn();
|
|
97
|
-
} finally {
|
|
98
|
-
contextStack.length = 0;
|
|
99
|
-
contextStack.push(...prev);
|
|
100
|
-
}
|
|
101
|
-
}
|
package/src/runtime/dom.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { effect, untrack } from './signals.js';
|
|
2
2
|
import { runInLifecycle, createComponentInstance, mountComponent, initLifecycleRoot } from './lifecycle.js';
|
|
3
3
|
import { reportErrorSafe } from './error-reporter.js';
|
|
4
|
-
import { captureContext, runInContext, readContext } from './context.js';
|
|
5
|
-
import { SuspenseContext } from './suspense.js';
|
|
4
|
+
import { captureContext, runInContext, readContext, SuspenseContext } from './context-shared.js';
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
const warnedSignals = new Set();
|
|
@@ -54,8 +53,9 @@ export function createElement(tag, props = {}, ...children) {
|
|
|
54
53
|
}
|
|
55
54
|
throw e;
|
|
56
55
|
}
|
|
56
|
+
|
|
57
57
|
reportErrorSafe(e, { phase: 'component.render', component: componentName });
|
|
58
|
-
|
|
58
|
+
throw e;
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -341,8 +341,9 @@ function appendChild(parent, child) {
|
|
|
341
341
|
}
|
|
342
342
|
throw new Error("cannot instance a lazy component outside a suspense");
|
|
343
343
|
}
|
|
344
|
+
|
|
344
345
|
reportErrorSafe(e, { phase: 'child.dynamic' });
|
|
345
|
-
|
|
346
|
+
throw e;
|
|
346
347
|
}
|
|
347
348
|
|
|
348
349
|
if (Array.isArray(val)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
let reporter = null;
|
|
2
3
|
|
|
3
4
|
export function setErrorReporter(fn) {
|
|
@@ -5,9 +6,16 @@ export function setErrorReporter(fn) {
|
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export function reportErrorSafe(error, info) {
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
if (reporter) {
|
|
10
|
+
try {
|
|
11
|
+
reporter(error, info);
|
|
12
|
+
return;
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
12
15
|
}
|
|
16
|
+
|
|
17
|
+
// Default: Descriptive console logging
|
|
18
|
+
const phase = info?.phase ? ` in phase "${info.phase}"` : "";
|
|
19
|
+
const component = info?.component ? ` of component <${info.component} />` : "";
|
|
20
|
+
console.error(`[round] Error${phase}${component}:`, error);
|
|
13
21
|
}
|
package/src/runtime/signals.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { onMount, triggerUpdate, getCurrentComponent } from './lifecycle.js';
|
|
2
2
|
import { reportErrorSafe } from './error-reporter.js';
|
|
3
|
+
import { readContext } from './context-shared.js';
|
|
3
4
|
|
|
4
5
|
let context = null;
|
|
5
6
|
let batchCount = 0;
|
|
@@ -143,10 +144,10 @@ export function effect(arg1, arg2, arg3) {
|
|
|
143
144
|
}
|
|
144
145
|
const res = callback();
|
|
145
146
|
if (typeof res === 'function') this._cleanup = res;
|
|
146
|
-
if (owner?.isMounted) triggerUpdate(owner);
|
|
147
147
|
} catch (e) {
|
|
148
|
-
if (
|
|
149
|
-
|
|
148
|
+
if (isPromiseLike(e)) throw e;
|
|
149
|
+
reportErrorSafe(e, { phase: 'effect', component: owner?.name });
|
|
150
|
+
throw e;
|
|
150
151
|
} finally {
|
|
151
152
|
context = prev;
|
|
152
153
|
}
|
|
@@ -311,6 +312,98 @@ export function bindable(initialValue) {
|
|
|
311
312
|
return attachHelpers(s);
|
|
312
313
|
}
|
|
313
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Create an async signal that loads data from an async function.
|
|
317
|
+
* Provides pending, error, and refetch capabilities.
|
|
318
|
+
* @param {Function} asyncFn - Async function that returns a promise
|
|
319
|
+
* @param {Object} options - Options: { immediate: true }
|
|
320
|
+
*/
|
|
321
|
+
export function asyncSignal(asyncFn, options = {}) {
|
|
322
|
+
if (typeof asyncFn !== 'function') {
|
|
323
|
+
throw new Error('[round] asyncSignal() expects an async function.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const immediate = options.immediate !== false;
|
|
327
|
+
const data = signal(undefined);
|
|
328
|
+
const pending = signal(immediate);
|
|
329
|
+
const error = signal(null);
|
|
330
|
+
|
|
331
|
+
let currentPromise = null;
|
|
332
|
+
|
|
333
|
+
async function execute() {
|
|
334
|
+
pending(true);
|
|
335
|
+
error(null);
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const promise = asyncFn();
|
|
339
|
+
currentPromise = promise;
|
|
340
|
+
|
|
341
|
+
if (!isPromiseLike(promise)) {
|
|
342
|
+
// Sync result
|
|
343
|
+
data(promise);
|
|
344
|
+
pending(false);
|
|
345
|
+
return promise;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = await promise;
|
|
349
|
+
|
|
350
|
+
// Only update if this is still the current request
|
|
351
|
+
if (currentPromise === promise) {
|
|
352
|
+
data(result);
|
|
353
|
+
pending(false);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return result;
|
|
357
|
+
} catch (e) {
|
|
358
|
+
if (currentPromise !== null) {
|
|
359
|
+
error(e);
|
|
360
|
+
pending(false);
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// The main signal function - returns current data value
|
|
367
|
+
const s = function (newValue) {
|
|
368
|
+
if (arguments.length > 0) {
|
|
369
|
+
return data(newValue);
|
|
370
|
+
}
|
|
371
|
+
if (context) {
|
|
372
|
+
// Subscribe to all internal signals so any change triggers a re-run
|
|
373
|
+
data();
|
|
374
|
+
pending();
|
|
375
|
+
error();
|
|
376
|
+
}
|
|
377
|
+
return data.peek();
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
s.peek = () => data.peek();
|
|
381
|
+
|
|
382
|
+
Object.defineProperty(s, 'value', {
|
|
383
|
+
enumerable: true,
|
|
384
|
+
configurable: true,
|
|
385
|
+
get() { return s(); },
|
|
386
|
+
set(v) { data(v); }
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Expose pending and error as signals
|
|
390
|
+
s.pending = pending;
|
|
391
|
+
s.error = error;
|
|
392
|
+
|
|
393
|
+
// Refetch function
|
|
394
|
+
s.refetch = execute;
|
|
395
|
+
|
|
396
|
+
// Mark as async signal
|
|
397
|
+
s.__asyncSignal = true;
|
|
398
|
+
|
|
399
|
+
// Execute immediately if requested
|
|
400
|
+
if (immediate) {
|
|
401
|
+
execute();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return s;
|
|
405
|
+
}
|
|
406
|
+
|
|
314
407
|
function getIn(obj, path) {
|
|
315
408
|
let cur = obj;
|
|
316
409
|
for (let i = 0; i < path.length; i++) {
|
package/src/runtime/suspense.js
CHANGED
|
@@ -6,7 +6,7 @@ function isPromiseLike(v) {
|
|
|
6
6
|
return v && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { SuspenseContext } from './context-shared.js';
|
|
10
10
|
export { SuspenseContext };
|
|
11
11
|
|
|
12
12
|
export function lazy(loader) {
|