@flighthq/flow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/clearFlowStack.d.ts +3 -0
  2. package/dist/clearFlowStack.d.ts.map +1 -0
  3. package/dist/clearFlowStack.js +11 -0
  4. package/dist/clearFlowStack.js.map +1 -0
  5. package/dist/createFlowStack.d.ts +3 -0
  6. package/dist/createFlowStack.d.ts.map +1 -0
  7. package/dist/createFlowStack.js +6 -0
  8. package/dist/createFlowStack.js.map +1 -0
  9. package/dist/getActiveFlowState.d.ts +3 -0
  10. package/dist/getActiveFlowState.d.ts.map +1 -0
  11. package/dist/getActiveFlowState.js +7 -0
  12. package/dist/getActiveFlowState.js.map +1 -0
  13. package/dist/getFlowStackDepth.d.ts +3 -0
  14. package/dist/getFlowStackDepth.d.ts.map +1 -0
  15. package/dist/getFlowStackDepth.js +5 -0
  16. package/dist/getFlowStackDepth.js.map +1 -0
  17. package/dist/getFlowStackVisibleStates.d.ts +3 -0
  18. package/dist/getFlowStackVisibleStates.d.ts.map +1 -0
  19. package/dist/getFlowStackVisibleStates.js +22 -0
  20. package/dist/getFlowStackVisibleStates.js.map +1 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +10 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/popFlowState.d.ts +3 -0
  26. package/dist/popFlowState.d.ts.map +1 -0
  27. package/dist/popFlowState.js +15 -0
  28. package/dist/popFlowState.js.map +1 -0
  29. package/dist/pushFlowState.d.ts +3 -0
  30. package/dist/pushFlowState.d.ts.map +1 -0
  31. package/dist/pushFlowState.js +11 -0
  32. package/dist/pushFlowState.js.map +1 -0
  33. package/dist/replaceFlowState.d.ts +3 -0
  34. package/dist/replaceFlowState.d.ts.map +1 -0
  35. package/dist/replaceFlowState.js +14 -0
  36. package/dist/replaceFlowState.js.map +1 -0
  37. package/dist/updateFlowStack.d.ts +3 -0
  38. package/dist/updateFlowStack.d.ts.map +1 -0
  39. package/dist/updateFlowStack.js +18 -0
  40. package/dist/updateFlowStack.js.map +1 -0
  41. package/package.json +37 -0
  42. package/src/clearFlowStack.test.ts +38 -0
  43. package/src/createFlowStack.test.ts +17 -0
  44. package/src/getActiveFlowState.test.ts +21 -0
  45. package/src/getFlowStackDepth.test.ts +18 -0
  46. package/src/getFlowStackVisibleStates.test.ts +61 -0
  47. package/src/popFlowState.test.ts +54 -0
  48. package/src/pushFlowState.test.ts +43 -0
  49. package/src/replaceFlowState.test.ts +47 -0
  50. package/src/updateFlowStack.test.ts +73 -0
@@ -0,0 +1,3 @@
1
+ import type { FlowStack } from '@flighthq/types';
2
+ export declare function clearFlowStack(stack: FlowStack): void;
3
+ //# sourceMappingURL=clearFlowStack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clearFlowStack.d.ts","sourceRoot":"","sources":["../src/clearFlowStack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAMrD"}
@@ -0,0 +1,11 @@
1
+ // Empty the stack, exiting every state top-to-bottom (`onExit` on the active top first, down to the
2
+ // bottom) so each unwinds in reverse of the order it entered. No `onPause`/`onResume` fire — the
3
+ // whole stack is being torn down, not layered. After this the stack has depth 0.
4
+ export function clearFlowStack(stack) {
5
+ const states = stack.states;
6
+ for (let i = states.length - 1; i >= 0; i--) {
7
+ states[i].onExit?.();
8
+ }
9
+ states.length = 0;
10
+ }
11
+ //# sourceMappingURL=clearFlowStack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clearFlowStack.js","sourceRoot":"","sources":["../src/clearFlowStack.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,iGAAiG;AACjG,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAAC,KAAgB;IAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;IACvB,CAAC;IACD,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;AACpB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowStack } from '@flighthq/types';
2
+ export declare function createFlowStack(): FlowStack;
3
+ //# sourceMappingURL=createFlowStack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createFlowStack.d.ts","sourceRoot":"","sources":["../src/createFlowStack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
@@ -0,0 +1,6 @@
1
+ // Allocate an empty flow-state stack. The only allocating function; every other stack function reads
2
+ // or mutates an existing stack in place. Push states onto it to drive the enter/pause lifecycle.
3
+ export function createFlowStack() {
4
+ return { states: [] };
5
+ }
6
+ //# sourceMappingURL=createFlowStack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createFlowStack.js","sourceRoot":"","sources":["../src/createFlowStack.ts"],"names":[],"mappings":"AAEA,qGAAqG;AACrG,iGAAiG;AACjG,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AACxB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowState, FlowStack } from '@flighthq/types';
2
+ export declare function getActiveFlowState(stack: Readonly<FlowStack>): FlowState | null;
3
+ //# sourceMappingURL=getActiveFlowState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getActiveFlowState.d.ts","sourceRoot":"","sources":["../src/getActiveFlowState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,SAAS,GAAG,IAAI,CAG/E"}
@@ -0,0 +1,7 @@
1
+ // The active top of the stack — the state currently receiving `onUpdate` — or `null` if the stack is
2
+ // empty.
3
+ export function getActiveFlowState(stack) {
4
+ const states = stack.states;
5
+ return states.length > 0 ? states[states.length - 1] : null;
6
+ }
7
+ //# sourceMappingURL=getActiveFlowState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getActiveFlowState.js","sourceRoot":"","sources":["../src/getActiveFlowState.ts"],"names":[],"mappings":"AAEA,qGAAqG;AACrG,SAAS;AACT,MAAM,UAAU,kBAAkB,CAAC,KAA0B;IAC3D,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowStack } from '@flighthq/types';
2
+ export declare function getFlowStackDepth(stack: Readonly<FlowStack>): number;
3
+ //# sourceMappingURL=getFlowStackDepth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getFlowStackDepth.d.ts","sourceRoot":"","sources":["../src/getFlowStackDepth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,MAAM,CAEpE"}
@@ -0,0 +1,5 @@
1
+ // The number of states on the stack.
2
+ export function getFlowStackDepth(stack) {
3
+ return stack.states.length;
4
+ }
5
+ //# sourceMappingURL=getFlowStackDepth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getFlowStackDepth.js","sourceRoot":"","sources":["../src/getFlowStackDepth.ts"],"names":[],"mappings":"AAEA,qCAAqC;AACrC,MAAM,UAAU,iBAAiB,CAAC,KAA0B;IAC1D,OAAO,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowState, FlowStack } from '@flighthq/types';
2
+ export declare function getFlowStackVisibleStates(stack: Readonly<FlowStack>, out: FlowState[]): void;
3
+ //# sourceMappingURL=getFlowStackVisibleStates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getFlowStackVisibleStates.d.ts","sourceRoot":"","sources":["../src/getFlowStackVisibleStates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAQ5D,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,CAc5F"}
@@ -0,0 +1,22 @@
1
+ // Fill `out` (cleared first) with the render-visible states in draw order, bottom-to-top: the active
2
+ // top plus the contiguous run of states beneath it reachable through `renderBelow`. Walking down from
3
+ // the top, each state that sets `renderBelow` keeps the one beneath it visible; the walk stops at the
4
+ // first opaque state (no `renderBelow`), which is itself visible but hides everything below it. The
5
+ // caller draws `out` front-to-back (index 0 first) so higher states paint over lower ones. `out` is
6
+ // empty for an empty stack.
7
+ export function getFlowStackVisibleStates(stack, out) {
8
+ out.length = 0;
9
+ const states = stack.states;
10
+ const top = states.length - 1;
11
+ if (top < 0) {
12
+ return;
13
+ }
14
+ let lowest = top;
15
+ while (lowest > 0 && states[lowest].renderBelow) {
16
+ lowest--;
17
+ }
18
+ for (let i = lowest; i <= top; i++) {
19
+ out.push(states[i]);
20
+ }
21
+ }
22
+ //# sourceMappingURL=getFlowStackVisibleStates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getFlowStackVisibleStates.js","sourceRoot":"","sources":["../src/getFlowStackVisibleStates.ts"],"names":[],"mappings":"AAEA,qGAAqG;AACrG,sGAAsG;AACtG,sGAAsG;AACtG,oGAAoG;AACpG,oGAAoG;AACpG,4BAA4B;AAC5B,MAAM,UAAU,yBAAyB,CAAC,KAA0B,EAAE,GAAgB;IACpF,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QACZ,OAAO;IACT,CAAC;IACD,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,OAAO,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAChD,MAAM,EAAE,CAAC;IACX,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,10 @@
1
+ export * from './clearFlowStack';
2
+ export * from './createFlowStack';
3
+ export * from './getActiveFlowState';
4
+ export * from './getFlowStackDepth';
5
+ export * from './getFlowStackVisibleStates';
6
+ export * from './popFlowState';
7
+ export * from './pushFlowState';
8
+ export * from './replaceFlowState';
9
+ export * from './updateFlowStack';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from './clearFlowStack';
2
+ export * from './createFlowStack';
3
+ export * from './getActiveFlowState';
4
+ export * from './getFlowStackDepth';
5
+ export * from './getFlowStackVisibleStates';
6
+ export * from './popFlowState';
7
+ export * from './pushFlowState';
8
+ export * from './replaceFlowState';
9
+ export * from './updateFlowStack';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowState, FlowStack } from '@flighthq/types';
2
+ export declare function popFlowState(stack: FlowStack): FlowState | null;
3
+ //# sourceMappingURL=popFlowState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popFlowState.d.ts","sourceRoot":"","sources":["../src/popFlowState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAK5D,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAU/D"}
@@ -0,0 +1,15 @@
1
+ // Pop the active top off the stack, exiting it (`onExit`) and resuming the state it uncovers
2
+ // (`onResume`) — the mirror of `pushFlowState`. Returns the popped state, or `null` if the stack was
3
+ // empty (a sentinel, not a throw — popping an empty stack is an expected no-op query).
4
+ export function popFlowState(stack) {
5
+ const states = stack.states;
6
+ if (states.length === 0) {
7
+ return null;
8
+ }
9
+ const popped = states.pop();
10
+ popped.onExit?.();
11
+ const revealed = states.length > 0 ? states[states.length - 1] : null;
12
+ revealed?.onResume?.();
13
+ return popped;
14
+ }
15
+ //# sourceMappingURL=popFlowState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popFlowState.js","sourceRoot":"","sources":["../src/popFlowState.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,qGAAqG;AACrG,uFAAuF;AACvF,MAAM,UAAU,YAAY,CAAC,KAAgB;IAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,EAAe,CAAC;IACzC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;IAClB,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC;IACvB,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowState, FlowStack } from '@flighthq/types';
2
+ export declare function pushFlowState(stack: FlowStack, state: Readonly<FlowState>): void;
3
+ //# sourceMappingURL=pushFlowState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pushFlowState.d.ts","sourceRoot":"","sources":["../src/pushFlowState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAK5D,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAMhF"}
@@ -0,0 +1,11 @@
1
+ // Push `state` onto the stack, making it the active top. The previous top (if any) is paused first
2
+ // (`onPause`), then `state` is pushed and entered (`onEnter`) — pause-then-enter, so the outgoing
3
+ // state is suspended before the incoming one starts.
4
+ export function pushFlowState(stack, state) {
5
+ const states = stack.states;
6
+ const previousTop = states.length > 0 ? states[states.length - 1] : null;
7
+ previousTop?.onPause?.();
8
+ states.push(state);
9
+ state.onEnter?.();
10
+ }
11
+ //# sourceMappingURL=pushFlowState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pushFlowState.js","sourceRoot":"","sources":["../src/pushFlowState.ts"],"names":[],"mappings":"AAEA,mGAAmG;AACnG,kGAAkG;AAClG,qDAAqD;AACrD,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,KAA0B;IACxE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzE,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;IACzB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;AACpB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowState, FlowStack } from '@flighthq/types';
2
+ export declare function replaceFlowState(stack: FlowStack, state: Readonly<FlowState>): void;
3
+ //# sourceMappingURL=replaceFlowState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replaceFlowState.d.ts","sourceRoot":"","sources":["../src/replaceFlowState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAM5D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAQnF"}
@@ -0,0 +1,14 @@
1
+ // Swap the active top for `state` in place, keeping the stack depth the same. The current top (if
2
+ // any) exits (`onExit`) and `state` enters (`onEnter`); the state beneath is NOT paused or resumed,
3
+ // because it stays covered throughout — replace swaps the top layer, it does not uncover the one
4
+ // below. On an empty stack this is just a push-and-enter.
5
+ export function replaceFlowState(stack, state) {
6
+ const states = stack.states;
7
+ if (states.length > 0) {
8
+ const previousTop = states.pop();
9
+ previousTop.onExit?.();
10
+ }
11
+ states.push(state);
12
+ state.onEnter?.();
13
+ }
14
+ //# sourceMappingURL=replaceFlowState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replaceFlowState.js","sourceRoot":"","sources":["../src/replaceFlowState.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,oGAAoG;AACpG,iGAAiG;AACjG,0DAA0D;AAC1D,MAAM,UAAU,gBAAgB,CAAC,KAAgB,EAAE,KAA0B;IAC3E,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,EAAe,CAAC;QAC9C,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;IACzB,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;AACpB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlowStack } from '@flighthq/types';
2
+ export declare function updateFlowStack(stack: Readonly<FlowStack>, deltaTime: number): void;
3
+ //# sourceMappingURL=updateFlowStack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateFlowStack.d.ts","sourceRoot":"","sources":["../src/updateFlowStack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAOjD,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAWnF"}
@@ -0,0 +1,18 @@
1
+ // Tick the stack for one frame. The active top always updates (`onUpdate(deltaTime)`); then, walking
2
+ // downward, each visited state that sets `updateBelow` also ticks the state immediately beneath it,
3
+ // continuing the chain until a visited state does not set `updateBelow`. So a run of transparent
4
+ // overlays all tick their underlying states, but an opaque overlay (no `updateBelow`) freezes
5
+ // everything below it. A no-op on an empty stack.
6
+ export function updateFlowStack(stack, deltaTime) {
7
+ const states = stack.states;
8
+ let index = states.length - 1;
9
+ if (index < 0) {
10
+ return;
11
+ }
12
+ states[index].onUpdate?.(deltaTime);
13
+ while (index > 0 && states[index].updateBelow) {
14
+ index--;
15
+ states[index].onUpdate?.(deltaTime);
16
+ }
17
+ }
18
+ //# sourceMappingURL=updateFlowStack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateFlowStack.js","sourceRoot":"","sources":["../src/updateFlowStack.ts"],"names":[],"mappings":"AAEA,qGAAqG;AACrG,oGAAoG;AACpG,iGAAiG;AACjG,8FAA8F;AAC9F,kDAAkD;AAClD,MAAM,UAAU,eAAe,CAAC,KAA0B,EAAE,SAAiB;IAC3E,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,IAAI,KAAK,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9B,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,OAAO;IACT,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,CAAC;IACpC,OAAO,KAAK,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9C,KAAK,EAAE,CAAC;QACR,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,CAAC;IACtC,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@flighthq/flow",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "description": "Application mode/screen flow-state stack — a plain-data stack of application states (boot, menu, play, pause, game-over) with a paired enter/exit/pause/resume/update lifecycle driven by push/pop/replace/clear transitions, transparent-overlay update-below/render-below flags, an update pass that ticks the active state and any opted-in states beneath it, and active-state/depth/visible-states queries; direct lifecycle callbacks (single-owner, deterministic order), not signals",
36
+ "sideEffects": false
37
+ }
@@ -0,0 +1,38 @@
1
+ import type { FlowState } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { clearFlowStack } from './clearFlowStack';
5
+ import { createFlowStack } from './createFlowStack';
6
+ import { pushFlowState } from './pushFlowState';
7
+
8
+ describe('clearFlowStack', () => {
9
+ it('exits every state top-to-bottom and empties the stack', () => {
10
+ const log: string[] = [];
11
+ const stack = createFlowStack();
12
+ pushFlowState(stack, labeledState('A', log));
13
+ pushFlowState(stack, labeledState('B', log));
14
+ pushFlowState(stack, labeledState('C', log));
15
+ log.length = 0;
16
+ clearFlowStack(stack);
17
+ expect(log).toEqual(['C.exit', 'B.exit', 'A.exit']);
18
+ expect(stack.states.length).toBe(0);
19
+ });
20
+
21
+ it('is a no-op on an empty stack', () => {
22
+ const stack = createFlowStack();
23
+ expect(() => clearFlowStack(stack)).not.toThrow();
24
+ expect(stack.states.length).toBe(0);
25
+ });
26
+ });
27
+
28
+ function labeledState(name: string, log: string[], extra: Partial<FlowState> = {}): FlowState {
29
+ return {
30
+ name,
31
+ onEnter: () => log.push(`${name}.enter`),
32
+ onExit: () => log.push(`${name}.exit`),
33
+ onPause: () => log.push(`${name}.pause`),
34
+ onResume: () => log.push(`${name}.resume`),
35
+ onUpdate: (deltaTime) => log.push(`${name}.update:${deltaTime}`),
36
+ ...extra,
37
+ };
38
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createFlowStack } from './createFlowStack';
4
+
5
+ describe('createFlowStack', () => {
6
+ it('allocates an empty stack', () => {
7
+ const stack = createFlowStack();
8
+ expect(stack.states).toEqual([]);
9
+ });
10
+
11
+ it('allocates a fresh independent stack each call', () => {
12
+ const a = createFlowStack();
13
+ const b = createFlowStack();
14
+ expect(a).not.toBe(b);
15
+ expect(a.states).not.toBe(b.states);
16
+ });
17
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createFlowStack } from './createFlowStack';
4
+ import { getActiveFlowState } from './getActiveFlowState';
5
+ import { pushFlowState } from './pushFlowState';
6
+
7
+ describe('getActiveFlowState', () => {
8
+ it('returns the top of the stack', () => {
9
+ const stack = createFlowStack();
10
+ const a = { name: 'A' };
11
+ const b = { name: 'B' };
12
+ pushFlowState(stack, a);
13
+ pushFlowState(stack, b);
14
+ expect(getActiveFlowState(stack)).toBe(b);
15
+ });
16
+
17
+ it('returns null on an empty stack', () => {
18
+ const stack = createFlowStack();
19
+ expect(getActiveFlowState(stack)).toBeNull();
20
+ });
21
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createFlowStack } from './createFlowStack';
4
+ import { getFlowStackDepth } from './getFlowStackDepth';
5
+ import { popFlowState } from './popFlowState';
6
+ import { pushFlowState } from './pushFlowState';
7
+
8
+ describe('getFlowStackDepth', () => {
9
+ it('counts the states on the stack', () => {
10
+ const stack = createFlowStack();
11
+ expect(getFlowStackDepth(stack)).toBe(0);
12
+ pushFlowState(stack, { name: 'A' });
13
+ pushFlowState(stack, { name: 'B' });
14
+ expect(getFlowStackDepth(stack)).toBe(2);
15
+ popFlowState(stack);
16
+ expect(getFlowStackDepth(stack)).toBe(1);
17
+ });
18
+ });
@@ -0,0 +1,61 @@
1
+ import type { FlowState } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createFlowStack } from './createFlowStack';
5
+ import { getFlowStackVisibleStates } from './getFlowStackVisibleStates';
6
+ import { pushFlowState } from './pushFlowState';
7
+
8
+ describe('getFlowStackVisibleStates', () => {
9
+ it('fills only the top when it is opaque', () => {
10
+ const stack = createFlowStack();
11
+ const a = { name: 'A' };
12
+ const b = { name: 'B' };
13
+ pushFlowState(stack, a);
14
+ pushFlowState(stack, b);
15
+ const out: FlowState[] = [];
16
+ getFlowStackVisibleStates(stack, out);
17
+ expect(out).toEqual([b]);
18
+ });
19
+
20
+ it('includes the contiguous renderBelow run bottom-to-top', () => {
21
+ const stack = createFlowStack();
22
+ const a = { name: 'A' };
23
+ const b: FlowState = { name: 'B', renderBelow: true };
24
+ const c: FlowState = { name: 'C', renderBelow: true };
25
+ pushFlowState(stack, a);
26
+ pushFlowState(stack, b);
27
+ pushFlowState(stack, c);
28
+ const out: FlowState[] = [];
29
+ getFlowStackVisibleStates(stack, out);
30
+ expect(out).toEqual([a, b, c]);
31
+ });
32
+
33
+ it('stops at the first opaque state below a translucent overlay', () => {
34
+ const stack = createFlowStack();
35
+ const a = { name: 'A' };
36
+ const b = { name: 'B' };
37
+ const c: FlowState = { name: 'C', renderBelow: true };
38
+ pushFlowState(stack, a);
39
+ pushFlowState(stack, b);
40
+ pushFlowState(stack, c);
41
+ const out: FlowState[] = [];
42
+ getFlowStackVisibleStates(stack, out);
43
+ expect(out).toEqual([b, c]);
44
+ });
45
+
46
+ it('clears the out array before filling and can be reused', () => {
47
+ const stack = createFlowStack();
48
+ const a = { name: 'A' };
49
+ pushFlowState(stack, a);
50
+ const out: FlowState[] = [{ name: 'stale' }, { name: 'leftover' }];
51
+ getFlowStackVisibleStates(stack, out);
52
+ expect(out).toEqual([a]);
53
+ });
54
+
55
+ it('leaves the out array empty for an empty stack', () => {
56
+ const stack = createFlowStack();
57
+ const out: FlowState[] = [{ name: 'stale' }];
58
+ getFlowStackVisibleStates(stack, out);
59
+ expect(out).toEqual([]);
60
+ });
61
+ });
@@ -0,0 +1,54 @@
1
+ import type { FlowState } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createFlowStack } from './createFlowStack';
5
+ import { popFlowState } from './popFlowState';
6
+ import { pushFlowState } from './pushFlowState';
7
+
8
+ describe('popFlowState', () => {
9
+ it('exits the top and resumes the revealed state, returning the popped state', () => {
10
+ const log: string[] = [];
11
+ const stack = createFlowStack();
12
+ const a = labeledState('A', log);
13
+ const b = labeledState('B', log);
14
+ pushFlowState(stack, a);
15
+ pushFlowState(stack, b);
16
+ log.length = 0;
17
+ const popped = popFlowState(stack);
18
+ expect(log).toEqual(['B.exit', 'A.resume']);
19
+ expect(popped).toBe(b);
20
+ expect(stack.states.length).toBe(1);
21
+ });
22
+
23
+ it('returns null when the stack is empty', () => {
24
+ const stack = createFlowStack();
25
+ expect(popFlowState(stack)).toBeNull();
26
+ });
27
+
28
+ it('exits the last state without resuming anything below', () => {
29
+ const log: string[] = [];
30
+ const stack = createFlowStack();
31
+ pushFlowState(stack, labeledState('A', log));
32
+ log.length = 0;
33
+ popFlowState(stack);
34
+ expect(log).toEqual(['A.exit']);
35
+ });
36
+
37
+ it('does not throw when the popped state omits callbacks', () => {
38
+ const stack = createFlowStack();
39
+ stack.states.push({});
40
+ expect(() => popFlowState(stack)).not.toThrow();
41
+ });
42
+ });
43
+
44
+ function labeledState(name: string, log: string[], extra: Partial<FlowState> = {}): FlowState {
45
+ return {
46
+ name,
47
+ onEnter: () => log.push(`${name}.enter`),
48
+ onExit: () => log.push(`${name}.exit`),
49
+ onPause: () => log.push(`${name}.pause`),
50
+ onResume: () => log.push(`${name}.resume`),
51
+ onUpdate: (deltaTime) => log.push(`${name}.update:${deltaTime}`),
52
+ ...extra,
53
+ };
54
+ }
@@ -0,0 +1,43 @@
1
+ import type { FlowState } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createFlowStack } from './createFlowStack';
5
+ import { pushFlowState } from './pushFlowState';
6
+
7
+ describe('pushFlowState', () => {
8
+ it('enters a state pushed onto an empty stack', () => {
9
+ const log: string[] = [];
10
+ const stack = createFlowStack();
11
+ pushFlowState(stack, labeledState('A', log));
12
+ expect(log).toEqual(['A.enter']);
13
+ });
14
+
15
+ it('pauses the previous top before entering the new state', () => {
16
+ const log: string[] = [];
17
+ const stack = createFlowStack();
18
+ pushFlowState(stack, labeledState('A', log));
19
+ log.length = 0;
20
+ pushFlowState(stack, labeledState('B', log));
21
+ expect(log).toEqual(['A.pause', 'B.enter']);
22
+ expect(stack.states.length).toBe(2);
23
+ });
24
+
25
+ it('does not throw when the states omit lifecycle callbacks', () => {
26
+ const stack = createFlowStack();
27
+ pushFlowState(stack, {});
28
+ expect(() => pushFlowState(stack, {})).not.toThrow();
29
+ expect(stack.states.length).toBe(2);
30
+ });
31
+ });
32
+
33
+ function labeledState(name: string, log: string[], extra: Partial<FlowState> = {}): FlowState {
34
+ return {
35
+ name,
36
+ onEnter: () => log.push(`${name}.enter`),
37
+ onExit: () => log.push(`${name}.exit`),
38
+ onPause: () => log.push(`${name}.pause`),
39
+ onResume: () => log.push(`${name}.resume`),
40
+ onUpdate: (deltaTime) => log.push(`${name}.update:${deltaTime}`),
41
+ ...extra,
42
+ };
43
+ }
@@ -0,0 +1,47 @@
1
+ import type { FlowState } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createFlowStack } from './createFlowStack';
5
+ import { pushFlowState } from './pushFlowState';
6
+ import { replaceFlowState } from './replaceFlowState';
7
+
8
+ describe('replaceFlowState', () => {
9
+ it('exits the top and enters the replacement without touching the state below', () => {
10
+ const log: string[] = [];
11
+ const stack = createFlowStack();
12
+ const a = labeledState('A', log);
13
+ pushFlowState(stack, a);
14
+ pushFlowState(stack, labeledState('B', log));
15
+ log.length = 0;
16
+ replaceFlowState(stack, labeledState('C', log));
17
+ expect(log).toEqual(['B.exit', 'C.enter']);
18
+ expect(stack.states.length).toBe(2);
19
+ expect(stack.states[0]).toBe(a);
20
+ });
21
+
22
+ it('just pushes and enters on an empty stack', () => {
23
+ const log: string[] = [];
24
+ const stack = createFlowStack();
25
+ replaceFlowState(stack, labeledState('C', log));
26
+ expect(log).toEqual(['C.enter']);
27
+ expect(stack.states.length).toBe(1);
28
+ });
29
+
30
+ it('does not throw when states omit callbacks', () => {
31
+ const stack = createFlowStack();
32
+ stack.states.push({});
33
+ expect(() => replaceFlowState(stack, {})).not.toThrow();
34
+ });
35
+ });
36
+
37
+ function labeledState(name: string, log: string[], extra: Partial<FlowState> = {}): FlowState {
38
+ return {
39
+ name,
40
+ onEnter: () => log.push(`${name}.enter`),
41
+ onExit: () => log.push(`${name}.exit`),
42
+ onPause: () => log.push(`${name}.pause`),
43
+ onResume: () => log.push(`${name}.resume`),
44
+ onUpdate: (deltaTime) => log.push(`${name}.update:${deltaTime}`),
45
+ ...extra,
46
+ };
47
+ }
@@ -0,0 +1,73 @@
1
+ import type { FlowState } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createFlowStack } from './createFlowStack';
5
+ import { pushFlowState } from './pushFlowState';
6
+ import { updateFlowStack } from './updateFlowStack';
7
+
8
+ describe('updateFlowStack', () => {
9
+ it('ticks only the top with the frame deltaTime', () => {
10
+ const log: string[] = [];
11
+ const stack = createFlowStack();
12
+ pushFlowState(stack, labeledState('A', log));
13
+ pushFlowState(stack, labeledState('B', log));
14
+ log.length = 0;
15
+ updateFlowStack(stack, 0.016);
16
+ expect(log).toEqual(['B.update:0.016']);
17
+ });
18
+
19
+ it('ticks the state beneath a transparent updateBelow overlay', () => {
20
+ const log: string[] = [];
21
+ const stack = createFlowStack();
22
+ pushFlowState(stack, labeledState('A', log));
23
+ pushFlowState(stack, labeledState('B', log, { updateBelow: true }));
24
+ log.length = 0;
25
+ updateFlowStack(stack, 0.02);
26
+ expect(log).toEqual(['B.update:0.02', 'A.update:0.02']);
27
+ });
28
+
29
+ it('does not tick below an opaque overlay', () => {
30
+ const log: string[] = [];
31
+ const stack = createFlowStack();
32
+ pushFlowState(stack, labeledState('A', log));
33
+ pushFlowState(stack, labeledState('B', log));
34
+ log.length = 0;
35
+ updateFlowStack(stack, 0.01);
36
+ expect(log).toEqual(['B.update:0.01']);
37
+ });
38
+
39
+ it('walks a chain of updateBelow overlays and stops at the first opaque one', () => {
40
+ const log: string[] = [];
41
+ const stack = createFlowStack();
42
+ pushFlowState(stack, labeledState('A', log));
43
+ pushFlowState(stack, labeledState('B', log));
44
+ pushFlowState(stack, labeledState('C', log, { updateBelow: true }));
45
+ pushFlowState(stack, labeledState('D', log, { updateBelow: true }));
46
+ log.length = 0;
47
+ updateFlowStack(stack, 0.5);
48
+ expect(log).toEqual(['D.update:0.5', 'C.update:0.5', 'B.update:0.5']);
49
+ });
50
+
51
+ it('is a no-op on an empty stack', () => {
52
+ const stack = createFlowStack();
53
+ expect(() => updateFlowStack(stack, 0.016)).not.toThrow();
54
+ });
55
+
56
+ it('does not throw when a ticked state omits onUpdate', () => {
57
+ const stack = createFlowStack();
58
+ stack.states.push({ updateBelow: true }, {});
59
+ expect(() => updateFlowStack(stack, 0.016)).not.toThrow();
60
+ });
61
+ });
62
+
63
+ function labeledState(name: string, log: string[], extra: Partial<FlowState> = {}): FlowState {
64
+ return {
65
+ name,
66
+ onEnter: () => log.push(`${name}.enter`),
67
+ onExit: () => log.push(`${name}.exit`),
68
+ onPause: () => log.push(`${name}.pause`),
69
+ onResume: () => log.push(`${name}.resume`),
70
+ onUpdate: (deltaTime) => log.push(`${name}.update:${deltaTime}`),
71
+ ...extra,
72
+ };
73
+ }