@zakkster/lite-raf
v1.3.1
Published
Zero-GC, frame-rate scheduling for @zakkster/lite-signal. A single requestAnimationFrame loop, frameTime/frameDelta/frameCount plus fps/jank as reactive signals, rafEffect() with pre/normal/post priority lanes, auto-pause on hidden tabs, and lerp/damp int
Maintainers
Readme
@zakkster/lite-raf
Zero-GC frame-rate scheduling for the @zakkster/lite-signal reactive graph. One requestAnimationFrame loop exposes the frame clock as signals (frameTime, frameDelta, frameCount) plus fps/jank monitors, and rafEffect() runs reactive effects at most once per frame and every frame the loop ticks -- with priority lanes, opt-in demand-driven sleep, and hostile-timestamp discipline. ESM-only. Peer-depends on @zakkster/lite-signal. Zero runtime deps. MIT.
The frame loop the signal ecosystem was missing
Reactive libraries are built for event cadence: a value changes, dependents re-run immediately. Render loops are built for frame cadence: do the work once per frame, no matter how many things changed. Preact Signals, SolidJS, and alien-signals all target the DOM/event side and leave the animation frame to you -- a hand-rolled requestAnimationFrame with its own dirty flag, its own lastTime math, and its own "did I already schedule a redraw?" bookkeeping, drifting out of sync with the state graph it is supposed to mirror.
lite-raf is that missing piece for @zakkster/lite-signal: it turns the animation frame into a reactive primitive. The frame clock becomes signals; rafEffect() becomes an effect that the loop coalesces to one run per frame; the dirty flag becomes the dependency graph, which cannot drift because it is the same graph driving the rest of your app.
npm install @zakkster/lite-raf @zakkster/lite-signal@zakkster/lite-signal is a peer dependency (install it alongside), and that is a correctness requirement, not a formality -- see the registry note.
import { signal } from '@zakkster/lite-signal';
import { rafEffect, frameDelta, startFrames } from '@zakkster/lite-raf';
const ctx = canvas.getContext('2d');
const x = signal(0);
const vx = 120; // px/sec
// Runs once per frame, integrating against the real frame delta.
rafEffect(() => {
x.update(px => px + vx * (frameDelta() / 1000));
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(x.peek(), 100, 40, 40);
});
startFrames();No requestAnimationFrame boilerplate, no manual lastTime/delta bookkeeping, no redraw-dedupe flag. The signal graph decides what re-runs; lite-raf decides when.
Table of contents
- Why this exists
- What you get
- Lanes, coalescing, and the fixed-rate-display trap
- API reference
- Registry note (read this before you import)
- Composability with the ecosystem
- Zero-GC design notes
- Design decisions worth knowing
- Testing
- What this is not
- Ecosystem
- License
Why this exists
Every canvas/WebGL project grows the same organ -- a hand-rolled frame loop:
// The loop you write first, in every project, slightly differently each time
let last = 0, rafId = 0, dirty = true;
function loop(now) {
rafId = requestAnimationFrame(loop);
const dt = last ? now - last : 0;
last = now;
if (!dirty) return; // ad-hoc "only redraw if something changed"
dirty = false;
update(dt);
render();
}
function markDirty() { dirty = true; } // call this from 14 places, forget it in 3
requestAnimationFrame(loop);It works until it doesn't. The dirty flag drifts out of sync with the actual state. Two systems both want to redraw and you get double-renders. You add a second requestAnimationFrame somewhere and now there are two loops. The delta math gets copy-pasted with subtle differences. A backwards or non-finite timestamp -- from a manual driver, a worker, or clock skew -- poisons every dt integration downstream.
If your state already lives in a reactive graph (@zakkster/lite-signal), the dependency tracking that powers your UI can power your render loop too. You read a signal inside a rafEffect; when it changes, the effect is marked for re-run; at the frame boundary it runs once, regardless of how many of its inputs changed. The dirty flag becomes the signal graph, which cannot drift because it is the same graph driving everything else.
What you get
rafEffect(fn, options?)-- a lite-signal effect scheduled to the frame boundary. Runs at most once per frame, and every frame the loop ticks if it reads a frame signal.options.lane("pre"/"normal"/"post") orders imperative work within a frame. Returns an idempotent dispose. Costs exactly one pooled lite-signal node; dispose returns it.- Frame signals --
frameTime,frameDelta,frameCount(the clock) plusfps,jank(monitors), each a read-only accessor: callable for a tracked read,.peek()for untracked,.subscribe(fn)for value-now-and-on-change. No.set-- only the loop drives them. startFrames(options?)/stopFrames()-- arm and cancel the single rAF loop. Options:pauseOnHidden,jankThresholdMs,fpsSmoothing,maxDeltaMs,autoSleep. Every one is fail-closed on garbage.lerp(a, b, t)/damp(current, target, smoothing, dt)-- two pure, allocation-free interpolators for frame-rate-correct motion. Not a tween engine; the per-frame math you call from inside arafEffect.VERSION-- the package version string, one of the three places the version lives.
Full TypeScript definitions ship in RAF.d.ts.
Lanes, coalescing, and the fixed-rate-display trap
One loop, frame signals, priority lanes
startFrames() arms a single requestAnimationFrame. Each frame the loop writes the new (time, delta, count) -- plus the fps / jank monitors -- into the frame signals inside one batch, so any effect that depends on them is marked dirty and enqueued exactly once. lite-signal's flush hands each dirty rafEffect to lite-raf's scheduler, which parks it in the pre-allocated queue for its lane. The loop drains those lanes -- pre, then normal, then post -- at the end of the same frame. Effects scheduled during a drain (cascades) land beyond the snapshot and run next frame, so the once-per-frame guarantee holds while lane order is honored.
Lanes order imperative side effects within a frame: integrate physics in pre, draw in normal, read back / sample stats in post. Reactive cascades between rafEffects still resolve on the following frame -- a signal a pre effect writes is seen by a normal effect one frame later, not the same one.
"At most once per frame" -- the coalescing guarantee
Within a frame the three frame signals each change, and your own code may write its state signals many times. lite-signal's dirty-marking sets a QUEUED flag the first time an effect is scheduled and clears it only when the effect actually runs -- at the frame boundary. Every intervening write finds the flag already set and skips re-queuing. So an effect touched a thousand times between frames still runs once, with the final values. (Scene 01 of the demo hammers a signal 5000 times per frame and shows one run.)
The fixed-rate-display trap
This is the bug lite-raf was hardened against. A reactive signal normally short-circuits an unchanged write: set(x) where x equals the current value does nothing, because re-running dependents would be wasted work. That is correct for application state. It is catastrophic for a frame delta.
On a display locked to a stable refresh rate with a steady compositor, consecutive requestAnimationFrame deltas can be bit-identical -- 16.6666... ms, frame after frame. If frameDelta short-circuited on equality, the effect reading it would stop firing the moment two deltas matched, and the animation would freeze -- but only on some displays, for some users, while running perfectly on the developer's jittery laptop. The worst class of bug: environment-dependent and invisible in dev.
lite-raf creates the three clock signals with a forced-propagation equality (equals: () => false), so every frame ticks every dependent regardless of whether the numeric value repeated. fps / jank keep default equality (they are dashboard values and re-notify only on change). Scene 03 of the demo runs the naive short-circuiting wiring beside the real one and freezes the naive panel on a fixed-rate clock.
API reference
rafEffect(fn, options?) => () => void
Register a frame-scheduled effect. fn runs at the end of each frame in which a tracked dependency changed -- at most once per frame, and every frame the loop ticks if it reads any frame signal. Returns an idempotent dispose function.
import { rafEffect, frameDelta } from '@zakkster/lite-raf';
const stopPhysics = rafEffect(() => integrate(frameDelta()), { lane: 'pre' });
const stopDraw = rafEffect(() => renderer.draw(scene)); // normal lane| Behaviour | Detail |
|---|---|
| Cadence | At most once per frame; reading a frame signal makes it run every frame. |
| Lanes | options.lane is "pre", "normal" (default), or "post". Each frame drains pre -> normal -> post. |
| Lifecycle | Runs only while the loop is running. Created before startFrames(), it runs once on the first started frame with the latest values. |
| Disposal | The returned fn disposes it. A trampoline already queued for the current frame is neutralised by lite-signal's generation guard -- the body will not run after disposal. Disposing twice is inert. |
| Cascade latency | A rafEffect writing a signal another rafEffect reads schedules the downstream effect for the next frame, including across lanes. |
| Errors | A throw is caught, logged via console.error, and isolated -- sibling effects still run. |
Frame signals
Each is a read-only accessor: call it for a tracked read, .peek() for an untracked read, .subscribe(fn) for a value-now-and-on-change subscription returning an unsubscribe fn. They have no .set.
import { frameTime, frameDelta, frameCount, fps, jank } from '@zakkster/lite-raf';| Signal | Type | Meaning |
|---|---|---|
| frameTime | () => number | Current frame timestamp (ms, DOMHighResTimeStamp). Pass-through from the driver. |
| frameDelta | () => number | Ms since the previous frame. 0 on the first frame. Your dt. Floored at 0 and clamped to maxDeltaMs; always finite. |
| frameCount | () => number | Frames since module load. 32-bit; wraps negative after ~414 days @ 60fps. |
| fps | () => number | Smoothed FPS, rounded (EMA of 1000/frameDelta). Re-notifies only when the integer changes -- not a per-frame firehose. |
| jank | () => number | Cumulative count of frames whose raw delta exceeded jankThresholdMs (default 50). A clamped hitch is still counted. |
frameTime / frameDelta / frameCount force-propagate every frame; fps / jank use ordinary equality.
Loop control
import { startFrames, stopFrames } from '@zakkster/lite-raf';| Function | Description |
|---|---|
| startFrames(options?) | Start the loop. A bare startFrames() while running is an idempotent no-op; startFrames(options) while running throws (the options would be silently ignored -- call stopFrames() first). Resets the delta baseline (first frame's delta is 0) and re-seeds the fps average. |
| stopFrames() | Stop the loop and cancel the pending frame. Effects are retained and resume on the next startFrames(). Removes the visibility listener and (under autoSleep) the frame-signal observer hooks. |
startFrames options:
| Option | Type | Default | Description |
|---|---|---|---|
| pauseOnHidden | boolean | false | Pause while the tab is hidden (Page Visibility) and resume on return, resetting the baseline so the hidden gap is not one giant delta. No-op where document is unavailable. |
| jankThresholdMs | number | 50 | Raw-delta threshold (ms) above which a frame counts toward jank. Must be finite and > 0. |
| fpsSmoothing | number | 0.1 | EMA factor for fps. Must be finite and in (0, 1]. |
| maxDeltaMs | number | Infinity | Opt-in upper clamp (ms) on the published frameDelta. Infinity = unclamped (1.1.x behavior). jank/fps still judge the raw delta. |
| autoSleep | boolean | false | Opt-in demand-driven sleep. When on, the loop parks itself when nothing is drawing and wakes on demand -- see Design decisions. Default OFF is byte-identical to the always-on clock. |
Interpolation helpers
import { lerp, damp } from '@zakkster/lite-raf';| Function | Description |
|---|---|
| lerp(a, b, t) | Linear interpolation. Unit-agnostic; t not clamped (pass outside 0..1 to extrapolate). |
| damp(current, target, smoothing, dt) | Frame-rate-independent exponential approach. Unlike lerp(current, target, 0.1) per frame -- whose speed depends on frame rate -- this converges at the same real-world rate at 30 or 144 fps. Pass dt in seconds (frameDelta() * 0.001); smoothing is an approximate rate (higher = snappier). |
rafEffect(() => {
camera.x = damp(camera.x, targetX(), 8, frameDelta() * 0.001);
});Constants and defaults
| Constant | Value | Meaning |
|---|---|---|
| VERSION | "1.3.1" | Package version string (exported). |
| Lanes | "pre", "normal", "post" | The three drain lanes, in order. "normal" is the rafEffect default. |
| Queue capacity | 4096 | Per-lane pre-allocated slots; grows once (and rarely) if a single lane schedules more in one frame, then is reused forever. |
| jankThresholdMs default | 50 | Frames slower than this count toward jank. |
| fpsSmoothing default | 0.1 | EMA factor for fps. |
| maxDeltaMs default | Infinity | Unclamped published delta (1.1.x behavior). |
Validation surface
A fail-closed options door guards both entry points, entirely on the cold path (call time, never in the frame loop):
- Unknown option key ->
TypeErrorwith a Levenshtein did-you-mean hint ({ pauseOnHiden: true }namespauseOnHidden). - Wrong type ->
TypeError(non-booleanpauseOnHidden/autoSleep, non-finite numeric option, non-functionfn; a missing globalrequestAnimationFrame/cancelAnimationFrameis checked BEFORE any state mutation). - Out of range ->
RangeError(jankThresholdMs/maxDeltaMsnot> 0,fpsSmoothingoutside(0, 1], an unknownlane). startFrames(options)while running ->Error(the options would be silently dropped).
Every message is prefixed lite-raf: and names the offending value. null is rejected as null, never coerced to a default; an explicit undefined is treated as absent. See Design decisions.
Registry note (read this before you import)
lite-raf's frame signals live in lite-signal's default registry, which is module-level singleton state. Two consequences that are invisible at the call site and silent when wrong:
A nested second copy of lite-signal breaks everything, silently. If lite-signal resolved to a copy nested under lite-raf instead of the shared top-level one, the frame clock would live in a different reactive graph than your app's signals and nothing would connect. Installing lite-signal as a peer dependency guarantees the single shared instance. Install both at the top level.
Five signals mint at module evaluation, so import order is load-bearing (H-F).
frameTime,frameDelta,frameCount,fps, andjankare created the momentRAF.jsis evaluated. If you swap the default registry withsetDefaultRegistry()after importing lite-raf, those five signals are stranded in the old graph: every dependent effect reads correctly once and then never re-runs -- no error, no warning. CallsetDefaultRegistry()before the first import of lite-raf:
import { createRegistry, setDefaultRegistry } from '@zakkster/lite-signal';
setDefaultRegistry(createRegistry({ maxNodes: 8192, onCapacityExceeded: 'grow' }));
// Only now import lite-raf -- its five signals mint into the registry you chose.
const { rafEffect, startFrames } = await import('@zakkster/lite-raf');This exact failure mode is asserted in a fresh subprocess by the torture gate (test/torture/t8-cross.mjs): importing lite-raf mints exactly five nodes in the default registry, and the stranded-clock poison reproduces as described. Consumers on a capped registry must count those five.
Composability with the ecosystem
lite-raf is the frame lane of the @zakkster stack. Five published packages already run on it. Here is a motion pipeline -- app state to reactive spring to canvas -- against the real peer APIs (read from each peer's llms.txt, not invented):
import { signal } from '@zakkster/lite-signal';
import { rafEffect, frameDelta, startFrames } from '@zakkster/lite-raf';
import { springSignal } from '@zakkster/lite-signal-spring';
import { createScene, circle } from '@zakkster/lite-scene';
// 1. App state is plain signals.
const targetX = signal(100);
// 2. lite-signal-spring springs a value toward the target ON lite-raf's loop:
// it subscribes to frameDelta and ticks the value once per frame while in
// motion, snapping and unsubscribing at rest (a settled spring costs nothing).
const x = springSignal(targetX, { stiffness: 170 });
// 3. lite-scene draws reactively -- pass the spring accessor straight as a prop;
// the scene redraws only when x changes, coalesced to one draw per tick.
const scene = createScene(canvas);
scene.add(circle({ x: () => x(), y: 120, radius: 24, fill: '#00c853' }));
// 4. Ordering within the frame is lite-raf's job: integrate extra state in the
// pre lane, sample stats in post (after the draw has happened).
rafEffect(() => { /* integrate against frameDelta() */ }, { lane: 'pre' });
rafEffect(() => { /* read back / sample stats */ }, { lane: 'post' });
// 5. One loop drives all of it.
startFrames();
// Redirect the spring by flipping a signal -- velocity is preserved mid-flight.
button.onclick = () => targetX.set(400);For a DI-wired systems route -- classes with an update(dt, time) method resolved once into flat per-lane arrays and driven by index -- reach for @zakkster/lite-di-ticker, which binds three rafEffect lanes over lite-raf and hard-gates 0 B/frame on its synchronous tick path:
import { Container } from '@zakkster/lite-di-container';
import { Ticker } from '@zakkster/lite-di-ticker';
const c = new Container(); // Ticker requires a DI container
c.value('world', myWorld);
c.value('scene', myScene);
const ticker = new Ticker(c);
ticker.system('physics', PhysicsSystem, { lane: 'pre', deps: ['world'] }) // pre-boot
.system('render', RenderSystem, { lane: 'normal', deps: ['scene'] });
c.boot();
ticker.start(); // resolves each lane once, binds rafEffect lanes, runs the loopEvery stage passes reactive accessors to the next; every stage is zero-GC on the hot path; the single frame clock keeps them all on one rAF.
Zero-GC design notes
The loop core allocates nothing per frame: each lane queue is one pre-grown array reused forever, the per-frame signal writes go through a single hoisted applyFrame function (no closure captured per frame), the lane schedulers are built once at module load, and the loop re-arms with the same loop reference each frame. autoSleep's park/wake machinery is entirely cold -- the frame body carries exactly two branches (if (parked) in the scheduler push, if (autoSleep) at the end of loop), both of which are dead when the feature is off.
| Operation | Steady-state allocations |
|---|---|
| Loop core per frame (clock writes, lane drains) | 0 |
| rafEffect dispatch per active effect per frame | 0 -- lite-signal caches node.schedulerThunk per effect and reuses it, so no per-frame dispatch closure is minted |
| lerp / damp | 0 (pure arithmetic) |
| maybePark / wake (autoSleep on) | 0 -- pure reads of lane lengths + hasObservers |
| Lane queue growth | once, if a single lane ever exceeds 4096 in one frame, then reused |
| Option validation (startFrames / rafEffect) | cold path, reachable only from a throw |
The torture harness (@zakkster/lite-leak + @zakkster/lite-gc-profiler, under --expose-gc) commits these as gated numbers, so a regression fails CI as loudly as a leak:
- Steady-state budget (
test/torture.mjs, autoSleep OFF):leak=size 0/0 findings=0 warnings=0 | gc major=0 minor=0 maxMs=0.00 | alloc=0.000 B/op. - autoSleep ON budget (
test/torture/t4-autosleep-alloc.mjs): ticking-without-parking0.034 B/opand repeated park/wake0.047 B/op, bothmajor=0-- noise-floor readings under the same unwidened ceiling as the OFF path. - Frozen-surface differential (A3): with
autoSleep:false, the run log is byte-identical to the pinned 1.2.1 baseline across 0 / 10,000 tuple mismatches.
For a render loop, "zero retained" is the number that matters: it is what lets an overlay run for an eight-hour stream without a slow climb into a GC death-spiral. The reproducible bench (npm run bench, deterministic frame clock, full-GC before/after) measures total retained growth of just a few kilobytes over 200,000 frames -- on the order of a few hundredths of a byte per frame (roughly 0.02-0.05, and no higher at 100 effects than at 10). That spread is GC measurement noise around zero, not accumulation: the frame path retains nothing.
Comparison against gsap.ticker (Node + synthetic pump -- a comparison, not an absolute). On an identical 210,000-tick workload -- same pump, no-op consumer -- the lite-raf frame body allocated 47.8 B/frame against gsap.ticker's 102.3 B/frame, with 0 major / 0 minor GC over 200,000 frames (reported by the scratch-card game integration BRIEF). Because these are Node numbers off a synthetic pump, treat the ratio as directional, not a browser guarantee. A rafEffect costs exactly one pooled lite-signal node; dispose returns it. And the exports map resolves identically under Node, webpack, and a browser importmap (node / import / default all -> ./RAF.js) -- no dual-build trap.
Design decisions worth knowing
- Force-propagate equality on the clock signals.
frameTime/frameDelta/frameCountare created withequals: () => falseso a bit-identical delta on a fixed-rate display still fires every dependent -- the fixed-rate-display trap.fps/jankkeep default equality; they are dashboards, not per-frame firehoses. - Lane lengths are snapshotted after the flush. Each frame the loop snapshots the three lane lengths after the batch flush populates them, then drains in order. Effects scheduled during a drain land beyond the snapshot and run next frame -- so lanes order imperative side effects within a frame while reactive cascades still resolve one frame later. This is what keeps the once-per-frame contract and the lane order both true.
- Delta discipline: no hostile timestamp reaches an integrator.
frameDeltais floored at 0 (a backwards stamp is not negative elapsed time) and clamped tomaxDeltaMs; a non-finite stamp yields0and re-baselines the next frame (one poisoned frame, not two).jank/fpsjudge the raw pre-clamp delta so a clamped hitch is still counted. See decisions/0004-delta-discipline.md. - Fail-closed options, named errors. Garbage input throws by name with the offending value and a did-you-mean hint, all on the cold path; a valid call is bit-identical to before the door existed. See decisions/0003-fail-closed-options.md.
- autoSleep is graph-derived, not refcounted. The park decision is re-derived every frame from the lite-signal graph (lane lengths +
hasObserverson the five raw frame nodes), so there is no counter to underflow -- disposing one effect twice while another animates keeps the loop arming. Wake is edge-triggered and idempotent from three sources (scheduler push, first-observer, visibility resume). See decisions/0005-autosleep.md. - The peer range is load-bearing in both directions. The fix that unbroke
.peek()on lite-signal >= 1.2.0 works on both peek generations; gates run against the freshly resolved peer, and the demo importmap never pins below the tested floor. See decisions/0001-peer-range.md.
Testing
77 tests across the suite -- 74 deterministic (a manual frame clock drives everything, no wall-clock flake) plus 3 GC-gated zero-alloc contract tests that auto-skip without --expose-gc -- and a tiered torture gate that proves both leak-freedom and the zero-alloc budget.
npm test # node --test test/*.test.js (74 pass + 3 GC-gated skips)
npm run test:gc # adds --expose-gc so the zero-GC contract tests run (77)
npm run torture # node --expose-gc test/torture.mjs; lite-leak + lite-gc-profiler, prints "ok"
npm run bench # node --expose-gc bench/bench.js; dispatch cost + retained-heap table
npm run verify # test:gc + torture -- the publish gateThe suites cover: frame signals and the once-per-frame contract, lane ordering, cascade lag, error isolation, lifecycle across stop/start, the fps/jank default-equality contract, frameDelta/frameCount force-propagation, pauseOnHidden edges, lerp/damp math, the fail-closed validation surface, hostile-timestamp discipline (backwards / non-finite stamps), the frozen-surface differential against the pinned 1.2.1 baseline, autoSleep parity and wake sources, a docs-drift guard (this README and llms.txt cannot cite an export the code dropped), and the torture tiers (T0 laws, T1 degenerate, T3 adversarial, T4 autoSleep alloc, T5 differential fuzz, T6 alloc gate, T7 soak, T8 cross-package, T9 controls). The torture runner prints exactly ok on exit 0; LITE_RAF_TORTURE_BREAK=1 proves the gate can fail.
What this is not
lite-raf schedules and times; it does not draw, tween, or simulate. Each of these is owned elsewhere in the stack:
| Not this | Reach for |
|---|---|
| Fixed-timestep physics stepping, pausable timers, tab-switch guard | @zakkster/lite-ticker (run an accumulator inside a rafEffect for a recipe) |
| timeScale / slow-mo, deterministic sim clocks | @zakkster/lite-clock |
| Tweening, timelines, sequencing | @zakkster/lite-tween-pro, @zakkster/lite-timeline, @zakkster/lite-keyframe |
| smoothDamp, spring physics, the full interpolation math | @zakkster/lite-lerp, @zakkster/lite-spring, @zakkster/lite-signal-spring |
| FPS/jank presentation and classification | @zakkster/lite-fps-meter (widget), @zakkster/lite-profiler (metrics) |
| Frame-budget priority scheduling | @zakkster/lite-scheduler |
| DI systems over lanes | @zakkster/lite-di-ticker |
| Multiple independent clocks / per-component loops | not in 1.x -- one loop, one frame clock (module singletons). createFrameScheduler(registry) is the natural extension if a real workflow needs it. |
lerp/damp are bare interpolators, deliberately not extended into a math kit; for the full set, use @zakkster/lite-lerp.
Ecosystem
Part of the @zakkster zero-GC stack. Five published packages peer-depend on lite-raf and make the case better than any copy:
lite-signal-- the reactive graph lite-raf schedules on (peer dependency)lite-signal-spring-- reactive spring values, driven on lite-raf'sframeDeltalite-di-ticker-- DI-wired system ticker over lite-raf's three laneslite-camera-max-- camera rig running on the frame looplite-depth-- depth/parallax layers on the frame clocklite-gl-- WebGL render loop on lite-raflite-scene-- reactive Canvas2D scene graph; pass it a frame-scheduled drawlite-raf-- this package
License
MIT (c) Zahary Shinikchiev [email protected]
