@grundyjs/algiviz 0.10.0 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.1 — 2026-11-05
4
+
5
+ - Validate both numeric positions in array swap/shift before reading or mutating
6
+ the trace, rejecting invalid JavaScript arguments without losing item identities
7
+ or changing snapshots, counters, markers or the next step index.
8
+ - Allow explicitly undefined sorted-region inputs with exactOptionalPropertyTypes,
9
+ matching the existing runtime behavior of preserving the current marker.
10
+ - Document array guarantees in English and Russian: fixed length, one held item,
11
+ trace-local IDs, immutable snapshots, counters and author-supplied sorted markers.
12
+ - Add an application-owned object lifecycle example and expand playback completion,
13
+ strict type and installed-package consumer checks. No new public exports.
14
+
15
+ ## 0.11.0 — 2026-11-02
16
+
17
+ - Add createStatePlayer() for lazy playback of plain immutable states without
18
+ step wrappers, event fields or an isTerminal predicate.
19
+ - Consume one state per request without lookahead. A request after the final
20
+ state detects exhaustion, preserves the last frame and index, and completes
21
+ playback or starts an explicitly configured final hold.
22
+ - Support manual and timed playback, infinite sources, disposal and source errors.
23
+ - Simplify the interactive minimal example and English/Russian READMEs; update
24
+ the highlighted GIF to show the extra completion request.
25
+
3
26
  ## 0.10.0 — 2026-10-27
4
27
 
5
28
  - Default finalHoldMs to zero for generic and array players.
package/README.md CHANGED
@@ -55,14 +55,14 @@ module through a bundler that resolves imports from the installed npm package.
55
55
  ```
56
56
 
57
57
  ```js
58
- import { createPlayer } from '@grundyjs/algiviz/playback';
58
+ import { createStatePlayer } from '@grundyjs/algiviz/playback';
59
59
  import { createSceneRenderer } from '@grundyjs/algiviz/scene';
60
60
 
61
61
  const ctx = document.querySelector('canvas').getContext('2d');
62
- function* steps() {
63
- for (const [index, x] of [40, 200, 360, 560].entries()) {
62
+ function* states() {
63
+ for (const x of [40, 200, 360, 560]) {
64
64
  const state = { objects: [{ id: 'circle', type: 'dot', data: { x } }] };
65
- yield { index, state, event: index === 3 ? 'done' : 'position' };
65
+ yield state;
66
66
  }
67
67
  }
68
68
  const renderer = createSceneRenderer({
@@ -73,8 +73,7 @@ const renderer = createSceneRenderer({
73
73
  ctx.fill();
74
74
  },
75
75
  });
76
- const player = createPlayer(steps(), {
77
- isTerminal: step => step.event === 'done',
76
+ const player = createStatePlayer(states(), {
78
77
  render(frame) {
79
78
  ctx.clearRect(0, 0, 600, 160);
80
79
  renderer.render(ctx, frame);
@@ -83,7 +82,9 @@ const player = createPlayer(steps(), {
83
82
  document.querySelector('#next').onclick = () => player.nextStep();
84
83
  ```
85
84
 
86
- The first frame (`x = 40`) is drawn immediately inside `createPlayer(...)`. Each
85
+ After the last state, one more click detects generator exhaustion and keeps the final position. No lookahead is performed.
86
+
87
+ The first frame (`x = 40`) is drawn immediately inside `createStatePlayer(...)`. Each
87
88
  click requests the next state: the player resumes the loop until the next
88
89
  `yield`, then renders. There is no interpolation here: the handler draws only
89
90
  `current`. Each snapshot is created afresh and never mutated. Call
@@ -101,13 +102,13 @@ the [local setup instructions](https://github.com/urffin/algiviz/blob/master/doc
101
102
  - **Scene:** describes objects with stable IDs, types and application-owned data.
102
103
  - **Handler:** chooses geometry and drawing behavior, including interpolation.
103
104
 
104
- Do not mutate emitted states. Players require an explicit terminal step;
105
- call `dispose()` when removing a visualization. Algorithms, layouts and animation
105
+ Do not mutate emitted states. `createStatePlayer` completes on source exhaustion;
106
+ `createPlayer` requires an explicit terminal step. Call `dispose()` when removing a visualization. Algorithms, layouts and animation
106
107
  scheduling belong to the application.
107
108
 
108
109
  ## Playback modes
109
110
 
110
- | | Generator: `createPlayer` | History: `createTimeline` |
111
+ | | Generator: `createPlayer` / `createStatePlayer` | History: `createTimeline` |
111
112
  | --- | --- | --- |
112
113
  | Steps | Consumed on demand | Collected up front |
113
114
  | Retention | Adjacent snapshots | Complete history |
@@ -141,6 +142,7 @@ Generators reduce retained history, not the cost of producing each snapshot.
141
142
  | [Binary search](https://github.com/urffin/algiviz/blob/master/examples/binary-search.html) | First occurrence in sorted input, closed search intervals and lazy playback |
142
143
  | [Graph BFS](https://github.com/urffin/algiviz/blob/master/examples/graph.html) | Queue, layers, shortest paths, unreachable targets and history/generator playback |
143
144
  | [Graph DFS](https://github.com/urffin/algiviz/blob/master/examples/dfs.html) | Recursive call frames, backtracking, tree depth and paths; history/generator playback |
145
+ | [Maze](https://github.com/urffin/algiviz/blob/master/examples/maze.html) | Randomized DFS generation, BFS wave, DFS backtracking, cycles and unreachable exits; history/generator playback |
144
146
  | [Tree traversal](https://github.com/urffin/algiviz/blob/master/examples/tree.html) | Custom nodes, edges and pointer with Canvas and retained SVG backends |
145
147
 
146
148
  [Full catalog, local setup and algorithm details](https://github.com/urffin/algiviz/blob/master/docs/examples.md).
@@ -1,7 +1,8 @@
1
1
  import type { Item, ArrayStep } from "./types.js";
2
+ /** Omitted or undefined markers preserve their current values. */
2
3
  export interface SortedRegions {
3
- readonly sortedPrefixLength?: number;
4
- readonly sortedSuffixLength?: number;
4
+ readonly sortedPrefixLength?: number | undefined;
5
+ readonly sortedSuffixLength?: number | undefined;
5
6
  }
6
7
  export interface ArrayTrace {
7
8
  readonly length: number;
@@ -54,6 +54,8 @@ export function createArrayTrace(values) {
54
54
  },
55
55
  swap(left, right, update) {
56
56
  active();
57
+ index(left);
58
+ index(right);
57
59
  const a = item(left), b = item(right);
58
60
  if (left === right)
59
61
  throw new RangeError("Swap requires distinct positions");
@@ -74,8 +76,9 @@ export function createArrayTrace(values) {
74
76
  },
75
77
  shift(from, to, update) {
76
78
  active();
77
- const moved = item(from);
79
+ index(from);
78
80
  index(to);
81
+ const moved = item(from);
79
82
  if (slots[to] !== null)
80
83
  throw new Error("Shift destination must be empty");
81
84
  regions(update);
@@ -1,4 +1,5 @@
1
1
  export { createPlayer } from "./player.js";
2
+ export { createStatePlayer } from "./state-player.js";
2
3
  export type { Player } from "./player.js";
3
4
  export { createTimeline } from "./timeline.js";
4
5
  export type { Step, Frame, Timeline } from "./types.js";
@@ -1,3 +1,4 @@
1
1
  export { createPlayer } from "./player.js";
2
+ export { createStatePlayer } from "./state-player.js";
2
3
  export { createTimeline } from "./timeline.js";
3
4
  export { createStepEmitter } from "./step-emitter.js";
@@ -21,3 +21,5 @@ export declare function createPlayer<S, E>(steps: Iterable<Step<S, E>>, options:
21
21
  /** Called initially and on each advance or nextStep, including after completion. */
22
22
  render?: (frame: Frame<S, E>) => void;
23
23
  }): Player<S, E>;
24
+ /** Internal engine shared by explicit-step and exhaustion-based sources. */
25
+ export declare function createPlayback<S, E>(steps: Iterable<Step<S, E>>, options: Parameters<typeof createPlayer<S, E>>[1], finishOnExhaustion?: boolean): Player<S, E>;
@@ -1,5 +1,9 @@
1
1
  /** Forward-only playback. The source must reach a step accepted by isTerminal. */
2
2
  export function createPlayer(steps, options) {
3
+ return createPlayback(steps, options);
4
+ }
5
+ /** Internal engine shared by explicit-step and exhaustion-based sources. */
6
+ export function createPlayback(steps, options, finishOnExhaustion = false) {
3
7
  const manual = options.stepDurationMs === undefined;
4
8
  // Unit endpoint for manual steps; positive timed advancement is disallowed.
5
9
  const { stepDurationMs = 1, finalHoldMs = 0 } = options;
@@ -38,8 +42,10 @@ export function createPlayer(steps, options) {
38
42
  let hold = 0;
39
43
  let disposed = false;
40
44
  let finished = false;
45
+ let exhausted = false;
46
+ const terminal = () => exhausted || options.isTerminal(current);
41
47
  const complete = () => {
42
- if (options.isTerminal(current) && elapsed === stepDurationMs) {
48
+ if (terminal() && elapsed === stepDurationMs) {
43
49
  previous = current;
44
50
  close();
45
51
  finished = hold >= finalHoldMs;
@@ -67,15 +73,20 @@ export function createPlayer(steps, options) {
67
73
  try {
68
74
  if (!finished) {
69
75
  if (elapsed === stepDurationMs) {
70
- if (options.isTerminal(current)) {
76
+ if (terminal()) {
71
77
  hold = finalHoldMs;
72
78
  }
73
79
  else {
74
80
  const next = iterator.next();
75
- if (next.done)
76
- throw new RangeError("Step source ended without a terminal step");
77
- previous = current;
78
- current = next.value;
81
+ if (next.done) {
82
+ if (!finishOnExhaustion)
83
+ throw new RangeError("Step source ended without a terminal step");
84
+ exhausted = true;
85
+ }
86
+ else {
87
+ previous = current;
88
+ current = next.value;
89
+ }
79
90
  }
80
91
  }
81
92
  // Assign the endpoint directly: time arithmetic can undershoot or
@@ -106,14 +117,19 @@ export function createPlayer(steps, options) {
106
117
  let remaining = deltaMs;
107
118
  while (remaining > 0 && !finished) {
108
119
  if (elapsed === stepDurationMs) {
109
- if (options.isTerminal(current)) {
120
+ if (terminal()) {
110
121
  hold += Math.min(remaining, finalHoldMs - hold);
111
122
  complete();
112
123
  break;
113
124
  }
114
125
  const next = iterator.next();
115
- if (next.done)
116
- throw new RangeError("Step source ended without a terminal step");
126
+ if (next.done) {
127
+ if (!finishOnExhaustion)
128
+ throw new RangeError("Step source ended without a terminal step");
129
+ exhausted = true;
130
+ complete();
131
+ continue;
132
+ }
117
133
  previous = current;
118
134
  current = next.value;
119
135
  elapsed = 0;
@@ -0,0 +1,9 @@
1
+ import { type Player } from './player.js';
2
+ import type { Frame } from './types.js';
3
+ /** Lazy state playback. Exhaustion is detected on the request after the last state.
4
+ * States must be immutable snapshots. Generator return values are ignored. */
5
+ export declare function createStatePlayer<S>(states: Iterable<S>, options?: {
6
+ stepDurationMs?: number | undefined;
7
+ finalHoldMs?: number | undefined;
8
+ render?: (frame: Frame<S, undefined>) => void;
9
+ }): Player<S, undefined>;
@@ -0,0 +1,36 @@
1
+ import { createPlayback } from './player.js';
2
+ /** Lazy state playback. Exhaustion is detected on the request after the last state.
3
+ * States must be immutable snapshots. Generator return values are ignored. */
4
+ export function createStatePlayer(states, options = {}) {
5
+ function* steps() {
6
+ const iterator = states[Symbol.iterator]();
7
+ let index = 0, exhausted = false, failed = false;
8
+ try {
9
+ while (true) {
10
+ const item = iterator.next();
11
+ if (item.done) {
12
+ exhausted = true;
13
+ return;
14
+ }
15
+ yield { index: index++, state: item.value, event: undefined };
16
+ }
17
+ }
18
+ catch (error) {
19
+ failed = true;
20
+ throw error;
21
+ }
22
+ finally {
23
+ if (!exhausted) {
24
+ if (failed) {
25
+ try {
26
+ iterator.return?.();
27
+ }
28
+ catch { /* Preserve source failure. */ }
29
+ }
30
+ else
31
+ iterator.return?.();
32
+ }
33
+ }
34
+ }
35
+ return createPlayback(steps(), { ...options, isTerminal: () => false }, true);
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grundyjs/algiviz",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "TypeScript toolkit for custom algorithm visualizations with step-by-step playback and extensible rendering",
5
5
  "type": "module",
6
6
  "files": [
package/ru/README.md CHANGED
@@ -54,14 +54,14 @@ npm install @grundyjs/algiviz
54
54
  ```
55
55
 
56
56
  ```js
57
- import { createPlayer } from '@grundyjs/algiviz/playback';
57
+ import { createStatePlayer } from '@grundyjs/algiviz/playback';
58
58
  import { createSceneRenderer } from '@grundyjs/algiviz/scene';
59
59
 
60
60
  const ctx = document.querySelector('canvas').getContext('2d');
61
- function* steps() {
62
- for (const [index, x] of [40, 200, 360, 560].entries()) {
61
+ function* states() {
62
+ for (const x of [40, 200, 360, 560]) {
63
63
  const state = { objects: [{ id: 'circle', type: 'dot', data: { x } }] };
64
- yield { index, state, event: index === 3 ? 'done' : 'position' };
64
+ yield state;
65
65
  }
66
66
  }
67
67
  const renderer = createSceneRenderer({
@@ -72,8 +72,7 @@ const renderer = createSceneRenderer({
72
72
  ctx.fill();
73
73
  },
74
74
  });
75
- const player = createPlayer(steps(), {
76
- isTerminal: step => step.event === 'done',
75
+ const player = createStatePlayer(states(), {
77
76
  render(frame) {
78
77
  ctx.clearRect(0, 0, 600, 160);
79
78
  renderer.render(ctx, frame);
@@ -82,7 +81,9 @@ const player = createPlayer(steps(), {
82
81
  document.querySelector('#next').onclick = () => player.nextStep();
83
82
  ```
84
83
 
85
- Первый кадр (`x = 40`) рисуется сразу внутри `createPlayer(...)`. Каждый клик
84
+ После последнего состояния ещё один клик обнаруживает конец генератора и сохраняет позицию круга. Чтения вперёд нет.
85
+
86
+ Первый кадр (`x = 40`) рисуется сразу внутри `createStatePlayer(...)`. Каждый клик
86
87
  запрашивает следующее состояние: плеер возобновляет цикл до следующего
87
88
  `yield`, затем вызывает отрисовку. Здесь нет плавного перехода: обработчик рисует
88
89
  только `current`. Каждый снимок создаётся заново и больше не изменяется. При
@@ -99,13 +100,14 @@ document.querySelector('#next').onclick = () => player.nextStep();
99
100
  - **Сцена** описывает объекты: стабильный `id`, тип и собственные данные.
100
101
  - **Обработчик** выбирает геометрию и способ рисования. Плавность — его решение.
101
102
 
102
- Состояния после выдачи нельзя изменять. Плееру нужен явный завершающий шаг;
103
+ Состояния после выдачи нельзя изменять. `createStatePlayer` завершается при исчерпании
104
+ источника; `createPlayer` требует явного завершающего шага;
103
105
  при удалении визуализации вызовите `dispose()`. Алгоритмы, раскладка и цикл
104
106
  анимации принадлежат приложению.
105
107
 
106
108
  ## Режимы воспроизведения
107
109
 
108
- | | Генератор: `createPlayer` | История: `createTimeline` |
110
+ | | Генератор: `createPlayer` / `createStatePlayer` | История: `createTimeline` |
109
111
  | --- | --- | --- |
110
112
  | Получение шагов | По мере продвижения | Все шаги заранее |
111
113
  | Хранение | Соседние снимки | Вся история |
@@ -139,6 +141,7 @@ document.querySelector('#next').onclick = () => player.nextStep();
139
141
  | [Бинарный поиск](https://github.com/urffin/algiviz/blob/master/examples/binary-search.html) | Первое вхождение в отсортированном массиве, замкнутый интервал поиска и ленивое воспроизведение |
140
142
  | [BFS графа](https://github.com/urffin/algiviz/blob/master/examples/graph.html) | Очередь, слои, кратчайшие пути, недостижимая цель и история/генератор |
141
143
  | [DFS графа](https://github.com/urffin/algiviz/blob/master/examples/dfs.html) | Стек вызовов, возвраты, глубина и путь дерева; история/генератор |
144
+ | [Лабиринт](https://github.com/urffin/algiviz/blob/master/examples/maze.html) | Генерация рандомизированным DFS, волна BFS, поиск DFS, циклы и недостижимый выход; история/генератор |
142
145
  | [Обход дерева](https://github.com/urffin/algiviz/blob/master/examples/tree.html) | Собственные узлы, рёбра и указатель с Canvas и SVG с сохранением элементов |
143
146
 
144
147
  [Полный каталог, локальный запуск и особенности алгоритмов](https://github.com/urffin/algiviz/blob/master/docs/ru/examples.md).