Publish npm packages workflow's "Verify version sync" guard correctly aborted (packages/*/package.json still 0.22.3 ≠ tag 0.22.4) and the [@netipar](https://github.com/netipar)/chunky-{core,vue3,react,alpine} clients were never published for 0.22.4. This release bumps all four workspace packages to 0.22.5 so the npm publish goes out and the npm clients are back in lockstep with the Composer package on Packagist. No functional change beyond the v0.22.4 migration-ordering fix — purely a release-process correction. The migration fix itself remains correctly published in v0.22.4 on Packagist.create_chunky_batches_table and create_chunked_uploads_table shipped without date prefixes, so they sorted after 2026_05_02_000000_add_chunked_uploads_batch_id_foreign_key — both under loadMigrationsFrom() autoload (database tracker) and after vendor:publish, because Laravel's publisher only re-stamps already-dated migrations and leaves undated ones untouched. The foreign-key migration therefore ran before its tables existed and failed with SQLSTATE[42S02] ... Base table or view not found: 'chunked_uploads'. The SQLite test suite never caught it because the FK migration early-returns on SQLite. The two create migrations are now dated 2026_05_01_000000 / 2026_05_01_000001, guaranteeing they run before the 2026_05_02_000000 FK migration in both the autoloaded and the published scenario. The existing Schema::hasTable() guards keep the rename idempotent for installs that already ran the old filenames.chunky.assembly.connection — the assemble job's queue connection is now configurable via CHUNKY_ASSEMBLY_CONNECTION, mirroring the existing chunky.assembly.queue knob. Set it to sync to run assembly in-process (no queue worker required) — useful for dev environments, small uploads, or apps that don't run a worker — without flipping the global QUEUE_CONNECTION and forcing every other queued job in the host app onto sync. A named connection from config/queue.php (e.g. redis-uploads, sqs-large-files) routes just the chunky assembly off the default queue. null (default) keeps the previous behaviour: the job uses config('queue.default'). Six regression tests cover the default / explicit / empty-string / queue + connection combinations and the existing tries / backoff / timeout wiring.Assembly job section in docs/configuration.md documenting the full chunky.assembly.* namespace, plus two new recipes: "Synchronous assembly (no queue worker)" and "Dedicated upload queue".AbstractChunkyEvent::broadcastWhen() falls back to a hard-coded default map when the host app's published config/chunky.php omits the broadcasting.events sub-array. Apps that upgraded from pre-0.18 chunky and never re-published their config still had broadcasting.enabled = true plus the old flat keys, but no events map — Laravel's mergeConfigFrom only merges top-level keys, so the published broadcasting array silently nuked the per-event flags shipped with v0.22. Result: every Chunky event silently never broadcast, including UploadFailed (the one a media-browser-style frontend relies on to surface validation errors as a visible state instead of hanging on "processing"). The four completion events (UploadCompleted, UploadFailed, BatchCompleted, BatchPartiallyCompleted, BatchCancelled) are now hard-coded default-on, matching the shipped config; high-frequency events stay default-off. Three regression tests cover the published-config-without-events-map, explicit-override, and globally-disabled paths.BatchMetadata::__construct() accepts CarbonImmutable. The expiresAt parameter was annotated ?Illuminate\Support\Carbon, which made the constructor fatally reject CarbonImmutable instances. Apps that opt into immutable dates via Date::use(CarbonImmutable::class) (Laravel default in newer 11.x boilerplates, explicit in many production apps) hit a TypeError on the very first batch upload because now() returns CarbonImmutable and ChunkyManager::initiateBatch() passes it straight through. The parameter is now typed ?\Carbon\CarbonInterface, the common parent of both Carbon and CarbonImmutable. Two regression tests cover both shapes.ChunkedUpload and ChunkyBatch [@property](https://github.com/property) annotations for completed_at / expires_at / created_at / updated_at switched from Carbon to CarbonInterface. PHPStan now infers the right type regardless of the host app's date factory choice.Closing-the-gaps minor — every remaining item from the v0.17 deep audit, plus React parity, hardened CI, and the long-promised release-via-workflow_dispatch automation.
SimpleDirectoryContext class form of the registry's "simple"
flow (src/Contexts/SimpleDirectoryContext.php). The
ContextRegistry::registerSimple() helper now instantiates it
rather than carrying inline closures, which makes the simple flow
subclassable and unit-testable in isolation.AssembleFileJob is fully configurable via the new
chunky.assembly namespace: tries, backoff, timeout, and
queue. The previous hard-coded $tries = 3, $backoff = 30 and
the implicit 60s queue timeout are gone — the queue worker default
was far too short for multi-GB assemblies.HasArrayPayload trait on the DTOs (UploadMetadata,
BatchMetadata). Centralises the snake_case ↔ camelCase mapping
the tracker storage layer uses, so adding a field touches one
place instead of three.routes/channels.php
callbacks now memoise the upload/batch lookup for
chunky.broadcasting.auth_cache_ttl_seconds (default 30s). A
client disconnect/reconnect storm no longer fans out to the
tracker on every subscription auth.UploadStatusController, BatchStatusController,
CancelUploadController, CancelBatchController accept
Illuminate\Http\Request via DI instead of pulling the user from
the auth() facade. Mockable from a unit test without booting the
auth manager.chunkUpload is missing
from the global JS scope, the rendered component shows a clear
"frontend not loaded" message instead of silently dying with an
Alpine console error.UploadChunkRequest.checksum regex validation (/^[a-f0-9]{64}$/i).
Prevents arbitrary strings from being stuffed into the cache key
via the idempotency lookup path.DatabaseTracker::claimForAssembly() explicit status allowlist
in the CAS query. Makes the legal source statuses declarative —
adding a new UploadStatus enum case can no longer slip through
the orWhere chain unnoticed.Metrics honours the MetricsListener contract first when a
class-string handler implements it. The interface stops being
vestigial.[@netipar](https://github.com/netipar)/chunky-core/tools exposes
EventEmitter and RetryPolicy for advanced consumers building
their own orchestrators on the same primitives the package uses.
Marked [@internal](https://github.com/internal) until v1.0; pin the package version exactly
when importing from this surface.mergeDefaults() + resetDefaults() functions on the core
defaults API (and mergeDefaults / reset on DefaultsScope).
Replaces the silent-overwrite footgun where back-to-back
setDefaults({ headers: ... }) + setDefaults({ context: ... })
would drop the headers.HeadersInit accepted everywhere for the headers option
(record / array tuple / Headers instance). normalizeHeaders()
flattens internally; the package still stores headers as a flat
record so individual keys (X-XSRF-TOKEN, Idempotency-Key)
can be read without .get().BatchUploader.validateBatchEndpoints() — refuses to
construct when batchUpload/batchStatus lack the {batchId}
placeholder. Catches typos at construction time instead of at
call time with a 404 on a literal {batchId} URL.BatchUploadOptions.chunkEndpoints / batchEndpoints split.
Lets new code keep the per-chunk and batch endpoints in separate
objects. The legacy mixed endpoints shape still works.useBatchCompletion hook on React — parity with the Vue
composable. Tracks caller callbacks via useRef so a re-render
with a new options literal does not trigger re-subscription
churn.BatchInitiateResponse is now an open type ([extra: string]: unknown),
so server-supplied extras pass through untyped instead of being
silently dropped at the type boundary.ChunkUploader.on().
The compile-time check catches typos in the typed call site, but
a (name as any) dispatch would silently never fire — this gives
a console hint.scripts/bump-version.sh is now consumed by a new
release.yml GitHub Actions workflow with a workflow_dispatch
trigger. Triggering "Run workflow" with a version input bumps
the four npm package.json files, commits, tags, and publishes
the GitHub Release end-to-end.UploadTracker::markChunkUploaded() no longer accepts the
unused ?string $checksum parameter. The signature was
defined but never persisted — the contract is now honest.ChunkyManager::handler() and tracker() [@internal](https://github.com/internal)
accessors removed. Resolve the underlying services through the
container instead.RetryPolicy (was
baseDelay + Math.random() * 250). Many parallel chunk workers
now genuinely de-synchronise on retry instead of all hammering
the server in lockstep.upload() resume falls through to a fresh initiate on 404
(was: surfaced the 404 as the upload's own error). Lets a
client transparently recover when the server's expiration
sweep collected the upload between pause and resume.ChunkUploader.uploadId! non-null assertion replaced by a
local capture. A future edit that resets this.uploadId
earlier in the flow can no longer crash the result-payload
build.BatchUploader honours the scope-supplied defaults in its
own fetchJson calls, not just in the per-file
ChunkUploaders it creates. Multi-scope setups now behave
consistently.BatchUploader clones its options snapshot at construction.
Caller-side mutations after the fact (common in reactive
frameworks) no longer bleed into in-flight uploads.useBatchUpload / useChunkUpload are mount-only on
options. The old useMemo(() => JSON.stringify(options))
threw on circular refs (Headers, functions) and silently lost
data on Date / Map values — neither edge case is a real
use, so the option snapshot is now taken via useRef once on
mount. Callers that need to change options at runtime should
unmount/remount the hook or use the imperative core
BatchUploader directly.useChunkyEcho / React useChunkyEcho track callbacks
via ref. A re-render that creates a fresh callbacks = { ... }
literal no longer dispatches into stale closures.broadcasting.user_channel, idempotency.enabled,
skip_local_disk_guard flags — already dropped in v0.18; this
release deletes the last remaining ?string $checksum shim from
the tracker contract and the ChunkyManager::handler()/tracker()
accessors.composer audit run in one quality job (instead of three identical
composer install jobs). Frontend gets a single js-quality
job covering typecheck + tests + build + pnpm audit.actionlint GitHub workflow lints every .github/workflows/*.yml
on every push. Catches a malformed YAML before the workflow
silently dies.markdownlint-cli2 runs against README, CHANGELOG, UPGRADE,
SECURITY, CONTRIBUTING, and docs/**/*.md. Continue-on-error so
a stylistic nit does not block the merge queue.release.yml workflow_dispatch trigger lets a maintainer
release v$X.Y.Z by clicking "Run workflow" — the workflow
validates the CHANGELOG entry exists, runs bump-version.sh,
commits the bumps, and creates the GitHub Release with the
CHANGELOG entry as release notes.build script
now runs tsc --noEmit -p tsconfig.json first, so a typo in
source surfaces during the build instead of after publish.mockFetchSequence helper bumped to support delayMs,
validateRequest, custom headers / raw body. The previous
signature didn't let a test assert what headers a chunk POST
carried.0.22.0 (frontend changes — tools
subpath, mergeDefaults, HeadersInit, useBatchCompletion on React,
validateBatchEndpoints, options clone, full jitter retry, resume
404 recovery, mount-only React options snapshot, callback-ref
tracking).Last "polish" minor before v1.0 — closes the gaps left over from the
v0.17 deep audit. Frontend test coverage triples, the README finally
gets a TOC + a separate config reference, the Alpine factory stops
using any, autoRetry becomes a proper policy, and the BatchUploader
queue can survive page reloads via a pluggable persistence adapter.
useBatchUpload reactivity, scope teardown, onComplete unsub, cancel state, and the polymorphic useUpload(File | File[]).[@testing-library](https://github.com/testing-library)/react. Covers useBatchUpload shape after mount, state during a successful upload, unmount cleanup, onComplete forwarding, and the useUpload polymorphism.Alpine instance captures the registered factory; tests instantiate the data object with a $dispatch spy and verify the lifecycle events fire on a real upload through a mocked fetch.AlpineLike and AlpineContext type interfaces ([@netipar](https://github.com/netipar)/chunky-alpine). The factories no longer take Alpine: any — they declare the minimal Alpine.data() shape, and the $dispatch casts go through as unknown as AlpineContext. Lets TypeScript catch typos in chunky:* event names without dragging Alpine into the package as a peer dep.autoRetry callback form ([@netipar](https://github.com/netipar)/chunky-core). ChunkUploadOptions.autoRetry accepts boolean | (error, { chunkIndex, retriesLeft }) => boolean. The default policy now refuses to retry HTTP 400/401/403/404/410/413/415/422 — those won't change on retry. Pass a callback to override.BatchPersistence<T> adapter interface ([@netipar](https://github.com/netipar)/chunky-core). Optional BatchUploadOptions.persistence lets a host-app IndexedDB / localStorage adapter persist the pending-batch queue across page reloads. Files themselves still need re-resolution by the host app (the type is generic over a serialised representation).scripts/bump-version.sh in the repo. Bumps the four npm package.json files in lockstep using node (preserves formatting). Replaces the previous out-of-repo bump-version.sh reference.docs/ directory with docs/configuration.md — the full configuration reference + deployment recipes. The README's giant config table moved here, README is back to a TOC + tutorial-shape document.ChunkUploader and BatchUploader public fields are [@deprecated](https://github.com/deprecated) in TypeScript. The replacement is the existing getState() method. The fields stay until v1.0 for back-compat; new code should read state through getState() or the stateChange event so the v1.0 private-state migration is mechanical.docs/configuration.md. The README is now intent-shaped (quickstart → install → usage → events) rather than reference-shaped.0.21.0 (frontend additions — BatchPersistence, autoRetry callback, AlpineLike/AlpineContext, [@deprecated](https://github.com/deprecated) field annotations).[@testing-library](https://github.com/testing-library)/react and react-dom added to root devDependencies for the React hook test suite.Polish minor: cleanup ergonomics, model docblocks, multi-language translations, deprecation warnings on closure metric handlers, and a substantial frontend test suite.
--limit=N to stop after N removals, --skip-uploads and --skip-batches to run only one half of the sweep. Useful for staged cleanup on large stores.BatchTracker::expiredBatchIds(). The DB driver enumerates chunky_batches rows past expires_at; the FS driver walks the per-batch directories.add_chunked_uploads_batch_id_foreign_key migration adds a chunked_uploads.batch_id → chunky_batches.batch_id foreign key with nullOnDelete(). Skipped on SQLite (which can't add FKs after table creation).[@property](https://github.com/property) annotations on ChunkedUpload and ChunkyBatch. PHPStan baseline cut from 45 entries to 6. Custom code that depends on Larastan's property inference now type-checks without help.pint.json enforces declare_strict_types and alphabetised imports across the codebase.sideEffects: false on every npm package — bundlers can tree-shake unused exports cleanly.de, es, fr, hu alongside the existing en. Publish with php artisan vendor:publish --tag=chunky-lang.Metrics::dispatch() triggers E_USER_DEPRECATED when a closure handler is invoked, pointing operators at the class-string alternative (which config:cache can serialise). Slated for removal in v1.0.actions/cache@v4 to reuse the vendor directory between runs — about 90 seconds saved per PR.extendTimeoutOnProgressMs + timeoutMs=0 fix.Metrics closure handlers are deprecated. They keep working for back-compat but trigger an E_USER_DEPRECATED notice. Migrate to class-string handlers (resolved through the container) before v1.0.EventCallback removed from the public export of [@netipar](https://github.com/netipar)/chunky-core. It was an internal type that crept onto the surface during the v0.17 hardening; consumers should use the per-uploader EventMap types when they need typed event listeners.mockFetchSequence returns typeof fetch instead of a raw vitest Mock, so callers can assign it to globalThis.fetch without per-test casts.0.20.0 (frontend additions — sideEffects: false, EventCallback unexport, test suite expansion).Architecture-maturity minor. Gate-aware authorizer, unified event base
class with per-event broadcast opt-in, full batch cancel API, two new
artisan generators, and a polymorphic useUpload hook on Vue/React.
AbstractChunkyEvent base class for the entire event pipeline. Every event extends it and gates broadcasting on a per-event chunky.broadcasting.events.{Key} flag. The four "completion" events (UploadCompleted, UploadFailed, BatchCompleted, BatchPartiallyCompleted) keep their default-on behaviour; per-chunk and per-init events default to off because broadcasting them is expensive.ChunkyManager::cancelBatch(), new BatchStatus::Cancelled enum case (already shipped in v0.18 carved out for forward-compat), new BatchCancelled event, new DELETE /api/chunky/batch/{batchId} route via CancelBatchController. The DB driver also cascades the cancel to every active per-file upload (each fires UploadCancelled); the FS driver leaves the per-file uploads to the cleanup sweep because there's no efficient batch-member walk on disk.UploadCancelled event now fires from ChunkyManager::cancel(). Subscribers can clear progress UI, decrement counters, etc. without polling.DefaultAuthorizer. When the host application registers Gate abilities viewChunkyUpload, cancelChunkyUpload, viewChunkyBatch, cancelChunkyBatch, the authorizer defers to them. Falls back to the existing ownership rule when no Gate is registered. Lets you express admin / team / shared-batch rules from AuthServiceProvider without subclassing.canCancelUpload() / canCancelBatch() on the Authorizer contract. Defaults to the access answer in DefaultAuthorizer; override in custom authorizers when cancel/delete needs tighter rules than read access.chunky.authorization.allow_anonymous config flag (default true). Set to false to refuse anonymous access even when the upload has no recorded userId. Opt-in because flipping it on legacy setups breaks unauthenticated reads.make:chunky-context artisan generator. Stubs out an app/Chunky/<Name>Context.php extending ChunkyContext, with placeholders for name() / rules() / save(). Replaces hand-writing the boilerplate from the README.make:chunky-authorizer artisan generator. Stubs out an app/Chunky/<Name>Authorizer.php implementing the full Authorizer contract, plus a service-provider binding example in a trailing comment.useUpload polymorphic hook on Vue 3 ([@netipar](https://github.com/netipar)/chunky-vue3) and React ([@netipar](https://github.com/netipar)/chunky-react). Accepts a single File or File[] — internally backed by useBatchUpload because every upload is a batch of N. Prefer this over useChunkUpload / useBatchUpload for new code; the two specific composables remain for back-compat.JsonObject / JsonValue / JsonPrimitive types exported from [@netipar](https://github.com/netipar)/chunky-core. The useUpload metadata signature uses JsonObject, narrowing what TypeScript will accept (no Date, Map, Set, class instances) so the wire format actually matches the type.Authorizer contract gained two methods (canCancelUpload, canCancelBatch). Custom implementations need to add them — the default behaviour is "same as access" so simply forwarding to the existing access methods is the minimal migration.UploadFailed / UploadCompleted etc. extend AbstractChunkyEvent instead of using their own Dispatchable + ShouldBroadcast setup. Behaviour is unchanged for the default broadcast set; custom code that exhaustively instanceof-checks individual events still works.examples/en/configuration.md cover the new authorization namespace and the per-event broadcasting.events map.0.19.0 (frontend additions — useUpload, JsonObject types).Structural-cleanup minor. Thinner ChunkyManager, namespaced config, deduplicated request validation, generic Echo channel, removed dead flags. The runtime behaviour is the same — see UPGRADE.md for the config & API migration details.
BatchTracker contract (NETipar\Chunky\Contracts\BatchTracker) with two implementations (DatabaseBatchTracker, FilesystemBatchTracker). The Database implementation runs the increment helpers in a transaction with lockForUpdate(), eliminating the read-modify-write race that the old in-manager markUploadCompleted/Failed had. Hidden behind the same chunky.tracker config switch.ContextRegistry support class holds the validation-rules + save-callback registry. ChunkyManager delegates the public register() / simple() / context() helpers to it for back-compat, but the registry can also be resolved directly from the container.ValidMetadata validation rule caps per-value length (default 1KB), total serialized size (default 16KB), and disallows non-scalar values. Wired up in the baseline InitiateUploadRequest rules.AbstractInitiateUploadRequest holds the shared baseline rules; InitiateUploadRequest and InitiateBatchUploadRequest are now thin subclasses that just apply context-specific rules. Adding a new constraint touches one place instead of two.EchoChannel<EventMap> interface (packages/core/src/echo.ts) lets typed Echo wrappers narrow event payloads instead of falling back to any.UploadError.cancelled flag lets the caller distinguish "user explicitly cancelled" from "transport/server failure" without parsing the message.UploadError.cause is now typed as UploadHttpError | Error instead of unknown.BatchStatus::Cancelled enum case (the v0.19 batch-cancel API will use it; carved out now to keep the enum forward-compatible).tsconfig.base.json at the repo root, extended by every package's tsconfig.json and tsconfig.build.json. Removes the 4-way copy-paste of compiler options.chunks, storage, lifecycle, limits, locking, idempotency, cache, broadcasting). See UPGRADE.md for the full mapping. Republish the config or hand-edit per the migration table.ChunkyManager constructor takes four arguments now — (ChunkHandler, UploadTracker, BatchTracker, ContextRegistry). Container-bound callers are unaffected.ChunkHandler::assemble() accepts UploadMetadata instead of (string $uploadId, string $fileName, int $totalChunks). Handlers get access to fileSize for the disk-space + integrity checks landed in v0.17.2; custom implementations need to update the signature.chunky.idempotency.enabled flag — idempotency is always on (always was, really; the flag had no realistic use case).chunky.skip_local_disk_guard flag — the boot-time guard now keys off chunky.locking.driver directly. Set the driver to cache for cloud disks.chunky.broadcasting.user_channel flag — the user channel is registered whenever an event has a non-null userId. No flag needed.TrackerDriver enum is now actively used in the service provider, the config validator, and every place that previously magic-strung config('chunky.tracker') === 'database'. Adding a new driver is now a single enum case + match arm.pause() aborts the in-flight chunk POST in ChunkUploader. The previous flag-only pause raced with cancel() — a paused chunk would complete naturally before the cancel registered. The catch path skips the error event when both aborted and isPaused are set, so the resume can pick up cleanly.listenForUser / listenForUploadEvents / listenForBatchComplete unsubscribe only the events the caller registered. The old implementation called stopListening for every event in the channel, which would detach a sibling consumer (two components on the same channel) on cleanup.BatchStatus::Cancelled notice.0.18.0 (frontend changes — EchoChannel<EventMap>, UploadError.cancelled, pause/cancel race fix, tsconfig.base.json extraction).Critical bug-fix release. Eleven concurrency / DOS / data-integrity bugs surfaced by a deep code audit, plus refreshed security and example documentation. No public API changes — drop-in over v0.17.1.
FilesystemTracker lock fall-through (race condition). Three branches (fopen failure, flock failure, cloud disk without path()) silently ran the critical section without a lock — every chunk write / status flip was a lost-update race. The branches now throw ChunkyException with actionable error messages, so the operator gets a fail-fast signal instead of corrupted metadata.ChunkyManager::withBatchLock() lock fall-through (race condition). The same silent fall-through pattern existed for batch counter updates — flock unavailable on cloud disks would silently bypass the lock. Now throws with guidance to use chunky.lock_driver = "cache" for cloud disks.BatchUploader.uploaders[] memory leak. Every per-file ChunkUploader was pushed and never removed, retaining 100MB+ File references plus closure state for the lifetime of the BatchUploader. A 10000-file enqueue session held all 10000 uploader instances in memory. The per-file uploader is now spliced out of the array and destroy()-ed in the finally block.ChunkUploader.cancel() did not reset lastFile / lastMetadata. A cancel() followed by retry() silently re-uploaded the cancelled file (the retry path detected the still-set lastFile). Cancel now drops the resume state too.ChunkUploader.on() and BatchUploader.on()). The queueMicrotask re-delivery of a cached complete/error to a late subscriber did not check whether the subscriber had synchronously unsubscribed before the microtask drained. The typical React useEffect cleanup-then-resubscribe pattern triggered the callback after unsub() returned. The microtask now confirms the listener is still in the Set before delivering.CompletionWatcher.extendTimeoutOnProgressMs no-op when timeoutMs = 0. The progress-extension safeguard only ran when an existing timeoutTimer was present, so opting into "extend on progress" with no static deadline silently never started the safeguard. The timer now (re)creates on every progress tick regardless of whether a static deadline was set.max_chunks_per_upload config cap (default 100k). A pathological initiate (file_size = 1TB, chunk_size = 1KB → 1 billion chunks) would explode the tracker row and grind the UI to a halt. The new file_size validation rule rejects any size that would require more chunks than the configured cap, with a helpful error message pointing at chunk_size / the cap setting.throttle:chunky rate limiter. The package now registers a RateLimiter::for('chunky') keyed by user id (or IP for anonymous traffic) and adds throttle:chunky to the default route middleware. Configurable via chunky.throttle.attempts / chunky.throttle.decay_minutes (default 120/min) — set attempts to 0 to disable. Without this, the public batch/upload status endpoints could be hammered with random UUIDs to drive S3 GET costs.Support\CacheKeys with a configurable chunky.cache.prefix (default chunky:v1:). Lets a future major release invalidate cached payloads cleanly without cooperating cache backends — and removes the collision risk with other packages using a bare chunky: prefix.DefaultChunkHandler. The assemble() method now accepts an optional ?int $expectedSize, and when supplied (a) refuses to start an assembly the staging volume can't hold (10% margin) and (b) verifies the assembled temp file matches the expected size before publishing, so silent chunk-corruption truncations can never reach the final disk. The AssembleFileJob passes $metadata->fileSize so the checks are active by default.Cache::lock try/finally release in FilesystemTracker::withCacheLock() and ChunkyManager::withBatchLock(). Previously a callback exception left the lock held until its TTL expired — a mild DoS amplifier under churn..github/workflows/dependabot-auto-merge.yml watches Dependabot PRs and, after the regular CI matrix passes, approves and enables auto-merge (squash) for version-update:semver-patch and version-update:semver-minor updates. Major bumps get a comment instead and stay open for manual review. The repository's "Allow auto-merge" and "Automatically delete head branches" settings are required and have been enabled.dependabot.yml per-package grouping. Each packages/{core,vue3,react,alpine}/ config now defines a single *-dev-dependencies group covering typescript, esbuild, [@types](https://github.com/types)/*, and the framework peer (vue / react). Last week's update produced 8 separate PRs for typescript+esbuild across the 4 packages; the same updates would now land as 4 grouped PRs (one per package). The github-actions ecosystem also got an actions group so all action bumps land in a single PR.SECURITY.md Supported Versions table updated to 0.17.x (was stuck at 0.14.x); Hardening Surface section now lists the rate limiter, lock-driver compatibility guard, and cache-key namespacing.examples/en/configuration.md rewritten as a recipe-focused companion to the canonical config/chunky.php reference. The previous version was stuck at v0.7-era keys.0.17.2 (frontend bug fixes — BatchUploader memory leak, sticky-replay race, ChunkUploader cancel, CompletionWatcher).^6.0 (which resolves to nothing on npm — the latest published Dropzone is 6.0.0-beta.2) to ^5.9 || 6.0.0-beta. The original constraint broke pnpm install --frozen-lockfile in the publish workflow, which is why v0.17.0 npm packages didn't make it out. v0.17.0 stays available on Packagist (PHP-only); v0.17.1 is the first npm-published cut of the Ops Hardening release.Operational hardening release: governance metadata, CI policy upgrades, reproducible publish flow with npm provenance, and Dependabot. No source-code changes — pure release engineering.
.editorconfig for cross-editor whitespace consistency (PHP files use 4-space indent, frontend files use 2-space, Markdown preserves trailing whitespace)..github/dependabot.yml with weekly schedules for Composer, GitHub Actions, root pnpm devDependencies, and each of the 4 npm packages. Related deps grouped (e.g. all illuminate/*, all [@types](https://github.com/types)/*, all vitest/[@vitest](https://github.com/vitest)/*/happy-dom) so the Dependabot inbox doesn't drown the maintainer..github/CODEOWNERS so PRs touching workflows or CHANGELOG.md are automatically routed for review.npm-publish.yml workflow now sets NPM_CONFIG_PROVENANCE: true and permissions: id-token: write, so published packages carry an attestation linking them back to this exact GitHub Actions run. Adds a "Verified" badge on the npmjs.com package pages.npm-publish.yml: refuses to publish if the 4 npm package.json versions disagree with the release tag.npm-publish.yml. The build script previously only ran the build itself; now pnpm typecheck and pnpm test are gating steps so a typo never reaches npm.workflow_dispatch trigger on npm-publish.yml with a tag input — lets a maintainer re-publish a previously failed release without creating a duplicate GitHub Release.0.x policy guidance.phpunit.xml hardened: explicit cacheDirectory, executionOrder="random" (catches order-dependent tests), failOnWarning="true" and failOnRisky="true" (catches PHP deprecations and "tests that don't actually test anything"), strict output assertions, an explicit <coverage> block with Clover/HTML/text reporters, and a <php> env block setting APP_ENV=testing, CACHE_DRIVER=array, QUEUE_CONNECTION=sync, etc..gitignore expanded to cover macOS .DS_Store, VSCode/Fleet IDE folders, PHPStan/PHPUnit caches, npm/pnpm logs, coverage outputs, and .env* (defensive — should never be in a package repo, but the entry is cheap).0.17.0 (no source changes — re-publish for version-sync consistency).Polish release: localisable HTTP error messages, runtime config validation, fewer magic strings, and a 503 path for Cache-lock contention. No new public features; everything here is hardening or readability.
lang/en/chunky.php translation file. The HTTP controller message payloads (upload_not_found, upload_finalized, batch_not_found, busy) now route through __('chunky::chunky.http.*'), so non-English Laravel projects can publish and translate them with php artisan vendor:publish --tag=chunky-lang. Internal ChunkyException messages stay hardcoded by design — they carry dynamic context (upload ids, status values) and are more useful to log than to translate.TrackerDriver enum (NETipar\Chunky\Enums\TrackerDriver) with Database / Filesystem cases and a current() helper. Replaces the inline config('chunky.tracker') === 'database' magic-string checks.BatchMetadata::resolveStatus() factor-out — the previous nested-ternary status fallback is now a small helper with explicit branches for instanceof BatchStatus, non-string, and string-with-tryFrom paths.chunky.tracker and chunky.lock_driver are checked against their allowed values (database/filesystem and flock/cache); a typo ('datbase') now throws a clear RuntimeException instead of silently falling through to a default.LockTimeoutException → 503 Service Unavailable in UploadChunkController. When the cache-backed lock times out under contention, the client sees a clear 503 with a "please retry" message instead of an opaque 500. The idempotency-key dedupes safely on retry.Symfony\Component\HttpFoundation\Response constants in UploadChunkController (Response::HTTP_GONE, HTTP_CONFLICT, HTTP_SERVICE_UNAVAILABLE). Same response codes, more searchable code.ChunkyManager::handler() and ::tracker() annotated [@internal](https://github.com/internal) (matching the existing Facade-level [@internal](https://github.com/internal) annotation). Not a behaviour change; just a clearer SemVer signal that these are not part of the public API.0.16.0 (no source changes in the JS packages — re-publish for version-sync consistency).lang/en/chunky.php file is auto-loaded by the service provider. Existing installations don't need to publish it unless they want to override the strings; HTTP responses will pick up the English defaults out of the box.This release closes the documentation and developer-experience gap that v0.10–v0.14 accumulated. No source-code changes to the upload pipeline; the focus is on bringing the README up to date with the v0.12+ feature set, shipping the standard governance documents, exposing the long-undocumented UploadFailed event end-to-end, and tightening the package's public metadata for Packagist / npm discoverability.
Authorizer interface contract, the DefaultAuthorizer semantics (anonymous vs. owned uploads), and a complete custom-Authorizer example for team-based access. Replaces the bare-bones "Authentication" subsection.enqueue() subsection — the v0.12.0 marquee feature was previously only documented in the CHANGELOG and JSDoc.user_id portability note explaining the v0.14.0 string-column change.SECURITY.md with the vulnerability disclosure policy, supported version table, and a hardening surface checklist. Both GitHub Private Vulnerability Reporting and dev@netipar.hu accepted as channels.CONTRIBUTING.md with the dev environment setup, the local check suite, code style rules, PR process, and a description of the release flow.UPGRADE.md consolidating the breaking-change migration notes from v0.12 → v0.13 → v0.14. The CHANGELOG remains authoritative for the per-release log; UPGRADE is the actionable migration playbook..github/PULL_REQUEST_TEMPLATE.md and .github/ISSUE_TEMPLATE/{bug_report,feature_request}.yml with structured forms (laravel/php version, frontend, tracker driver, disk, reproduction, expected vs actual). The YAML form spec ensures empty issues can't slip through.UploadFailed broadcast event end-to-end exposure. New UploadFailedData type. New listenForUploadEvents(echo, uploadId, { onComplete, onFailed }) core helper. listenForUser now accepts an onUploadFailed callback. All four wrappers (core, vue3, react, alpine) re-export the type. Closes a documentation gap that existed since v0.9.2 — the backend has been broadcasting UploadFailed for 6 minor versions, but no frontend helper made it visible.UploadCompletedData.finalPath and disk typed as optional (?: string) since v0.13.0's expose_internal_paths flag defaults to false. Frontends that read these fields now get a TypeScript warning if they don't handle the absence.composer.json keywords expanded from 4 to 15 (laravel, chunk, upload, file, file-upload, resumable, large-files, batch-upload, s3, echo, broadcasting, vue, react, alpine, livewire). New homepage, support (issues / source / docs / security URLs), and richer author metadata. Doubles Packagist search visibility.Chunky Facade docblock now lists handler() and tracker() — both annotated [@internal](https://github.com/internal) — so static analysers and IDEs no longer report them as undefined while still discouraging public use.uploadId, fileName, fileSize only). Includes the opt-in note for expose_internal_paths.[@netipar](https://github.com/netipar)/chunky-vue3 peer dependencies now explicitly list dropzone: ^6.0 (was missing despite being declared optional in peerDependenciesMeta). Consumers who use ChunkDropzone get a proper resolution; everyone else still skips it.DELETE /upload/{uploadId} cancel route (added in v0.10.0) was previously undocumented.0.15.0 (core, vue3, react, alpine).This release establishes the Static Analysis Baseline for the package: PHPStan/Larastan integration, frontend test infrastructure, full PHP strict_types, and a CI matrix that finally covers Laravel 13. It also makes the user_id column portable across int / UUID / ULID user-id schemes — closing a previously hidden assumption that user IDs are always integers.
phpstan.neon config and phpstan-baseline.neon (45 pre-existing findings frozen). New composer analyse script and a phpstan CI job. The Livewire component is excluded from analysis since livewire/livewire is a composer suggest, not a hard dependency.happy-dom. Initial spec coverage: 16 tests across http, ChunkUploader, and CompletionWatcher — pinning down the v0.13.x cancel/idempotency/file-mismatch invariants against regressions.tsc --noEmit typecheck at the workspace root (pnpm typecheck). Catches cross-package type errors that the build's tsc --emitDeclarationOnly doesn't surface.composer.json advertising ^11.0|^12.0|^13.0. Matrix expanded to PHP 8.2/8.3/8.4 × Laravel 11/12/13 × Testbench 9/10/11 (with appropriate excludes; Laravel 13 excludes PHP 8.2). Added phpstan, js-typecheck, and audit (composer + pnpm) jobs.pest --coverage and uploads to Codecov.composer.json require-dev explicitly lists mockery/mockery: ^1.6 (was only available transitively via Pest) and larastan/larastan: ^2.9|^3.0.Metrics::emit() accepts class-string handlers (carry-over from the prior unreleased work, now formally part of v0.14.0). Resolved through the container with constructor DI, calling __invoke() or handle() — config:cache-compatible. New optional NETipar\Chunky\Support\MetricsListener interface.user_id columns are now string instead of unsignedBigInteger. Both chunked_uploads and chunky_batches migrations changed. This makes the package work out-of-the-box with any user-id shape — auto-increment ints, UUIDs, ULIDs, or arbitrary strings. The UploadMetadata::$userId, BatchMetadata::$userId, BatchCompleted::$userId, BatchPartiallyCompleted::$userId, and ChunkyManager::resolveUserId() types are all ?string. The DefaultAuthorizer and the chunky.user.{userId} channel auth callback compare with (string) casts. Existing installations need to run a migration to alter the column type — a published migration helper will land in v0.14.1 if there's demand.declare(strict_types=1); added to every PHP file in src/ and tests/. PHP no longer silently coerces string → int at type boundaries inside the package; callers passing the wrong type get a TypeError at the boundary instead of a silently-wrong cast deep in the call stack. No public API change for consumers using the typed Facade methods or DTOs — those signatures already required the right types.ChunkUploader/BatchUploader listener storage moved from Set<Function> to a generic EventCallback<T> shape, exposed as a public type from [@netipar](https://github.com/netipar)/chunky-core. Removes the Function top-type leak that prevented strict TypeScript projects from passing typecheck.BatchCancelEvent, Unsubscribe, UploadHttpError (class), EchoChannel, FileProgressEvent, watchBatchCompletion, BatchCompletionResult, CompletionWatcherOptions, and friends. Previously some symbols only made it through the Vue 3 wrapper, forcing React consumers to import from [@netipar](https://github.com/netipar)/chunky-core directly.0.14.0 (core, vue3, react, alpine).user_id schema change: existing installations need an ALTER TABLE to change the column type. Publish and edit the migration locally, or apply directly:
Schema::table('chunked_uploads', function (Blueprint $table) {
$table->string('user_id')->nullable()->change();
});
Schema::table('chunky_batches', function (Blueprint $table) {
$table->string('user_id')->nullable()->change();
});
Existing integer values are preserved (stored as their string representation).strict_types: callers that were relying on PHP's type-coercion at the package boundary (e.g. $manager->initiate('file.bin', '1024') with file_size as a string) now get a TypeError. Cast at the call site.Test-coverage and operational hardening pass on top of v0.13.0. No new public API; one new boot-time guard that fails fast on a misconfigured Cache lock driver, and 19 new tests covering the v0.13.0 features.
chunky.lock_driver = 'cache'. Throws RuntimeException at service-provider boot if cache.default is array or file — both drivers silently no-op on Cache::lock() and would let upload races slip through unnoticed. Switch to Redis / Memcached / DB / DynamoDB, or set chunky.lock_driver back to flock.AssembleClaimTest): simulate a worker crash mid-assembly, verify a retry can take over the claim, run to Completed, and emit UploadCompleted. Also covers expiredUploadIds() skipping fresh Assembling rows but including stale ones — the cleanup-doesn't-leak invariant.FilesystemTracker boot-guard tests: local disk boots fine; non-local disk + flock mode throws with a helpful message; non-local disk + cache mode boots fine; skip_local_disk_guard = true escape hatch works.UploadCompleted and UploadFailed strip disk / finalPath by default; opt-in via chunky.broadcasting.expose_internal_paths = true puts them back. Locks the v0.13.0 wire-format change.PathTraversalTest): the assembler's basename() defence-in-depth strips leading directories from hostile file names, and rejects dot-only names that collapse to empty. The simple() context save callback applies the same guard end-to-end (verified by inspecting where the moved file actually lands on a fake disk).LockDriverCompatTest covers the four cache.default × lock_driver combinations relevant to the new boot guard.0.13.1 (no source changes in the JS packages — re-publish for version-sync consistency with the PHP release).This release applies the remaining items from the v0.12.0 deep review (cross-cutting concerns + nits) plus a few new findings from the implementation pass. The headline change is opt-in support for cloud disks in the FilesystemTracker via Cache-backed locks, alongside built-in idempotency for chunk POSTs and a metrics-callback surface for observability integrations.
chunky.lock_driver = 'cache' — Cache::lock-backed locking for FilesystemTracker mutations and the batch counter. Required when running against S3, GCS, or any non-local Flysystem disk; works with any Laravel cache driver that supports atomic locks (Redis, Memcached, DB, DynamoDB). Defaults to flock for backward compatibility. Tunable via chunky.lock_ttl_seconds (30) and chunky.lock_wait_seconds (5).POST /upload/{uploadId}/chunks endpoint now caches its response by (uploadId, chunkIndex, Idempotency-Key OR checksum) for chunky.idempotency_ttl_seconds (default 300). A network retry of a chunk the server already accepted replays the cached payload byte-for-byte instead of double-firing ChunkUploaded events and double-dispatching AssembleFileJob. The frontend ChunkUploader automatically attaches an Idempotency-Key header ({uploadId}:{chunkIndex}).chunky.metrics). Five lifecycle events fire through NETipar\Chunky\Support\Metrics::emit(): chunk_uploaded, chunk_upload_failed, assembly_started, assembly_completed, assembly_failed. Wire any of them to Datadog / Prometheus / StatsD via a callable in config. Callback exceptions are swallowed so an observability bug cannot break the upload pipeline.UploadStatus::isTerminal() and BatchStatus::isTerminal() helpers, replacing 5+ inline in_array($status, [Completed, Failed, …]) lists across the codebase. Single source of truth for "is this state final?" semantics.chunky.staging_directory config — local filesystem directory used while assembling chunks. Defaults to sys_get_temp_dir(). Set to a path on a volume with enough free space when accepting uploads larger than your /tmp partition (cloud-disk targets buffer the full file locally before upload).chunky.metadata.max_keys config (default 50) — bounded user-supplied metadata, so a misbehaving client can't balloon DB rows or broadcast payloads with megabyte-sized metadata blobs.chunky.broadcasting.expose_internal_paths — opt-in flag to include the storage disk and absolute finalPath in the UploadCompleted/UploadFailed broadcast payloads. Defaults to false so server-internal paths don't leak over WebSocket. The Livewire component honours the same flag.CompletionWatcher poll backoff and progress-aware timeout. New options: pollMaxIntervalMs (default 30000), pollBackoffFactor (default 1.5), and extendTimeoutOnProgressMs (default 0 = static deadline). The poll cadence now grows over time so a long-running batch doesn't hammer the status endpoint at the initial cadence; with extendTimeoutOnProgressMs > 0, observed progress extends the wall-clock deadline.useBatchCompletion (Vue 3) debounceMs option (default 50) — protects against batchId flapping on rapid route param changes, which would otherwise teardown/setup an Echo subscription on every tick.useChunkUpload / useBatchUpload and Alpine wrappers expose destroy() for explicit teardown. Brings parity with the Vue 3 wrappers shipped in v0.12.0.ChunkUploader.upload() no longer falls back to a 1MB chunk size silently. A missing chunk_size from the server (and no client override) now throws — guessing the slice size produces silently corrupted output if the guess disagrees with the backend.ChunkUploader.uploadChunks() is O(N) instead of O(N²) on the pending list. A Set replaces the previous Array.filter() rebuild on every chunk completion. For a 10000-chunk file that's ~50M ops saved.BatchUploader.aggregateProgress() uses a for loop instead of Array.reduce — same time complexity, less closure overhead per progress event.CompletionWatcher no longer hammers a 401/403/404 endpoint. All three status codes are now treated as fatal alongside 404 (added in v0.12.0); previously 401/403 looped at 2-second intervals until the wall-clock timeout.ChunkyContext::name() empty string is rejected at registration time rather than producing a silently broken $contexts[''] entry.simple() save-callback applies a defence-in-depth basename() to the destination, mirroring the assembler's path-traversal guard.Livewire/ChunkUpload::completeUpload() now performs an Authorizer ownership check on the upload before broadcasting the completion event — closes the same IDOR surface that v0.12.0 closed for the HTTP layer.UploadCompleted and UploadFailed broadcast payloads no longer include disk or finalPath by default. Set chunky.broadcasting.expose_internal_paths = true to opt back in if a consumer depends on these.Models\ChunkedUpload::markChunkUploaded() dropped the unused ?string $checksum parameter (already nullable; never persisted).onBeforeUnmount lifecycle hooks are now onScopeDispose everywhere (already partially done in v0.12.0; the rest converted in this release).0.13.0 (core, vue3, react, alpine).chunky.broadcasting.expose_internal_paths default of false is technically a wire-format change for UploadCompleted / UploadFailed. Frontends that read event.finalPath or event.disk from broadcast payloads need to either set the flag back to true in config/chunky.php or fetch those fields from GET /api/chunky/upload/{uploadId} server-side instead.chunky.lock_driver = 'cache' requires a Laravel cache driver that supports Cache::lock(). array and file drivers do not — use Redis, Memcached, DB, or DynamoDB.This release is a substantial hardening pass driven by an end-to-end review of the package: race conditions, security-sensitive paths, fault tolerance, and frontend ergonomics. Highlights below; more detail in the per-section entries.
file_name validation now rejects /, \, NUL, Windows reserved characters, and . / .. outright (regex on InitiateUploadRequest and InitiateBatchUploadRequest). The DefaultChunkHandler::assemble() additionally applies a defence-in-depth basename() and refuses empty / dot names. Existing valid file names are unaffected; previously-accepted malicious names like ../../public/.htaccess now fail validation with 422.Authorizer interface (NETipar\Chunky\Authorization\Authorizer) bound by default to DefaultAuthorizer, which enforces auth()->id() === upload->userId whenever the upload/batch was created with an owner. Anonymous uploads (no user_id) keep the old "anyone with the id can use it" semantics for backward compatibility, but as soon as auth middleware is in place, IDOR is blocked. UploadChunkRequest::authorize(), InitiateBatchUploadRequest::authorize(), and UploadStatusController / BatchStatusController / CancelUploadController all enforce this; non-owners see a 404 (not 403) so upload IDs aren't leaked through error response timing.routes/channels.php registered automatically when broadcasting is enabled; the upload, batch, and user channels delegate to the same Authorizer so HTTP and WebSocket access stay in sync. Disable with chunky.broadcasting.register_channels = false if you prefer to register your own.GET /upload/{uploadId} response no longer includes the storage disk, the absolute final_path, or the owner user_id. Use UploadMetadata::toArray() server-side if you need them. New UploadMetadata::toPublicArray() helper exposes the trimmed payload.ChunkedUpload and ChunkyBatch now define explicit $fillable arrays instead of $guarded = [].BatchUploader.enqueue() in [@netipar](https://github.com/netipar)/chunky-core. Same signature as upload() but doesn't throw Batch upload already in progress. if a batch is already running — the files are held in an internal queue and run as their own batch when the current one finishes (success or failure). When no batch is active, enqueue() immediately delegates to upload(), so callers can use it as a drop-in replacement that "just keeps working" across overlapping calls. The returned promise resolves with that batch's BatchResult, or rejects with Batch upload cancelled before queued upload could start. / BatchUploader destroyed before queued upload could start. if cancel() / destroy() runs before the queued batch starts — so callers don't leak hanging promises. useBatchUpload in both the Vue 3 and React wrappers exposes the matching enqueue method. Strict upload() keeps its existing behaviour for callers that want to detect overlap.onFileProgress in the React useBatchUpload hook and a matching chunky:batch-file-progress DOM event on the Alpine batchUpload data component. Now exposed in all three wrappers — Vue 3, React, Alpine.Authorizer and DefaultAuthorizer services (NETipar\Chunky\Authorization\*). Bind a custom implementation to override ownership rules (admin overrides, shared batches, etc.).AssembleFileJob retry support. tries = 3 with backoff = 30s, plus stale-claim recovery: if a worker crashed mid-assembly, a subsequent retry can re-claim the upload after chunky.assembly_stale_after_minutes (default 10).BatchMetadata::successProgress() alongside progress(), for callers that want the success-only view.destroy() method, so they remain safe to use outside a component scope (Pinia stores, plain modules).chunky.max_files_per_batch config (default 1000) — DOS protection on the batch initiate endpoint.AssembleFileJob "stuck in Assembling" recovery. Previously, if a worker crashed between assemble() and updateStatus(Completed), the upload was wedged in Assembling forever: claimForAssembly() only allowed Pending → Assembling, and chunky:cleanup skipped Assembling rows. Now both trackers (DB and FS) treat an Assembling claim older than assembly_stale_after_minutes as recoverable — a queue retry will take it over and re-run the job. The cleanup command applies the same window so abandoned assemblies don't leak storage forever.AssembleFileJob cleanup ordering. The chunk cleanup() call moved from before the save callback to after updateStatus(Completed), so a save-callback failure or worker crash mid-flight no longer leaves us with no chunks AND no completed file — a retry can recover.AssembleFileJob::failed() no longer flips Completed → Failed. Previously, if handle() succeeded but the queue retried it for an unrelated reason (post-Completed dispatch failure, broker hiccup), failed() would overwrite the upload status with Failed, double-increment the batch's failure counter, and broadcast a contradictory UploadFailed event. The early-return now matches any terminal state, not just Failed.markChunkUploaded() rejects late chunks. Both the DB and FS trackers now refuse chunk POSTs against uploads that aren't in Pending (cancelled, completed, assembling, failed, expired). The HTTP layer surfaces this as 409 Conflict. A pre-flight check in ChunkyManager::uploadChunk() short-circuits before the chunk hits disk, so cancel-races no longer leave orphan chunk files behind.BatchUploader cancel-during-finalise race. If cancel() ran in the very last tick of an upload() (after all worker promises resolved but before the success path emitted complete), both cancel and complete events fired for the same batch, contradicting each other. The success path now checks the cancel/abort flag and returns the partial result silently.BatchUploader cancel suppresses redundant error event. A user-driven cancel() would surface the resulting AbortError as an error event in addition to cancel, so UI components showed both a "cancelled" indicator and an "error" toast for the same action. After cancel(), the catch branch tears down in-flight per-file uploaders and rethrows without re-emitting error.BatchUploader exception path tears down per-file uploaders. A failure in initiateBatch (or any pre-loop step) used to leave the per-file ChunkUploader workers running in the background, continuing to POST chunks against an uploader the caller had already given up on. The catch branch now calls cancel() on each before re-emitting error.ChunkUploader.upload() no longer corrupts a new upload by reusing a stale uploadId. Previously, calling upload(B) after a failed upload(A) (without cancel()) would resume the A upload on the server with B's chunk bytes, producing a hybrid file under A's id. The reuse branch now requires lastFile === file referential equality; otherwise it falls through to a fresh initiate.CompletionWatcher treats 401/403 as fatal. Previously only 404 stopped polling; auth failures looped at 2-second intervals until the wall-clock timeout. They now invoke onError(err, true) and stop.CompletionWatcher skips polling once Echo subscribed. When the Echo subscription confirms, the deferred poll start is cancelled — saves one HTTP request per active wait, which adds up on dashboards. If the subscribe fails, polling kicks off immediately instead of waiting out the start delay.BatchUploader.enqueue() race when cancelling the active batch. drainQueue() used to shift() the next queued batch before the deferring microtask ran. If cancel() (or destroy()) ran in the gap — for example from a catch handler attached to the previous batch's rejection — the queued promise was rejected via rejectPendingQueue(), but the microtask had already captured the shifted entry and started a fresh upload anyway. The shift now happens inside the microtask and re-checks isUploading / queue length.BatchUploader.destroy() rejection reason. destroy() called cancel() first and then rejectPendingQueue('… destroyed …'). Since cancel() already drains the queue under the generic "cancelled" reason, the destroy-specific reject became a silent no-op. Reordered so the destroy reason is applied first.BatchMetadata::progress() now includes failed files. Previously a 4-file batch with 3 successes and 1 failure reported 75% progress while the batch was actually done; now it reports 100%, and the success-only view is available via successProgress().InitiateBatchUploadRequest validation uses the batch's context, not the request's. Closes a validation-bypass where a caller could declare context: documents (max 50MB) in the request while the batch was created with context: photos (max 5MB) — the request validated against documents, but the save callback ran under photos. The request also rejects new uploads against terminal batches (Completed / PartiallyCompleted / Expired) at the validation layer.validateBatchExists rejects terminal batches. A Completed / PartiallyCompleted / Expired batch can no longer accept further initiateInBatch calls — previously it silently grew past total_files, leaving the counters inconsistent.FilesystemTracker refuses to boot on non-local disks. The tracker's flock()-based mutation paths only work when the configured chunky.disk exposes a real local path. Booting against an S3/GCS-style disk previously fell back silently to lock-free writes — every chunk-write/claim was a lost-update race. Boot-time check now throws ChunkyException with a clear message; switch chunky.tracker to database, or set chunky.skip_local_disk_guard = true (for advanced users with external locking) to bypass.UploadStatusController JSON response no longer includes disk, final_path, user_id. See Security above.Models\ChunkedUpload::markChunkUploaded() signature dropped its unused ?string $checksum parameter. The tracker contract UploadTracker::markChunkUploaded() still accepts it for forward compatibility.UploadChunkController maps ChunkyException to 409 Conflict and UploadExpiredException to 410 Gone.AssembleFileJob is now retryable. Custom queue configurations that assumed a single attempt may need to be aware of tries=3, backoff=30. Override at dispatch time if needed.0.12.0 (core, vue3, react, alpine).getCurrentInstance() + onBeforeUnmount to getCurrentScope() + onScopeDispose. This preserves component-scoped behaviour but additionally cleans up uploader instances when used inside Pinia stores or effectScope blocks. Composables also expose an explicit destroy() for manual teardown.This release smooths out batch progress reporting and ships a new CompletionWatcher API that lets the frontend wait for batch completion via Echo broadcasts with a polling fallback — the same primitive useBatchCompletion is built on. No PHP changes; this is a frontend-only release.
BatchUploader used to assign this.progress a per-step value ((filesCompleted / total) * 100) on every fileComplete, which clobbered the smooth, chunk-driven progress that the worker loop had been interpolating into it. The result was a UI bar that crept up smoothly during a file, then snapped to a discrete step value at the boundary, then crept again. Removes the step assignment so progress advances continuously across the whole batch as chunks finish, regardless of file count.CompletionWatcher keeps polling on transient errors. The watcher's onError callback used to fire with a single (error) argument and the consumer had no way to distinguish a one-off network blip from a fatal subscription failure. Signature is now (error, isFatal) — the watcher itself only flags isFatal=true when the Echo subscription gives up; transient HTTP failures during polling no longer cancel the watch. useBatchCompletion mirrors this and only flips isWaiting to false on the fatal branch, so the composable stays in the "waiting" state through retryable errors.CompletionWatcher and watchBatchCompletion() in [@netipar](https://github.com/netipar)/chunky-core. A broadcast-or-poll primitive: subscribes to the batch's private channel via Echo and resolves on BatchCompleted / BatchPartiallyCompleted, with a status-endpoint poll as a fallback when Echo is unavailable or slow to subscribe. Surfaces lifecycle through onComplete, onPartiallyCompleted, onError(error, isFatal), plus the cancel() returned from watchBatchCompletion. Useful when the upload finishes on one tab/process and the UI that needs to react to it lives somewhere else.useBatchCompletion Vue 3 composable in [@netipar](https://github.com/netipar)/chunky-vue3. Wraps CompletionWatcher with reactive isWaiting, isComplete, result, and error refs and an auto-cancel on unmount. Mirrors the new fatal/transient error semantics — the composable stays in the waiting state through transient failures.fileProgress event on BatchUploader ({ batchId, uploadId, file, progress, fileIndex }) emitted on every chunk progress tick, so consumers can drive a per-file progress bar without subscribing to the active uploader. useBatchUpload exposes the matching onFileProgress callback.BatchUploader.progress now interpolates across the whole batch (file boundary smoothing baked into the chunk-driven progress emission) — see the matching Fixed entry above.listenForUser and listenForBatchComplete accept optional onSubscribed / onSubscribeError callbacks that route to the underlying channel's subscribed() / error() hooks when available. EchoChannel's subscribed? and error? hooks are now part of the type, with error callback typed as (err: unknown) instead of any.FileProgressEvent.uploadId is now string (was string | null). The id is always set by the time a fileProgress event fires.EchoChannel.error? callback parameter is unknown (was any). Preserves the runtime contract while removing the implicit-any escape hatch for consumers.http.ts util in [@netipar](https://github.com/netipar)/chunky-core consolidates CSRF token discovery and request-header construction. Three call sites (BatchUploader, ChunkUploader, CompletionWatcher) now share one implementation instead of each rolling their own.0.11.0 (core, vue3, react, alpine). React and Alpine carry no source changes; they re-publish for version-sync consistency. Sister packages continue to require [@netipar](https://github.com/netipar)/chunky-core via workspace:^, which resolves to ^0.11.0 on publish.CompletionWatcher.onError signature change (error) → (error, isFatal) is on a brand-new, previously-unpublished API; no existing consumer is affected. The FileProgressEvent.uploadId and EchoChannel.error? type tightenings are non-breaking on the JS runtime — they only fail TypeScript builds that were relying on the looser types, and in both cases the looser types were unsound in practice.This release fixes the long-standing "the file uploaded but the UI shows it failed" symptom on large uploads, plus a related cluster of race conditions and a couple of long-overdue ergonomics gaps. There are intentional contract changes — see Breaking changes at the bottom.
AssembleFileJob runs no longer corrupt each other. UploadChunkController dispatches the assembly job whenever the tracker reports is_complete=true. The v0.9.3 lockForUpdate only protected the chunk-list write, not the subsequent isComplete read, so two parallel chunk requests near the end of a large upload could each observe completion and each enqueue a job. The first job assembled the file and ran cleanup(); the second job crashed mid-assemble() because the chunks were gone, dispatched UploadFailed, and the user saw a failure even though the file was on disk. Adds UploadTracker::claimForAssembly() (CAS update from Pending to Assembling on the database tracker, status-guarded write under flock on the filesystem tracker), and AssembleFileJob returns silently when another worker already won the claim.is_complete response. When the backend reported is_complete=true, only the worker that received the response returned — the other workers continued POSTing their already-in-flight chunks, which fed the server-side race above. A shared completed flag now bails out the remaining workers before the next request.DefaultChunkHandler::assemble() now works on every Flysystem driver. The previous implementation called $disk->path() and used fopen()/mkdir() directly, which only works on the local driver — S3, GCS, and friends raised a RuntimeException for every assembly and the chunks were never cleaned up. Streams chunk-by-chunk through readStream() into a sys_get_temp_dir() temp file, then uploads with writeStream(). Memory stays at the 8 KB read buffer regardless of file size, and the temp file is unlinked even if an error is thrown. Missing chunks now raise a descriptive RuntimeException with the chunk index instead of a warning-level fopen() failure.ChunkUploader resets its internal state after a successful upload. When the same instance was reused for a second file (the default for ChunkDropzone, useChunkUpload, and any UI that holds a single uploader reference), the leftover uploadId from the previous run made upload() enter the resume branch, hit /status with the stale id, and either upload nothing or throw. Clears uploadId, pendingChunks, lastFile, and lastMetadata when emitting the complete event. isComplete, progress, and currentFile are intentionally preserved so the UI can still display the finished file.complete / error replay. Both ChunkUploader and BatchUploader fired complete and error synchronously, so any listener that registered after the upload finished — for example because the parent component mounted while the upload was in flight — never received the event. on('complete', cb) and on('error', cb) now schedule the callback in a microtask if the event has already happened. The cache is cleared on the next upload() and on cancel().pause()/resume()/retry() no longer leak unhandled promise rejections. The fire-and-forget this.upload(...) call inside resume() and retry() had no .catch(), so a network failure surfaced as an UnhandledPromiseRejection in browser devtools. The error itself was already delivered through the error event, so we just swallow the rejection.ChunkyManager::uploadChunk() previously called markChunkUploaded + getMetadata + isComplete — three reads per chunk, on top of the chunk write. For a 1000-chunk upload that was 3000+ DB queries, and the unlocked isComplete read was part of the assembly race above. markChunkUploaded() now returns the freshly updated UploadMetadata from inside the lockForUpdate transaction; uploadChunk() consumes that one snapshot.VerifyChunkIntegrity middleware and DefaultChunkHandler::store() no longer buffer the chunk twice into memory. Each chunk request used to allocate 2 × chunk_size of PHP heap (one read for the SHA-256, one for the disk write). The middleware now hash_file()s the upload's temp path; the handler streams it via writeStream(). Both fall back to getContent() when no temp file is available.BatchUploader.pause() actually pauses the batch worker loop. It used to pause only the active per-file uploaders; the outer worker loop kept pulling files from the queue and starting fresh uploaders. Adds a Promise-based barrier the loop awaits between files when isPausedBatch is true.BatchUploader.cancel() resets isComplete and emits a dedicated cancel event ({ batchId }). Previously a cancel after the last fileComplete left isComplete=true, and consumers had no way to tell apart "user cancelled" from "upload finished". The sticky event cache is also cleared so a late listener cannot replay a stale event after the user cancelled.BatchUploader.fetchJson captures the AbortSignal locally. It used to read this.abortController?.signal at await-time, which could attach a request to a freshly-replaced controller in destroy/cancel flows.fetchJson paths used to collapse non-2xx responses into new Error('HTTP {status}: {body}'), hiding Laravel validation arrays behind an opaque string. They now throw UploadHttpError with status and parsed body fields. Existing error.message consumers keep working.FilesystemTracker mutations are guarded by flock(). v0.9.3 fixed the DatabaseTracker race; the filesystem tracker still did a bare read-modify-write on metadata.json, which dropped chunk indices under concurrent writes. markChunkUploaded, updateStatus, and claimForAssembly now run under an exclusive lock on a sibling .lock file. The guard is best-effort: when the disk does not expose a local path (S3, etc.) the callback runs unguarded — that combination was already unsupported.AssembleFileJob workers finished within the same tick, each one persisted the terminal status and dispatched a BatchCompleted event, so the frontend received N notifications for one logical transition. The DB path now uses a CAS UPDATE that only matches non-terminal statuses; the filesystem path runs its check under the new batch flock.DELETE /api/chunky/upload/{uploadId} cancel endpoint (CancelUploadController). The frontend ChunkUploader.cancel() now fires a background DELETE against it so the chunks are released immediately instead of waiting for the expiration sweep.UploadStatus::Cancelled enum case.chunky:cleanup Artisan command. Removes expired uploads (chunk files + tracker metadata) for both database and filesystem trackers. Supports --dry-run. The previously-orphaned auto_cleanup config option is now respected — when true, the service provider schedules the command daily with withoutOverlapping().UploadHttpError exported from [@netipar](https://github.com/netipar)/chunky-core with status + parsed body for granular client error handling.BatchCancelEvent event on BatchUploader and corresponding type export.chunk_index validation now rejects values above the upload's total_chunks (resolved through the tracker).UploadTracker contract. Custom tracker implementations must update:
markChunkUploaded() returns UploadMetadata instead of void (the freshly updated snapshot from inside the lock).claimForAssembly(string $uploadId): bool, expiredUploadIds(): array<int, string>, forget(string $uploadId): void.UploadStatus::Cancelled case. Any consumer doing an exhaustive match on UploadStatus needs to add a branch.DELETE /api/chunky/upload/{uploadId} route. If you publish and customise routes/api.php, re-run php artisan vendor:publish --tag=chunky-routes (if you publish them) or merge the new route manually.0.10.0 (core, vue3, react, alpine). Sister packages now require [@netipar](https://github.com/netipar)/chunky-core: ^0.10.0.BatchProgressEvent.currentFile.progress was stuck at 0% until a file finished uploading. BatchUploader.emitProgress() populated currentFile.progress from this.progress, which is the batch-level percentage — that value only advances when a whole file completes. So every chunk-progress tick emitted an event claiming the in-flight file was at 0% until the final chunk flipped it to 100%. Frontends that bind a per-file progress bar to currentFile.progress (including the Alpine and Vue composables shipped with this package) showed a frozen 0% bar for the entire duration of each upload. Fix plumbs the active ChunkUploader through to emitProgress(uploader) so currentFile carries the uploader's real per-file progress and currentFile.name. The post-file-completion emitProgress() call inside the worker loop now emits currentFile: null (instead of the stale previous filename with a bogus progress: 0), which also prevents a 100% → 0% UI flash between files in concurrent batches.DatabaseTracker::markChunkUploaded() — concurrent chunk uploads lost writes. The previous implementation read uploaded_chunks from the model, mutated the PHP array in memory, and wrote it back with update() — a classic read-modify-write without locking. With the default client-side concurrency of 3 parallel chunk requests, two workers would read the same pre-state, both append their own index, and the second update() would clobber the first. Symptom: a file with N chunks would end up stuck at is_complete: false with fewer than N indices in uploaded_chunks, and the BatchCompleted broadcast never fired. Fix wraps the read-mutate-write in DB::transaction() + lockForUpdate(), so the row is locked for the duration of the update and writes serialize correctly. Works on MySQL, Postgres, and SQLite (SQLite serializes the transaction at the BEGIN level; set busy_timeout and journal_mode=WAL on the connection if you see database is locked errors under load).DatabaseTrackerTest covers the transactional path and asserts that markChunkUploaded opens a DB transaction.AssembleFileJob no longer marks an upload as Completed before the context save callback runs — if the callback throws, the upload is now correctly marked Failed and its batch failure counter is incrementedCompleted state with no failure eventUploadFailed broadcast event dispatched on save callback errors and queue job failures (carries uploadId, disk, fileName, fileSize, context, reason)failed() job callback short-circuits when status is already Failed to avoid double-dispatching UploadFailed and double-incrementing batch failure countersAssembleFileJobTest covering save callback failure paths, batch failure propagation, and failed() idempotencyBatchUploader always creates a batch, even for single files — no more special-case single-file pathuploadSingle() internal method — 1 file = batch of 1batchId, every upload fires BatchCompletedThe frontend shouldn't need to decide upfront whether it's a single or multi-file upload. With this change, useBatchUpload is the single entry point for all uploads. The only overhead for a single file is one extra HTTP request (batch initiation).
user_id nullable indexed column on chunked_uploads and chunky_batches tablesChunkyManager auto-captures auth()->id() on upload and batch initiationUploadMetadata and BatchMetadata DTOs now include ?int $userId property{prefix}.user.{userId} — all upload/batch events for the authenticated user on a single channel (no need to know uploadId/batchId)broadcasting.user_channel config option (default: true) to enable user channel broadcastinglistenForUser() core helper — subscribe to all chunky events for a user on one channeluseUserEcho() Vue 3 composable with reactive userId watch and auto-cleanupuseUserEcho() React hook with useEffect cleanupBatchCompleted and BatchPartiallyCompleted events now carry ?int $userIdDatabaseTracker persists user_id from UploadMetadataUploadCompleted, BatchCompleted, BatchPartiallyCompleted implement ShouldBroadcastbroadcastWhen() guard: zero overhead when broadcasting is disabled (default)chunky.broadcasting.channel_prefix) and broadcast queue{prefix}.uploads.{uploadId}, {prefix}.batches.{batchId}config/chunky.php new broadcasting section (enabled, channel_prefix, queue)listenForUploadComplete(), listenForBatchComplete() with typed interfacesuseUploadEcho(), useBatchEcho() with auto-cleanupuseUploadEcho(), useBatchEcho() with useEffect cleanupEchoInstance, EchoChannel, UploadCompletedData, BatchCompletedData, BatchPartiallyCompletedData interfacesChunkyBatch Eloquent model with chunky_batches migration (auto-loaded for database tracker)BatchStatus enum: Pending, Processing, Completed, PartiallyCompleted, ExpiredUploadStatus::Failed case for handling assembly job failuresChunkyManager::initiateBatch() to create a batch and return BatchMetadata DTOChunkyManager::initiateInBatch() to add file uploads to an existing batchChunkyManager::getBatchStatus() returns typed BatchMetadata DTOChunkyManager::markBatchUploadCompleted() / markBatchUploadFailed() with atomic countersAssembleFileJob::failed() method — marks upload as Failed and updates batch counters on assembly errorBatchCompleted or BatchPartiallyCompleted event (lenient failure policy)BatchInitiated event dispatched on batch creationPOST /batch, POST /batch/{batchId}/upload, GET /batch/{batchId}chunky/temp/batches/{batchId}/batch.jsonBatchUploader class in [@netipar](https://github.com/netipar)/chunky-core for multi-file batch uploadsmaxConcurrentFiles option (default: 2) for parallel file uploads within a batchBatchUploader skips batch creation for 1 fileuseBatchUpload() composable for Vue 3 with reactive batch stateuseBatchUpload() hook for React with full state managementregisterBatchUpload() Alpine.js data component with DOM events (chunky:batch-progress, chunky:batch-complete, etc.)BatchProgressEvent, BatchResult, BatchUploaderState, BatchUploaderEventMapChunkyManager::initiate() now returns InitiateResult DTO (was array)ChunkyManager::uploadChunk() now returns ChunkUploadResult DTO (was array)ChunkyManager::initiateBatch() returns BatchMetadata DTOChunkyManager::initiateInBatch() returns InitiateResult DTO (with batchId)ChunkyManager::getBatchStatus() returns ?BatchMetadata DTOUploadMetadata DTO now includes ?string $batchId propertychunked_uploads migration adds batch_id nullable indexed columntracker === 'database')NETipar\Chunky\Data\)InitiateResult — uploadId, chunkSize, totalChunks, ?batchIdChunkUploadResult — isComplete, metadata (UploadMetadata)BatchMetadata — batchId, totalFiles, completedFiles, failedFiles, status, ?contextChunkyContext abstract class for class-based upload context registrationChunky::register() method for class-based contextsChunky::simple() one-line context registration with validate-and-moveUploadMetadata::withStatus() immutable method for status transitionsupdateStatus() added to UploadTracker contract (both drivers implement it)UploadCompleted event now carries full UploadMetadata DTO via $event->upload (backward-compatible shorthand properties preserved)contexts array for auto-registering class-based contexts on bootXSRF-TOKEN cookie for zero-config CSRF in Laravel appscreateDefaults() factory for isolated config scopes (no global state pollution)checksum option to disable per-chunk SHA-256 computation ({ checksum: false })upload() throws if already in progressresume() and retry() return boolean indicating success{uploadId} placeholder required)chunky:progress and chunky:chunk-uploaded DOM events in Alpine componentsetDefaults/getDefaults/createDefaults re-exports across all wrapper packagesinstanceof DatabaseTracker coupling removed from AssembleFileJobFilesystemTracker now supports updateStatus() with completed_at timestampsetDefaults() / getDefaults() in [@netipar](https://github.com/netipar)/chunky-coreChunkUploader constructor merges global defaults with per-instance options (headers are deep-merged)setDefaults and getDefaults re-exported from [@netipar](https://github.com/netipar)/chunky-vue3, [@netipar](https://github.com/netipar)/chunky-react, and [@netipar](https://github.com/netipar)/chunky-alpinesetDefaults({ headers: { 'X-CSRF-TOKEN': token } })->default('[]') from uploaded_chunks JSON column in migration (MariaDB/MySQL does not allow default values on JSON columns)illuminate/* ^13.0)^11.0 support^4.0 supportchunked_uploads table already exists (added Schema::hasTable check)[@netipar](https://github.com/netipar)/chunky-core, [@netipar](https://github.com/netipar)/chunky-vue3, [@netipar](https://github.com/netipar)/chunky-react, [@netipar](https://github.com/netipar)/chunky-alpineuseChunkUpload) with full pause/resume/cancel/retry supportchunkUpload) with DOM event dispatchingChunky::context()UploadMetadata DTO with UploadStatus enum for structured upload state<livewire:chunky-upload /> component with Alpine.js integrationFilesystemTracker infinite recursion on expired uploadsUploadMetadata::fromArray() handling of UploadStatus enum from Eloquent model castsInitial release.
AssembleFileJobUploadInitiated, ChunkUploaded, ChunkUploadFailed, FileAssembled, UploadCompleteduseChunkUpload) with reactive stateChunkDropzone component (Dropzone.js wrapper)HeadlessChunkUpload renderless componentChunkUploader TypeScript core with typed event emitterChunkyManager facade with programmatic APIChunkHandler and UploadTracker contracts for custom implementationsHow can I help you explore Laravel packages today?