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 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.19.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",
@@ -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"}
@@ -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
+ }