diff --git a/Package.resolved b/Package.resolved index d4c3e35..1dce0db 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ba32fc2d55e8b869cd782a194d27670458b550ee5f25318653cfaa47f4aa88ac", + "originHash" : "ca0999052a4897ba5a0213555d86f405627fd84d5f775410d62d2270bdf1ab5b", "pins" : [ { "identity" : "devtesting", diff --git a/Package.swift b/Package.swift index b870716..f84c6df 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( targets: [ .target( name: "DevFoundation", - swiftSettings: swiftSettings + swiftSettings: swiftSettings, ), .testTarget( name: "DevFoundationTests", diff --git a/Sources/DevFoundation/Paging/OffsetPage.swift b/Sources/DevFoundation/Paging/OffsetPage.swift new file mode 100644 index 0000000..d1e7c34 --- /dev/null +++ b/Sources/DevFoundation/Paging/OffsetPage.swift @@ -0,0 +1,26 @@ +// +// OffsetPage.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/8/25. +// + +import Foundation + +/// A type that has an offset from the first item in a page sequence. +public protocol OffsetPage: Sendable { + /// The offset of the page from the first page. + /// + /// The first page has a page offset of 0. + var pageOffset: Int { get } +} + + +extension OffsetPage { + /// The offset of the next page. + /// + /// This is equivalent to `pageOffset + 1`. + public var nextPageOffset: Int { + pageOffset + 1 + } +} diff --git a/Sources/DevFoundation/Paging/RandomAccessPager.swift b/Sources/DevFoundation/Paging/RandomAccessPager.swift new file mode 100644 index 0000000..b6fd6d3 --- /dev/null +++ b/Sources/DevFoundation/Paging/RandomAccessPager.swift @@ -0,0 +1,109 @@ +// +// RandomAccessPager.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/8/25. +// + +import Foundation +import Synchronization + +/// A type that can load pages on behalf of a random access pager. +public protocol RandomAccessPageLoader: SequentialPageLoader { + /// Returns whether a page exists at the given offset. + /// + /// - Parameter offset: The offset of the page whose existence is being checked. + func pageExists(at offset: Int) -> Bool + + /// Loads the page at a given offset. + /// + /// It is a programming error to call this function with a page for which ``pageExists(at:)`` returns false. + /// Conforming types should trap if this happens. + /// + /// - Parameter offset: The offset of the page to load. The first page is at offset 0. + func loadPage(at offset: Int) async throws -> Page +} + + +extension RandomAccessPageLoader { + public func pageExists(after page: Page) -> Bool { + return pageExists(at: page.nextPageOffset) + } + + + public func loadPage(after page: Page?) async throws -> Page { + return try await loadPage(at: page?.nextPageOffset ?? 0) + } +} + + +/// A type that can load pages in any order and provide fast access to previously loaded pages. +/// +/// In general, you should use a ``RandomAccessPager`` instead of creating your own conforming type. +public protocol RandomAccessPaging: RandomAccessPageLoader, SequentialPaging { + /// The page offset of the last loaded page. + /// + /// In this context, last refers to the largest page offset, not the most recently loaded page. + var lastLoadedPageOffset: Int? { get } +} + + +/// A random access page loader that provides fast access to previously loaded pages. +/// +/// Each random access pager uses a ``RandomAccessPageLoader`` to load pages on its behalf. You will typically create +/// custom page loaders that, e.g., fetch pages from a web service, and use a `RandomAccessPager` to load those pages as +/// needed and provide access to them. +public final class RandomAccessPager: RandomAccessPaging where Page: OffsetPage { + private struct LoadedPages { + var loadedPagesByOffset: [Int: Page] = [:] + var lastLoadedPageOffset: Int? + } + + + /// The page loader that the pager uses to load its pages. + private let pageLoader: any RandomAccessPageLoader + + /// A mutex that synchronizes access to the instance’s loaded pages. + /// + /// This dictionary is ordered by loaded page’s page offsets. + private let loadedPagesMutex: Mutex = Mutex(.init()) + + + /// Creates a new sequential pager with the specified page loader. + /// + /// - Parameter pageLoader: The page loader to use to load pages. + public init(pageLoader: some RandomAccessPageLoader) { + self.pageLoader = pageLoader + } + + + public var loadedPages: [Page] { + return loadedPagesMutex.withLock { (loadedPages) in + loadedPages.loadedPagesByOffset.values.sorted { $0.pageOffset < $1.pageOffset } + } + } + + + public var lastLoadedPageOffset: Int? { + return loadedPagesMutex.withLock(\.lastLoadedPageOffset) + } + + + public func pageExists(at offset: Int) -> Bool { + return pageLoader.pageExists(at: offset) + } + + + public func loadPage(at offset: Int) async throws -> Page { + if let loadedPage = loadedPagesMutex.withLock({ $0.loadedPagesByOffset[offset] }) { + return loadedPage + } + + let loadedPage = try await pageLoader.loadPage(at: offset) + loadedPagesMutex.withLock { (loadedPages) in + loadedPages.loadedPagesByOffset[offset] = loadedPage + loadedPages.lastLoadedPageOffset = max(loadedPages.lastLoadedPageOffset ?? .min, loadedPage.pageOffset) + } + return loadedPage + } +} diff --git a/Sources/DevFoundation/Paging/SequentialPager.swift b/Sources/DevFoundation/Paging/SequentialPager.swift new file mode 100644 index 0000000..50725e2 --- /dev/null +++ b/Sources/DevFoundation/Paging/SequentialPager.swift @@ -0,0 +1,107 @@ +// +// SequentialPager.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/8/25. +// + +import Foundation +import Synchronization + +/// A type that can load pages on behalf of a sequential pager. +public protocol SequentialPageLoader: Sendable { + /// The type of page that is loaded. + associatedtype Page: OffsetPage + + /// Returns whether a page exists after a given page. + /// + /// - Parameter page: The page immediately preceding the one whose existence is being checked. + func pageExists(after page: Page) -> Bool + + /// Loads the page after a given page. + /// + /// If `page` is `nil`, loads the first page. + /// + /// It is a programming error to call this function with a page for which ``pageExists(after:)`` returns false. + /// Conforming types should trap if this happens. + /// + /// - Parameter page: The page immediately preceding the one being loaded. + func loadPage(after page: Page?) async throws -> Page +} + + +/// A type that can load pages sequentially and provide fast access to previously loaded pages. +/// +/// This protocol exists so that random access pagers can be used in generic algorithms requiring sequential paging +/// functionality. In general, you should use ``SequentialPager`` or ``RandomAccessPager`` instead of creating your own +/// conforming type. +public protocol SequentialPaging: SequentialPageLoader { + /// The pages that have been previously loaded. + var loadedPages: [Page] { get } + + /// The last loaded page. + /// + /// This is the loaded page with the greatest page offset. If `nil`, no pages have been loaded. A default + /// implementation is provided that simply returns `loadedPages.last`. + var lastLoadedPage: Page? { get } +} + + +extension SequentialPaging { + public var lastLoadedPage: Page? { + return loadedPages.last + } +} + + +/// A sequential page loader that provides fast access to previously loaded pages. +/// +/// Each sequential pager uses a ``SequentialPageLoader`` to load pages on its behalf. You will typically create custom +/// page loaders that, e.g., fetch pages from a web service, and use a `SequentialPager` to load those pages as needed +/// and provide access to them. +public final class SequentialPager: SequentialPaging where Page: OffsetPage { + /// The page loader that the pager uses to load its pages. + private let pageLoader: any SequentialPageLoader + + /// A mutex that synchronizes access to the instance’s loaded pages. + private let loadedPagesMutex = Mutex<[Page]>([]) + + + /// Creates a new sequential pager with the specified page loader. + /// + /// - Parameter pageLoader: The page loader to use to load pages. + public init(pageLoader: some SequentialPageLoader) { + self.pageLoader = pageLoader + } + + + public var loadedPages: [Page] { + return loadedPagesMutex.withLock { $0 } + } + + + public func pageExists(after page: Page) -> Bool { + return pageLoader.pageExists(after: page) + } + + + public func loadPage(after page: Page?) async throws -> Page { + let pageOffset = page?.nextPageOffset ?? 0 + + if let loadedPage = loadedPagesMutex.withLock({ pageOffset < $0.endIndex ? $0[pageOffset] : nil }) { + return loadedPage + } + + let loadedPage = try await pageLoader.loadPage(after: page) + + loadedPagesMutex.withLock { (loadedPages) in + if pageOffset >= loadedPages.count { + loadedPages.append(loadedPage) + } else { + loadedPages[pageOffset] = loadedPage + } + } + + return loadedPage + } +} diff --git a/Tests/DevFoundationTests/Paging/OffsetPageTests.swift b/Tests/DevFoundationTests/Paging/OffsetPageTests.swift new file mode 100644 index 0000000..f05e5af --- /dev/null +++ b/Tests/DevFoundationTests/Paging/OffsetPageTests.swift @@ -0,0 +1,26 @@ +// +// OffsetPageTests.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/9/25. +// + +import DevFoundation +import DevTesting +import Foundation +import Testing + +struct OffsetPageTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test + mutating func nextPageOffsetIsCorrect() { + for _ in 0 ..< 10 { + let mockPage = MockOffsetPage() + let offset = random(Int.self, in: .min ..< .max) + mockPage.pageOffsetStub = Stub(defaultReturnValue: offset) + #expect(mockPage.nextPageOffset == offset + 1) + } + } +} diff --git a/Tests/DevFoundationTests/Paging/RandomAccessPagerTests.swift b/Tests/DevFoundationTests/Paging/RandomAccessPagerTests.swift new file mode 100644 index 0000000..3d0b73e --- /dev/null +++ b/Tests/DevFoundationTests/Paging/RandomAccessPagerTests.swift @@ -0,0 +1,211 @@ +// +// RandomAccessPagerTests.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/9/25. +// + +import DevFoundation +import DevTesting +import Foundation +import Testing + +struct RandomAccessPagerTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test + func initializationSetsUpEmptyPager() { + let mockLoader = MockRandomAccessPageLoader() + let pager = RandomAccessPager(pageLoader: mockLoader) + + #expect(pager.loadedPages.isEmpty) + #expect(pager.lastLoadedPage == nil) + #expect(pager.lastLoadedPageOffset == nil) + } + + + @Test + mutating func pageExistsDelegatesToLoader() { + let mockLoader = MockRandomAccessPageLoader() + mockLoader.pageExistsStub = Stub(defaultReturnValue: true) + + let pager = RandomAccessPager(pageLoader: mockLoader) + let offset = random(Int.self, in: 0 ... 100) + + #expect(pager.pageExists(at: offset)) + #expect(mockLoader.pageExistsStub.callArguments == [offset]) + } + + + @Test + mutating func pageExistsAfterUsesDefaultImplementation() { + let mockLoader = MockRandomAccessPageLoader() + mockLoader.pageExistsStub = Stub(defaultReturnValue: false) + + let pageOffset = random(Int.self, in: 0 ... 100) + let page = MockOffsetPage() + page.pageOffsetStub = Stub(defaultReturnValue: pageOffset) + + #expect(!mockLoader.pageExists(after: page)) + #expect(mockLoader.pageExistsStub.callArguments == [pageOffset + 1]) + } + + + @Test + mutating func loadPageAtLoadsPage() async throws { + let mockLoader = MockRandomAccessPageLoader() + let offset = random(Int.self, in: 0 ... 100) + let expectedPage = MockOffsetPage() + expectedPage.pageOffsetStub = Stub(defaultReturnValue: offset) + mockLoader.loadPageStub = ThrowingStub(defaultResult: .success(expectedPage)) + + let pager = RandomAccessPager(pageLoader: mockLoader) + + let loadedPage = try await pager.loadPage(at: offset) + + #expect(loadedPage === expectedPage) + #expect(pager.loadedPages == [expectedPage]) + #expect(pager.lastLoadedPage === expectedPage) + #expect(pager.lastLoadedPageOffset == offset) + #expect(mockLoader.loadPageStub.callArguments == [offset]) + } + + + @Test + mutating func loadPageAfterWithPageUsesDefaultImplementation() async throws { + let mockLoader = MockRandomAccessPageLoader() + let pageOffset = random(Int.self, in: 0 ... 100) + let nextOffset = pageOffset + 1 + let expectedPage = MockOffsetPage() + expectedPage.pageOffsetStub = Stub(defaultReturnValue: nextOffset) + mockLoader.loadPageStub = ThrowingStub(defaultResult: .success(expectedPage)) + + let page = MockOffsetPage() + page.pageOffsetStub = Stub(defaultReturnValue: pageOffset) + + let loadedPage = try await mockLoader.loadPage(after: page) + + #expect(loadedPage === expectedPage) + #expect(mockLoader.loadPageStub.callArguments == [nextOffset]) + } + + + @Test + mutating func loadPageAfterWithNilUsesDefaultImplementation() async throws { + let mockLoader = MockRandomAccessPageLoader() + let expectedPage = MockOffsetPage() + expectedPage.pageOffsetStub = Stub(defaultReturnValue: 0) + mockLoader.loadPageStub = ThrowingStub(defaultResult: .success(expectedPage)) + + let loadedPage = try await mockLoader.loadPage(after: nil) + + #expect(loadedPage === expectedPage) + #expect(mockLoader.loadPageStub.callArguments == [0]) + } + + + @Test + mutating func loadPageAtReturnsCachedPage() async throws { + let mockLoader = MockRandomAccessPageLoader() + let offset = random(Int.self, in: 0 ... 100) + let page = MockOffsetPage() + page.pageOffsetStub = Stub(defaultReturnValue: offset) + mockLoader.loadPageStub = ThrowingStub(defaultResult: .success(page)) + + let pager = RandomAccessPager(pageLoader: mockLoader) + + // Load page first time + _ = try await pager.loadPage(at: offset) + + // Load same page again - should return cached version + let cachedPage = try await pager.loadPage(at: offset) + + #expect(cachedPage === page) + #expect(pager.loadedPages == [page]) + #expect(mockLoader.loadPageStub.callArguments == [offset]) // Only called once + } + + + @Test + mutating func loadPageAtMaintainsOrder() async throws { + let mockLoader = MockRandomAccessPageLoader() + let offset0 = random(Int.self, in: 0 ... 30) + let offset1 = random(Int.self, in: 31 ... 60) + let offset2 = random(Int.self, in: 61 ... 100) + + let page0 = MockOffsetPage() + page0.pageOffsetStub = Stub(defaultReturnValue: offset0) + let page1 = MockOffsetPage() + page1.pageOffsetStub = Stub(defaultReturnValue: offset1) + let page2 = MockOffsetPage() + page2.pageOffsetStub = Stub(defaultReturnValue: offset2) + + mockLoader.loadPageStub = ThrowingStub( + defaultResult: .success(page1), + resultQueue: [.success(page2), .success(page0)] + ) + + let pager = RandomAccessPager(pageLoader: mockLoader) + + // Load pages out of order + _ = try await pager.loadPage(at: offset2) + _ = try await pager.loadPage(at: offset0) + _ = try await pager.loadPage(at: offset1) + + // Pages should be ordered by offset + #expect(pager.loadedPages == [page0, page1, page2]) + #expect(pager.lastLoadedPageOffset == offset2) + } + + + @Test + mutating func loadPageAtThrowsError() async { + let mockLoader = MockRandomAccessPageLoader() + let offset = random(Int.self, in: 0 ... 100) + let expectedError = randomError() + mockLoader.loadPageStub = ThrowingStub(defaultResult: .failure(expectedError)) + + let pager = RandomAccessPager(pageLoader: mockLoader) + + await #expect(throws: expectedError) { + try await pager.loadPage(at: offset) + } + + #expect(pager.loadedPages.isEmpty) + #expect(pager.lastLoadedPageOffset == nil) + } + + + @Test + mutating func loadPageAtHandlesConcurrentLoads() async throws { + let mockLoader = MockRandomAccessPageLoader() + let offset = random(Int.self, in: 0 ... 100) + let page1 = MockOffsetPage() + page1.pageOffsetStub = Stub(defaultReturnValue: offset) + let page2 = MockOffsetPage() + page2.pageOffsetStub = Stub(defaultReturnValue: offset) + + mockLoader.loadPageStub = ThrowingStub( + defaultResult: .success(page2), + resultQueue: [.success(page1)] + ) + + // Add delay to allow both calls to get past cache check + mockLoader.loadPagePrologue = { + try await Task.sleep(for: .seconds(0.5)) + } + + let pager = RandomAccessPager(pageLoader: mockLoader) + + // Start two concurrent loads of the same page + async let firstLoad = pager.loadPage(at: offset) + async let secondLoad = pager.loadPage(at: offset) + + let (first, second) = try await (firstLoad, secondLoad) + + #expect(first != second) + #expect(pager.loadedPages == [page1] || pager.loadedPages == [page2]) + #expect(mockLoader.loadPageStub.callArguments == [offset, offset]) + } +} diff --git a/Tests/DevFoundationTests/Paging/SequentialPagerTests.swift b/Tests/DevFoundationTests/Paging/SequentialPagerTests.swift new file mode 100644 index 0000000..3cf211a --- /dev/null +++ b/Tests/DevFoundationTests/Paging/SequentialPagerTests.swift @@ -0,0 +1,134 @@ +// +// SequentialPagerTests.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/9/25. +// + +import DevFoundation +import DevTesting +import Foundation +import Testing + +struct SequentialPagerTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + + @Test + func initializationSetsUpEmptyPager() { + let mockLoader = MockSequentialPageLoader() + let pager = SequentialPager(pageLoader: mockLoader) + + #expect(pager.loadedPages.isEmpty) + #expect(pager.lastLoadedPage == nil) + } + + + @Test + func pageExistsDelegatesToLoader() { + let mockLoader = MockSequentialPageLoader() + mockLoader.pageExistsStub = Stub(defaultReturnValue: true) + + let pager = SequentialPager(pageLoader: mockLoader) + let page = MockOffsetPage() + page.pageOffsetStub = Stub(defaultReturnValue: 0) + + #expect(pager.pageExists(after: page)) + #expect(mockLoader.pageExistsStub.callArguments == [page]) + } + + + @Test + func loadPageLoadsFirstPage() async throws { + let mockLoader = MockSequentialPageLoader() + let expectedPage = MockOffsetPage() + expectedPage.pageOffsetStub = Stub(defaultReturnValue: 0) + mockLoader.loadPageStub = ThrowingStub(defaultResult: .success(expectedPage)) + + let pager = SequentialPager(pageLoader: mockLoader) + + let loadedPage = try await pager.loadPage(after: nil) + + #expect(loadedPage === expectedPage) + #expect(pager.loadedPages == [expectedPage]) + #expect(pager.lastLoadedPage === expectedPage) + #expect(mockLoader.loadPageStub.callArguments == [nil]) + } + + + @Test + func loadPageReturnsCachedPage() async throws { + let mockLoader = MockSequentialPageLoader() + let page1 = MockOffsetPage() + page1.pageOffsetStub = Stub(defaultReturnValue: 0) + let page2 = MockOffsetPage() + page2.pageOffsetStub = Stub(defaultReturnValue: 1) + + mockLoader.loadPageStub = ThrowingStub( + defaultResult: .success(page2), + resultQueue: [.success(page1)] + ) + + let pager = SequentialPager(pageLoader: mockLoader) + + // Load first two pages + _ = try await pager.loadPage(after: nil) + _ = try await pager.loadPage(after: page1) + + // Load first page again - should return cached version + let cachedPage = try await pager.loadPage(after: nil) + + #expect(cachedPage === page1) + #expect(pager.loadedPages == [page1, page2]) + #expect(mockLoader.loadPageStub.callArguments == [nil, page1]) + } + + + @Test + mutating func loadPageThrowsError() async { + let mockLoader = MockSequentialPageLoader() + let expectedError = randomError() + mockLoader.loadPageStub = ThrowingStub(defaultResult: .failure(expectedError)) + + let pager = SequentialPager(pageLoader: mockLoader) + + await #expect(throws: expectedError) { + try await pager.loadPage(after: nil) + } + + #expect(pager.loadedPages.isEmpty) + #expect(pager.lastLoadedPage == nil) + } + + + @Test + func loadPageHandlesConcurrentLoads() async throws { + let mockLoader = MockSequentialPageLoader() + let page1 = MockOffsetPage() + page1.pageOffsetStub = Stub(defaultReturnValue: 0) + let page2 = MockOffsetPage() + page2.pageOffsetStub = Stub(defaultReturnValue: 0) + + mockLoader.loadPageStub = ThrowingStub( + defaultResult: .success(page2), + resultQueue: [.success(page1)] + ) + + // Add delay to allow both calls to get past cache check + mockLoader.loadPagePrologue = { + try await Task.sleep(for: .seconds(0.5)) + } + + let pager = SequentialPager(pageLoader: mockLoader) + + // Start two concurrent loads of the same page + async let firstLoad = pager.loadPage(after: nil) + async let secondLoad = pager.loadPage(after: nil) + + let (first, second) = try await (firstLoad, secondLoad) + + #expect(first != second) + #expect(pager.loadedPages == [page1] || pager.loadedPages == [page2]) + #expect(mockLoader.loadPageStub.callArguments == [nil, nil]) + } +} diff --git a/Tests/DevFoundationTests/Testing Helpers/MockOffsetPage.swift b/Tests/DevFoundationTests/Testing Helpers/MockOffsetPage.swift new file mode 100644 index 0000000..e54b847 --- /dev/null +++ b/Tests/DevFoundationTests/Testing Helpers/MockOffsetPage.swift @@ -0,0 +1,19 @@ +// +// MockOffsetPage.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/9/25. +// + +import DevFoundation +import DevTesting +import Foundation + +final class MockOffsetPage: OffsetPage, HashableByID { + nonisolated(unsafe) var pageOffsetStub: Stub! + + + var pageOffset: Int { + pageOffsetStub() + } +} diff --git a/Tests/DevFoundationTests/Testing Helpers/MockRandomAccessPageLoader.swift b/Tests/DevFoundationTests/Testing Helpers/MockRandomAccessPageLoader.swift new file mode 100644 index 0000000..ea9e205 --- /dev/null +++ b/Tests/DevFoundationTests/Testing Helpers/MockRandomAccessPageLoader.swift @@ -0,0 +1,27 @@ +// +// MockRandomAccessPageLoader.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/9/25. +// + +import DevFoundation +import DevTesting +import Foundation + +final class MockRandomAccessPageLoader: RandomAccessPageLoader where Page: OffsetPage { + nonisolated(unsafe) var pageExistsStub: Stub! + nonisolated(unsafe) var loadPagePrologue: (() async throws -> Void)? + nonisolated(unsafe) var loadPageStub: ThrowingStub! + + + func pageExists(at offset: Int) -> Bool { + pageExistsStub(offset) + } + + + func loadPage(at offset: Int) async throws -> Page { + try await loadPagePrologue?() + return try loadPageStub(offset) + } +} diff --git a/Tests/DevFoundationTests/Testing Helpers/MockSequentialPageLoader.swift b/Tests/DevFoundationTests/Testing Helpers/MockSequentialPageLoader.swift new file mode 100644 index 0000000..05a2ad3 --- /dev/null +++ b/Tests/DevFoundationTests/Testing Helpers/MockSequentialPageLoader.swift @@ -0,0 +1,27 @@ +// +// MockSequentialPageLoader.swift +// DevFoundation +// +// Created by Prachi Gauriar on 8/9/25. +// + +import DevFoundation +import DevTesting +import Foundation + +final class MockSequentialPageLoader: SequentialPageLoader where Page: OffsetPage { + nonisolated(unsafe) var pageExistsStub: Stub! + nonisolated(unsafe) var loadPagePrologue: (() async throws -> Void)? + nonisolated(unsafe) var loadPageStub: ThrowingStub! + + + func pageExists(after page: Page) -> Bool { + pageExistsStub(page) + } + + + func loadPage(after page: Page?) async throws -> Page { + try await loadPagePrologue?() + return try loadPageStub(page) + } +}