refactor: nodeList generator

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-09-04 20:56:51 -04:00
parent 4d589fff31
commit 8e98e0749a
3 changed files with 75 additions and 17 deletions
+5 -12
View File
@@ -334,7 +334,7 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
// If not forcing, ensure the collection is empty // If not forcing, ensure the collection is empty
if (!$force) { if (!$force) {
$children = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $identifier, false); $children = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $identifier, false);
if (!empty($children)) { if ($children->valid()) {
throw new InvalidParameterException("Collection is not empty: $identifier"); throw new InvalidParameterException("Collection is not empty: $identifier");
} }
} }
@@ -808,18 +808,11 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
// Node operations (unified collections + entities) // Node operations (unified collections + entities)
public function nodeList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): array { public function nodeList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator {
$location = $this->normalizeLocation($location); $location = $this->normalizeLocation($location);
$entries = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $location, false, $filter, $sort, $range); // Do not retain yielded nodes in the service caches: large consumers must
// cache nodes // be able to release each node as they advance through the cursor.
foreach ($entries as $id => $node) { yield from $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $location, false, $filter, $sort, $range);
if ($node instanceof CollectionResource) {
$this->serviceCollectionCache[$id] = $node;
} elseif ($node instanceof EntityResource) {
$this->serviceEntityCache[$id] = $node;
}
}
return $entries;
} }
public function nodeListFilter(): IFilter { public function nodeListFilter(): IFilter {
+7 -5
View File
@@ -531,9 +531,11 @@ class MetaStore {
// ========== Node Operations (Unified/Recursive) ========== // ========== Node Operations (Unified/Recursive) ==========
/** /**
* List all nodes (collections and entities) * Lazily yield nodes from the database cursor, keyed by identifier.
*
* @return \Generator<string|int,CollectionResource|EntityResource>
*/ */
public function nodeList(string $tenantId, string $userId, string|int|null $location = null, bool $recursive = false, ?Filter $filter = null, ?Sort $sort = null, ?IRange $range = null): array { public function nodeList(string $tenantId, string $userId, string|int|null $location = null, bool $recursive = false, ?Filter $filter = null, ?Sort $sort = null, ?IRange $range = null): \Generator {
$query = [ $query = [
'tid' => $tenantId, 'tid' => $tenantId,
'uid' => $userId, 'uid' => $userId,
@@ -573,17 +575,17 @@ class MetaStore {
} }
$cursor = $this->_store->selectCollection($this->_NodeTable)->find($query, $options); $cursor = $this->_store->selectCollection($this->_NodeTable)->find($query, $options);
$list = [];
foreach ($cursor as $entry) { foreach ($cursor as $entry) {
$nodeType = $entry['type']; $nodeType = $entry['type'];
if ($nodeType === NodeType::Collection->value) { if ($nodeType === NodeType::Collection->value) {
$node = (new CollectionResource())->fromStore($entry); $node = (new CollectionResource())->fromStore($entry);
} else if ($nodeType === NodeType::Entity->value) { } else if ($nodeType === NodeType::Entity->value) {
$node = (new EntityResource())->fromStore($entry); $node = (new EntityResource())->fromStore($entry);
} else {
continue;
} }
$list[$node->identifier()] = $node; yield $node->identifier() => $node;
} }
return $list;
} }
/** /**
+63
View File
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace KTXT\ProviderLocalDocuments\Tests\Unit;
use KTXC\Db\Collection;
use KTXC\Db\Cursor;
use KTXC\Db\DataStore;
use KTXF\Files\Node\NodeType;
use KTXF\Resource\Range\RangeTally;
use KTXM\ProviderLocalDocuments\Providers\Personal\CollectionResource;
use KTXM\ProviderLocalDocuments\Providers\Personal\EntityResource;
use KTXM\ProviderLocalDocuments\Store\MetaStore;
use PHPUnit\Framework\TestCase;
final class NodeListTest extends TestCase
{
public function testCursorIsConsumedLazilyAndRangeIsPreserved(): void
{
$consumed = 0;
$rows = (static function () use (&$consumed): \Generator {
$consumed++;
yield ['nid' => 'folder', 'cid' => 'parent', 'type' => NodeType::Collection->value];
$consumed++;
yield ['nid' => 'file', 'cid' => 'parent', 'type' => NodeType::Entity->value];
})();
$collection = $this->createMock(Collection::class);
$collection->expects(self::once())->method('find')->with(
['tid' => 'tenant', 'uid' => 'user', 'cid' => 'parent'],
self::callback(static fn(array $options): bool =>
$options['skip'] === 64 && $options['limit'] === 64 && $options['sort']['type'] === 1),
)->willReturn(new Cursor($rows));
$database = $this->createMock(DataStore::class);
$database->expects(self::once())->method('selectCollection')->willReturn($collection);
$range = new RangeTally();
$range->setPosition(64);
$range->setTally(64);
$nodes = (new MetaStore($database))->nodeList('tenant', 'user', 'parent', false, null, null, $range);
self::assertSame(0, $consumed);
self::assertTrue($nodes->valid());
self::assertSame(1, $consumed);
self::assertSame('folder', $nodes->key());
self::assertInstanceOf(CollectionResource::class, $nodes->current());
$nodes->next();
self::assertSame(2, $consumed);
self::assertSame('file', $nodes->key());
self::assertInstanceOf(EntityResource::class, $nodes->current());
$nodes->next();
self::assertFalse($nodes->valid());
}
public function testEmptyCursorProducesAnEmptyGenerator(): void
{
$collection = $this->createStub(Collection::class);
$collection->method('find')->willReturn(new Cursor(new \ArrayIterator([])));
$database = $this->createStub(DataStore::class);
$database->method('selectCollection')->willReturn($collection);
$nodes = (new MetaStore($database))->nodeList('tenant', 'user', 'parent');
self::assertFalse($nodes->valid());
}
}