@astrale-os/adapter-cloudflare 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +202 -0
  2. package/package.json +4 -4
  3. package/template/.agents/skills/astrale-cli/SKILL.md +633 -0
  4. package/template/.domain-studio/comments.json +4 -0
  5. package/template/.env.example +3 -6
  6. package/template/CLAUDE.md +8 -6
  7. package/template/README.md +28 -20
  8. package/template/client/__tests__/app.test.tsx +39 -248
  9. package/template/client/__tests__/harness.ts +0 -47
  10. package/template/client/__tests__/shell.test.ts +46 -0
  11. package/template/client/package.json +1 -1
  12. package/template/client/src/app.tsx +6 -8
  13. package/template/client/src/shell/index.ts +1 -0
  14. package/template/client/src/shell/transformers.ts +36 -33
  15. package/template/client/src/shell/use-node.ts +21 -13
  16. package/template/client/src/ui/index.ts +4 -7
  17. package/template/core/README.md +23 -0
  18. package/template/deps.ts +10 -9
  19. package/template/domain.ts +11 -9
  20. package/template/env.ts +3 -5
  21. package/template/functions/index.ts +14 -37
  22. package/template/icons.ts +25 -0
  23. package/template/integrations/README.md +22 -0
  24. package/template/package.json +3 -3
  25. package/template/runtime/index.ts +25 -72
  26. package/template/schema/index.ts +10 -10
  27. package/template/tsconfig.json +1 -0
  28. package/template/views/index.ts +15 -12
  29. package/template/client/__tests__/kernel.test.ts +0 -77
  30. package/template/client/__tests__/seam.test.tsx +0 -115
  31. package/template/client/src/status/components/StatusCard.tsx +0 -160
  32. package/template/client/src/status/components/index.ts +0 -1
  33. package/template/client/src/status/hooks/index.ts +0 -3
  34. package/template/client/src/status/hooks/useCheck.mutation.ts +0 -16
  35. package/template/client/src/status/hooks/useCheckable.query.ts +0 -72
  36. package/template/client/src/status/index.ts +0 -7
  37. package/template/client/src/status/status.api.ts +0 -29
  38. package/template/client/src/status/status.mappers.ts +0 -102
  39. package/template/client/src/status/status.types.ts +0 -26
  40. package/template/client/src/ui/StatusBadge.tsx +0 -31
  41. package/template/client/src/views/status.tsx +0 -28
  42. package/template/core/monitor/health.ts +0 -34
  43. package/template/core/monitor/index.ts +0 -9
  44. package/template/core/monitor/keys.ts +0 -41
  45. package/template/core/monitor/node.ts +0 -63
  46. package/template/integrations/prober/http.ts +0 -32
  47. package/template/integrations/prober/mock.ts +0 -18
  48. package/template/integrations/prober/port.ts +0 -26
  49. package/template/integrations/prober/registry.ts +0 -65
  50. package/template/runtime/monitoring/index.ts +0 -8
  51. package/template/runtime/monitoring/monitor/check.ts +0 -29
  52. package/template/runtime/monitoring/monitor/index.ts +0 -10
  53. package/template/runtime/monitoring/monitor/seed.ts +0 -104
  54. package/template/runtime/monitoring/monitor/watch.ts +0 -31
  55. package/template/runtime/monitoring/page/add.ts +0 -21
  56. package/template/runtime/monitoring/page/check.ts +0 -50
  57. package/template/runtime/monitoring/page/create.ts +0 -24
  58. package/template/runtime/monitoring/page/index.ts +0 -9
  59. package/template/runtime/shared.ts +0 -21
  60. package/template/schema/monitor.ts +0 -92
  61. package/template/views/status-page.ts +0 -16
  62. package/template/views/welcome.ts +0 -35
@@ -1,41 +0,0 @@
1
- /**
2
- * Compiled-schema accessors for the monitor context — the ONE place class paths,
3
- * method paths, and qualified prop keys come from. Pure (schema-derived); never
4
- * hand-write key strings. `D` (the compiled domain) comes from `schema/index`,
5
- * re-exported here so the rest of the context reads it from one place.
6
- */
7
- import { K } from '@astrale-os/kernel-core'
8
-
9
- import { D } from '../../schema'
10
-
11
- export { D, K }
12
-
13
- /** Kernel ops/classes the logic addresses. */
14
- export const NODE_CREATE = K.Node.createNode.path.method.raw
15
- export const FOLDER_CLASS = K.Folder.path.class.raw
16
- export const NAME_KEY = K.Named.name.key
17
-
18
- /** Domain class/edge paths. */
19
- export const MONITOR_CLASS = D.Monitor.path.class.raw
20
- export const STATUS_PAGE_CLASS = D.StatusPage.path.class.raw
21
- export const WATCHES_EDGE = D.watches.path.class.raw
22
-
23
- /** Qualified storage keys for Monitor node props. */
24
- export const MONITOR_KEYS = {
25
- url: D.Monitor.url.key,
26
- status: D.Monitor.status.key,
27
- statusCode: D.Monitor.statusCode.key,
28
- latencyMs: D.Monitor.latencyMs.key,
29
- lastCheckedAt: D.Monitor.lastCheckedAt.key,
30
- } as const
31
-
32
- /** Qualified storage key for the StatusPage's rolled-up status. */
33
- export const PAGE_KEYS = {
34
- status: D.StatusPage.status.key,
35
- } as const
36
-
37
- /** Qualified key for the `watches` edge's `critical` prop. Edge-prop accessors
38
- * aren't typed yet (an SDK gap), so this casts to read the key. */
39
- export const WATCHES_KEYS = {
40
- critical: (D.watches as unknown as { critical: { key: string } }).critical.key,
41
- } as const
@@ -1,63 +0,0 @@
1
- /**
2
- * Node identity — PURE domain constants + slug logic (no I/O): where monitors and
3
- * status pages live in the graph, how a new node's slug is formed, and the seed
4
- * set. `runtime/` consumes these; the IMPURE bits (the slug's entropy suffix, the
5
- * node writes) stay in `runtime/`, so this file is deterministic and testable.
6
- */
7
-
8
- /** Where monitoring nodes live in the graph (domain layout; `seed` creates the folders). */
9
- export const MONITORING_PARENT = '/monitoring'
10
- export const MONITORS_PARENT = `${MONITORING_PARENT}/monitors`
11
- export const STATUS_PAGES_PARENT = `${MONITORING_PARENT}/pages`
12
-
13
- /** Deterministic URL-/name-safe stem (lowercased, non-alnum → `-`, clamped). Pure. */
14
- export function slugify(text: string): string {
15
- return (
16
- text
17
- .replace(/^https?:\/\//i, '')
18
- .toLowerCase()
19
- .replace(/[^a-z0-9]+/g, '-')
20
- .replace(/^-|-$/g, '')
21
- .slice(0, 40) || 'node'
22
- )
23
- }
24
-
25
- /**
26
- * Slug for a NEW node: the stem + a caller-supplied `suffix`. Pure — `runtime/`
27
- * injects the `suffix` (clock/RNG entropy for collision safety) so core stays
28
- * deterministic. `seed` uses fixed slugs (not this) for idempotency.
29
- */
30
- export function uniqueSlug(text: string, suffix: string): string {
31
- return `${slugify(text)}-${suffix}`
32
- }
33
-
34
- export interface StarterMonitor {
35
- slug: string
36
- name: string
37
- url: string
38
- /** Whether the seeded status page treats this monitor as critical. */
39
- critical: boolean
40
- }
41
-
42
- /**
43
- * Monitors laid down by `seed` (postInstall). httpbin returns a deterministic
44
- * status code, so the second starter is a stable demo target; the first points at
45
- * a real site. Fixed slugs keep `seed` idempotent across reinstalls.
46
- */
47
- export const STARTERS: readonly StarterMonitor[] = [
48
- {
49
- slug: 'astrale',
50
- name: 'Astrale homepage',
51
- url: 'https://astrale.ai',
52
- critical: true,
53
- },
54
- {
55
- slug: 'httpbin-200',
56
- name: 'HTTP 200 smoke check',
57
- url: 'https://httpbin.org/status/200',
58
- critical: false,
59
- },
60
- ]
61
-
62
- /** The status page `seed` creates, watching every starter monitor. */
63
- export const STARTER_PAGE = { slug: 'status', name: 'Public status' } as const
@@ -1,32 +0,0 @@
1
- /**
2
- * HTTP prober — the REAL external adapter: a single keyless `fetch` to the
3
- * target, timing the round-trip. This is the shape your domain's external calls
4
- * take — a timeout via `AbortSignal`, failures mapped to a domain result rather
5
- * than thrown. NO API key: a fresh scaffold makes a true external call out of
6
- * the box (it's the registry default).
7
- */
8
- import type { Prober, ProbeResult } from './port'
9
-
10
- export interface HttpProberConfig {
11
- /** Per-probe timeout in ms (default 10000). */
12
- timeoutMs?: number
13
- }
14
-
15
- const DEFAULT_TIMEOUT_MS = 10_000
16
-
17
- /** Build the keyless HTTP prober (a `GET` with a timeout). */
18
- export function createHttpProber(config: HttpProberConfig = {}): Prober {
19
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
20
- return {
21
- async probe(url): Promise<ProbeResult> {
22
- const start = Date.now()
23
- try {
24
- const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(timeoutMs) })
25
- return { statusCode: res.status, latencyMs: Date.now() - start }
26
- } catch {
27
- // Unreachable / timeout / DNS failure — a `down` result, not an error.
28
- return { statusCode: 0, latencyMs: Date.now() - start }
29
- }
30
- },
31
- }
32
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * Mock prober — offline + deterministic, no network. Used by tests (construct it
3
- * directly with the status you want) and by `PROBER=mock` for an offline dev
4
- * loop. Returns a fixed result regardless of URL.
5
- */
6
- import type { Prober, ProbeResult } from './port'
7
-
8
- /** Build an offline prober that always returns `opts` (default: a healthy 200). */
9
- export function createMockProber(opts: { statusCode?: number; latencyMs?: number } = {}): Prober {
10
- const statusCode = opts.statusCode ?? 200
11
- const latencyMs = opts.latencyMs ?? 1
12
- const result: ProbeResult = { statusCode, latencyMs }
13
- return {
14
- probe() {
15
- return Promise.resolve(result)
16
- },
17
- }
18
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Prober — the PORT between the domain's logic and whatever performs the actual
3
- * uptime probe. This is the external-API seam every real domain has, reduced to
4
- * the narrowest interface the logic needs.
5
- *
6
- * `runtime/` logic depends on THIS interface only — never on `fetch`, an SDK, or
7
- * `env`. Adapters in this folder implement it: `http.ts` (a real, keyless HTTP
8
- * request) and `mock.ts` (offline + deterministic, for tests). The registry
9
- * picks one from `env`; the handler logic only ever sees the resolved port. Swap
10
- * or add a backend = one adapter + one arm in `registry.ts`.
11
- */
12
- export interface ProbeResult {
13
- /** Observed HTTP status code, or `0` if the host was unreachable. */
14
- statusCode: number
15
- /** Round-trip time in ms. */
16
- latencyMs: number
17
- }
18
-
19
- export interface Prober {
20
- /**
21
- * Probe a URL once. MUST resolve, never reject: an unreachable host or a
22
- * timeout is a `down` RESULT (`statusCode: 0`), not an error — a probe failing
23
- * is the normal case a monitor exists to observe.
24
- */
25
- probe(url: string): Promise<ProbeResult>
26
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Prober registry — the ONE place the worker env (and the probed node) become the
3
- * live `Prober` port. Adding a backend = one more arm in `selectProber`; handler
4
- * logic only ever sees the resolved port.
5
- *
6
- * Resolution is PER-REQUEST and PER-NODE: each `prober(target)` call selects +
7
- * builds the port from env AND the monitor being probed — nothing is built at
8
- * `deps()` / isolate setup. A per-node choice can't be memoized at the isolate
9
- * level, which is exactly why `deps` exposes `prober()` as a FUNCTION, not a
10
- * value. The default is the keyless `http` prober (so the scaffold makes a real
11
- * probe out of the box); `PROBER=mock` forces the offline prober.
12
- */
13
- import type { Env } from '../../env'
14
- import type { Prober } from './port'
15
-
16
- import { createHttpProber } from './http'
17
- import { createMockProber } from './mock'
18
-
19
- /**
20
- * The node a probe is for — what the registry may branch on. Mirrors how the
21
- * `services` domain picks a provider from a node's class; here we also look at
22
- * the target url. The handler passes this from `self.node()`; omit it for
23
- * node-less callers (e.g. `seed`, which just wants the env default).
24
- */
25
- export interface ProbeTarget {
26
- /** The monitor node's class path (`node.class.raw`). */
27
- class: string
28
- /** The url this monitor probes (`node.props[MONITOR_KEYS.url]`). */
29
- url: string
30
- }
31
-
32
- export interface ProberRegistry {
33
- /** Resolve the prober for a probe — built per call from env + the target node. */
34
- prober(target?: ProbeTarget): Prober
35
- }
36
-
37
- /** Build the registry from the worker env. The port is resolved per request. */
38
- export function buildProberRegistry(env: Env): ProberRegistry {
39
- return {
40
- prober(target) {
41
- return selectProber(env, target)
42
- },
43
- }
44
- }
45
-
46
- /** Bind the abstract prober port to the concrete adapter for this env + target. */
47
- function selectProber(env: Env, target?: ProbeTarget): Prober {
48
- if (target && isLocalTarget(target.url)) {
49
- return createMockProber()
50
- }
51
- // Default: the real, keyless HTTP prober. Pass the env override (if any); the
52
- // adapter owns the default timeout, so there's no second copy of it here.
53
- return createHttpProber({ timeoutMs: parsePositiveInt(env.PROBE_TIMEOUT_MS) })
54
- }
55
-
56
- /** A loopback / unspecified host the public edge can't reach. */
57
- function isLocalTarget(url: string): boolean {
58
- return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/i.test(url)
59
- }
60
-
61
- function parsePositiveInt(value: string | undefined): number | undefined {
62
- if (!value) return undefined
63
- const n = Number.parseInt(value, 10)
64
- return Number.isFinite(n) && n > 0 ? n : undefined
65
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Monitoring runtime context — method implementations grouped by the domain
3
- * slice they serve. `runtime/index.ts` wires these into SDK method handlers.
4
- */
5
- import * as monitor from './monitor'
6
- import * as page from './page'
7
-
8
- export { monitor, page }
@@ -1,29 +0,0 @@
1
- import type { Prober } from '../../../integrations/prober/port'
2
- import type { CallableKernel } from '../../shared'
3
-
4
- /**
5
- * `Monitor.check` (instance) — implements `Checkable.check` for a single endpoint:
6
- * probe the target via the resolved `Prober` port, classify (pure, `core/monitor`),
7
- * record status/latency on the node, and return the verdict. `url` + `prober` are
8
- * resolved by the wiring (`runtime/index.ts`) from `self.node()`.
9
- */
10
- import { classify, MONITOR_KEYS } from '../../../core/monitor'
11
-
12
- export async function check(
13
- kernel: CallableKernel,
14
- prober: Prober,
15
- selfPathRaw: string,
16
- url: string,
17
- ): Promise<{ status: string }> {
18
- const result = await prober.probe(url)
19
- const status = classify(result.statusCode)
20
- await kernel.call(`${selfPathRaw}::update`, {
21
- props: {
22
- [MONITOR_KEYS.status]: status,
23
- [MONITOR_KEYS.statusCode]: result.statusCode,
24
- [MONITOR_KEYS.latencyMs]: result.latencyMs,
25
- [MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
26
- },
27
- })
28
- return { status }
29
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Monitor operations inside the monitoring bounded context — one file per
3
- * method, assembled here for the composition root (`runtime/index.ts`). Each is
4
- * transport-agnostic logic over a
5
- * `CallableKernel` (+ the `Prober` port where it probes), delegating pure
6
- * decisions to `core/monitor` — so all are unit-testable with fakes.
7
- */
8
- export { check } from './check'
9
- export { seed } from './seed'
10
- export { watch } from './watch'
@@ -1,104 +0,0 @@
1
- import type { Prober } from '../../../integrations/prober/port'
2
-
3
- /**
4
- * The domain's post-install bootstrap — exposed as the standalone `seed` function
5
- * (`functions/`) and wired as `postInstall` in `domain.ts`. The kernel calls it
6
- * ONCE after install, as __SYSTEM__, so the
7
- * domain comes up demonstrable: a `/monitoring` folder, `/monitoring/monitors`
8
- * starter Monitors (each probed once), and `/monitoring/pages/status`, a
9
- * StatusPage that `watches` them (its initial status rolled up from the probes).
10
- * Idempotent: node creates swallow `PATH_CONFLICT` (fixed paths) and re-linking
11
- * a `watches` edge is a graph no-op.
12
- * It grants nothing — the installing owner reaches these nodes via root-propagated
13
- * perms; grant explicitly here for non-owner access.
14
- */
15
- import {
16
- classify,
17
- FOLDER_CLASS,
18
- MONITOR_CLASS,
19
- MONITOR_KEYS,
20
- MONITORING_PARENT,
21
- MONITORS_PARENT,
22
- NAME_KEY,
23
- NODE_CREATE,
24
- rollup,
25
- STARTER_PAGE,
26
- STARTERS,
27
- STATUS_PAGE_CLASS,
28
- STATUS_PAGES_PARENT,
29
- PAGE_KEYS,
30
- WATCHES_EDGE,
31
- WATCHES_KEYS,
32
- } from '../../../core/monitor'
33
- import { type CallableKernel, isPathConflict } from '../../shared'
34
-
35
- /** Create a node, swallowing a re-seed's `PATH_CONFLICT`. Returns true if newly created. */
36
- async function ensure(kernel: CallableKernel, params: unknown): Promise<boolean> {
37
- try {
38
- await kernel.call(NODE_CREATE, params)
39
- return true
40
- } catch (e) {
41
- if (!isPathConflict(e)) throw e
42
- return false
43
- }
44
- }
45
-
46
- export async function seed(kernel: CallableKernel, prober: Prober): Promise<{ seeded: number }> {
47
- await ensure(kernel, {
48
- class: FOLDER_CLASS,
49
- path: MONITORING_PARENT,
50
- props: { [NAME_KEY]: 'monitoring' },
51
- })
52
-
53
- await ensure(kernel, {
54
- class: FOLDER_CLASS,
55
- path: MONITORS_PARENT,
56
- props: { [NAME_KEY]: 'monitors' },
57
- })
58
-
59
- // Probe + create each starter Monitor; collect the verdict for the page roll-up.
60
- const members: { status: string; critical: boolean }[] = []
61
- let seeded = 0
62
- for (const s of STARTERS) {
63
- const result = await prober.probe(s.url)
64
- const status = classify(result.statusCode)
65
- members.push({ status, critical: s.critical })
66
- const created = await ensure(kernel, {
67
- class: MONITOR_CLASS,
68
- path: `${MONITORS_PARENT}/${s.slug}`,
69
- props: {
70
- [NAME_KEY]: s.name,
71
- [MONITOR_KEYS.url]: s.url,
72
- [MONITOR_KEYS.status]: status,
73
- [MONITOR_KEYS.statusCode]: result.statusCode,
74
- [MONITOR_KEYS.latencyMs]: result.latencyMs,
75
- [MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
76
- },
77
- })
78
- if (created) seeded++
79
- }
80
-
81
- // A StatusPage watching every starter, its status rolled up from the probes.
82
- await ensure(kernel, {
83
- class: FOLDER_CLASS,
84
- path: STATUS_PAGES_PARENT,
85
- props: { [NAME_KEY]: 'pages' },
86
- })
87
- const pagePath = `${STATUS_PAGES_PARENT}/${STARTER_PAGE.slug}`
88
- await ensure(kernel, {
89
- class: STATUS_PAGE_CLASS,
90
- path: pagePath,
91
- props: { [NAME_KEY]: STARTER_PAGE.name, [PAGE_KEYS.status]: rollup(members) },
92
- })
93
- // Re-linking the same (page, monitor) `watches` edge is a graph no-op, so this
94
- // needs no conflict guard.
95
- for (const s of STARTERS) {
96
- await kernel.call(`${pagePath}::link`, {
97
- edgeClass: WATCHES_EDGE,
98
- target: `${MONITORS_PARENT}/${s.slug}`,
99
- props: { [WATCHES_KEYS.critical]: s.critical },
100
- })
101
- }
102
-
103
- return { seeded }
104
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * `Monitor.watch` (static) — create a Monitor node under `/monitoring/monitors`.
3
- * The slug format is pure (`core/monitor`'s `uniqueSlug`); the entropy suffix
4
- * (impure, clock/RNG) comes from `runtime/shared`, keeping core deterministic.
5
- */
6
- import {
7
- MONITOR_CLASS,
8
- MONITOR_KEYS,
9
- MONITORS_PARENT,
10
- NAME_KEY,
11
- NODE_CREATE,
12
- uniqueSlug,
13
- } from '../../../core/monitor'
14
- import { type CallableKernel, randomSuffix } from '../../shared'
15
-
16
- export async function watch(
17
- kernel: CallableKernel,
18
- params: { url: string; name?: string },
19
- ): Promise<{ id: string; path: string }> {
20
- const path = `${MONITORS_PARENT}/${uniqueSlug(params.url, randomSuffix())}`
21
- const created = (await kernel.call(NODE_CREATE, {
22
- class: MONITOR_CLASS,
23
- path,
24
- props: {
25
- [NAME_KEY]: params.name ?? params.url,
26
- [MONITOR_KEYS.url]: params.url,
27
- [MONITOR_KEYS.status]: 'unknown',
28
- },
29
- })) as { id: string }
30
- return { id: created.id, path }
31
- }
@@ -1,21 +0,0 @@
1
- import type { CallableKernel } from '../../shared'
2
-
3
- /**
4
- * `StatusPage.add` (instance) — watch a Monitor: create a `watches` edge from
5
- * this page to `monitor`, tagged `critical` (default false). `monitor` is any
6
- * node address (a tree path or `@<id>`).
7
- */
8
- import { WATCHES_EDGE, WATCHES_KEYS } from '../../../core/monitor'
9
-
10
- export async function add(
11
- kernel: CallableKernel,
12
- selfPathRaw: string,
13
- params: { monitor: string; critical?: boolean },
14
- ): Promise<{ watched: string }> {
15
- await kernel.call(`${selfPathRaw}::link`, {
16
- edgeClass: WATCHES_EDGE,
17
- target: params.monitor,
18
- props: { [WATCHES_KEYS.critical]: params.critical ?? false },
19
- })
20
- return { watched: params.monitor }
21
- }
@@ -1,50 +0,0 @@
1
- import type { CallableKernel } from '../../shared'
2
-
3
- /**
4
- * `StatusPage.check` (instance) — implements `Checkable.check` for a page: walk
5
- * the `watches` edges, re-check each monitor (a cross-node `@<id>::check` call),
6
- * roll the verdicts up (pure, `core/monitor`), record the page status, return it.
7
- *
8
- * `getLinks` returns raw `Edge`s whose endpoint/prop accessors aren't typed yet
9
- * (an SDK gap) — `parseWatches` is the small cast that bridges it.
10
- */
11
- import { PAGE_KEYS, rollup, WATCHES_EDGE, WATCHES_KEYS } from '../../../core/monitor'
12
-
13
- /** A raw `watches` edge as `getLinks` returns it (endpoints are string-or-`{raw}`). */
14
- type RawEdge = {
15
- class?: string | { raw: string }
16
- target?: string | { raw: string }
17
- props?: Record<string, unknown>
18
- }
19
-
20
- function rawOf(p: RawEdge['target']): string | undefined {
21
- if (typeof p === 'string') return p
22
- return typeof p?.raw === 'string' ? p.raw : undefined
23
- }
24
-
25
- /** The monitors this page watches: each target ref + its `critical` flag. */
26
- function parseWatches(raw: unknown): { monitor: string; critical: boolean }[] {
27
- const edges = (Array.isArray(raw) ? raw : []) as RawEdge[]
28
- const out: { monitor: string; critical: boolean }[] = []
29
- for (const e of edges) {
30
- if (rawOf(e.class) !== WATCHES_EDGE) continue
31
- const monitor = rawOf(e.target)
32
- if (monitor) out.push({ monitor, critical: e.props?.[WATCHES_KEYS.critical] === true })
33
- }
34
- return out
35
- }
36
-
37
- export async function check(
38
- kernel: CallableKernel,
39
- selfPathRaw: string,
40
- ): Promise<{ status: string }> {
41
- const links = await kernel.call(`${selfPathRaw}::getLinks`, { direction: 'out' })
42
- const members = []
43
- for (const w of parseWatches(links)) {
44
- const { status } = (await kernel.call(`${w.monitor}::check`, {})) as { status: string }
45
- members.push({ status, critical: w.critical })
46
- }
47
- const status = rollup(members)
48
- await kernel.call(`${selfPathRaw}::update`, { props: { [PAGE_KEYS.status]: status } })
49
- return { status }
50
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * `StatusPage.create` (static) — create a StatusPage node under `/monitoring/pages`.
3
- */
4
- import {
5
- NAME_KEY,
6
- NODE_CREATE,
7
- STATUS_PAGE_CLASS,
8
- STATUS_PAGES_PARENT,
9
- uniqueSlug,
10
- } from '../../../core/monitor'
11
- import { type CallableKernel, randomSuffix } from '../../shared'
12
-
13
- export async function create(
14
- kernel: CallableKernel,
15
- params: { name: string },
16
- ): Promise<{ id: string; path: string }> {
17
- const path = `${STATUS_PAGES_PARENT}/${uniqueSlug(params.name, randomSuffix())}`
18
- const created = (await kernel.call(NODE_CREATE, {
19
- class: STATUS_PAGE_CLASS,
20
- path,
21
- props: { [NAME_KEY]: params.name },
22
- })) as { id: string }
23
- return { id: created.id, path }
24
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * StatusPage operations inside the monitoring bounded context — one file per
3
- * method, assembled here for the composition root (`runtime/index.ts`).
4
- * Transport-agnostic logic over a `CallableKernel`, delegating the roll-up
5
- * decision to `core/monitor`.
6
- */
7
- export { add } from './add'
8
- export { check } from './check'
9
- export { create } from './create'
@@ -1,21 +0,0 @@
1
- /**
2
- * Runtime-internal helpers shared across the context's operations. Not exported
3
- * from the domain — just the seam the operation files agree on.
4
- */
5
-
6
- /**
7
- * The minimal kernel surface the operations need — kept narrow so each is
8
- * unit-testable with a fake `{ call }`. The SDK hands the real client at the
9
- * `runtime/index.ts` seam.
10
- */
11
- export type CallableKernel = { call(path: string, params: unknown): Promise<unknown> }
12
-
13
- /** True for the kernel's create-collision error — lets `seed` stay idempotent. */
14
- export function isPathConflict(e: unknown): boolean {
15
- return e instanceof Error && e.message.includes('PATH_CONFLICT')
16
- }
17
-
18
- /** Short clock+RNG entropy to dodge same-ms / same-stem slug collisions (impure). */
19
- export function randomSuffix(): string {
20
- return `${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
21
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * Monitoring context — the domain's single bounded slice: a `Checkable` contract
3
- * implemented by two classes, plus the edge that binds them.
4
- *
5
- * - Interface `Checkable` one `abstract` method, `check()`. Abstract → each
6
- * implementing class brings its OWN body, and the
7
- * kernel dispatches by the node's class.
8
- * - Class `Monitor` a single HTTP check. `check()` probes `url`;
9
- * `watch` creates one. (The demo set is laid down by
10
- * the standalone `seed` function — see `functions/`.)
11
- * - Class `StatusPage` a roll-up. `check()` re-checks the monitors it
12
- * `watches` and aggregates; `create` makes a page;
13
- * `add` watches a monitor.
14
- * - Edge `watches` StatusPage → Monitor, with a `critical` flag.
15
- */
16
- import { edgeClass, nodeClass, nodeInterface } from '@astrale-os/kernel-core'
17
- import { fn } from '@astrale-os/kernel-dsl'
18
- import { z } from 'zod'
19
-
20
- /** Thin ref to a created node — what the static factories return (a full Node
21
- * value does not round-trip over the worker wire). */
22
- export const NodeRef = z.object({ id: z.string(), path: z.string() })
23
-
24
- /** What `check()` reports — the subject's current health verdict. */
25
- export const CheckResult = z.object({ status: z.string() })
26
-
27
- export const Checkable = nodeInterface({
28
- methods: {
29
- // `abstract` → no shared body; every implementer supplies its own `check`,
30
- // wired per-class in `runtime/`. `@<node>::check` dispatches by the node's class.
31
- check: fn({ inheritance: 'abstract', returns: CheckResult }),
32
- },
33
- })
34
-
35
- export const Monitor = nodeClass({
36
- implements: [Checkable],
37
- props: {
38
- /** The target URL this monitor probes. */
39
- url: z.string(),
40
- // Live status, written by `check`. PLAIN STRING, not `z.enum()`: `::update`
41
- // silently drops `z.enum()` props. Values: 'up' | 'down' | 'unknown'.
42
- status: z.string().optional(),
43
- /** Last observed HTTP status code (0 = host unreachable). */
44
- statusCode: z.number().int().optional(),
45
- /** Last observed round-trip latency, in ms. */
46
- latencyMs: z.number().int().optional(),
47
- /** ISO timestamp of the last check. */
48
- lastCheckedAt: z.string().optional(),
49
- },
50
- methods: {
51
- // Implements `Checkable.check` for a single endpoint (probe `url`). Redeclared
52
- // here — a concrete class must declare each abstract method it implements, so
53
- // the compiler materializes Monitor's OWN `check` node for per-class dispatch.
54
- // The body lives in `runtime/monitoring/monitor/check.ts`.
55
- check: fn({ returns: CheckResult }),
56
- /** Create a Monitor under `/monitoring/monitors`. */
57
- watch: fn({
58
- static: true,
59
- params: { url: z.string(), name: z.string().optional() },
60
- returns: NodeRef,
61
- }),
62
- },
63
- })
64
-
65
- export const StatusPage = nodeClass({
66
- implements: [Checkable],
67
- props: {
68
- /** Rolled-up status, written by `check`. 'up' | 'degraded' | 'down' | 'unknown'. */
69
- status: z.string().optional(),
70
- },
71
- methods: {
72
- // Implements `Checkable.check` for a page (roll up the watched monitors).
73
- // Redeclared so StatusPage materializes its OWN `check` node. Body in
74
- // `runtime/monitoring/page/check.ts`.
75
- check: fn({ returns: CheckResult }),
76
- /** Create a StatusPage under `/monitoring/pages`. */
77
- create: fn({ static: true, params: { name: z.string() }, returns: NodeRef }),
78
- /** Watch a monitor (a `watches` edge); `critical` decides the roll-up weight. */
79
- add: fn({
80
- params: { monitor: z.string(), critical: z.boolean().optional() },
81
- returns: z.object({ watched: z.string() }),
82
- }),
83
- },
84
- })
85
-
86
- export const watches = edgeClass(
87
- { as: 'page', types: [StatusPage] },
88
- { as: 'monitor', types: [Monitor] },
89
- // `critical` drives the roll-up: a critical monitor down ⇒ page `down`; a
90
- // non-critical one down ⇒ `degraded`.
91
- { props: { critical: z.boolean() } },
92
- )