devcore_tests created;addrstore, filesystem bugfixes#4
Merged
5133n merged 1 commit intofrstrtr:origin/sharechain/async_threadfrom Apr 23, 2021
Merged
Conversation
frstrtr
added a commit
that referenced
this pull request
Apr 20, 2026
Extracts dashboard.html's getColor() (lines 5404-5428) and render()
(lines 4672-4763) paint body into coin-agnostic, pure-function
primitives plus a thin DOM-touching adapter. Palette values preserved
verbatim so M2 pixel-diff holds for LTC once the host page switches.
src/explorer/colors.ts (~110 LOC)
- type ShareClass = 'dead' | 'stale' | 'unverified' | 'fee' |
'native' | 'signaling' | 'legacy'
- classifyShare(share, ctx): ShareClass
Pure classifier with dashboard.html priority preserved:
dead > stale > unverified > fee > native > signaling > legacy
Threshold (coin.shareVersion) is a ctx parameter so Dash V16 and
LTC V36 share the same classifier (M1 D5).
- getColor(share, ctx, palette?): string
"Mine" variants (nativeMine, signalingMine, legacyMine) apply only
to the three version-tier classes; dead/stale/unverified/fee are
terminal-coloured regardless of address match. Empty or undefined
`ctx.myAddress` never marks a share as mine.
- LTC_COLOR_PALETTE: frozen ColorPalette — byte-identical values to
dashboard.html's hex strings. Tests assert parity.
- ColorsPlugin: id 'explorer.colors', provides 'explorer.colors'
capability exposing { classifyShare, getColor, LTC_COLOR_PALETTE }.
src/explorer/grid-paint.ts (~180 LOC)
- type PaintCommand = setTransform | fillBackground | fillCell |
strokeRect | textCenter
- buildPaintProgram(opts): PaintCommand[]
Pure function: takes a GridLayout + shares + userContext + palette
+ dpr, produces the exact ordered sequence of canvas ops that
dashboard.html's render() issues. Empty-chain path preserved (the
"Waiting for shares..." text). hoveredIndex appends a strokeRect.
- executePaintProgram(ctx, program): void
Applies commands to a CanvasLike (narrow interface covering the
eight context members we use) — easily mocked in Node for tests.
- createGridRenderer(canvas): { paint, buildProgram, destroy }
DOM-touching adapter. Sizes canvas for dpr per dashboard.html
(canvas.width = cssW*dpr; ctx.setTransform(dpr,0,0,dpr,0,0));
destroy() flips a local flag so subsequent paint() is a no-op.
- GridCanvasPlugin: id 'explorer.grid.canvas', provides
'renderer.grid.canvas', fills slot 'explorer.main.grid'.
Explorer bundle public API (src/explorer/index.ts):
- Re-exports ColorsPlugin + GridCanvasPlugin alongside the existing
GridLayoutPlugin.
- registerExplorerBaseline(host) now also registers them.
Tests:
- tests/unit/colors.test.ts (~21 tests)
Priority-order proofs for classifyShare, palette mapping for every
class including mine variants, threshold-from-context (Dash V16),
empty myAddress does NOT mark as mine, custom palette override,
verbatim palette value parity with dashboard.html:5404-5428.
- tests/unit/grid-paint.test.ts (~10 tests)
buildPaintProgram: empty chain emits setTransform+bg+emptyText;
one share emits fillCell with expected colour + geometry; mixed
shares (dead, signaling, native-mine) produce correct palette;
hoveredIndex appends strokeRect; negative/OOB hover ignored.
executePaintProgram: mock CanvasLike proves commands map to the
correct context calls in the correct order. Round-trip stress
test with 4320 shares + full mix exercises the hot path.
Status: 94/94 tests pass (63 + 21 + 10). Typecheck clean under
strict + exactOptionalPropertyTypes + noUncheckedIndexedAccess.
Bundles: shared-core 25.1 KB / 40 (63%), sharechain-explorer 29.8
KB / 120 (25%). Pipeline green.
Next: Phase B #3 — Delta merger (merge window + new shares with
fork-switch detection and windowSize slice). The algorithmic core
of real-time updates; strong candidate for property-based tests
against fast-check. Then #4 — Animator phases 1-3.
frstrtr
added a commit
that referenced
this pull request
Apr 20, 2026
Combines the end-to-end composition proof (demo page) with the
algorithmic core of real-time updates (delta merger). Two small
increments in one commit because they validate each other — the
demo shakes out papercuts that unit tests miss; the delta merger
is what the demo will drive once real-time mode lands.
Demo page (demo/)
- demo.html CSP-compliant static shell per delta v1
§B.3 (no inline script, no 'unsafe-eval');
header toolbar with add/rotate/reset
controls; canvas in a scrollable wrap.
- demo.mjs Pure ES module import from
../dist/sharechain-explorer.js. Seeded
mulberry32 RNG (seed=42) → deterministic
synthetic 4320-share chain across the
full palette mix (V36 native + V35→V36
signalling + V35 legacy + stale/dead/
unverified/fee + "mine" address). Wires
Host + registerExplorerBaseline + paint
+ mouse hit-test via cellAtPoint.
window.__demo exposes host/renderer/
getShares/paint for devtools poking.
- README.md How to run (python -m http.server 8080
then open demo.html). file:// note
because ESM imports require an origin.
Proves the following plugins compose without a c2pool server:
shared.transport.http, shared.theme.dark, shared.addr.hue-hash,
shared.treemap.squarified, shared.hashrate.si-units,
shared.i18n.en, explorer.grid.layout, explorer.colors,
explorer.grid.canvas — plus the full middleware chain.
Delta merger (src/explorer/delta.ts)
- mergeDelta(current, delta, opts) → MergeResult
Pure; reconciles §5.3 spec:
* prepend delta.shares (newest first)
* dedup by short-hash (both within delta AND against current)
* slice to windowSize, collecting evicted short-hashes
* fork_switch: true → rebuild from delta alone, evict whole
current window (with dedup + cap preserved)
Returns { shares, added, evicted, forkSwitch, tip } so callers
can drive animation decisions (Phase B #4).
- windowSize 0 + maxShares undefined = unlimited (useful for demos);
maxShares override controls the cap independently of windowSize.
- Opaque payload fields pass through — works with DeltaShare<T>
generic so callers can type their full share shape.
- DeltaMergerPlugin: id 'explorer.delta.merger-default',
provides 'merger.delta', registered via registerExplorerBaseline.
Tests (tests/unit/delta.test.ts — 19 tests)
Spec contract coverage: empty inputs, normal prepend with order
preservation, multi-share delta, opaque payload passthrough, dedup
both directions (delta vs current; within-delta), eviction math,
windowSize 0 unlimited, maxShares override, fork_switch rebuild
with cap + dedup + empty edge case, immutability of inputs,
1000-iteration saturation stress with duplicate-injection
invariant (no-duplicates after any merge sequence).
Status
- 113/113 tests pass in 2.2s (94 prior + 19 new)
- Typecheck clean
- Bundles: shared-core 25.1 KB / 40, sharechain-explorer 30.6 KB /
120 — both under budget
- HTTP probe confirms bundle + demo assets serve correctly
Next: Phase B #4 — Animator phases 1-3 (death/wave/birth); will
consume mergeDelta's { added, evicted } to drive transitions.
Or (parallel track) Qt refactor step 1 — CMake deps + option flags
in ui/c2pool-qt/CMakeLists.txt.
frstrtr
added a commit
that referenced
this pull request
Apr 20, 2026
Three-phase animation state machine (spec §6, dashboard.html:4866-
5400). This commit ships the core — timing math, stagger schedules,
position interpolation for dying/wave/born tracks, and the
controller (start/tick/queueNext/reset). Scale, colour-lerp,
particle dissolution and card overlays land in subsequent commits;
each is a clearly isolable addition that the pure-function structure
accommodates without redesign.
Phase timing (verbatim from dashboard.html:4977-4982):
phase1Dur = 3000 DYING
phase2Dur = fast ? 2000 : 4000 WAVE (spec §6 text has typo,
code is authoritative)
phase2Start = phase1Dur
phase3Start = phase2Start + phase2Dur * 0.7 (overlap)
phase3Dur = 3000 BORN
duration = phase3Start + phase3Dur
Stagger schedules (all preserved verbatim from dashboard.html):
DYING — last dying share first, 150 ms per share. Newer end of
the window dies visibly before older shares (mirrors the
natural tail-eviction order).
WAVE — tail-first per dashboard.html:5146-5151:
distFromTail = (N-1) - newIndex
fraction = distFromTail / (N-1)
shareStart = fraction * phase2Dur * 0.7
shareT = clamp01((p2elapsed - shareStart) / 600)
ease = 1 - (1-shareT)^3 (easeOut-cubic)
Tail starts first, head last; each share animates over a
600 ms window regardless of phase2Dur.
BORN — newest share first, 150 ms per share, full phase3Dur
window per share.
Input / output:
AnimationInput {
oldShares, newShares, addedHashes, evictedHashes,
oldLayout, newLayout, userContext, palette,
hashOf, fast?
}
AnimationPlan {
tEnd, phase1Start/Dur, phase2Start/Dur, phase3Start/Dur,
frameAt(t): FrameSpec
}
FrameSpec {
cells: CellFrame[], // per-share {x,y,size,color,alpha,track}
backgroundColor,
layout // post-merge layout
}
Controller:
createAnimationController() → {
isRunning(), start(plan, now), tick(now), queueNext(plan), reset()
}
- tick() returns the frame to paint, or null when idle.
- queueNext() during running: queued plan starts automatically on
next tick after current tEnd (§6 threshold rule #2, _animDeferred).
- queueNext() while idle: queued plan starts on the next tick.
- reset() drops current + queued.
Also exposed:
SKIP_ANIMATION_NEW_COUNT_THRESHOLD = 100 (§6 rule #1, callers
skip build/start when
newCount >= 100)
Helpers: clamp01, lerp, easeInOut, computePhaseTiming(fast?)
Plugin: explorer.animator.three-phase (provides 'animator.grid')
Tests (19): phase-timing constants for slow + fast; clamp/lerp/ease
sanity; empty input edge; fast vs slow tEnd; wave position
interpolation at t=0 and t=tEnd; wave stagger (tail moves before
head at early phase2 tick); dying staggered alpha decay; dying at
phase1 end alpha ~= 0; born spawns below grid and lands at (0,0);
born alpha fade-in over first 30%; controller idle tick; controller
start+tick+finish becomes idle; controller queueNext during running;
controller queueNext on idle starts next tick; controller reset
drops both.
Status
- 132/132 tests pass in 2.2s (113 prior + 19 new)
- Typecheck clean
- Bundles: shared-core 25.1 KB / 40, sharechain-explorer 33.9 KB /
120 — both under budget
- Pipeline green end-to-end via npm run verify
Next commits layered onto this core:
Phase B #5 scale effects (lift-slide-land wave scale, dying-rise
scale, born shrink from bornScale→1x). Requires no new
types — just richer CellFrame.size interpolation and
paint-program extension for centred-scale fillRect.
Phase B #6 colour lerp — dying lerps toward palette.dead across
its stagger window; born coalesces from unverified to
coin colour.
Phase B #7 particle effects (ash dissolution, birth coalescence)
and card overlays (miner addr + PPLNS % text during
hold frames). Largest increment — may split again.
Phase B #8 wire mergeDelta + animator + gridRenderer into a
RealTime plugin (SSE subscription + delta application
+ animation trigger). Unlocks the demo against a live
c2pool server.
frstrtr
added a commit
that referenced
this pull request
Apr 24, 2026
Crash reproducibly hit on first SML sync timer fire (PID dies right after the [SML] sync request log line, before any mnlistdiff arrives). Apport core dump bt: Thread 1 SIGSEGV #0 0x...3f0 (??) ← jumped to garbage #1 initiate_async_wait::operator()<std::function<...>&>(...) #2 main::{lambda(...)#3}::operator()(...) ← timer lambda body #3 wait_handler::do_complete(...) #4 scheduler::run(error_code&) #5 main Bug: the persistent std::function `sml_sync_tick` was passed BY LVALUE to async_wait, captured `&sml_sync_tick` for self-reference. boost:: asio's perfect-forwarding into the internal handler queue moved-from the lvalue on first dispatch (universal-reference deduction + std::forward semantics), leaving the outer std::function empty. The copy that ran on first fire then re-armed by passing the now-empty outer function — second fire dereferenced the empty std::function and SIGSEGV'd into garbage. Fix: hold the persistent std::function on the heap via shared_ptr, and NEVER pass it directly to async_wait. Instead, schedule via a fresh wrapper lambda that captures the shared_ptr by value and invokes (*sml_sync_tick)(ec) when fired. Each schedule() call hands async_wait a brand-new lambda; boost::asio can move-from the temporary as much as it wants without affecting the persistent function. Verified: pattern is the canonical chained-timer idiom. The shared_ptr keeps the tick function + captures alive across reschedules; each fresh wrapper lambda holds its own copy of the shared_ptr (refcount++) so the timer chain is self-sustaining until io_context exits.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.