@flowtty/core 1.0.0-alpha.1

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.
@@ -0,0 +1,63 @@
1
+ import { B as Buffer } from '../cells-CaXEx4lH.js';
2
+ import { B as Backend, K as Key } from '../backend-BcB7VR87.js';
3
+
4
+ declare class TestBackend implements Backend {
5
+ private readonly cols;
6
+ private readonly rows;
7
+ frames: string[];
8
+ private buffers;
9
+ private readonly subscribers;
10
+ constructor(cols?: number, rows?: number);
11
+ size(): {
12
+ width: number;
13
+ height: number;
14
+ };
15
+ draw(buffer: Buffer): void;
16
+ get lastFrame(): string;
17
+ get lastBuffer(): Buffer | null;
18
+ onKey(handler: (key: Key) => void): () => void;
19
+ /** Synchronously deliver one Key to every subscriber. */
20
+ press(key: Partial<Key> & {
21
+ name: string;
22
+ }): void;
23
+ /** Emit one Key per character; printable chars only. */
24
+ type(text: string): void;
25
+ dispose(): void;
26
+ }
27
+
28
+ /**
29
+ * Resolve after pending microtasks have drained. Use after `backend.press(...)`
30
+ * to wait for React's state update + the scheduled repaint:
31
+ *
32
+ * backend.press({ name: 'a' });
33
+ * await flush();
34
+ * expect(backend.lastFrame).toBe('a');
35
+ */
36
+ declare function flush(): Promise<void>;
37
+ /**
38
+ * Wait for React scheduler-driven re-renders triggered by `setState` inside
39
+ * `useEffect` (e.g. a Form field registering, then the group auto-focusing the
40
+ * first field) to fully commit.
41
+ *
42
+ * A passive-effect `setState` lands on React's default lane, which the
43
+ * production Scheduler only drains on a macrotask — there is no synchronous
44
+ * escape hatch in react-reconciler. A single `setTimeout(0)` (the old impl)
45
+ * therefore advances only ONE step of a multi-step effect cascade and can also
46
+ * lose the race against the Scheduler's own MessageChannel macrotask; both
47
+ * surface as a stale/empty `lastFrame`.
48
+ *
49
+ * Passing the `TestBackend` makes this deterministic: each round yields one
50
+ * macrotask (draining the Scheduler, which flushes that commit's passive
51
+ * effects exactly as in production) plus a microtask pair (for the coalesced
52
+ * repaint), and we stop once two consecutive rounds add no new frame. Two
53
+ * rounds — not one — so a single macrotask-ordering inversion (Scheduler work
54
+ * landing just after our `setTimeout`) can't read as premature quiescence.
55
+ *
56
+ * Called with no backend it falls back to a single macrotask round (the legacy
57
+ * behavior) for callers that don't need cascade-settling.
58
+ */
59
+ declare function flushAsync(backend?: {
60
+ readonly frames: readonly unknown[];
61
+ }): Promise<void>;
62
+
63
+ export { TestBackend, flush, flushAsync };
@@ -0,0 +1,72 @@
1
+ // src/testing/test-backend.ts
2
+ var TestBackend = class {
3
+ constructor(cols = 40, rows = 10) {
4
+ this.cols = cols;
5
+ this.rows = rows;
6
+ }
7
+ cols;
8
+ rows;
9
+ frames = [];
10
+ buffers = [];
11
+ subscribers = /* @__PURE__ */ new Set();
12
+ size() {
13
+ return { width: this.cols, height: this.rows };
14
+ }
15
+ draw(buffer) {
16
+ this.frames.push(buffer.toString());
17
+ this.buffers.push(buffer);
18
+ }
19
+ get lastFrame() {
20
+ return this.frames[this.frames.length - 1] ?? "";
21
+ }
22
+ get lastBuffer() {
23
+ return this.buffers[this.buffers.length - 1] ?? null;
24
+ }
25
+ onKey(handler) {
26
+ this.subscribers.add(handler);
27
+ return () => {
28
+ this.subscribers.delete(handler);
29
+ };
30
+ }
31
+ /** Synchronously deliver one Key to every subscriber. */
32
+ press(key) {
33
+ const k = {
34
+ sequence: key.sequence ?? "",
35
+ ctrl: key.ctrl ?? false,
36
+ meta: key.meta ?? false,
37
+ shift: key.shift ?? false,
38
+ name: key.name
39
+ };
40
+ for (const h of [...this.subscribers]) h(k);
41
+ }
42
+ /** Emit one Key per character; printable chars only. */
43
+ type(text) {
44
+ for (const ch of text) this.press({ name: ch, sequence: ch });
45
+ }
46
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
47
+ dispose() {
48
+ }
49
+ };
50
+
51
+ // src/testing/index.ts
52
+ async function flush() {
53
+ await Promise.resolve();
54
+ await Promise.resolve();
55
+ }
56
+ async function flushAsync(backend) {
57
+ if (!backend) {
58
+ await new Promise((resolve) => setTimeout(resolve, 0));
59
+ return;
60
+ }
61
+ const MAX_ROUNDS = 20;
62
+ let stableRounds = 0;
63
+ for (let i = 0; i < MAX_ROUNDS && stableRounds < 2; i++) {
64
+ const before = backend.frames.length;
65
+ await new Promise((resolve) => setTimeout(resolve, 0));
66
+ await Promise.resolve();
67
+ await Promise.resolve();
68
+ stableRounds = backend.frames.length === before ? stableRounds + 1 : 0;
69
+ }
70
+ }
71
+
72
+ export { TestBackend, flush, flushAsync };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@flowtty/core",
3
+ "version": "1.0.0-alpha.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mellonis/flowtty.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "homepage": "https://github.com/mellonis/flowtty#readme",
12
+ "bugs": "https://github.com/mellonis/flowtty/issues",
13
+ "description": "Framework-free core: Buffer, Cell, Style, Key, Backend interface. Every adapter (React, Svelte) and every backend (TTY, Test, Electron) depends on this.",
14
+ "keywords": [
15
+ "flowtty",
16
+ "terminal",
17
+ "tui",
18
+ "terminal-ui",
19
+ "cli",
20
+ "react-reconciler",
21
+ "yoga",
22
+ "flexbox",
23
+ "ansi",
24
+ "buffer"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./host": {
32
+ "types": "./dist/host/index.d.ts",
33
+ "import": "./dist/host/index.js"
34
+ },
35
+ "./testing": {
36
+ "types": "./dist/testing/index.d.ts",
37
+ "import": "./dist/testing/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "dependencies": {
48
+ "yoga-layout": "^3.2.1"
49
+ },
50
+ "devDependencies": {
51
+ "tsup": "^8"
52
+ }
53
+ }