UNPKG

notion-page-tree

Version:

Recursively fetch nested Notion pages from the root page/database/block node.

497 lines (436 loc) β€’ 74.7 kB
# Notion Page Tree > Fetch nested Notion pages from the root page/database/block. ![npm](https://img.shields.io/npm/v/notion-page-tree) ![size](https://img.shields.io/bundlephobia/minzip/notion-page-tree) ![prettier](https://img.shields.io/badge/codestyle-prettier-brightgreen) --- - [Notion Page Tree](#notion-page-tree) - [Why Would I Want This?](#why-would-i-want-this) - [πŸ™Œ Use the official Notion API.](#-use-the-official-notion-api) - [πŸ•Έ Fetch nested children pages.](#-fetch-nested-children-pages) - [πŸ›  Handle API Errors gracefully.](#-handle-api-errors-gracefully) - [Other Features](#other-features) - [πŸ’Ύ It saves fetch results to your local disk.](#-it-saves-fetch-results-to-your-local-disk) - [πŸ₯ž It builds basic page server.](#-it-builds-basic-page-server) - [πŸ”Ž It builds basic page search indexes (missing feature of the official Notion API).](#-it-builds-basic-page-search-indexes-missing-feature-of-the-official-notion-api) - [Advices](#advices) - [Usage](#usage) - [`.env` File Configuration](#env-file-configuration) - [Basic Usage](#basic-usage) - [Usage with More Options](#usage-with-more-options) - [Shape of Data](#shape-of-data) - [Notion's Original Block Tree Structure](#notions-original-block-tree-structure) - [Fetched Page Structure](#fetched-page-structure) - [Entity Data Types](#entity-data-types) - [`Entity`](#entity) - [`PlainEntity`](#plainentity) - [`Page | Database | Block`](#page--database--block) - [Fetch Result Data Types](#fetch-result-data-types) - [`NotionPageTree.prototype.page_collection`](#notionpagetreeprototypepage_collection) - [`NotionPageTree.prototype.root`](#notionpagetreeprototyperoot) - [`NotionPageTree.prototype.search_index`](#notionpagetreeprototypesearch_index) - [`NotionPageTree.prototype.search_suggestion`](#notionpagetreeprototypesearch_suggestion) - [How It Creates Fetch Queue](#how-it-creates-fetch-queue) - [NodeJS Promise Queue Example](#nodejs-promise-queue-example) - [Flowchart](#flowchart) --- ## Why Would I Want This? ### πŸ™Œ Use the official Notion API. - Popular `/loadpagechunk/` endpoint is not public and may not be stable in future updates. - Official API can be integrated with private key, so you can keep your database private. ### πŸ•Έ Fetch nested children pages. - Pages inside non-page blocks are also fetched. - Max request-depth can be set in your preference. - Main fetch loop uses nodejs timers, so it's safe from maxing out recursion depth. ### πŸ›  Handle API Errors gracefully. - Maximum fetch concurrency is set to avoid `rate_limited` error. - On `rate_limited` error, it stops and waits for some minutes. - Other errors are automatically retried. Max retry count can be set in your preference. --- ## Other Features ### πŸ’Ύ It saves fetch results to your local disk. - Set parameter `private_file_path` to your custom path. ### πŸ₯ž It builds basic page server. - `/page/:id/` endpoint for retrieving page and its childrens' id. - `/tree/:id/` endpoint for retrieving all nested pages from the page. ### πŸ”Ž It builds basic page search indexes (missing feature of the official Notion API). - Uses lunr.js. - Page's properties and chilren are converted into plain text for building search index. - `/search?keyword=` endpoint for searching page properties and retrieving page ids. - `/suggestion?keyword=` endpoint for looking for search index's tokens. --- ## Advices ⚠️ This library is not for fetching the whole nested page content. - This library is for listing nested pages to some depth and retrieve their properties. - Page's block children are fetched to boost up the search index results, not to display them. - If you want to render the whole page, use amazing libraries like `react-notion-x` (yet you should share your pages publically to the web). ⚠️ I recommend to keep `maxRequestDepth` lower than 5 and `maxBlockDepth` lower than 2. - Increasing max request depth will increase request count exponentially - If you want to fetch deeply nested pages, don't put them under plain blocks. Rather put them directly on the page's root level. --- ## Usage > See `./sample/index.ts` for full example file. ### `.env` File Configuration Write directly on `<package_root>/.env` ```text NOTION_ENTRY_ID = <root page/database/block's id> NOTION_ENTRY_KEY = <root's integration key> NOTION_ENTRY_TYPE = <page/database/block> ``` ### Basic Usage ```js import NotionPageTree from 'notion-page-tree'; async function simple_use() { const notionPageTree = new NotionPageTree(); // construct main class instance const server = notionPageTree.setupServer({ port: 8889 }); // Setup servers for listing and searching pages. (will respond 503 if pages are not fetched yet) await notionPageTree.parseCachedDocument(); // Look for cached documents in private_file_path. await notionPageTree.setRequestParameters({ prompt: true }); // Set environment variables that are needed for requesting Notion API. await notionPageTree.fetchOnce(); // Fetch pages once asynchronously. notionPageTree.startFetchLoop(1000 * 10); // Create an asynchronouse fetch loop. Wait for some milliseconds between each fetch. setTimeout(() => { notionPageTree.stopFetchLoop(); // Stopping fetch loop immediately. server.close(); // Stopping servers immediately. }, 1000 * 30); } simple_use(); ``` ### Usage with More Options ```js import NotionPageTree from 'notion-page-tree'; import path from 'path'; async function use_more_options() { const notionPageTree = new NotionPageTree({ private_file_path: path.resolve('./results/'), // path to save serialized page data searchIndexing: false, // turn off search indexing createFetchQueueOptions: { maxConcurrency: 3, // Current official rate limit is 3 requests per second. Notion api would likely to throw error when you increase this value. maxRetry: 2, // How many times errored request are retried ("rate_limited" error will wait some minutes before retrying) maxRequestDepth: 3, // Search depth applied to all the entities. maxBlockDepth: 2, // Search depth applied only to plain blocks (not page or database, relative depth to the nearest parent page). databaseQueryFilter: { // Use filters when querying databases (Find details in official notion API). property: 'isPublished', checkbox: { equals: true } } } }); await notionPageTree.parseCachedDocument(); const server = notionPageTree.setupServer({ port: 8888 }); await notionPageTree.setRequestParameters({ prompt: true, // Prompt and rewrite .env if parameters don't exist. forceRewrite: false // Prompt and rewrite .env even if parameters exist. }); await notionPageTree.fetchOnce(); notionPageTree.startFetchLoop(1000 * 10); setTimeout(() => { notionPageTree.stopFetchLoop(); server.close(); }, 1000 * 30); } ``` --- ## Shape of Data ### Notion's Original Block Tree Structure <div style="overflow-x: scroll !important; white-space: pre-wrap !important; width: 100%"> <pre style="display: inline-block;"> <code> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ database A β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ page A β”‚ β”‚ page B β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ check list β”‚ β”‚ bullet-list β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚toggle list β”‚ β”‚ page C β”‚ β”‚ page D β”‚ β”‚list elementβ”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ page E β”‚ β”‚ database B β”‚ β”‚list Elementβ”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ </code> </pre></div> ### Fetched Page Structure <div style="overflow-x: scroll !important; white-space: pre-wrap !important;"> <pre style="display: inline-block;"> <code> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ database A β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚page A β”‚ β”‚page B β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ blockChildren β”‚ β”‚ blockChildren β”‚ β”‚ PlainText β”‚ β”‚ PlainText β”‚ β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚β”‚ check list β”‚β”‚ β”‚β”‚ bullet list β”‚β”‚ β”‚β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚ β”‚β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚ β”‚β”‚ toggle list β”‚β”‚ β”‚β”‚list element β”‚β”‚ β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚β”‚list element β”‚β”‚ β–Ό β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ page E β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β” β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ page C β”‚ β”‚ page D β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ database B β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ </code></pre></div> --- ## Entity Data Types ### `Entity` Entity that has `children` as direct reference. ```typescript type Entity = Commons & (Page | Database | Block); interface Commons { id: string; depth: number; blockContentPlainText: string; parent?: Entity; children: Entity[]; } ``` ### `PlainEntity` Entity that has `children` as id. ```typescript type FlatEntity = FlatCommons & (Page | Database | Block); interface FlatCommons { id: string; depth: number; blockContentPlainText: string; parent?: string; children: string[]; } ``` ### `Page | Database | Block` Notion API's fetch request result for each entity types, with typed properties included. ```ts export interface Page { type: 'page'; metadata: Extract<GetPageResponse, { last_edited_time: string }>; } export interface Database { type: 'database'; metadata: Extract<GetDatabaseResponse, { last_edited_time: string }>; } export interface Block { type: 'block'; metadata: Extract<GetBlockResponse, { type: string }>; } ``` --- ## Fetch Result Data Types ### `NotionPageTree.prototype.page_collection` Key, value collection of `id` and `PlainEntity` ```ts page_collection: Record<string, FlatEntity> | undefined; ``` ### `NotionPageTree.prototype.root` Root `Entity` that has nested children `Entity`s. ```ts root: Entity | undefined; ``` ### `NotionPageTree.prototype.search_index` `lunr.Index` built with `page_collection` entities' `blockContentPlainText`. ```ts search_index: lunr.Index | undefined; ``` ### `NotionPageTree.prototype.search_suggestion` Search tokens extracted from `lunr.Index`. ```ts search_suggestion: string[] | undefined; ``` --- ## How It Creates Fetch Queue ### NodeJS Promise Queue Example Try it on the Stackblitz. https://stackblitz.com/edit/react-sp2zy3?embed=1&file=src/job.js <div style="overflow-x: scroll !important; white-space: pre-wrap !important; width: 100%"> <pre style="display: inline-block;"> <code> ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Request Ready Queue ┃ ┃ ┃ ┃ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃ ┃ β”‚ job β”‚ job β”‚ ... ┃ ┃ β”‚description β”‚description β”‚ ┃ ┃ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ ┃ β”‚ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ β”‚ If promise β”” queue has ─ ┐ empty slot β”‚ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Request Promise Queue β”‚ ┃ ┃ β–Ό ┃ ┃ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œ ─ ─ ─ ─ ─ ─ ┃ ┃ β”‚ queryable β”‚β”‚ queryable β”‚ (Empty Slot)│┃ ┃ β”‚ promise β”‚β”‚ promise β”‚β”‚ ┃ ┃ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ─ ─ ─ ─ ─ ─ β”˜β”ƒ ┃ β”‚ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ β”‚ If promise is setteled β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ promise β”‚ β”” ─ ─ β–Άβ”‚ handler() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ </code></pre></div> ### Flowchart <div style="overflow-x: scroll !important; white-space: pre-wrap !important; width: 100%"> <pre style="display: inline-block;"> <code>/******************************************************************************************************************************************************************************************************************************************************************************************************************************\ * * * * * * * * * ┏━━━━━━━Main Routine━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ * * ┃ ┃ * * ┃ ┃ * * ┃ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃ * * ┃ β”Œβ”€β”€β”€β–Άβ”‚ page_collection β”‚ β”ŒclearTimeout()───▢│ Request Promise Timer β”‚ ┃ * * ┃ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ * * ┃ β”‚ Ξ› β”‚ ┃ * * ┃ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•± β•² β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃ * * ┃ β”œβ”€β”€β”€β–Άβ”‚ page_tree β”‚ β•± β•² β”œclearTimeout()───▢│ Request Ready Timer β”‚ ┃ * * ┃ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•± β•² β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ * * ┃ β”‚ β•± β•² β”‚ ┃ * * ┃ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•± check β•² β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃ * * ┃ β”œβ”€β”€β”€β–Άβ”‚ Request Promise Queue │──────┐ β•± routine β•² β”‚ β”Œβ”€β”€β”€update ───▢│ page_collection β”‚ ┃ * * ┃ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β•± ↻ β•² β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ * * ┃ β”‚ Fetcher Routine │───── β”œβ”€β”€β”€β”€β”€β–Άβ–• promise = 0 ▏──┬─true──┼──── ┃ * * ┃ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β•² ready = 0 β•± β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃ * * ┃ β–² β”œβ”€β”€β”€β–Άβ”‚ Request Ready Queue β”‚β”€β”€β”€β”€β”€β”€β”˜ β•² ↻ β•± β”‚ β”‚ └───update────▢│ page_tree β”‚ ┃ * * ┃ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•² 0ms β•± β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ * * ┃ β”‚ β”‚ β•² β•± β”‚ β”‚ ┃ * * ┃ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•² β•± β”‚ β”‚ ┃ * * ┃ β”‚ β”œβ”€β”€β”€β–Άβ”‚ Request Promise Timer β”‚ β•² β•± β”‚ └──────────────────▢ wait for some minutes ─ ─ ┐ ┃ * * ┃ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•² β•± β”‚ ┃ * * ┃ β”‚ β”‚ V β”‚ β”‚ ┃ * * ┃ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–² β”‚ ┃ * * ┃ β”‚ └───▢│ Request Ready Timer β”‚ └──falseβ”€β”€β”€β”˜ β”‚ ┃ * * ┃ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ * * ┃ β”‚ β”‚ ┃ * * ┃ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ create new fetcher routine β—€ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┃ * * ┃ ┃ * * ┗━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ * * * * β”‚ * * * * β”‚ * * * * ┏━━━Fetcher Routine━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ * * ┃ β”Œβ”€.plaintext += plaintext────────────────────────────────────────────────────┐ ┃ * * ┃ β”‚ β”‚ ┃ * * ┃ ╔══════════════════════════╗ β”‚ ╔══════════════════════╗ β”‚ ┃ * * ┃ β”Œβ”€β”€false───┐ β•‘ Promise (resolved) β•‘ β”œβ”€.children.push()┐ β•‘Connector β•‘ β”‚ ┃ * * ┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ β”‚ β”‚ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ β”‚ β”‚ β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ β”‚ ┃ * * ┃ ┃ Request Promise Queue ┃ β”‚ β”‚ β•‘β”‚parentToAssign: Entity β”‚β—€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ β”‚toAssigned: itselfβ”‚ β•‘ β”‚ ┃ * * ┃ ┃ ╔══════════════════════════╗ ┃ β”‚ Ξ› β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘ β”Œβ”€β”€β”€β”€β–Άβ”‚ is Page/Database │──create──▢║ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ║──────────────│─────────────────┐ ┃ * * ┃ ┃ β•‘ Promise (pending) β•‘ ┃ β”‚ β•± β•² β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ β”‚ β”‚ ┃ * * ┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ β”‚ β•± β•² β•‘β”‚parentToRequest: Entityβ”‚ β•‘ β”‚ β”‚ β•‘β”‚toRequested: itself β”‚β•‘ β”‚ β”‚ ┃ * * ┃ ┃ New Requests β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•¬β–Άβ”‚parentToAssign: Entity β”‚β•‘ ┃ β”‚ β•± β•² β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘ β”‚ β”‚ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ β”‚ β”‚ ┃ * * ┃ ┃ ╔═════════════════════════╗ β”‚ ┃ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃ β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘...┃ β”‚ β•± β•² β”Œβ”€β”€β–Άβ”‚ is Fulfilled β”‚β”€β”€β–Άβ•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• β”‚ β”‚ ┃ * * ┃ ┃ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ β”‚ ┃ β”‚ β”‚ ┃ β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ β”‚ β•± set β•² β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘β”‚ children: β”‚ β•‘ β”‚ β”‚ β”‚ β”‚ ┃ * * ┃ ┃ β•‘β”‚parentToAssign: Entity β”‚β• β”€β”€β”€β”˜ ┃ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” List Block └─╋─╬▢│parentToRequest: Entityβ”‚β•‘ ┃ β”‚ β•± interval β•² β”‚ β•‘β”‚ QueryablePromise │─╬──── β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ┃ * * ┃ ┃ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β”œβ”€β”€β–Άβ”‚is Block/Page│───▢Children()─────┐ ┃ β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β”‚ β•± ↻ β•² β”‚ β•‘β”‚ Entity[] β”‚ β•‘ β”‚ β”‚ extractPlainText β”‚ Concatenated β”‚ β”‚ ┃ * * ┃ ┃ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ... ┃ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ┃ β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃─┴─▢▕ promise > 0 ▏──── β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘ β”‚ β”‚ β”Œβ”€β–Ά FromBlock ────▢.reduce()───▢│ Plain Text β”‚ β”‚ ┃ * * ┃ ┃ β•‘β”‚parentToRequest: Entityβ”‚β• β”€β”€β”€β”€β”€β”€β”€β”˜ .push()┃ β•‘ β”‚ children: β”‚β•‘ ┃ β•² isSetteled β•± β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ┃ * * ┃ ┃ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Query β”Œβ”€β”€β”€β”€β•¬β–Άβ”‚ QueryablePromise β”‚β•‘ ┃ β•² ↻ β•± └──▢│ is Rejected β”‚ β•‘β”‚ retry: number β”‚ β•‘ β”‚ β”‚ β”‚ β”‚ ┃ * * ┃ ┃ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ └──▢│ is Database │───▢Database()β”€β”€β”€β”€β”€β”˜ ┃ β•‘ β”‚ Entity[] β”‚β•‘ ┃ β•² 0ms β•± β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β” β”‚ β”‚ ┃ * * ┃ ┃ β•‘β”‚ retry: number β”‚β•‘ ┃ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β•² β•± β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• └────▢│ is Block β”‚ │─── β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ┃ * * ┃ ┃ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ ┃ β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ β•² β•± β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”˜ β”‚ β”‚ is in Traverse β”‚ β”‚ ┃ * * ┃ ┃ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ┃ ┃ β•‘ β”‚ retry: number β”‚β•‘ ┃ β•² β•± β”‚ set maxConcurrency to 0 β”‚ β”‚ β”Œβ”€β”€β”€β”€β–Άβ”‚ Exclusion List β”‚ ╔═══════════╬══════════╗ ┃ * * ┃ ┗━━━━━━━━━━━━━━▲━━━━━━━━━━━━━━━━━━┛ ┃ β•‘ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β•² β•± β–Ό empty promise_queue β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘Connector β”‚ β•‘ ┃ * * ┃ β”‚ ┃ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ┃ V rate_limited──true──▢ move promises to ready_queue β”‚ └─▢.filter()───── β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β•‘ ┃ * * ┃ β”‚ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ β”‚ wait for some minutes β”‚ β”‚ β”‚is NOT in Traverseβ”‚ β•‘ β”‚toAssigned: PARENTβ”‚ β•‘ ┃ * * ┃ β”‚ β”‚ set maxConcurrency to 3 β”‚ └────▢│ Exclusion List │──create──▢║ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘ ┃ * * ┃ β”‚ ╔═════════════════════════╗ false β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ * * ┃ β”‚ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ β”‚ β”‚ β•‘β”‚toRequested: itself β”‚β•‘ ┃ * * ┃ .splice(concurrency - β•‘β”‚parentToAssign: Entity β”‚β•‘ β”‚ β”‚ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ * * ┃ promise.length) β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ β–Ό β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•¬β•β•β•β•β•β•β•β•β•β•β• ┃ * * ┃ β”‚ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ retry < β”‚ β”‚ ┃ * * ┃ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β•‘β”‚parentToRequest: Entity│║◀───true───── retryCount ──false──▢ console.error β”‚ β”‚ ┃ * * ┃ β”‚ β”‚ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ β”‚ β”‚ ┃ * * ┃ β”‚ β”‚ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ β”‚ β”‚ ┃ * * ┃ β”‚ β”Œβ”€β”€false───┐ β”‚ β•‘β”‚ retry: +=1 β”‚β•‘ β”‚ β”‚ ┃ * * ┃ β”‚ β”‚ β”‚ β”‚ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ β”‚ β”‚ ┃ * * ┃ β”‚ β”‚ β”‚ .unshift() β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• β”‚ β”‚ ┃ * * ┃ β”‚ Ξ› β”‚ β–Ό β”‚ β”‚ ┃ * * ┃ β”‚ β•± β•² β”‚ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ β”‚ β”‚ ┃ * * ┃ β”‚ β•± β•² β”‚ ┃ Request Ready Queue ┃ β”‚ β”‚ ┃ * * ┃ β”‚ β•± β•² β”‚ ┃ ╔═════════════════════════╗ ╔═════════════════════════╗ ┃ β”‚ β”‚ ┃ * * ┃ β”‚ β•± β•² β”‚ ┃ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ β”‚ β”‚ ┃ * * ┃ β”‚ β•± check β•² β”‚ ┃ β•‘β”‚parentToAssign: Entity β”‚β•‘ β•‘β”‚parentToAssign: Entity β”‚β•‘ ┃ β”‚ β”‚ ┃ * * ┃ β”‚ β•± routine β•² β”‚ ┃ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β”‚ β”‚ ┃ * * ┃ β”‚ β•± ↻ β•² β”‚ ┃ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘...┃ β”‚ β”‚ ┃ * * ┃ └──────true───────▕ ready > 0 ▏◀─┴────────────┃ β•‘β”‚parentToRequest: Entityβ”‚β•‘ β•‘β”‚parentToRequest: Entityβ”‚β•‘ ┃◀────────────.push()β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ * * ┃ β•² promise < 3 β•± ┃ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β”‚ ┃ * * ┃ β•² β•± ┃ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ β”‚ ┃ * * ┃ β•² ↻ β•± ┃ β•‘β”‚ retry: 0 β”‚β•‘ β•‘β”‚ retry: 0 β”‚β•‘ ┃ β”‚ ┃ * * ┃ β•² 250ms β•± ┃ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘ ┃ β”‚ ┃ * * ┃ β•² β•± ┃ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ┃ β”‚ ┃ * * ┃ β•² β•± ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ β”‚ ┃ * * ┃ β•² β•± β–² β”‚ ┃ * * ┃ V β”‚ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ β”‚ ┃ * * ┃ (init) ┃ Page Collection ┃ β”‚ ┃ * * ┃ β”‚ ┃ β”Œβ”€β”€β”€β”€β•¦β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•— ┃ β”‚ ┃ * * ┃ β”‚ ┃ β”‚ id β•‘ page: Entity β•‘ ┃ β”‚ ┃ * * ┃ β”‚ ┃ └────╩═════════════════╝ ┃ β”‚ ┃ * * ┃ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ┃ β”Œβ”€β”€β”€β”€β•¦β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•— ┃ β”‚ ┃ * * ┃ β”‚ ROOT │───(init)─▢┃ β”‚ id β•‘ page: Entity β•‘ ┃◀────────────assign with keyβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ * * ┃ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ┃ └────╩═════════════════╝ ┃ ┃ * * ┃ β”‚ ┃ β”Œβ”€β”€β”€β”€β•¦β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•— ┃ ┃ * * ┃ ┃ β”‚ id β•‘ page: Entity β•‘ ┃ ┃ * * ┃ β”‚ ┃ └────╩═════════════════╝ ┃ ┃ * * ┃ ┃ .... ┃ ┃ * * ┃ β”‚ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ * * ┃ β”‚ ┃ * * ┃ β”” ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┃ * * ┃ ┃ * * ┃ β”‚ ┃ * * ┃ ╔══════════════════════╗ ┃ * * ┃ β•‘ Entity β•‘ ┃ * * ┃ β•‘β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β•‘ ┃ * * ┃ β•‘β”‚ id: string β”‚β•‘ ┃ * * ┃ β•‘β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β•‘