@filamind-app/core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -45
- package/dist/index.d.ts +18 -18
- package/dist/index.js +24 -15
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,40 +1,68 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @filamind-app/core
|
|
2
4
|
|
|
3
|
-
The shared foundation of the FilaMind suite
|
|
4
|
-
|
|
5
|
+
The shared foundation of the FilaMind suite - one framework-agnostic TypeScript library
|
|
6
|
+
that talks to the printer, holds its state honestly, and looks the same everywhere.
|
|
7
|
+
|
|
8
|
+
**Built by Egyptian makers, for world makers. Happy printing.** 🇪🇬
|
|
9
|
+
|
|
10
|
+
A small-team hobby project, built and tested on real printers. The code is all here to read.
|
|
11
|
+
|
|
12
|
+
[](https://ko-fi.com/I2I119XEIV)
|
|
5
13
|
|
|
6
14
|
[](https://github.com/filamind-app/filamind-core/actions/workflows/ci.yml)
|
|
7
15
|
[](https://www.npmjs.com/package/@filamind-app/core)
|
|
8
16
|
[](LICENSE)
|
|
9
17
|
[](https://www.typescriptlang.org)
|
|
18
|
+
[](https://www.klipper3d.org)
|
|
19
|
+
[](https://moonraker.readthedocs.io)
|
|
20
|
+
|
|
21
|
+
[Install](#install) · [What's inside](#whats-inside) · [Quick start](#quick-start) · [Tested on real printers](#tested-on-real-printers) · [Languages](#languages) · [Docs](#documentation) · [Support](#support)
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
`@filamind-app/core` is the shared core of the FilaMind suite. It is the one place that knows how
|
|
26
|
+
to open a Moonraker connection, keep printer state honest, remember a user's settings, theme the UI,
|
|
27
|
+
and translate it - so the three FilaMind surfaces (**3d** on the web, **screen** on a touch panel,
|
|
28
|
+
and the **flow** touch app) all behave the same way without copying that logic three times. It is
|
|
29
|
+
pure TypeScript with no UI framework inside: the reactive layer is a plain observable, and each
|
|
30
|
+
surface adapts it to Vue, signals, or whatever it uses. One core, many faces.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
It is published to npm as `@filamind-app/core`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @filamind-app/core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The package is ESM-only and ships its own type declarations. It has **zero runtime dependencies** -
|
|
41
|
+
nothing is pulled in behind it. Inside the FilaMind suite it is consumed through the workspace during
|
|
42
|
+
development, so changes to the core are picked up by 3d, screen, and flow without a publish step.
|
|
43
|
+
|
|
44
|
+
## What's inside
|
|
45
|
+
|
|
46
|
+
| Piece | What it gives you |
|
|
13
47
|
| --- | --- |
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
| `i18n/locale-meta.ts` | the 19 shipped locales `{ code, name, rtl, dir }` + RTL list + CLDR `pluralCategory` (`Intl.PluralRules`) |
|
|
33
|
-
| `i18n/translator.ts` | framework-agnostic `translate` / `Translator` + the backend message-code contract (`resolveMessage`) |
|
|
34
|
-
| `i18n/locales/*.json` | drop-in catalogs (en + ar shipped as proof, incl. the full Arabic plural set) |
|
|
35
|
-
| `settings/settings.ts` | unified user settings (theme · locale · density · motif · reduced-motion): `SettingsStore` (patch/hydrate/reset/export/import), **`migrate()`** (versioned, enum-coercing — `import()` trusts no foreign JSON), `applySettings` (re-theme + RTL dir), persistence seam (Moonraker-DB / localStorage) |
|
|
36
|
-
|
|
37
|
-
## Wiring (the consumer entry point)
|
|
48
|
+
| **Moonraker client** | A reconnecting JSON-RPC WebSocket plus a REST file channel, behind a backend-agnostic connector seam. Backs off with jitter, re-subscribes on reconnect, and times out stuck requests. |
|
|
49
|
+
| **Zero-config discovery** | Finds the printer's WebSocket for you by racing the likely endpoints; the first to open wins, and an explicit override always wins first. |
|
|
50
|
+
| **FilaMindSession** | The orchestrator. It connects, identifies, queries, subscribes, and routes every update into one place - and re-seeds itself after a firmware restart so the UI never shows stale data as live. |
|
|
51
|
+
| **Printer state** | One reactive, merge-patched model of the printer, coalesced so the UI isn't thrashed, with a fast path so live motion stays smooth. |
|
|
52
|
+
| **Provenance** | "Never-lie" stamped values that carry where they came from and when, so a widget can dim or strike anything that is stale or unknown. |
|
|
53
|
+
| **WriteArbiter** | A single fail-closed chokepoint every mutation passes through: if state isn't trustworthy, or safe-mode is on, the write is refused. |
|
|
54
|
+
| **Settings store** | One unified, persisted user-settings model (theme, language, density, motifs) that roams across every surface on the same printer, with strict versioned migration. |
|
|
55
|
+
| **Theme tokens** | The FilaMind palette as `--fm-*` CSS variables - three signature Pharaonic themes plus a neutral light and dark - so one switch restyles everything. |
|
|
56
|
+
| **i18n** | Metadata for 19 locales (including right-to-left), correct plural rules, and a tiny framework-agnostic translator that 3d, screen, and flow all share. |
|
|
57
|
+
| **Widget registry & dashboard** | Features register themselves without touching the core, declare which surfaces they target, and resolve into one adaptive dashboard per screen size. |
|
|
58
|
+
| **Remote control bus** | A UI-only command bus so one FilaMind surface can steer another (navigate, message, locate). It can never move the printer - those still go through the WriteArbiter. |
|
|
59
|
+
| **Backup, identity, logging** | Reversible restore points, a stable per-machine id, and a ring-buffer logger that feeds the diagnostics bundle. |
|
|
60
|
+
|
|
61
|
+
The full public surface is exported from a single entry point; the details of how each piece works
|
|
62
|
+
live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
38
66
|
```ts
|
|
39
67
|
import { MoonrakerClient, FilaMindSession, FULL_CONTROL } from '@filamind-app/core'
|
|
40
68
|
|
|
@@ -45,33 +73,100 @@ const session = new FilaMindSession(connector, {
|
|
|
45
73
|
})
|
|
46
74
|
await session.start()
|
|
47
75
|
|
|
48
|
-
// reactive
|
|
49
|
-
session.live.subscribe((
|
|
50
|
-
session.printer.objects.subscribe((
|
|
51
|
-
session.prompt.subscribe((
|
|
76
|
+
// reactive and framework-agnostic - bind these to Pinia, signals, or anything:
|
|
77
|
+
session.live.subscribe((live) => {/* dim the UI when the data isn't trustworthy-live */})
|
|
78
|
+
session.printer.objects.subscribe((objects) => {/* render telemetry */})
|
|
79
|
+
session.prompt.subscribe((dialog) => {/* show a Klipper modal */})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Don't know the printer's address? Let the core find it:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { resolveMoonrakerUrl, MoonrakerClient } from '@filamind-app/core'
|
|
86
|
+
|
|
87
|
+
const url = await resolveMoonrakerUrl() // races the likely endpoints, first to open wins
|
|
88
|
+
const connector = new MoonrakerClient({ url })
|
|
52
89
|
```
|
|
53
90
|
|
|
54
|
-
|
|
91
|
+
There is a worked example for settings, theming, and adding a widget in
|
|
92
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#using-the-core).
|
|
93
|
+
|
|
94
|
+
## Tested on real printers
|
|
95
|
+
|
|
96
|
+
The core is exercised by the FilaMind suite on two machines that disagree on almost everything that
|
|
97
|
+
matters to a control surface. The first is a Sovol SV08: an STM32F103 mainboard, TMC2209 drivers
|
|
98
|
+
over UART, a USB toolhead, and a BTT CB1 host. The second is a Voron-class CoreXY: an STM32H723
|
|
99
|
+
mainboard, six TMC5160 drivers on a shared software-SPI bus, a CAN toolhead, and a Raspberry Pi 4.
|
|
100
|
+
The pieces here that hurt to get wrong - the reconnect and re-seed logic, the Klippy-aware live gate,
|
|
101
|
+
the fail-closed write path - are the ones those two printers stress, and the library ships **101
|
|
102
|
+
pure-logic tests** that pin that behaviour down without needing a printer attached.
|
|
103
|
+
|
|
104
|
+
## How it's built
|
|
105
|
+
|
|
106
|
+
Pure TypeScript in `strict` mode, no UI framework imported anywhere in the core. Reactivity is a
|
|
107
|
+
small hand-written observable that each surface adapts to its own framework. Everything that touches
|
|
108
|
+
the network - the WebSocket, the discovery probes - is injectable, so the whole library is unit-tested
|
|
109
|
+
without a real socket. It builds to a single ESM bundle with type declarations via
|
|
110
|
+
[tsup](https://tsup.egoist.dev), and a CI guard keeps the published code free of any reference to
|
|
111
|
+
other projects. The deeper design is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
112
|
+
|
|
113
|
+
## Languages
|
|
114
|
+
|
|
115
|
+
The core carries the suite's shared internationalization foundation: metadata for **19 locales**,
|
|
116
|
+
the correct CLDR plural rules for each, and a tiny translator that 3d, screen, and flow all use so
|
|
117
|
+
they translate identically. The catalogs themselves are drop-in JSON, one folder per locale - adding
|
|
118
|
+
a language needs no code change. English and Arabic (right-to-left, with the full Arabic plural set)
|
|
119
|
+
ship in the package as a working proof; the surfaces carry the rest.
|
|
120
|
+
|
|
121
|
+
| | | | |
|
|
122
|
+
| --- | --- | --- | --- |
|
|
123
|
+
| English | Español | 简体中文 (Simplified Chinese) | 日本語 (Japanese) |
|
|
124
|
+
| العربية (Arabic, RTL) | Français | 繁體中文 (Traditional Chinese) | 한국어 (Korean) |
|
|
125
|
+
| Deutsch (German) | Русский (Russian) | Português (Brasil) | Tiếng Việt (Vietnamese) |
|
|
126
|
+
| Italiano | Nederlands (Dutch) | Polski (Polish) | Bahasa Indonesia |
|
|
127
|
+
| Türkçe (Turkish) | Українська (Ukrainian) | हिन्दी (Hindi) | |
|
|
128
|
+
|
|
129
|
+
## Develop
|
|
130
|
+
|
|
55
131
|
```bash
|
|
56
132
|
npm install
|
|
57
|
-
npm run type-check # tsc --noEmit
|
|
58
|
-
npm test # vitest (
|
|
59
|
-
npm run build # tsup
|
|
133
|
+
npm run type-check # tsc --noEmit (strict)
|
|
134
|
+
npm test # vitest run (101 tests, 13 files)
|
|
135
|
+
npm run build # tsup > dist/ (ESM + .d.ts)
|
|
60
136
|
```
|
|
61
137
|
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
138
|
+
`npm run dev` rebuilds on change. CI runs the type check, the tests, the build, and the
|
|
139
|
+
no-external-references guard on every push and pull request. Releases are cut by bumping the
|
|
140
|
+
version and pushing a matching `vX.Y.Z` tag; the workflow builds, publishes to npm (with provenance,
|
|
141
|
+
when a token is configured), and creates a GitHub Release from the changelog. The full contributor
|
|
142
|
+
and release flow is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#building-testing-and-releasing).
|
|
143
|
+
|
|
144
|
+
## Documentation
|
|
145
|
+
|
|
146
|
+
| Document | What's inside |
|
|
147
|
+
| -------- | ------------- |
|
|
148
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design, key modules, data flow, build/release internals, and conventions |
|
|
149
|
+
| [CHANGELOG.md](CHANGELOG.md) | Release history |
|
|
150
|
+
| [SECURITY.md](SECURITY.md) | How to report a vulnerability privately |
|
|
151
|
+
|
|
152
|
+
## Support
|
|
153
|
+
|
|
154
|
+
`@filamind-app/core` is free and open source, built and maintained in spare time. If the suite it
|
|
155
|
+
powers saved you a tuning session, or you just want to see it grow, a coffee helps keep the work
|
|
156
|
+
going. Code, data, and ideas are just as welcome.
|
|
157
|
+
|
|
158
|
+
<div align="center">
|
|
159
|
+
|
|
160
|
+
[](https://ko-fi.com/I2I119XEIV)
|
|
161
|
+
|
|
162
|
+
</div>
|
|
68
163
|
|
|
69
164
|
## Credits
|
|
70
165
|
|
|
71
|
-
Built and maintained by the DeltaFabs team
|
|
166
|
+
Built and maintained by the DeltaFabs team. **Built by Egyptian makers, for world makers.**
|
|
72
167
|
|
|
73
|
-
-
|
|
74
|
-
- Ahmed
|
|
168
|
+
- Abdelmonem Awad - <eg2@live.com>
|
|
169
|
+
- Ahmed Bebars - <Ahmedbebars1@gmail.com>
|
|
75
170
|
- Kareem Salama - <Golden.kiko@gmail.com>
|
|
76
171
|
|
|
77
172
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type Source = 'live' | 'cache' | 'optimistic' | 'unknown';
|
|
2
2
|
interface Stamped<T> {
|
|
3
3
|
value: T;
|
|
4
|
-
/** when this value arrived (ms epoch, client clock
|
|
4
|
+
/** when this value arrived (ms epoch, client clock - Moonraker has no server timestamp) */
|
|
5
5
|
ts: number;
|
|
6
6
|
source: Source;
|
|
7
7
|
/** ms after `ts` when the value should be treated as stale (omit = never goes stale on its own) */
|
|
@@ -169,7 +169,7 @@ type NotifyEvent = {
|
|
|
169
169
|
/** Parse a raw (method, params) notify into a typed event, or null for an unhandled method. */
|
|
170
170
|
declare function parseNotifyEvent(method: string, params: unknown): NotifyEvent | null;
|
|
171
171
|
|
|
172
|
-
/** Minimal WebSocket surface the client uses
|
|
172
|
+
/** Minimal WebSocket surface the client uses - the DOM WebSocket satisfies it; tests fake it. */
|
|
173
173
|
interface WebSocketLike {
|
|
174
174
|
send(data: string): void;
|
|
175
175
|
close(): void;
|
|
@@ -185,7 +185,7 @@ interface MoonrakerClientOptions {
|
|
|
185
185
|
url: string;
|
|
186
186
|
requestTimeoutMs?: number;
|
|
187
187
|
maxBackoffMs?: number;
|
|
188
|
-
/** stop reconnecting after this many consecutive attempts
|
|
188
|
+
/** stop reconnecting after this many consecutive attempts > terminal 'closed' (default: unlimited) */
|
|
189
189
|
maxReconnectAttempts?: number;
|
|
190
190
|
wsFactory?: (url: string) => WebSocketLike;
|
|
191
191
|
logger?: Logger;
|
|
@@ -223,13 +223,13 @@ declare class MoonrakerClient implements Connector {
|
|
|
223
223
|
private onMessage;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
/** A minimal view of `location`
|
|
226
|
+
/** A minimal view of `location` - the DOM Location satisfies it; tests pass a plain object. */
|
|
227
227
|
interface LocationLike {
|
|
228
228
|
protocol?: string;
|
|
229
229
|
hostname?: string;
|
|
230
230
|
}
|
|
231
231
|
interface DiscoveryOptions {
|
|
232
|
-
/** Explicit runtime override (e.g. from settings). If set, it wins immediately
|
|
232
|
+
/** Explicit runtime override (e.g. from settings). If set, it wins immediately - no probing. */
|
|
233
233
|
override?: string;
|
|
234
234
|
/** Candidate ws(s):// URLs to race. If omitted, they are derived from `location`. */
|
|
235
235
|
candidates?: string[];
|
|
@@ -262,7 +262,7 @@ declare const FULL_CONTROL: SubscriptionMap;
|
|
|
262
262
|
type SubscriptionTier = 'narrow' | 'full';
|
|
263
263
|
declare function tier(t: SubscriptionTier): SubscriptionMap;
|
|
264
264
|
/** Union of subscription maps (`null` = "all fields" wins). The single source for the
|
|
265
|
-
* null-wins merge rule
|
|
265
|
+
* null-wins merge rule - consumers compose a baseline tier with widget-declared needs. */
|
|
266
266
|
declare function mergeSubscriptions(...maps: SubscriptionMap[]): SubscriptionMap;
|
|
267
267
|
|
|
268
268
|
type KlippyState = 'ready' | 'startup' | 'shutdown' | 'error' | 'disconnected';
|
|
@@ -344,7 +344,7 @@ interface SessionOptions {
|
|
|
344
344
|
subscriptions?: SubscriptionMap;
|
|
345
345
|
identify?: IdentifyInfo;
|
|
346
346
|
logger?: Logger;
|
|
347
|
-
/** Receive cross-surface agent events (notify_agent_event)
|
|
347
|
+
/** Receive cross-surface agent events (notify_agent_event) - e.g. remote-control commands from
|
|
348
348
|
* another FilaMind surface. Discrete (fired per event, never replayed), so no stale re-fire. */
|
|
349
349
|
onAgentEvent?: (ev: AgentEvent) => void;
|
|
350
350
|
}
|
|
@@ -366,7 +366,7 @@ declare class FilaMindSession {
|
|
|
366
366
|
setSubscriptions(subs: SubscriptionMap): void;
|
|
367
367
|
start(): Promise<void>;
|
|
368
368
|
stop(): void;
|
|
369
|
-
/** Staged init
|
|
369
|
+
/** Staged init - runs on first connect AND on reconnect AND on notify_klippy_ready.
|
|
370
370
|
* Guarded so overlapping triggers (reconnect + klippy_ready) can't interleave two seeds. */
|
|
371
371
|
private bootstrap;
|
|
372
372
|
private onUpdate;
|
|
@@ -396,7 +396,7 @@ declare class WriteArbiter {
|
|
|
396
396
|
run<T>(action: string, fn: () => Promise<T>): Promise<T>;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
/** FNV-1a 32-bit hex
|
|
399
|
+
/** FNV-1a 32-bit hex - deterministic, dependency-free. */
|
|
400
400
|
declare function fnv1a(s: string): string;
|
|
401
401
|
/** Derive a stable `fm-<hash>` machine id from Moonraker. Falls back to `fm-unknown`. */
|
|
402
402
|
declare function deriveMachineId(connector: Connector): Promise<string>;
|
|
@@ -438,7 +438,7 @@ interface WidgetDefinition {
|
|
|
438
438
|
title: string;
|
|
439
439
|
icon?: string;
|
|
440
440
|
description?: string;
|
|
441
|
-
/** lazy component loader
|
|
441
|
+
/** lazy component loader - the host framework (Vue) resolves it (kept as `unknown` here) */
|
|
442
442
|
component: () => Promise<unknown>;
|
|
443
443
|
defaultSize?: {
|
|
444
444
|
w: number;
|
|
@@ -454,7 +454,7 @@ declare function getWidget(id: string): WidgetDefinition | undefined;
|
|
|
454
454
|
declare function getWidgets(target?: SurfaceTarget): WidgetDefinition[];
|
|
455
455
|
/** The union of the given widgets' declared subscriptions (subscribe once). */
|
|
456
456
|
declare function aggregateSubscriptions(ids: string[]): SubscriptionMap;
|
|
457
|
-
/** Test/host helper
|
|
457
|
+
/** Test/host helper - clear the registry. */
|
|
458
458
|
declare function _resetRegistry(): void;
|
|
459
459
|
|
|
460
460
|
type SurfaceHint = 'dense-desktop' | 'thumb-phone' | 'big-touch';
|
|
@@ -562,14 +562,14 @@ interface BackendMessage {
|
|
|
562
562
|
declare function resolveMessage(catalog: Catalog, m: BackendMessage, locale?: string): string;
|
|
563
563
|
|
|
564
564
|
interface CommandSenderOptions {
|
|
565
|
-
/** Stable agent name (pinned
|
|
565
|
+
/** Stable agent name (pinned - a drifting name registers phantom duplicates on reconnect). */
|
|
566
566
|
client_name: string;
|
|
567
567
|
version: string;
|
|
568
568
|
/** Required: Moonraker documents `url` as mandatory for identify; omitting it can make a strict
|
|
569
569
|
* host reject the agent identify, silently stranding the bus. */
|
|
570
570
|
url: string;
|
|
571
571
|
logger?: Logger;
|
|
572
|
-
/** Fires whenever readiness flips (connected+identified)
|
|
572
|
+
/** Fires whenever readiness flips (connected+identified) - lets a UI gate its send affordances. */
|
|
573
573
|
onReadyChange?: (ready: boolean) => void;
|
|
574
574
|
}
|
|
575
575
|
declare class CommandSender {
|
|
@@ -582,14 +582,14 @@ declare class CommandSender {
|
|
|
582
582
|
private retryDelayMs;
|
|
583
583
|
private readonly log;
|
|
584
584
|
constructor(connector: Connector, opts: CommandSenderOptions);
|
|
585
|
-
/** true only when the bus is connected AND identified
|
|
585
|
+
/** true only when the bus is connected AND identified - gate remote-control affordances on this. */
|
|
586
586
|
get ready(): boolean;
|
|
587
587
|
/** Open the agent connection and identify once. Call alongside the app's main session start. */
|
|
588
588
|
start(): Promise<void>;
|
|
589
589
|
/** Close the agent connection (call on app teardown so the socket doesn't linger). */
|
|
590
590
|
stop(): void;
|
|
591
591
|
/** Broadcast a UI-only command to the other FilaMind surfaces. Best-effort: a command issued
|
|
592
|
-
* while the bus is down is dropped (logged), never queued/replayed
|
|
592
|
+
* while the bus is down is dropped (logged), never queued/replayed - a stale "navigate" fired
|
|
593
593
|
* minutes later would yank a screen unexpectedly. */
|
|
594
594
|
send(cmd: RemoteCommand): Promise<void>;
|
|
595
595
|
navigate(view: RemoteView): Promise<void>;
|
|
@@ -622,10 +622,10 @@ interface UserSettings {
|
|
|
622
622
|
dashboardLayout?: DashboardLayout;
|
|
623
623
|
}
|
|
624
624
|
declare const DEFAULT_SETTINGS: UserSettings;
|
|
625
|
-
/** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings
|
|
625
|
+
/** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings - drops unknown keys,
|
|
626
626
|
* validates every enum, stamps the current version. Used by hydrate() + import(). */
|
|
627
627
|
declare function migrate(raw: unknown): UserSettings;
|
|
628
|
-
/** App-provided persistence
|
|
628
|
+
/** App-provided persistence - the Moonraker-DB impl is keyed by machineUUID; tests use memory. */
|
|
629
629
|
interface SettingsPersistence {
|
|
630
630
|
load(): Promise<Partial<UserSettings>>;
|
|
631
631
|
save(settings: UserSettings): Promise<void>;
|
|
@@ -633,7 +633,7 @@ interface SettingsPersistence {
|
|
|
633
633
|
declare function memoryPersistence(): SettingsPersistence;
|
|
634
634
|
declare function localStoragePersistence(storageKey?: string): SettingsPersistence;
|
|
635
635
|
/** Settings persisted in the printer's Moonraker database, so they roam across every FilaMind
|
|
636
|
-
* surface on that printer (3d, screen, …)
|
|
636
|
+
* surface on that printer (3d, screen, …) - the F10 remote-config substrate. */
|
|
637
637
|
declare function moonrakerDbPersistence(connector: Connector): SettingsPersistence;
|
|
638
638
|
/** Roam a (locally-persisted) SettingsStore across surfaces via a shared remote store: pull the
|
|
639
639
|
* shared settings whenever the connection goes live (the shared copy wins on connect), and
|
package/dist/index.js
CHANGED
|
@@ -291,7 +291,8 @@ var MoonrakerClient = class {
|
|
|
291
291
|
if (!p) return;
|
|
292
292
|
clearTimeout(p.timer);
|
|
293
293
|
this.pending.delete(msg.id);
|
|
294
|
-
if (msg.error)
|
|
294
|
+
if (msg.error)
|
|
295
|
+
p.reject(new RpcError(msg.error.message ?? "rpc error", msg.error.code, msg.error.data));
|
|
295
296
|
else p.resolve(msg.result);
|
|
296
297
|
return;
|
|
297
298
|
}
|
|
@@ -617,18 +618,18 @@ var FilaMindSession = class {
|
|
|
617
618
|
this.connector.close();
|
|
618
619
|
this.setLive(false);
|
|
619
620
|
}
|
|
620
|
-
/** Staged init
|
|
621
|
+
/** Staged init - runs on first connect AND on reconnect AND on notify_klippy_ready.
|
|
621
622
|
* Guarded so overlapping triggers (reconnect + klippy_ready) can't interleave two seeds. */
|
|
622
623
|
async bootstrap() {
|
|
623
624
|
if (this.bootstrapping) return;
|
|
624
625
|
this.bootstrapping = true;
|
|
625
626
|
try {
|
|
626
627
|
if (this.opts.identify) {
|
|
627
|
-
await this.connector.call("server.connection.identify", { ...this.opts.identify }).catch(
|
|
628
|
-
(e) => this.log.warn("identify failed", String(e))
|
|
629
|
-
);
|
|
628
|
+
await this.connector.call("server.connection.identify", { ...this.opts.identify }).catch((e) => this.log.warn("identify failed", String(e)));
|
|
630
629
|
}
|
|
631
|
-
const info = await this.connector.call(
|
|
630
|
+
const info = await this.connector.call(
|
|
631
|
+
"server.info"
|
|
632
|
+
);
|
|
632
633
|
this.capabilities.set(info?.components ?? []);
|
|
633
634
|
const ks = deriveKlippyState(info?.klippy_state);
|
|
634
635
|
this.klippy.set(ks);
|
|
@@ -754,7 +755,10 @@ function memoryRestoreStore() {
|
|
|
754
755
|
byMachine.set(m, arr);
|
|
755
756
|
},
|
|
756
757
|
remove: async (m, id) => {
|
|
757
|
-
byMachine.set(
|
|
758
|
+
byMachine.set(
|
|
759
|
+
m,
|
|
760
|
+
(byMachine.get(m) ?? []).filter((p) => p.id !== id)
|
|
761
|
+
);
|
|
758
762
|
}
|
|
759
763
|
};
|
|
760
764
|
}
|
|
@@ -854,7 +858,9 @@ function resolveDashboard(layout, available, hint) {
|
|
|
854
858
|
);
|
|
855
859
|
const used = /* @__PURE__ */ new Set();
|
|
856
860
|
const widgets = [];
|
|
857
|
-
for (const slot of [...slots].filter((s) => !(s.hideOn ?? []).includes(hint)).sort(
|
|
861
|
+
for (const slot of [...slots].filter((s) => !(s.hideOn ?? []).includes(hint)).sort(
|
|
862
|
+
(a, b) => (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY)
|
|
863
|
+
)) {
|
|
858
864
|
const w = byId.get(slot.widgetId);
|
|
859
865
|
if (w && !used.has(w.id)) {
|
|
860
866
|
widgets.push(w);
|
|
@@ -888,7 +894,7 @@ function coerceDashboardLayout(raw) {
|
|
|
888
894
|
|
|
889
895
|
// src/theme/tokens.ts
|
|
890
896
|
var themes = {
|
|
891
|
-
// flagship default
|
|
897
|
+
// flagship default - obsidian + gold + lapis + turquoise
|
|
892
898
|
tutankhamun: {
|
|
893
899
|
bg: "#0E0F12",
|
|
894
900
|
surface: "#17171C",
|
|
@@ -936,7 +942,7 @@ var themes = {
|
|
|
936
942
|
warning: "#D98E2B",
|
|
937
943
|
danger: "#8C1F1A"
|
|
938
944
|
},
|
|
939
|
-
// neutral light
|
|
945
|
+
// neutral light - a conventional bright UI for users who prefer it
|
|
940
946
|
light: {
|
|
941
947
|
bg: "#F7F8FA",
|
|
942
948
|
surface: "#FFFFFF",
|
|
@@ -952,7 +958,7 @@ var themes = {
|
|
|
952
958
|
warning: "#B45309",
|
|
953
959
|
danger: "#DC2626"
|
|
954
960
|
},
|
|
955
|
-
// neutral dark
|
|
961
|
+
// neutral dark - a conventional dark UI, distinct from the warm Pharaonic palettes
|
|
956
962
|
dark: {
|
|
957
963
|
bg: "#0F1115",
|
|
958
964
|
surface: "#171A21",
|
|
@@ -1068,7 +1074,7 @@ var CommandSender = class {
|
|
|
1068
1074
|
this.opts = opts;
|
|
1069
1075
|
this.log = opts.logger ?? NULL_LOGGER;
|
|
1070
1076
|
connector.setCallbacks({
|
|
1071
|
-
// Moonraker forgets the agent identity on disconnect
|
|
1077
|
+
// Moonraker forgets the agent identity on disconnect > re-identify on every genuine reconnect.
|
|
1072
1078
|
onReconnected: () => {
|
|
1073
1079
|
this.setIdentified(false);
|
|
1074
1080
|
void this.ensureIdentified().catch(() => this.scheduleRetry());
|
|
@@ -1088,7 +1094,7 @@ var CommandSender = class {
|
|
|
1088
1094
|
retryTimer;
|
|
1089
1095
|
retryDelayMs = 500;
|
|
1090
1096
|
log;
|
|
1091
|
-
/** true only when the bus is connected AND identified
|
|
1097
|
+
/** true only when the bus is connected AND identified - gate remote-control affordances on this. */
|
|
1092
1098
|
get ready() {
|
|
1093
1099
|
return this._ready;
|
|
1094
1100
|
}
|
|
@@ -1104,7 +1110,7 @@ var CommandSender = class {
|
|
|
1104
1110
|
this.setIdentified(false);
|
|
1105
1111
|
}
|
|
1106
1112
|
/** Broadcast a UI-only command to the other FilaMind surfaces. Best-effort: a command issued
|
|
1107
|
-
* while the bus is down is dropped (logged), never queued/replayed
|
|
1113
|
+
* while the bus is down is dropped (logged), never queued/replayed - a stale "navigate" fired
|
|
1108
1114
|
* minutes later would yank a screen unexpectedly. */
|
|
1109
1115
|
async send(cmd) {
|
|
1110
1116
|
if (this.connector.state !== "ready") {
|
|
@@ -1113,7 +1119,10 @@ var CommandSender = class {
|
|
|
1113
1119
|
}
|
|
1114
1120
|
try {
|
|
1115
1121
|
await this.ensureIdentified();
|
|
1116
|
-
await this.connector.call("connection.send_event", {
|
|
1122
|
+
await this.connector.call("connection.send_event", {
|
|
1123
|
+
event: FILAMIND_COMMAND_EVENT,
|
|
1124
|
+
data: cmd
|
|
1125
|
+
});
|
|
1117
1126
|
} catch (e) {
|
|
1118
1127
|
this.log.warn("command send failed", String(e));
|
|
1119
1128
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@filamind-app/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Shared foundation for the FilaMind suite
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Shared foundation for the FilaMind suite - Moonraker client, reactive printer state, provenance-stamped values, design tokens, and the widget/plugin registry. Framework-agnostic TypeScript.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
34
34
|
"type-check": "tsc --noEmit",
|
|
35
35
|
"test": "vitest run",
|
|
36
|
-
"prepublishOnly": "npm run build"
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"format": "prettier --write \"src/**/*.ts\" \"*.{ts,js,json}\"",
|
|
38
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"*.{ts,js,json}\""
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
41
|
+
"prettier": "3.9.6",
|
|
39
42
|
"tsup": "^8.5.1",
|
|
40
43
|
"typescript": "~6.0.3",
|
|
41
|
-
"vitest": "^4.1.
|
|
44
|
+
"vitest": "^4.1.10"
|
|
42
45
|
}
|
|
43
46
|
}
|