From c714e5d17ebed86fe93260f7381a0844d9697958 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 3 Oct 2023 09:45:19 +0100 Subject: [PATCH] feat(recs,store-sync): skip update stream when hydrating (#1661) --- packages/recs/src/Component.ts | 26 +++++++--- packages/store-sync/src/recs/recsStorage.ts | 56 ++++++++++++++------- packages/store-sync/src/recs/syncToRecs.ts | 38 ++++++++++---- 3 files changed, 86 insertions(+), 34 deletions(-) diff --git a/packages/recs/src/Component.ts b/packages/recs/src/Component.ts index dc4be8750c..58a56e8047 100644 --- a/packages/recs/src/Component.ts +++ b/packages/recs/src/Component.ts @@ -18,6 +18,11 @@ import { import { isFullComponentValue, isIndexer } from "./utils"; import { getEntityString, getEntitySymbol } from "./Entity"; +export type ComponentMutationOptions = { + /** Skip publishing this mutation to the component's update stream. Mostly used internally during initial hydration. */ + skipUpdateStream?: boolean; +}; + function getComponentName(component: Component) { return ( component.metadata?.componentName ?? @@ -80,7 +85,8 @@ export function defineComponent( component: Component, entity: Entity, - value: ComponentValue + value: ComponentValue, + options: ComponentMutationOptions = {} ) { const entitySymbol = getEntitySymbol(entity); const prevValue = getComponentValue(component, entity); @@ -109,7 +115,9 @@ export function setComponent( } } } - component.update$.next({ entity, value: [value, prevValue], component }); + if (!options.skipUpdateStream) { + component.update$.next({ entity, value: [value, prevValue], component }); + } } /** @@ -132,16 +140,17 @@ export function updateComponent( component: Component, entity: Entity, value: Partial>, - initialValue?: ComponentValue + initialValue?: ComponentValue, + options: ComponentMutationOptions = {} ) { const currentValue = getComponentValue(component, entity); if (currentValue === undefined) { if (initialValue === undefined) { throw new Error(`Can't update component ${getComponentName(component)} without a current value or initial value`); } - setComponent(component, entity, { ...initialValue, ...value }); + setComponent(component, entity, { ...initialValue, ...value }, options); } else { - setComponent(component, entity, { ...currentValue, ...value }); + setComponent(component, entity, { ...currentValue, ...value }, options); } } @@ -153,14 +162,17 @@ export function updateComponent( */ export function removeComponent( component: Component, - entity: Entity + entity: Entity, + options: ComponentMutationOptions = {} ) { const entitySymbol = getEntitySymbol(entity); const prevValue = getComponentValue(component, entity); for (const key of Object.keys(component.values)) { component.values[key].delete(entitySymbol); } - component.update$.next({ entity, value: [undefined, prevValue], component }); + if (!options.skipUpdateStream) { + component.update$.next({ entity, value: [undefined, prevValue], component }); + } } /** diff --git a/packages/store-sync/src/recs/recsStorage.ts b/packages/store-sync/src/recs/recsStorage.ts index f4c3672d2a..54d34c02a5 100644 --- a/packages/store-sync/src/recs/recsStorage.ts +++ b/packages/store-sync/src/recs/recsStorage.ts @@ -20,6 +20,7 @@ export type RecsStorageOptions = { world: RecsWorld; // TODO: make config optional? config: TConfig; + shouldSkipUpdateStream?: () => boolean; }; export type RecsStorageAdapter = { @@ -33,6 +34,7 @@ export type RecsStorageAdapter = { export function recsStorage({ world, config, + shouldSkipUpdateStream, }: RecsStorageOptions): RecsStorageAdapter { world.registerEntity({ id: singletonEntity }); @@ -53,7 +55,12 @@ export function recsStorage({ existingTable: getComponentValue(components.RegisteredTables, tableEntity)?.table, }); } else { - setComponent(components.RegisteredTables, tableEntity, { table: newTable }); + setComponent( + components.RegisteredTables, + tableEntity, + { table: newTable }, + { skipUpdateStream: shouldSkipUpdateStream?.() } + ); } } @@ -88,12 +95,17 @@ export function recsStorage({ entity, value, }); - setComponent(component, entity, { - ...value, - __staticData: log.args.staticData, - __encodedLengths: log.args.encodedLengths, - __dynamicData: log.args.dynamicData, - }); + setComponent( + component, + entity, + { + ...value, + __staticData: log.args.staticData, + __encodedLengths: log.args.encodedLengths, + __dynamicData: log.args.dynamicData, + }, + { skipUpdateStream: shouldSkipUpdateStream?.() } + ); } else if (log.eventName === "Store_SpliceStaticData") { // TODO: add tests that this works when no record had been set before const previousValue = getComponentValue(component, entity); @@ -113,10 +125,15 @@ export function recsStorage({ previousValue, newValue, }); - setComponent(component, entity, { - ...newValue, - __staticData: newStaticData, - }); + setComponent( + component, + entity, + { + ...newValue, + __staticData: newStaticData, + }, + { skipUpdateStream: shouldSkipUpdateStream?.() } + ); } else if (log.eventName === "Store_SpliceDynamicData") { // TODO: add tests that this works when no record had been set before const previousValue = getComponentValue(component, entity); @@ -137,18 +154,23 @@ export function recsStorage({ previousValue, newValue, }); - setComponent(component, entity, { - ...newValue, - __encodedLengths: log.args.encodedLengths, - __dynamicData: newDynamicData, - }); + setComponent( + component, + entity, + { + ...newValue, + __encodedLengths: log.args.encodedLengths, + __dynamicData: newDynamicData, + }, + { skipUpdateStream: shouldSkipUpdateStream?.() } + ); } else if (log.eventName === "Store_DeleteRecord") { debug("deleting component", { namespace: table.namespace, name: table.name, entity, }); - removeComponent(component, entity); + removeComponent(component, entity, { skipUpdateStream: shouldSkipUpdateStream?.() }); } } } diff --git a/packages/store-sync/src/recs/syncToRecs.ts b/packages/store-sync/src/recs/syncToRecs.ts index 2d439e0512..4759e5f538 100644 --- a/packages/store-sync/src/recs/syncToRecs.ts +++ b/packages/store-sync/src/recs/syncToRecs.ts @@ -1,5 +1,5 @@ import { StoreConfig } from "@latticexyz/store"; -import { World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs"; +import { Component as RecsComponent, World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs"; import { SyncOptions, SyncResult } from "../common"; import { RecsStorageAdapter, recsStorage } from "./recsStorage"; import { createStoreSync } from "../createStoreSync"; @@ -28,7 +28,12 @@ export async function syncToRecs({ indexerUrl, startSync = true, }: SyncToRecsOptions): Promise> { - const { storageAdapter, components } = recsStorage({ world, config }); + const { storageAdapter, components } = recsStorage({ + world, + config, + shouldSkipUpdateStream: (): boolean => + getComponentValue(components.SyncProgress, singletonEntity)?.step !== SyncStep.LIVE, + }); const storeSync = await createStoreSync({ storageAdapter, @@ -40,14 +45,27 @@ export async function syncToRecs({ indexerUrl, initialState, onProgress: ({ step, percentage, latestBlockNumber, lastBlockNumberProcessed, message }) => { - if (getComponentValue(components.SyncProgress, singletonEntity)?.step !== SyncStep.LIVE) { - setComponent(components.SyncProgress, singletonEntity, { - step, - percentage, - latestBlockNumber, - lastBlockNumberProcessed, - message, - }); + // already live, no need for more progress updates + if (getComponentValue(components.SyncProgress, singletonEntity)?.step === SyncStep.LIVE) return; + + setComponent(components.SyncProgress, singletonEntity, { + step, + percentage, + latestBlockNumber, + lastBlockNumberProcessed, + message, + }); + + // when we switch to live, trigger update for all entities in all components + if (step === SyncStep.LIVE) { + for (const _component of Object.values(components)) { + // downcast component for easier calling of generic methods on all components + const component = _component as RecsComponent; + for (const entity of component.entities()) { + const value = getComponentValue(component, entity); + component.update$.next({ component, entity, value: [value, value] }); + } + } } }, });