@moonan_ai/sdk 0.0.0-stage → 0.0.5-alpha1
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/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/data.js +90 -0
- package/dist/data.js.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/model-provider.js +5 -0
- package/dist/model-provider.js.map +1 -0
- package/dist/routes.js +11 -0
- package/dist/routes.js.map +1 -0
- package/dist/web-facet-catalog.js +66 -0
- package/dist/web-facet-catalog.js.map +1 -0
- package/dist/web.js +174 -0
- package/dist/web.js.map +1 -0
- package/package.json +60 -3
- package/src/data.ts +184 -0
- package/src/index.ts +83 -0
- package/src/model-provider.ts +45 -0
- package/src/routes.ts +23 -0
- package/src/web-facet-catalog.ts +70 -0
- package/src/web.ts +230 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pheobe-Southwood
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @moonan_ai/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Moonan 插件 SDK:契约、注册表与浏览器钩子。写 Moonan 插件只需要这一个包——Chord 的原语
|
|
4
|
+
(`remote`、`local`、`method`、`hostNamed`、`defineFacet`、`defineRegistry`)由它再导出。
|
|
5
|
+
|
|
6
|
+
Moonan 是一个本地运行的插件工作台:平台只提供宿主、端口、插件发现与切面装载,所有功能——包括
|
|
7
|
+
AI 对话——都是插件。
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @moonan_ai/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 两个入口
|
|
14
|
+
|
|
15
|
+
| 入口 | 给谁用 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `@moonan_ai/sdk` | 服务端切面、以及需要契约/注册表的浏览器代码 |
|
|
18
|
+
| `@moonan_ai/sdk/web` | 浏览器切面:`Panels`、`useRemote`、`useReadable`、`useRefStatus` |
|
|
19
|
+
|
|
20
|
+
浏览器切面不要自行打包 React、Chord 或本包:外壳的 import map 已经提供了它们,而且只有一份。
|
|
21
|
+
|
|
22
|
+
## 服务端切面
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { Type, defineFacet, hostNamed, method, remote } from "@moonan_ai/sdk";
|
|
26
|
+
|
|
27
|
+
const Counter = remote("example.counter", {
|
|
28
|
+
version: 1,
|
|
29
|
+
input: Type.Object({}),
|
|
30
|
+
host: hostNamed("moonan"),
|
|
31
|
+
variants: { v1: { state: { n: Type.Number() }, methods: { bump: method() } } },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default defineFacet((api) => {
|
|
35
|
+
api.serve(Counter, (_input, _principal, self) => {
|
|
36
|
+
const n = self.state("n", Type.Number(), 0);
|
|
37
|
+
return { variant: "v1", n, bump: (ctx) => n.change(ctx, (value) => value + 1) };
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 浏览器切面
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { Panels } from "@moonan_ai/sdk/web";
|
|
46
|
+
import { Type, defineFacet, local, method } from "@moonan_ai/sdk";
|
|
47
|
+
import { createElement as h } from "react";
|
|
48
|
+
|
|
49
|
+
const Root = local("example.counter.web", { version: 1, input: Type.Object({}), members: { noop: method() } });
|
|
50
|
+
|
|
51
|
+
export default defineFacet((api) => {
|
|
52
|
+
api.serve(Root, (_input, _p, self) => {
|
|
53
|
+
self.register(Panels, { counter: { id: "counter", title: "Counter", order: 10, component: () => h("div") } });
|
|
54
|
+
return { noop: () => undefined };
|
|
55
|
+
}, { lifetime: "host" });
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 约定
|
|
60
|
+
|
|
61
|
+
- 契约在两端**各写一份**:Chord 认指纹(名字、版本、成员、类型住在哪个宿主),不认模块实例。
|
|
62
|
+
- 注册表贡献(`Routes`、`Panels`、`DataCodecs`、`ModelProviders`)必须来自 `{ lifetime: "host" }` 的对象。
|
|
63
|
+
- 需要统一导出/导入的数据放 `DataStore`,并注册 `DataCodecs`;需要给其他插件用模型时注册 `ModelProviders`。
|
|
64
|
+
- 服务端宿主名固定为 `"moonan"`;写别的名字,`api.serve` 会当场拒绝。
|
|
65
|
+
- 浏览器切面是**单个 ESM 文件**:浏览器没有模块 hook,重载只换入口。
|
|
66
|
+
|
|
67
|
+
## 统一数据与模型
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { DataCodecs, DataStore, ModelProviders } from "@moonan_ai/sdk";
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `DataStore` 是平台唯一的统一数据文件(local、host-lifetime);`read` 返回 `{ value, version }`,`update` 是串行读改写,`exportAll`/`importAll` 由平台 HTTP 表面调用。
|
|
74
|
+
- `DataCodecs` 是插件贡献的导出/导入 codec 注册表;没有 codec 的 namespace 不会进入统一导出。
|
|
75
|
+
- `ModelProviders` 是服务端模型提供商注册表;`list()` 是 key-free 目录,`runtime()` 是 server-only 运行时,不能给浏览器。
|
|
76
|
+
|
|
77
|
+
完整写法:<https://github.com/Pheobe-Southwood/Moonan/blob/main/docs/plugins.md> ·
|
|
78
|
+
词汇表:<https://github.com/Pheobe-Southwood/Moonan/blob/main/CONTEXT.md>
|
|
79
|
+
|
|
80
|
+
## 许可
|
|
81
|
+
|
|
82
|
+
MIT
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineRegistry, local, method } from "@earendil-works/chord";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
/**
|
|
4
|
+
* One value in the platform's unified data store.
|
|
5
|
+
*
|
|
6
|
+
* `version` belongs to the dataset, not to the file and not to the contract. A plugin
|
|
7
|
+
* raises it when the stored shape or meaning changes and supplies a migration through its
|
|
8
|
+
* {@link DatasetCodec}; the platform never interprets the value.
|
|
9
|
+
*/
|
|
10
|
+
export const StoredDatasetSchema = Type.Object({
|
|
11
|
+
value: Type.Any(),
|
|
12
|
+
version: Type.Number(),
|
|
13
|
+
}, { additionalProperties: false });
|
|
14
|
+
/** Identity helper; keeps the codec's type while documenting intent. */
|
|
15
|
+
export function defineDataset(codec) {
|
|
16
|
+
return codec;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The registry of dataset codecs.
|
|
20
|
+
*
|
|
21
|
+
* A contribution must come from an object of `lifetime: "host"`, like every other Chord
|
|
22
|
+
* registry. The platform reads the table for the code version of the root that asks.
|
|
23
|
+
*/
|
|
24
|
+
export const DataCodecs = defineRegistry("moonan.data-codecs", {
|
|
25
|
+
conflict: "error",
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* What the data store reports about its file. This is a state member, so a plugin can read
|
|
29
|
+
* it synchronously once the store is live; the platform also exposes it over HTTP.
|
|
30
|
+
*/
|
|
31
|
+
export const DataStoreStorageInfoSchema = Type.Object({
|
|
32
|
+
file: Type.String(),
|
|
33
|
+
backup: Type.String(),
|
|
34
|
+
exists: Type.Boolean(),
|
|
35
|
+
mode: Type.Optional(Type.String()),
|
|
36
|
+
writable: Type.Boolean(),
|
|
37
|
+
revision: Type.Number(),
|
|
38
|
+
datasets: Type.Number(),
|
|
39
|
+
lastSavedAt: Type.Optional(Type.String()),
|
|
40
|
+
notice: Type.Optional(Type.String()),
|
|
41
|
+
}, { additionalProperties: false });
|
|
42
|
+
/**
|
|
43
|
+
* The wire/JSON envelope for a complete export. The file itself has a different, richer
|
|
44
|
+
* shape (it also keeps per-dataset timestamps); this is the portable subset.
|
|
45
|
+
*/
|
|
46
|
+
export const DataBundleSchema = Type.Object({
|
|
47
|
+
format: Type.Literal("moonan.data"),
|
|
48
|
+
formatVersion: Type.Literal(1),
|
|
49
|
+
createdAt: Type.String(),
|
|
50
|
+
revision: Type.Number(),
|
|
51
|
+
datasets: Type.Array(Type.Object({
|
|
52
|
+
namespace: Type.String(),
|
|
53
|
+
version: Type.Number(),
|
|
54
|
+
value: Type.Any(),
|
|
55
|
+
}, { additionalProperties: false })),
|
|
56
|
+
skipped: Type.Optional(Type.Array(Type.String())),
|
|
57
|
+
}, { additionalProperties: false });
|
|
58
|
+
export const DataImportReportSchema = Type.Object({
|
|
59
|
+
imported: Type.Array(Type.String()),
|
|
60
|
+
revision: Type.Number(),
|
|
61
|
+
skipped: Type.Optional(Type.Array(Type.String())),
|
|
62
|
+
}, { additionalProperties: false });
|
|
63
|
+
/**
|
|
64
|
+
* The platform's unified data service.
|
|
65
|
+
*
|
|
66
|
+
* It is local and host-lifetime: exactly one object exists for the server host, it owns
|
|
67
|
+
* the only data file, and every plugin reads/writes through it. It is not a remote type;
|
|
68
|
+
* browsers get data through plugin methods or the platform's export/import HTTP surface.
|
|
69
|
+
*
|
|
70
|
+
* `read` returns the raw `{ value, version }` and deliberately does not consult a codec:
|
|
71
|
+
* a plugin's `config` cell may initialize before its codec registration is published, so
|
|
72
|
+
* the plugin validates/migrates its own first value. `write` replaces a whole dataset;
|
|
73
|
+
* `update` is the read-modify-write form that runs inside the store's serial queue.
|
|
74
|
+
*/
|
|
75
|
+
export const DataStore = local("moonan.data", {
|
|
76
|
+
version: 1,
|
|
77
|
+
input: Type.Object({}),
|
|
78
|
+
members: {
|
|
79
|
+
revision: Type.Number(),
|
|
80
|
+
storage: DataStoreStorageInfoSchema,
|
|
81
|
+
read: method(Type.Object({ namespace: Type.String() }), Type.Any()),
|
|
82
|
+
write: method(Type.Object({ namespace: Type.String(), version: Type.Number(), value: Type.Any() }), StoredDatasetSchema),
|
|
83
|
+
update: method(Type.Object({ namespace: Type.String(), version: Type.Number(), update: Type.Any() }), StoredDatasetSchema),
|
|
84
|
+
remove: method(Type.Object({ namespace: Type.String() })),
|
|
85
|
+
reload: method(undefined, DataStoreStorageInfoSchema),
|
|
86
|
+
exportAll: method(Type.Object({ includeSecrets: Type.Optional(Type.Boolean()) }), DataBundleSchema),
|
|
87
|
+
importAll: method(Type.Object({ bundle: DataBundleSchema }), DataImportReportSchema),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=data.js.map
|
package/dist/data.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.js","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@moonan_ai/sdk` — everything a plugin needs on either side of the wire.
|
|
3
|
+
*
|
|
4
|
+
* The root entry is shared by the server facet and the browser facet: contracts,
|
|
5
|
+
* registries (including `DataCodecs` and `ModelProviders`) and the Chord primitives they
|
|
6
|
+
* are built from. Browser-only surface (the `Panels` registry and the React hooks) lives
|
|
7
|
+
* in `@moonan_ai/sdk/web`, because it needs React and the root entry must stay importable
|
|
8
|
+
* from a Node facet.
|
|
9
|
+
*/
|
|
10
|
+
export { WebFacetCatalog, FacetSetSchema, WebFacetSchema, } from "./web-facet-catalog.js";
|
|
11
|
+
export { DataStore, DataCodecs, DataBundleSchema, DataImportReportSchema, DataStoreStorageInfoSchema, StoredDatasetSchema, defineDataset, } from "./data.js";
|
|
12
|
+
export { ModelProviders, } from "./model-provider.js";
|
|
13
|
+
export { Routes } from "./routes.js";
|
|
14
|
+
/** Chord primitives, re-exported so a plugin needs two dependencies instead of three. */
|
|
15
|
+
export { ChordError, defineFacet, defineRegistry, forbidden, hostFamily, hostNamed, invalid, isChordError, local, method, opaque, remote, } from "@earendil-works/chord";
|
|
16
|
+
/** Explicit cancellation/context helpers; browser-safe, same entry points as chord. */
|
|
17
|
+
export { BACKGROUND_CONTEXT, awaitWithContext, traceIn, withAbortSignal, withCancel, withoutAbortSignal, } from "@earendil-works/chord/context";
|
|
18
|
+
/** The schema builder every contract is written with. */
|
|
19
|
+
export { Type } from "typebox";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACN,eAAe,EACf,cAAc,EACd,cAAc,GAGd,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,EAC1B,mBAAmB,EACnB,aAAa,GAOb,MAAM,WAAW,CAAC;AACnB,OAAO,EACN,cAAc,GAGd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAqB,MAAM,aAAa,CAAC;AAExD,yFAAyF;AACzF,OAAO,EACN,UAAU,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,UAAU,EACV,SAAS,EACT,OAAO,EACP,YAAY,EACZ,KAAK,EACL,MAAM,EACN,MAAM,EACN,MAAM,GAiBN,MAAM,uBAAuB,CAAC;AAE/B,uFAAuF;AACvF,OAAO,EACN,kBAAkB,EAClB,gBAAgB,EAChB,OAAO,EACP,eAAe,EACf,UAAU,EACV,kBAAkB,GAElB,MAAM,+BAA+B,CAAC;AAEvC,yDAAyD;AACzD,OAAO,EAAE,IAAI,EAA6B,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-provider.js","sourceRoot":"","sources":["../src/model-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA0CvD,MAAM,CAAC,MAAM,cAAc,GAAG,cAAc,CAAgB,wBAAwB,EAAE;IACrF,QAAQ,EAAE,OAAO;CACjB,CAAC,CAAC"}
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineRegistry } from "@earendil-works/chord";
|
|
2
|
+
/**
|
|
3
|
+
* The server-side way out of the object protocol.
|
|
4
|
+
*
|
|
5
|
+
* Chord covers everything object-to-object; it has no opinion about plain HTTP, and the
|
|
6
|
+
* platform serves exactly one port for both. This registry is the hook between them:
|
|
7
|
+
* the keys are `"GET /path"` for one method, or `"/path"` for any. A request that matches
|
|
8
|
+
* nothing falls through to the built-in endpoints, the facet assets and the shell.
|
|
9
|
+
*/
|
|
10
|
+
export const Routes = defineRegistry("moonan.routes", { conflict: "error" });
|
|
11
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAcvD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,cAAc,CAAe,eAAe,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { hostNamed, remote } from "@earendil-works/chord";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
/**
|
|
4
|
+
* A browser facet: the module URL and a hash of its exact file contents.
|
|
5
|
+
*
|
|
6
|
+
* The URL alone cannot tell a client that the file changed; a server reload with the same
|
|
7
|
+
* path would otherwise look identical. The hash is what makes a browser reload only when
|
|
8
|
+
* its web code actually changed.
|
|
9
|
+
*/
|
|
10
|
+
export const WebFacetSchema = Type.Object({
|
|
11
|
+
/** Absolute path on this server (`/facets/<key>/web.js`). */
|
|
12
|
+
url: Type.String(),
|
|
13
|
+
/** SHA-256 of the file contents, truncated for display. */
|
|
14
|
+
hash: Type.String(),
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* One plugin's facets, as the browser needs to know them.
|
|
18
|
+
*
|
|
19
|
+
* `name` is a display label derived from the directory or package; it is not an identity.
|
|
20
|
+
* The runtime identity of a facet is its module URL. A server-only plugin has no `web`, and
|
|
21
|
+
* a plugin that failed to resolve carries `error` instead — the browser still receives it,
|
|
22
|
+
* so the shell can report the failure rather than show a silently missing tab.
|
|
23
|
+
*/
|
|
24
|
+
export const FacetSetSchema = Type.Object({
|
|
25
|
+
name: Type.String(),
|
|
26
|
+
version: Type.String(),
|
|
27
|
+
/** Where the facets came from: an installed package, or a directory on disk. */
|
|
28
|
+
source: Type.Union([Type.Literal("package"), Type.Literal("directory")]),
|
|
29
|
+
web: Type.Optional(WebFacetSchema),
|
|
30
|
+
error: Type.Optional(Type.String()),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* The only object the platform publishes: the catalog of browser facets the browser host
|
|
34
|
+
* must load.
|
|
35
|
+
*
|
|
36
|
+
* This is the seam that keeps the platform free of feature state. The loader knows which
|
|
37
|
+
* modules exist; the browser cannot find that out alone (it has no filesystem and no
|
|
38
|
+
* registry to ask), so exactly one read-only object carries it across.
|
|
39
|
+
*
|
|
40
|
+
* The name is deliberately about what it carries: a catalog of web facets. It is not a
|
|
41
|
+
* package manifest and it is not a general application manifest. This is a fresh contract
|
|
42
|
+
* (version 1) with no compatibility adapter: an old shell gets `upgrade_required` and
|
|
43
|
+
* reloads the page.
|
|
44
|
+
*/
|
|
45
|
+
export const WebFacetCatalog = remote("moonan.web-facets", {
|
|
46
|
+
version: 1,
|
|
47
|
+
input: Type.Object({}),
|
|
48
|
+
host: hostNamed("moonan"),
|
|
49
|
+
variants: {
|
|
50
|
+
v1: {
|
|
51
|
+
state: {
|
|
52
|
+
/** The platform's own version, so a client can tell which build it is talking to. */
|
|
53
|
+
version: Type.String(),
|
|
54
|
+
facets: Type.Array(FacetSetSchema),
|
|
55
|
+
/**
|
|
56
|
+
* chord's code version (`host.codeVersion`): 1 at startup, one higher after every
|
|
57
|
+
* reload that committed. This is Moonan application state, not chord's removed
|
|
58
|
+
* `Shape.codeVersion`; a browser decides whether to reload from each `web.hash`,
|
|
59
|
+
* and never from this number as a protocol value.
|
|
60
|
+
*/
|
|
61
|
+
revision: Type.Number(),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=web-facet-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-facet-catalog.js","sourceRoot":"","sources":["../src/web-facet-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAe,MAAM,SAAS,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IACzC,6DAA6D;IAC7D,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE;IAClB,2DAA2D;IAC3D,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IACzC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE;IACnB,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE;IACtB,gFAAgF;IAChF,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACxE,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;IAClC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;CACnC,CAAC,CAAC;AAGH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,mBAAmB,EAAE;IAC1D,OAAO,EAAE,CAAC;IACV,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IACtB,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC;IACzB,QAAQ,EAAE;QACT,EAAE,EAAE;YACH,KAAK,EAAE;gBACN,qFAAqF;gBACrF,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE;gBACtB,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC;gBAClC;;;;;mBAKG;gBACH,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE;aACvB;SACD;KACD;CACD,CAAC,CAAC"}
|
package/dist/web.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { defineRegistry, isChordError, } from "@earendil-works/chord";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
export { isChordError } from "@earendil-works/chord";
|
|
4
|
+
/**
|
|
5
|
+
* A `gone` error that no new reference can fix: the consumer was built against a contract the
|
|
6
|
+
* provider no longer covers. For the shell's own contracts (the platform `WebFacetCatalog`, or a
|
|
7
|
+
* protocol mismatch) the page needs the new build. For a plugin's contract, chord's remedy is
|
|
8
|
+
* to reload the web facets; the shell does that when the WebFacetCatalog reports changed facets.
|
|
9
|
+
*/
|
|
10
|
+
export function isFatalContractError(error) {
|
|
11
|
+
return isChordError(error, "contract_mismatch") || isChordError(error, "upgrade_required");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The browser-side way into the shell's navigation.
|
|
15
|
+
*
|
|
16
|
+
* Chord registries are per host, and a panel component cannot cross the wire, so the
|
|
17
|
+
* registry lives in the browser host and is filled by the plugin's own browser facet.
|
|
18
|
+
* The shell subscribes and renders; it never imports a panel.
|
|
19
|
+
*/
|
|
20
|
+
export const Panels = defineRegistry("moonan.panels", { conflict: "error" });
|
|
21
|
+
/**
|
|
22
|
+
* Reads a state member, falling back while it has no value yet.
|
|
23
|
+
*
|
|
24
|
+
* `hold` returns before the object is live, and chord reports that as `not_ready` when the
|
|
25
|
+
* member is read — a transition, not a failure. `not_in_variant` is the other expected one:
|
|
26
|
+
* the provider's variant does not carry this member. Anything else is a bug in the read
|
|
27
|
+
* path and is reported rather than hidden behind the fallback.
|
|
28
|
+
*/
|
|
29
|
+
function readOr(readable, fallback) {
|
|
30
|
+
if (!readable)
|
|
31
|
+
return fallback;
|
|
32
|
+
try {
|
|
33
|
+
return readable.value;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (isChordError(error, "not_ready") || isChordError(error, "not_in_variant"))
|
|
37
|
+
return fallback;
|
|
38
|
+
console.error("[chord] reading state failed", error);
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Acquires a reference from the browser host with `host.hold`.
|
|
44
|
+
*
|
|
45
|
+
* `hold` is the API chord designates for UI runtimes: synchronous, possibly still
|
|
46
|
+
* `loading`, and re-held by the runtime itself when the link comes back, so a reconnect
|
|
47
|
+
* costs no new reference. It is acquired in a layout effect rather than during render:
|
|
48
|
+
* that keeps the impossible-to-render intermediate commit off the screen, and makes the
|
|
49
|
+
* acquire/release pair symmetrical under StrictMode's mount → unmount → mount (a
|
|
50
|
+
* reference taken during render would leak the discarded one).
|
|
51
|
+
*/
|
|
52
|
+
export function useRemote(host, contract, input, options = {}) {
|
|
53
|
+
// The input is a plain JSON address, and a panel commonly passes a fresh `{}` each
|
|
54
|
+
// render. Key on its content so identity churn does not re-acquire the reference.
|
|
55
|
+
const key = JSON.stringify(input);
|
|
56
|
+
const [ref, setRef] = useState(null);
|
|
57
|
+
const [attempt, setAttempt] = useState(0);
|
|
58
|
+
const retries = useRef(0);
|
|
59
|
+
const fatalReported = useRef(false);
|
|
60
|
+
const warned = useRef(false);
|
|
61
|
+
const onFatal = useRef(options.onFatal);
|
|
62
|
+
onFatal.current = options.onFatal;
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
if (!host) {
|
|
65
|
+
setRef(null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let held;
|
|
69
|
+
try {
|
|
70
|
+
held = host.hold(contract, JSON.parse(key));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// A type this host cannot place (an unknown name, a local type it does not serve).
|
|
74
|
+
console.error("[chord] hold failed", error);
|
|
75
|
+
setRef(null);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
setRef(held);
|
|
79
|
+
return () => {
|
|
80
|
+
setRef(null);
|
|
81
|
+
held.release();
|
|
82
|
+
};
|
|
83
|
+
}, [host, contract, key, attempt]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!ref)
|
|
86
|
+
return;
|
|
87
|
+
return ref.onStatus((status, error) => {
|
|
88
|
+
// A reconnect re-holds in place (`loading`, last values readable) and needs nothing.
|
|
89
|
+
// `gone` is terminal for this reference — the object was forced away while held —
|
|
90
|
+
// and chord's guidance for a UI is to take a new one. Bounded, so a provider that
|
|
91
|
+
// keeps answering `gone` cannot spin the page.
|
|
92
|
+
if (status === "live") {
|
|
93
|
+
retries.current = 0;
|
|
94
|
+
fatalReported.current = false;
|
|
95
|
+
warned.current = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (status !== "gone")
|
|
99
|
+
return;
|
|
100
|
+
// A contract that the provider no longer covers cannot be fixed by taking another
|
|
101
|
+
// reference. The shell owns the official remedy (reload the web facets when the
|
|
102
|
+
// WebFacetCatalog reports changed facets); a panel only reports it upward, and without an
|
|
103
|
+
// `onFatal` handler it says once what the developer can do.
|
|
104
|
+
if (isFatalContractError(error)) {
|
|
105
|
+
const handler = onFatal.current;
|
|
106
|
+
if (handler !== undefined) {
|
|
107
|
+
if (!fatalReported.current) {
|
|
108
|
+
fatalReported.current = true;
|
|
109
|
+
handler(error);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!warned.current) {
|
|
114
|
+
warned.current = true;
|
|
115
|
+
console.warn("[chord] a reference is gone because its contract no longer binds. The shell reloads web facets when the WebFacetCatalog reports changed facets; if this persists, the plugin's web code and contract are out of sync.", error);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (retries.current < 3) {
|
|
120
|
+
retries.current += 1;
|
|
121
|
+
setAttempt((value) => value + 1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!fatalReported.current) {
|
|
125
|
+
fatalReported.current = true;
|
|
126
|
+
onFatal.current?.(error);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}, [ref]);
|
|
130
|
+
return ref;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Follows a reference's status: `loading | live | released | gone`.
|
|
134
|
+
*
|
|
135
|
+
* `hold` returns before the object is live, so a panel renders from this instead of
|
|
136
|
+
* guessing. `gone` is the state that needs a new reference — a forced disposal, a host
|
|
137
|
+
* that closed, a contract the provider no longer serves.
|
|
138
|
+
*/
|
|
139
|
+
export function useRefStatus(ref) {
|
|
140
|
+
const [status, setStatus] = useState(() => ref?.status ?? null);
|
|
141
|
+
// Layout effect: the reference is acquired in one, and this is what turns that commit
|
|
142
|
+
// into a painted one. With a passive effect the first paint would still say "loading".
|
|
143
|
+
useLayoutEffect(() => {
|
|
144
|
+
if (!ref) {
|
|
145
|
+
setStatus(null);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setStatus(ref.status);
|
|
149
|
+
return ref.onStatus((next) => setStatus(next));
|
|
150
|
+
}, [ref]);
|
|
151
|
+
return status;
|
|
152
|
+
}
|
|
153
|
+
/** Renders the current value of a chord `Readable`, and every later one. */
|
|
154
|
+
export function useReadable(readable, fallback) {
|
|
155
|
+
const [value, setValue] = useState(() => readOr(readable, fallback));
|
|
156
|
+
useLayoutEffect(() => {
|
|
157
|
+
if (!readable) {
|
|
158
|
+
setValue(fallback);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let cancelled = false;
|
|
162
|
+
setValue(readOr(readable, fallback));
|
|
163
|
+
const stop = readable.subscribe((next) => {
|
|
164
|
+
if (!cancelled)
|
|
165
|
+
setValue(next);
|
|
166
|
+
});
|
|
167
|
+
return () => {
|
|
168
|
+
cancelled = true;
|
|
169
|
+
stop();
|
|
170
|
+
};
|
|
171
|
+
}, [readable]);
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=web.js.map
|
package/dist/web.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonan_ai/sdk",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.5-alpha1",
|
|
4
|
+
"description": "Moonan plugin SDK: contracts, registries and browser hooks for plugin authors.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Pheobe-Southwood/Moonan.git",
|
|
10
|
+
"directory": "packages/sdk"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"development": "./src/index.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./web": {
|
|
19
|
+
"types": "./src/web.ts",
|
|
20
|
+
"development": "./src/web.ts",
|
|
21
|
+
"default": "./dist/web.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@earendil-works/chord": "1.4.0",
|
|
31
|
+
"typebox": "1.3.27"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"react": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.20.5",
|
|
43
|
+
"@types/react": "^19.3.0",
|
|
44
|
+
"react": "^19.3.1"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"moonan",
|
|
48
|
+
"plugin",
|
|
49
|
+
"chord",
|
|
50
|
+
"sdk"
|
|
51
|
+
],
|
|
52
|
+
"homepage": "https://github.com/Pheobe-Southwood/Moonan#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/Pheobe-Southwood/Moonan/issues"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
61
|
+
"build": "tsc -p tsconfig.build.json"
|
|
62
|
+
}
|
|
6
63
|
}
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { defineRegistry, local, method, type JsonValue } from "@earendil-works/chord";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One value in the platform's unified data store.
|
|
6
|
+
*
|
|
7
|
+
* `version` belongs to the dataset, not to the file and not to the contract. A plugin
|
|
8
|
+
* raises it when the stored shape or meaning changes and supplies a migration through its
|
|
9
|
+
* {@link DatasetCodec}; the platform never interprets the value.
|
|
10
|
+
*/
|
|
11
|
+
export const StoredDatasetSchema = Type.Object(
|
|
12
|
+
{
|
|
13
|
+
value: Type.Any(),
|
|
14
|
+
version: Type.Number(),
|
|
15
|
+
},
|
|
16
|
+
{ additionalProperties: false },
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type StoredDataset = {
|
|
20
|
+
value: JsonValue;
|
|
21
|
+
version: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What every plugin must contribute to make its dataset exportable and importable.
|
|
26
|
+
*
|
|
27
|
+
* The codec is a registry value, not a Chord contract: it lives in the server host, can
|
|
28
|
+
* carry functions, and never crosses a link. The platform calls it when an administrator
|
|
29
|
+
* exports or imports the unified data file.
|
|
30
|
+
*/
|
|
31
|
+
export interface DatasetCodec<T = unknown> {
|
|
32
|
+
/** Stable dataset key, conventionally `"<package>/<dataset>"`. */
|
|
33
|
+
readonly namespace: string;
|
|
34
|
+
/** Current stored shape version; see {@link StoredDataset.version}. */
|
|
35
|
+
readonly version: number;
|
|
36
|
+
/** Convert an older stored value to the current stored value. */
|
|
37
|
+
migrate(value: JsonValue, fromVersion: number): T;
|
|
38
|
+
/** Convert the current stored value to a portable export value (redact secrets here). */
|
|
39
|
+
export(value: T, options: DatasetExportOptions): JsonValue;
|
|
40
|
+
/**
|
|
41
|
+
* Convert an imported export value to the current stored value.
|
|
42
|
+
*
|
|
43
|
+
* `current` is the value already stored at this namespace, if any. A codec may use it
|
|
44
|
+
* to preserve secrets that a redacted export deliberately omitted.
|
|
45
|
+
*/
|
|
46
|
+
import(value: JsonValue, fromVersion: number, current: JsonValue | undefined): T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DatasetExportOptions {
|
|
50
|
+
/** False by default on the HTTP surface; a codec must redact secrets when false. */
|
|
51
|
+
readonly includeSecrets: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Identity helper; keeps the codec's type while documenting intent. */
|
|
55
|
+
export function defineDataset<T>(codec: DatasetCodec<T>): DatasetCodec<T> {
|
|
56
|
+
return codec;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The registry of dataset codecs.
|
|
61
|
+
*
|
|
62
|
+
* A contribution must come from an object of `lifetime: "host"`, like every other Chord
|
|
63
|
+
* registry. The platform reads the table for the code version of the root that asks.
|
|
64
|
+
*/
|
|
65
|
+
export const DataCodecs = defineRegistry<DatasetCodec>("moonan.data-codecs", {
|
|
66
|
+
conflict: "error",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* What the data store reports about its file. This is a state member, so a plugin can read
|
|
71
|
+
* it synchronously once the store is live; the platform also exposes it over HTTP.
|
|
72
|
+
*/
|
|
73
|
+
export const DataStoreStorageInfoSchema = Type.Object(
|
|
74
|
+
{
|
|
75
|
+
file: Type.String(),
|
|
76
|
+
backup: Type.String(),
|
|
77
|
+
exists: Type.Boolean(),
|
|
78
|
+
mode: Type.Optional(Type.String()),
|
|
79
|
+
writable: Type.Boolean(),
|
|
80
|
+
revision: Type.Number(),
|
|
81
|
+
datasets: Type.Number(),
|
|
82
|
+
lastSavedAt: Type.Optional(Type.String()),
|
|
83
|
+
notice: Type.Optional(Type.String()),
|
|
84
|
+
},
|
|
85
|
+
{ additionalProperties: false },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export type DataStoreStorageInfo = {
|
|
89
|
+
file: string;
|
|
90
|
+
backup: string;
|
|
91
|
+
exists: boolean;
|
|
92
|
+
mode?: string;
|
|
93
|
+
writable: boolean;
|
|
94
|
+
revision: number;
|
|
95
|
+
datasets: number;
|
|
96
|
+
lastSavedAt?: string;
|
|
97
|
+
notice?: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The wire/JSON envelope for a complete export. The file itself has a different, richer
|
|
102
|
+
* shape (it also keeps per-dataset timestamps); this is the portable subset.
|
|
103
|
+
*/
|
|
104
|
+
export const DataBundleSchema = Type.Object(
|
|
105
|
+
{
|
|
106
|
+
format: Type.Literal("moonan.data"),
|
|
107
|
+
formatVersion: Type.Literal(1),
|
|
108
|
+
createdAt: Type.String(),
|
|
109
|
+
revision: Type.Number(),
|
|
110
|
+
datasets: Type.Array(
|
|
111
|
+
Type.Object(
|
|
112
|
+
{
|
|
113
|
+
namespace: Type.String(),
|
|
114
|
+
version: Type.Number(),
|
|
115
|
+
value: Type.Any(),
|
|
116
|
+
},
|
|
117
|
+
{ additionalProperties: false },
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
skipped: Type.Optional(Type.Array(Type.String())),
|
|
121
|
+
},
|
|
122
|
+
{ additionalProperties: false },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export type DataBundle = {
|
|
126
|
+
format: "moonan.data";
|
|
127
|
+
formatVersion: 1;
|
|
128
|
+
createdAt: string;
|
|
129
|
+
revision: number;
|
|
130
|
+
datasets: { namespace: string; version: number; value: JsonValue }[];
|
|
131
|
+
skipped?: string[];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const DataImportReportSchema = Type.Object(
|
|
135
|
+
{
|
|
136
|
+
imported: Type.Array(Type.String()),
|
|
137
|
+
revision: Type.Number(),
|
|
138
|
+
skipped: Type.Optional(Type.Array(Type.String())),
|
|
139
|
+
},
|
|
140
|
+
{ additionalProperties: false },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
export type DataImportReport = {
|
|
144
|
+
imported: string[];
|
|
145
|
+
revision: number;
|
|
146
|
+
skipped?: string[];
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The platform's unified data service.
|
|
151
|
+
*
|
|
152
|
+
* It is local and host-lifetime: exactly one object exists for the server host, it owns
|
|
153
|
+
* the only data file, and every plugin reads/writes through it. It is not a remote type;
|
|
154
|
+
* browsers get data through plugin methods or the platform's export/import HTTP surface.
|
|
155
|
+
*
|
|
156
|
+
* `read` returns the raw `{ value, version }` and deliberately does not consult a codec:
|
|
157
|
+
* a plugin's `config` cell may initialize before its codec registration is published, so
|
|
158
|
+
* the plugin validates/migrates its own first value. `write` replaces a whole dataset;
|
|
159
|
+
* `update` is the read-modify-write form that runs inside the store's serial queue.
|
|
160
|
+
*/
|
|
161
|
+
export const DataStore = local("moonan.data", {
|
|
162
|
+
version: 1,
|
|
163
|
+
input: Type.Object({}),
|
|
164
|
+
members: {
|
|
165
|
+
revision: Type.Number(),
|
|
166
|
+
storage: DataStoreStorageInfoSchema,
|
|
167
|
+
read: method(Type.Object({ namespace: Type.String() }), Type.Any()),
|
|
168
|
+
write: method(
|
|
169
|
+
Type.Object({ namespace: Type.String(), version: Type.Number(), value: Type.Any() }),
|
|
170
|
+
StoredDatasetSchema,
|
|
171
|
+
),
|
|
172
|
+
update: method(
|
|
173
|
+
Type.Object({ namespace: Type.String(), version: Type.Number(), update: Type.Any() }),
|
|
174
|
+
StoredDatasetSchema,
|
|
175
|
+
),
|
|
176
|
+
remove: method(Type.Object({ namespace: Type.String() })),
|
|
177
|
+
reload: method(undefined, DataStoreStorageInfoSchema),
|
|
178
|
+
exportAll: method(
|
|
179
|
+
Type.Object({ includeSecrets: Type.Optional(Type.Boolean()) }),
|
|
180
|
+
DataBundleSchema,
|
|
181
|
+
),
|
|
182
|
+
importAll: method(Type.Object({ bundle: DataBundleSchema }), DataImportReportSchema),
|
|
183
|
+
},
|
|
184
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@moonan_ai/sdk` — everything a plugin needs on either side of the wire.
|
|
3
|
+
*
|
|
4
|
+
* The root entry is shared by the server facet and the browser facet: contracts,
|
|
5
|
+
* registries (including `DataCodecs` and `ModelProviders`) and the Chord primitives they
|
|
6
|
+
* are built from. Browser-only surface (the `Panels` registry and the React hooks) lives
|
|
7
|
+
* in `@moonan_ai/sdk/web`, because it needs React and the root entry must stay importable
|
|
8
|
+
* from a Node facet.
|
|
9
|
+
*/
|
|
10
|
+
export {
|
|
11
|
+
WebFacetCatalog,
|
|
12
|
+
FacetSetSchema,
|
|
13
|
+
WebFacetSchema,
|
|
14
|
+
type FacetSetEntry,
|
|
15
|
+
type WebFacet,
|
|
16
|
+
} from "./web-facet-catalog.js";
|
|
17
|
+
export {
|
|
18
|
+
DataStore,
|
|
19
|
+
DataCodecs,
|
|
20
|
+
DataBundleSchema,
|
|
21
|
+
DataImportReportSchema,
|
|
22
|
+
DataStoreStorageInfoSchema,
|
|
23
|
+
StoredDatasetSchema,
|
|
24
|
+
defineDataset,
|
|
25
|
+
type DataBundle,
|
|
26
|
+
type DataImportReport,
|
|
27
|
+
type DataStoreStorageInfo,
|
|
28
|
+
type DatasetCodec,
|
|
29
|
+
type DatasetExportOptions,
|
|
30
|
+
type StoredDataset,
|
|
31
|
+
} from "./data.js";
|
|
32
|
+
export {
|
|
33
|
+
ModelProviders,
|
|
34
|
+
type ModelInfo,
|
|
35
|
+
type ModelProvider,
|
|
36
|
+
} from "./model-provider.js";
|
|
37
|
+
export { Routes, type RouteHandler } from "./routes.js";
|
|
38
|
+
|
|
39
|
+
/** Chord primitives, re-exported so a plugin needs two dependencies instead of three. */
|
|
40
|
+
export {
|
|
41
|
+
ChordError,
|
|
42
|
+
defineFacet,
|
|
43
|
+
defineRegistry,
|
|
44
|
+
forbidden,
|
|
45
|
+
hostFamily,
|
|
46
|
+
hostNamed,
|
|
47
|
+
invalid,
|
|
48
|
+
isChordError,
|
|
49
|
+
local,
|
|
50
|
+
method,
|
|
51
|
+
opaque,
|
|
52
|
+
remote,
|
|
53
|
+
type AnyContract,
|
|
54
|
+
type Ctx,
|
|
55
|
+
type Facet,
|
|
56
|
+
type FacetApi,
|
|
57
|
+
type Host,
|
|
58
|
+
type InputOf,
|
|
59
|
+
type JsonValue,
|
|
60
|
+
type LocalContract,
|
|
61
|
+
type Readable,
|
|
62
|
+
type Ref,
|
|
63
|
+
type RefStatus,
|
|
64
|
+
type Registry,
|
|
65
|
+
type RegistryTable,
|
|
66
|
+
type RegistryView,
|
|
67
|
+
type RemoteContract,
|
|
68
|
+
type Shape,
|
|
69
|
+
} from "@earendil-works/chord";
|
|
70
|
+
|
|
71
|
+
/** Explicit cancellation/context helpers; browser-safe, same entry points as chord. */
|
|
72
|
+
export {
|
|
73
|
+
BACKGROUND_CONTEXT,
|
|
74
|
+
awaitWithContext,
|
|
75
|
+
traceIn,
|
|
76
|
+
withAbortSignal,
|
|
77
|
+
withCancel,
|
|
78
|
+
withoutAbortSignal,
|
|
79
|
+
type Context,
|
|
80
|
+
} from "@earendil-works/chord/context";
|
|
81
|
+
|
|
82
|
+
/** The schema builder every contract is written with. */
|
|
83
|
+
export { Type, type Static, type TSchema } from "typebox";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineRegistry } from "@earendil-works/chord";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provider-neutral metadata for one configured chat model.
|
|
5
|
+
*
|
|
6
|
+
* This is plain JSON: the registry value never crosses a link, but keeping the catalog
|
|
7
|
+
* serializable lets a plugin expose it over HTTP or in a panel without a second shape.
|
|
8
|
+
*/
|
|
9
|
+
export interface ModelInfo {
|
|
10
|
+
readonly providerId: string;
|
|
11
|
+
readonly modelId: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly contextWindow?: number;
|
|
14
|
+
readonly maxTokens?: number;
|
|
15
|
+
readonly input: readonly ("text" | "image")[];
|
|
16
|
+
readonly reasoning: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A model provider contribution.
|
|
21
|
+
*
|
|
22
|
+
* `list()` is the key-free catalog. `runtime()` is a server-only handle whose concrete
|
|
23
|
+
* shape belongs to the provider package; ai-gateway returns its pi-ai `MutableModels`
|
|
24
|
+
* there. The SDK stays free of pi-ai so a generic plugin does not inherit an AI dependency.
|
|
25
|
+
*
|
|
26
|
+
* A consumer looks the provider up with the code version of its root:
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const providers = api.registry(ModelProviders);
|
|
30
|
+
* const provider = providers.get("ai-gateway", ctx);
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The registry value is not a Chord contract: there is no `uses` entry and no wire
|
|
34
|
+
* compatibility rule. Its table is replaced with each code version like every registry.
|
|
35
|
+
*/
|
|
36
|
+
export interface ModelProvider {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly title: string;
|
|
39
|
+
list(): readonly ModelInfo[];
|
|
40
|
+
runtime(): unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const ModelProviders = defineRegistry<ModelProvider>("moonan.model-providers", {
|
|
44
|
+
conflict: "error",
|
|
45
|
+
});
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineRegistry } from "@earendil-works/chord";
|
|
2
|
+
import type { Ctx } from "@earendil-works/chord";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One HTTP handler a plugin contributes.
|
|
7
|
+
*
|
|
8
|
+
* `ctx` is the root the platform opened for this request: a handler that calls into an
|
|
9
|
+
* object passes it along, so the call belongs to the request and ends with it. Handlers
|
|
10
|
+
* are short — a response that needs to outlive the request (a stream, a long poll) is
|
|
11
|
+
* the wrong shape here, and should hold its state in a cell and answer in another call.
|
|
12
|
+
*/
|
|
13
|
+
export type RouteHandler = (request: IncomingMessage, response: ServerResponse, ctx: Ctx) => void | Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The server-side way out of the object protocol.
|
|
17
|
+
*
|
|
18
|
+
* Chord covers everything object-to-object; it has no opinion about plain HTTP, and the
|
|
19
|
+
* platform serves exactly one port for both. This registry is the hook between them:
|
|
20
|
+
* the keys are `"GET /path"` for one method, or `"/path"` for any. A request that matches
|
|
21
|
+
* nothing falls through to the built-in endpoints, the facet assets and the shell.
|
|
22
|
+
*/
|
|
23
|
+
export const Routes = defineRegistry<RouteHandler>("moonan.routes", { conflict: "error" });
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { hostNamed, remote } from "@earendil-works/chord";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A browser facet: the module URL and a hash of its exact file contents.
|
|
6
|
+
*
|
|
7
|
+
* The URL alone cannot tell a client that the file changed; a server reload with the same
|
|
8
|
+
* path would otherwise look identical. The hash is what makes a browser reload only when
|
|
9
|
+
* its web code actually changed.
|
|
10
|
+
*/
|
|
11
|
+
export const WebFacetSchema = Type.Object({
|
|
12
|
+
/** Absolute path on this server (`/facets/<key>/web.js`). */
|
|
13
|
+
url: Type.String(),
|
|
14
|
+
/** SHA-256 of the file contents, truncated for display. */
|
|
15
|
+
hash: Type.String(),
|
|
16
|
+
});
|
|
17
|
+
export type WebFacet = Static<typeof WebFacetSchema>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One plugin's facets, as the browser needs to know them.
|
|
21
|
+
*
|
|
22
|
+
* `name` is a display label derived from the directory or package; it is not an identity.
|
|
23
|
+
* The runtime identity of a facet is its module URL. A server-only plugin has no `web`, and
|
|
24
|
+
* a plugin that failed to resolve carries `error` instead — the browser still receives it,
|
|
25
|
+
* so the shell can report the failure rather than show a silently missing tab.
|
|
26
|
+
*/
|
|
27
|
+
export const FacetSetSchema = Type.Object({
|
|
28
|
+
name: Type.String(),
|
|
29
|
+
version: Type.String(),
|
|
30
|
+
/** Where the facets came from: an installed package, or a directory on disk. */
|
|
31
|
+
source: Type.Union([Type.Literal("package"), Type.Literal("directory")]),
|
|
32
|
+
web: Type.Optional(WebFacetSchema),
|
|
33
|
+
error: Type.Optional(Type.String()),
|
|
34
|
+
});
|
|
35
|
+
export type FacetSetEntry = Static<typeof FacetSetSchema>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The only object the platform publishes: the catalog of browser facets the browser host
|
|
39
|
+
* must load.
|
|
40
|
+
*
|
|
41
|
+
* This is the seam that keeps the platform free of feature state. The loader knows which
|
|
42
|
+
* modules exist; the browser cannot find that out alone (it has no filesystem and no
|
|
43
|
+
* registry to ask), so exactly one read-only object carries it across.
|
|
44
|
+
*
|
|
45
|
+
* The name is deliberately about what it carries: a catalog of web facets. It is not a
|
|
46
|
+
* package manifest and it is not a general application manifest. This is a fresh contract
|
|
47
|
+
* (version 1) with no compatibility adapter: an old shell gets `upgrade_required` and
|
|
48
|
+
* reloads the page.
|
|
49
|
+
*/
|
|
50
|
+
export const WebFacetCatalog = remote("moonan.web-facets", {
|
|
51
|
+
version: 1,
|
|
52
|
+
input: Type.Object({}),
|
|
53
|
+
host: hostNamed("moonan"),
|
|
54
|
+
variants: {
|
|
55
|
+
v1: {
|
|
56
|
+
state: {
|
|
57
|
+
/** The platform's own version, so a client can tell which build it is talking to. */
|
|
58
|
+
version: Type.String(),
|
|
59
|
+
facets: Type.Array(FacetSetSchema),
|
|
60
|
+
/**
|
|
61
|
+
* chord's code version (`host.codeVersion`): 1 at startup, one higher after every
|
|
62
|
+
* reload that committed. This is Moonan application state, not chord's removed
|
|
63
|
+
* `Shape.codeVersion`; a browser decides whether to reload from each `web.hash`,
|
|
64
|
+
* and never from this number as a protocol value.
|
|
65
|
+
*/
|
|
66
|
+
revision: Type.Number(),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
package/src/web.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineRegistry,
|
|
3
|
+
isChordError,
|
|
4
|
+
type AnyContract,
|
|
5
|
+
type Host,
|
|
6
|
+
type InputOf,
|
|
7
|
+
type Readable,
|
|
8
|
+
type Ref,
|
|
9
|
+
type RefStatus,
|
|
10
|
+
} from "@earendil-works/chord";
|
|
11
|
+
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
|
12
|
+
|
|
13
|
+
export type { InputOf, Readable, Ref, RefStatus } from "@earendil-works/chord";
|
|
14
|
+
export { isChordError } from "@earendil-works/chord";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `gone` error that no new reference can fix: the consumer was built against a contract the
|
|
18
|
+
* provider no longer covers. For the shell's own contracts (the platform `WebFacetCatalog`, or a
|
|
19
|
+
* protocol mismatch) the page needs the new build. For a plugin's contract, chord's remedy is
|
|
20
|
+
* to reload the web facets; the shell does that when the WebFacetCatalog reports changed facets.
|
|
21
|
+
*/
|
|
22
|
+
export function isFatalContractError(error: unknown): boolean {
|
|
23
|
+
return isChordError(error, "contract_mismatch") || isChordError(error, "upgrade_required");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Options of {@link useRemote}. */
|
|
27
|
+
export interface UseRemoteOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Called once when the reference can no longer be recovered: a non-retryable contract
|
|
30
|
+
* error, or a `gone` reference after the bounded retries. The shell uses this to reload
|
|
31
|
+
* the page instead of rendering a permanently loading screen.
|
|
32
|
+
*/
|
|
33
|
+
onFatal?: (error: unknown) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What the shell hands to a panel: the browser chord host, and nothing else.
|
|
38
|
+
*
|
|
39
|
+
* Every reference a panel holds comes from here — `useRemote(host, …)`. The shell owns
|
|
40
|
+
* no application state to pass down, which is what keeps it from acquiring opinions.
|
|
41
|
+
*/
|
|
42
|
+
export interface PanelProps {
|
|
43
|
+
host: Host<unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One top-level page a browser facet contributes. */
|
|
47
|
+
export interface PanelContribution {
|
|
48
|
+
id: string;
|
|
49
|
+
/** The tab label. Data, not a shell dictionary key: the shell has no dictionary. */
|
|
50
|
+
title: string;
|
|
51
|
+
/** Ascending; ties keep registration order. */
|
|
52
|
+
order?: number;
|
|
53
|
+
component: (props: PanelProps) => ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The browser-side way into the shell's navigation.
|
|
58
|
+
*
|
|
59
|
+
* Chord registries are per host, and a panel component cannot cross the wire, so the
|
|
60
|
+
* registry lives in the browser host and is filled by the plugin's own browser facet.
|
|
61
|
+
* The shell subscribes and renders; it never imports a panel.
|
|
62
|
+
*/
|
|
63
|
+
export const Panels = defineRegistry<PanelContribution>("moonan.panels", { conflict: "error" });
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Reads a state member, falling back while it has no value yet.
|
|
67
|
+
*
|
|
68
|
+
* `hold` returns before the object is live, and chord reports that as `not_ready` when the
|
|
69
|
+
* member is read — a transition, not a failure. `not_in_variant` is the other expected one:
|
|
70
|
+
* the provider's variant does not carry this member. Anything else is a bug in the read
|
|
71
|
+
* path and is reported rather than hidden behind the fallback.
|
|
72
|
+
*/
|
|
73
|
+
function readOr<T>(readable: Readable<T> | undefined, fallback: T): T {
|
|
74
|
+
if (!readable) return fallback;
|
|
75
|
+
try {
|
|
76
|
+
return readable.value;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (isChordError(error, "not_ready") || isChordError(error, "not_in_variant")) return fallback;
|
|
79
|
+
console.error("[chord] reading state failed", error);
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Acquires a reference from the browser host with `host.hold`.
|
|
86
|
+
*
|
|
87
|
+
* `hold` is the API chord designates for UI runtimes: synchronous, possibly still
|
|
88
|
+
* `loading`, and re-held by the runtime itself when the link comes back, so a reconnect
|
|
89
|
+
* costs no new reference. It is acquired in a layout effect rather than during render:
|
|
90
|
+
* that keeps the impossible-to-render intermediate commit off the screen, and makes the
|
|
91
|
+
* acquire/release pair symmetrical under StrictMode's mount → unmount → mount (a
|
|
92
|
+
* reference taken during render would leak the discarded one).
|
|
93
|
+
*/
|
|
94
|
+
export function useRemote<C extends AnyContract>(
|
|
95
|
+
host: Host<unknown> | null | undefined,
|
|
96
|
+
contract: C,
|
|
97
|
+
input: InputOf<C>,
|
|
98
|
+
options: UseRemoteOptions = {},
|
|
99
|
+
): Ref<C> | null {
|
|
100
|
+
// The input is a plain JSON address, and a panel commonly passes a fresh `{}` each
|
|
101
|
+
// render. Key on its content so identity churn does not re-acquire the reference.
|
|
102
|
+
const key = JSON.stringify(input);
|
|
103
|
+
const [ref, setRef] = useState<Ref<C> | null>(null);
|
|
104
|
+
const [attempt, setAttempt] = useState(0);
|
|
105
|
+
const retries = useRef(0);
|
|
106
|
+
const fatalReported = useRef(false);
|
|
107
|
+
const warned = useRef(false);
|
|
108
|
+
const onFatal = useRef(options.onFatal);
|
|
109
|
+
onFatal.current = options.onFatal;
|
|
110
|
+
|
|
111
|
+
useLayoutEffect(() => {
|
|
112
|
+
if (!host) {
|
|
113
|
+
setRef(null);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let held: Ref<C>;
|
|
117
|
+
try {
|
|
118
|
+
held = host.hold(contract, JSON.parse(key) as InputOf<C>);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// A type this host cannot place (an unknown name, a local type it does not serve).
|
|
121
|
+
console.error("[chord] hold failed", error);
|
|
122
|
+
setRef(null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setRef(held);
|
|
126
|
+
return () => {
|
|
127
|
+
setRef(null);
|
|
128
|
+
held.release();
|
|
129
|
+
};
|
|
130
|
+
}, [host, contract, key, attempt]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!ref) return;
|
|
134
|
+
return ref.onStatus((status: RefStatus, error: unknown) => {
|
|
135
|
+
// A reconnect re-holds in place (`loading`, last values readable) and needs nothing.
|
|
136
|
+
// `gone` is terminal for this reference — the object was forced away while held —
|
|
137
|
+
// and chord's guidance for a UI is to take a new one. Bounded, so a provider that
|
|
138
|
+
// keeps answering `gone` cannot spin the page.
|
|
139
|
+
if (status === "live") {
|
|
140
|
+
retries.current = 0;
|
|
141
|
+
fatalReported.current = false;
|
|
142
|
+
warned.current = false;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (status !== "gone") return;
|
|
146
|
+
|
|
147
|
+
// A contract that the provider no longer covers cannot be fixed by taking another
|
|
148
|
+
// reference. The shell owns the official remedy (reload the web facets when the
|
|
149
|
+
// WebFacetCatalog reports changed facets); a panel only reports it upward, and without an
|
|
150
|
+
// `onFatal` handler it says once what the developer can do.
|
|
151
|
+
if (isFatalContractError(error)) {
|
|
152
|
+
const handler = onFatal.current;
|
|
153
|
+
if (handler !== undefined) {
|
|
154
|
+
if (!fatalReported.current) {
|
|
155
|
+
fatalReported.current = true;
|
|
156
|
+
handler(error);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!warned.current) {
|
|
161
|
+
warned.current = true;
|
|
162
|
+
console.warn(
|
|
163
|
+
"[chord] a reference is gone because its contract no longer binds. The shell reloads web facets when the WebFacetCatalog reports changed facets; if this persists, the plugin's web code and contract are out of sync.",
|
|
164
|
+
error,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (retries.current < 3) {
|
|
171
|
+
retries.current += 1;
|
|
172
|
+
setAttempt((value) => value + 1);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!fatalReported.current) {
|
|
176
|
+
fatalReported.current = true;
|
|
177
|
+
onFatal.current?.(error);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}, [ref]);
|
|
181
|
+
|
|
182
|
+
return ref;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Follows a reference's status: `loading | live | released | gone`.
|
|
187
|
+
*
|
|
188
|
+
* `hold` returns before the object is live, so a panel renders from this instead of
|
|
189
|
+
* guessing. `gone` is the state that needs a new reference — a forced disposal, a host
|
|
190
|
+
* that closed, a contract the provider no longer serves.
|
|
191
|
+
*/
|
|
192
|
+
export function useRefStatus<C extends AnyContract>(ref: Ref<C> | null): RefStatus | null {
|
|
193
|
+
const [status, setStatus] = useState<RefStatus | null>(() => ref?.status ?? null);
|
|
194
|
+
|
|
195
|
+
// Layout effect: the reference is acquired in one, and this is what turns that commit
|
|
196
|
+
// into a painted one. With a passive effect the first paint would still say "loading".
|
|
197
|
+
useLayoutEffect(() => {
|
|
198
|
+
if (!ref) {
|
|
199
|
+
setStatus(null);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setStatus(ref.status);
|
|
203
|
+
return ref.onStatus((next: RefStatus) => setStatus(next));
|
|
204
|
+
}, [ref]);
|
|
205
|
+
|
|
206
|
+
return status;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Renders the current value of a chord `Readable`, and every later one. */
|
|
210
|
+
export function useReadable<T>(readable: Readable<T> | undefined, fallback: T): T {
|
|
211
|
+
const [value, setValue] = useState<T>(() => readOr(readable, fallback));
|
|
212
|
+
|
|
213
|
+
useLayoutEffect(() => {
|
|
214
|
+
if (!readable) {
|
|
215
|
+
setValue(fallback);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let cancelled = false;
|
|
219
|
+
setValue(readOr(readable, fallback));
|
|
220
|
+
const stop = readable.subscribe((next: T) => {
|
|
221
|
+
if (!cancelled) setValue(next);
|
|
222
|
+
});
|
|
223
|
+
return () => {
|
|
224
|
+
cancelled = true;
|
|
225
|
+
stop();
|
|
226
|
+
};
|
|
227
|
+
}, [readable]);
|
|
228
|
+
|
|
229
|
+
return value;
|
|
230
|
+
}
|