Preserve concurrent article mappings during static sync

This commit is contained in:
Codex 2026-10-30 12:03:17 +08:00
parent 25e0f87d97
commit 9d2cd9a895
3 changed files with 37 additions and 3 deletions

View File

@ -20,6 +20,7 @@ public function handle(LocalIllustrations $local): int
return self::FAILURE;
}
try {
$baseline = $local->selectionBaseline();
$snapshots = [];
$expected = [];
foreach (config('illustrations.categories') as $category => $pool) {
@ -33,7 +34,7 @@ public function handle(LocalIllustrations $local): int
$snapshots[$pool] = $snapshot;
$expected[$pool] = $keys;
}
$result = $local->synchronize($snapshots);
$result = $local->synchronize($snapshots, $baseline);
foreach ($expected as $pool => $keys) {
foreach ($keys as $key) {
if (! $local->thumbnail($pool, $key)) $result['pending'][$pool.':'.$key] ??= 'local_copy_unavailable';

View File

@ -128,9 +128,16 @@ public function storeSelection(string $pool, string $slug, array $image): string
}
/** One atomic manifest switch after all downloadable files pass verification. */
public function synchronize(array $snapshots): array
public function selectionBaseline(): array
{
return $this->locked(function () use ($snapshots) {
$this->manifest = null;
return $this->read()['selections'];
}
public function synchronize(array $snapshots, ?array $baseline = null): array
{
$baseline ??= $this->selectionBaseline();
return $this->locked(function () use ($snapshots, $baseline) {
$state = $this->read();
$pending = [];
foreach ($snapshots as $pool => $snapshot) {
@ -155,6 +162,18 @@ public function synchronize(array $snapshots): array
$pending[$pool.':'.$slug] = $reason;
}
}
// Receivers may publish new mappings while the remote snapshot is in flight.
// Never replace a local change made after this sync's baseline was captured.
$current = $state['selections'][$pool] ?? [];
$before = $baseline[$pool] ?? [];
foreach (array_unique(array_merge(array_keys($before), array_keys($current))) as $slug) {
$exists = array_key_exists($slug, $current);
if ($exists !== array_key_exists($slug, $before) || ($current[$slug] ?? null) !== ($before[$slug] ?? null)) {
if ($exists) $selections[$slug] = $current[$slug];
else unset($selections[$slug]);
unset($pending[$pool.':'.$slug]);
}
}
$state['selections'][$pool] = $selections;
}
$this->save($state);

View File

@ -125,4 +125,18 @@ public function test_missing_manifest_never_fetches_resource_or_returns_remote_f
$this->assertSame('/tutorial.png', IllustrationPool::thumbnail(['iCategoryId' => 3, 'sSlug' => 'guide'], '/tutorial.png'));
Http::assertNothingSent();
}
public function test_sync_does_not_overwrite_concurrent_article_assignments(): void
{
$this->fakeImages();$local = app(LocalIllustrations::class);$old = $this->image();
$local->storeSelection('freenode', 'fn1', $old);
$baseline = $local->selectionBaseline();
$new = $this->image('33333333-3333-4333-8333-333333333333');
$local->storeSelection('freenode', 'fn1', $new);
$local->storeSelection('freenode', 'fn2', $new);
$local->synchronize(['freenode' => ['items' => [$old], 'selections' => ['fn1' => $old], 'unavailable' => []]], $baseline);
$state = json_decode(file_get_contents($this->dir.'/private/manifest.json'), true);
$this->assertSame($new['asset_id'], $state['selections']['freenode']['fn1']);
$this->assertSame($new['asset_id'], $state['selections']['freenode']['fn2']);
}
}