kopular 0.19.0 → 0.20.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.
- package/LLM.md +24 -0
- package/README.md +35 -0
- package/package.json +3 -2
- package/src/resource.js +38 -0
- package/src/resource.js.map +1 -0
- package/src/resource.ks +29 -0
package/LLM.md
CHANGED
|
@@ -584,6 +584,30 @@ Dependencies are explicit constructor arguments, not automatically tracked. Fixe
|
|
|
584
584
|
source, 2 sources) — same array-of-function-values reason `CombineValidators2/3` are fixed;
|
|
585
585
|
add a `Computed3<A, B, C, R>` the same way if ever needed.
|
|
586
586
|
|
|
587
|
+
## `Resource<T>` (`resource.ks`)
|
|
588
|
+
|
|
589
|
+
```ks
|
|
590
|
+
enum AsyncStatus { Loading, Success, Failure }
|
|
591
|
+
|
|
592
|
+
Resource<Response> r = new Resource<Response>(Http.Get(url)); // task already in flight
|
|
593
|
+
r.Status.Subscribe((AsyncStatus s) => this.Update());
|
|
594
|
+
|
|
595
|
+
match r.Status.Value {
|
|
596
|
+
AsyncStatus.Loading => ...,
|
|
597
|
+
AsyncStatus.Success => ... r.Data.Value ..., // T?
|
|
598
|
+
AsyncStatus.Failure => ... r.Error.Value ... // string?
|
|
599
|
+
};
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
The loading/success/failure shape around a `task<T>`, as `state<T>` a `Component` renders
|
|
603
|
+
and `Subscribe`s to — most commonly wrapping an `Http` call. `Status` starts at
|
|
604
|
+
`AsyncStatus.Loading` immediately; the constructor's own handling of the task is
|
|
605
|
+
fire-and-forget (there's no way to construct a `task` value outside an `async` function
|
|
606
|
+
body to `await` it from the constructor itself — see "Async" above), so the caller must
|
|
607
|
+
pass an ALREADY-STARTED task (call the `async` function first — `Http.Get(url)`, not
|
|
608
|
+
something the constructor starts). `Status`/`Data`/`Error` transition together, exactly
|
|
609
|
+
once, inside a `try`/`catch` around the `await` — whichever branch the task actually takes.
|
|
610
|
+
|
|
587
611
|
## `kopular/testing` (`testing.js`, hand-written JS, not compiled from `.ks`)
|
|
588
612
|
|
|
589
613
|
```ts
|
package/README.md
CHANGED
|
@@ -674,6 +674,41 @@ has no array-of-function-values type, and no way to express "N sources of N diff
|
|
|
674
674
|
types" without one type parameter per source. Add a `Computed3<A, B, C, R>` the same way if
|
|
675
675
|
a real derivation ever needs three.
|
|
676
676
|
|
|
677
|
+
## Async data — `Resource<T>`
|
|
678
|
+
|
|
679
|
+
The loading/success/failure shape almost every real app needs around a `task<T>` — most
|
|
680
|
+
commonly wrapping an `Http` call — as reactive `state<T>` a `Component` can render and
|
|
681
|
+
`Subscribe` to, instead of hand-rolling the same three fields and try/catch every time:
|
|
682
|
+
|
|
683
|
+
```ks
|
|
684
|
+
using "./resource";
|
|
685
|
+
|
|
686
|
+
Resource<Response> dogs = new Resource<Response>(Http.Get("https://dog.ceo/api/breeds/list/all"));
|
|
687
|
+
dogs.Status.Subscribe((AsyncStatus s) => this.Update());
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
```ks
|
|
691
|
+
public override VElement Render() {
|
|
692
|
+
return match dogs.Status.Value {
|
|
693
|
+
AsyncStatus.Loading => this.BuildSpinner(),
|
|
694
|
+
AsyncStatus.Success => this.BuildList(dogs.Data.Value),
|
|
695
|
+
AsyncStatus.Failure => this.BuildError(dogs.Error.Value)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
`Status` starts at `AsyncStatus.Loading` the moment `Resource` is constructed — the
|
|
701
|
+
operation is already in flight (`Http.Get(...)` above is called before `Resource` ever
|
|
702
|
+
sees it; a `task<T>` value can only be produced by calling an `async` function, and
|
|
703
|
+
`Resource`'s own constructor isn't one, so it always receives an already-started
|
|
704
|
+
operation, never starts one itself). Its internal handling of that task is genuinely
|
|
705
|
+
fire-and-forget from the constructor's point of view (there's no way to construct a
|
|
706
|
+
`task` value outside an async function body to await it there instead — see KopScript's
|
|
707
|
+
own "Async" docs) — `Status`/`Data`/`Error` transition together, exactly once, whichever
|
|
708
|
+
branch the `task` actually takes. `match` over `AsyncStatus` gets the same real
|
|
709
|
+
exhaustiveness checking any other enum `match` does — leaving out a case is a compile
|
|
710
|
+
error, not a runtime blank screen.
|
|
711
|
+
|
|
677
712
|
## Starting a new project: `kp new`
|
|
678
713
|
|
|
679
714
|
Everything in the next section — the `extern` bindings, plus a `vendor/kopular/` copy of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"./http": "./src/http.js",
|
|
35
35
|
"./forms": "./src/forms.js",
|
|
36
36
|
"./computed": "./src/computed.js",
|
|
37
|
+
"./resource": "./src/resource.js",
|
|
37
38
|
"./testing": "./src/testing.js"
|
|
38
39
|
},
|
|
39
40
|
"files": [
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"LLM.md"
|
|
44
45
|
],
|
|
45
46
|
"scripts": {
|
|
46
|
-
"build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks && ks build src/forms.ks && ks build src/computed.ks",
|
|
47
|
+
"build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks && ks build src/forms.ks && ks build src/computed.ks && ks build src/resource.ks",
|
|
47
48
|
"prepublishOnly": "npm run build",
|
|
48
49
|
"pretest": "npm run build",
|
|
49
50
|
"test": "vitest run",
|
package/src/resource.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class __KopState {
|
|
2
|
+
constructor(value) {
|
|
3
|
+
this._value = value;
|
|
4
|
+
this._listeners = [];
|
|
5
|
+
}
|
|
6
|
+
get Value() { return this._value; }
|
|
7
|
+
set Value(v) {
|
|
8
|
+
this._value = v;
|
|
9
|
+
for (const listener of this._listeners) listener(v);
|
|
10
|
+
}
|
|
11
|
+
Subscribe(listener) {
|
|
12
|
+
this._listeners.push(listener);
|
|
13
|
+
return () => { this._listeners = this._listeners.filter((l) => l !== listener); };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const AsyncStatus = Object.freeze({ Loading: 0, Success: 1, Failure: 2 });
|
|
18
|
+
export class Resource {
|
|
19
|
+
constructor(operation) {
|
|
20
|
+
this.Status = new __KopState(AsyncStatus.Loading);
|
|
21
|
+
this.Data = new __KopState(null);
|
|
22
|
+
this.Error = new __KopState(null);
|
|
23
|
+
this.Run(operation);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async Run(operation) {
|
|
27
|
+
try {
|
|
28
|
+
let result = await operation;
|
|
29
|
+
this.Data.Value = result;
|
|
30
|
+
this.Status.Value = AsyncStatus.Success;
|
|
31
|
+
} catch (message) {
|
|
32
|
+
this.Error.Value = message;
|
|
33
|
+
this.Status.Value = AsyncStatus.Failure;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.js","sources":["resource.ks"],"sourcesContent":["// Resource<T>: the \"loading / success / failure\" shape around a task<T>,\n// as reactive state<T> a Component can render and Subscribe to — React's\n// (commonly hand-rolled, or via a data-fetching library) loading-state\n// pattern / Angular's resource() equivalent.\nenum AsyncStatus { Loading, Success, Failure }\n\nclass Resource<T> {\n public state<AsyncStatus> Status;\n public state<T?> Data;\n public state<string?> Error;\n\n constructor(task<T> operation) {\n this.Status = state(AsyncStatus.Loading);\n this.Data = state(null);\n this.Error = state(null);\n this.Run(operation);\n }\n\n private async task Run(task<T> operation) {\n try {\n T result = await operation;\n this.Data.Value = result;\n this.Status.Value = AsyncStatus.Success;\n } catch (string message) {\n this.Error.Value = message;\n this.Status.Value = AsyncStatus.Failure;\n }\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAIA;AAEA;EAKE;IACc;IACF;IACC;IACH;;;EAGI;IACZ;MACE;MACgB;MACE;;MAED;MACC"}
|
package/src/resource.ks
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Resource<T>: the "loading / success / failure" shape around a task<T>,
|
|
2
|
+
// as reactive state<T> a Component can render and Subscribe to — React's
|
|
3
|
+
// (commonly hand-rolled, or via a data-fetching library) loading-state
|
|
4
|
+
// pattern / Angular's resource() equivalent.
|
|
5
|
+
enum AsyncStatus { Loading, Success, Failure }
|
|
6
|
+
|
|
7
|
+
class Resource<T> {
|
|
8
|
+
public state<AsyncStatus> Status;
|
|
9
|
+
public state<T?> Data;
|
|
10
|
+
public state<string?> Error;
|
|
11
|
+
|
|
12
|
+
constructor(task<T> operation) {
|
|
13
|
+
this.Status = state(AsyncStatus.Loading);
|
|
14
|
+
this.Data = state(null);
|
|
15
|
+
this.Error = state(null);
|
|
16
|
+
this.Run(operation);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async task Run(task<T> operation) {
|
|
20
|
+
try {
|
|
21
|
+
T result = await operation;
|
|
22
|
+
this.Data.Value = result;
|
|
23
|
+
this.Status.Value = AsyncStatus.Success;
|
|
24
|
+
} catch (string message) {
|
|
25
|
+
this.Error.Value = message;
|
|
26
|
+
this.Status.Value = AsyncStatus.Failure;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|