store sync bugfix
This commit is contained in:
@@ -91,7 +91,7 @@ export const DEFAULT_STORES: StoreConfig[] = [
|
||||
{
|
||||
name: 'shapes',
|
||||
persistent: true,
|
||||
initialState: () => ({}),
|
||||
initialState: () => ({ shapes: {} }),
|
||||
},
|
||||
{
|
||||
name: 'indicators',
|
||||
@@ -285,7 +285,7 @@ export interface Shape {
|
||||
/**
|
||||
* Shapes store - persistent, stores TradingView drawings and annotations.
|
||||
*/
|
||||
export type ShapesStore = Record<string, Shape>;
|
||||
export type ShapesStore = { shapes: Record<string, Shape> };
|
||||
|
||||
/**
|
||||
* Parameter schema entry for a custom indicator.
|
||||
|
||||
@@ -90,7 +90,14 @@ export class WorkspaceManager {
|
||||
|
||||
const states = await this.containerSync.loadAllStores(persistentStores);
|
||||
for (const [storeName, state] of states) {
|
||||
this.registry.setState(storeName, state);
|
||||
let migratedState = state;
|
||||
// Migrate shapes from old flat format { shapeId: {...} } to wrapped { shapes: { shapeId: {...} } }
|
||||
if (storeName === 'shapes' && state && typeof state === 'object' && !('shapes' in (state as object))) {
|
||||
migratedState = { shapes: state };
|
||||
this.dirtyStores.add(storeName); // persist migrated format immediately
|
||||
this.logger.info({ store: storeName }, 'Migrated shapes store to wrapped format');
|
||||
}
|
||||
this.registry.setState(storeName, migratedState);
|
||||
this.logger.debug({ store: storeName }, 'Loaded persistent store');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,16 @@ import type { BackendMessage, FrontendMessage, HelloMessage, PatchMessage } from
|
||||
import { wsManager } from './useWebSocket';
|
||||
|
||||
function deepReplace(target: Record<string, any>, source: Record<string, any>) {
|
||||
// Only delete target keys when source has content. An empty source {} is an initial-state
|
||||
// snapshot — skipping deletion preserves structural wrapper keys in composition API Pinia
|
||||
// stores (e.g. shapes, indicators, types) so $subscribe reactivity keeps working.
|
||||
if (Object.keys(source).length > 0) {
|
||||
for (const key of Object.keys(target)) {
|
||||
if (!(key in source)) {
|
||||
delete target[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
||||
|
||||
@@ -430,7 +430,7 @@ export function useTradingViewShapes(tvWidget: IChartingLibraryWidget) {
|
||||
function setupStoreWatchers() {
|
||||
// Watch for shape store changes and apply to TradingView
|
||||
watch(
|
||||
() => shapeStore.$state,
|
||||
() => shapeStore.shapes,
|
||||
async (newShapes, oldShapes) => {
|
||||
if (isUpdatingStore || !isChartReady) return
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { CustomIndicatorMetadata } from './indicators'
|
||||
|
||||
export interface CustomIndicatorType {
|
||||
@@ -10,12 +11,7 @@ export interface CustomIndicatorType {
|
||||
modified_at: number
|
||||
}
|
||||
|
||||
// Options API: flat $state = Record<string, CustomIndicatorType> matches backend initial state {}.
|
||||
// Composition API would produce $state = { types: {} }, causing deepReplace to delete 'types' on
|
||||
// the initial empty snapshot from the backend, which breaks $subscribe reactivity tracking.
|
||||
export const useIndicatorTypesStore = defineStore('indicator_types', {
|
||||
state: (): Record<string, CustomIndicatorType> => ({}),
|
||||
getters: {
|
||||
types: (state): Record<string, CustomIndicatorType> => state,
|
||||
},
|
||||
export const useIndicatorTypesStore = defineStore('indicator_types', () => {
|
||||
const types = ref<Record<string, CustomIndicatorType>>({})
|
||||
return { types }
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface CustomIndicatorParam {
|
||||
type: 'int' | 'float' | 'bool' | 'string'
|
||||
@@ -75,48 +76,48 @@ export interface IndicatorInstance {
|
||||
custom_metadata?: CustomIndicatorMetadata
|
||||
}
|
||||
|
||||
// Options API: flat $state = Record<string, IndicatorInstance> matches backend initial state {}.
|
||||
// Composition API would produce $state = { indicators: {} }, causing deepReplace to delete
|
||||
// 'indicators' on the initial empty snapshot, which breaks $subscribe reactivity tracking.
|
||||
// The 'indicators' getter maintains backwards compatibility with existing callers.
|
||||
export const useIndicatorStore = defineStore('indicators', {
|
||||
state: (): Record<string, IndicatorInstance> => ({}),
|
||||
export const useIndicatorStore = defineStore('indicators', () => {
|
||||
const indicators = ref<Record<string, IndicatorInstance>>({})
|
||||
|
||||
getters: {
|
||||
indicators: (state): Record<string, IndicatorInstance> => state,
|
||||
},
|
||||
|
||||
actions: {
|
||||
addIndicator(indicator: IndicatorInstance) {
|
||||
this.$patch({ [indicator.id]: indicator } as Partial<Record<string, IndicatorInstance>>)
|
||||
},
|
||||
|
||||
updateIndicator(id: string, updates: Partial<IndicatorInstance>) {
|
||||
const existing = (this.$state as Record<string, IndicatorInstance>)[id]
|
||||
if (existing) {
|
||||
this.$patch({
|
||||
[id]: { ...existing, ...updates, modified_at: Math.floor(Date.now() / 1000) }
|
||||
} as Partial<Record<string, IndicatorInstance>>)
|
||||
// Helper methods
|
||||
const addIndicator = (indicator: IndicatorInstance) => {
|
||||
indicators.value[indicator.id] = indicator
|
||||
}
|
||||
},
|
||||
|
||||
removeIndicator(id: string) {
|
||||
this.$patch((state) => {
|
||||
delete (state as Record<string, IndicatorInstance>)[id]
|
||||
})
|
||||
},
|
||||
|
||||
getIndicator(id: string): IndicatorInstance | undefined {
|
||||
return (this.$state as Record<string, IndicatorInstance>)[id]
|
||||
},
|
||||
|
||||
getAllIndicators(): IndicatorInstance[] {
|
||||
return Object.values(this.$state as Record<string, IndicatorInstance>)
|
||||
},
|
||||
|
||||
getIndicatorsBySymbol(symbol: string): IndicatorInstance[] {
|
||||
return Object.values(this.$state as Record<string, IndicatorInstance>)
|
||||
.filter(ind => ind.symbol === symbol)
|
||||
},
|
||||
},
|
||||
const updateIndicator = (id: string, updates: Partial<IndicatorInstance>) => {
|
||||
if (indicators.value[id]) {
|
||||
const updated = {
|
||||
...indicators.value[id],
|
||||
...updates,
|
||||
modified_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
indicators.value[id] = updated
|
||||
}
|
||||
}
|
||||
|
||||
const removeIndicator = (id: string) => {
|
||||
delete indicators.value[id]
|
||||
}
|
||||
|
||||
const getIndicator = (id: string): IndicatorInstance | undefined => {
|
||||
return indicators.value[id]
|
||||
}
|
||||
|
||||
const getAllIndicators = (): IndicatorInstance[] => {
|
||||
return Object.values(indicators.value)
|
||||
}
|
||||
|
||||
const getIndicatorsBySymbol = (symbol: string): IndicatorInstance[] => {
|
||||
return Object.values(indicators.value).filter(ind => ind.symbol === symbol)
|
||||
}
|
||||
|
||||
return {
|
||||
indicators,
|
||||
addIndicator,
|
||||
updateIndicator,
|
||||
removeIndicator,
|
||||
getIndicator,
|
||||
getAllIndicators,
|
||||
getIndicatorsBySymbol
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface ResearchType {
|
||||
display_name: string
|
||||
@@ -7,11 +8,7 @@ export interface ResearchType {
|
||||
modified_at: number
|
||||
}
|
||||
|
||||
// Options API: flat $state matches backend initial state {}.
|
||||
// See indicatorTypes.ts for explanation of why composition API breaks $subscribe.
|
||||
export const useResearchTypesStore = defineStore('research_types', {
|
||||
state: (): Record<string, ResearchType> => ({}),
|
||||
getters: {
|
||||
types: (state): Record<string, ResearchType> => state,
|
||||
},
|
||||
export const useResearchTypesStore = defineStore('research_types', () => {
|
||||
const types = ref<Record<string, ResearchType>>({})
|
||||
return { types }
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface ControlPoint {
|
||||
time: number
|
||||
@@ -20,38 +21,44 @@ export interface Shape {
|
||||
original_id?: string // Original ID from backend/agent before TradingView assigns its own ID
|
||||
}
|
||||
|
||||
// Use Options API so $state is a flat Record<string, Shape> matching the backend's shape store structure.
|
||||
// Composition API would produce $state = { shapes: {} } (extra nesting), which causes deepReplace in
|
||||
// useStateSync to delete the 'shapes' key when the backend sends an empty {} snapshot, breaking $subscribe.
|
||||
export const useShapeStore = defineStore('shapes', {
|
||||
state: () => ({} as Record<string, Shape>),
|
||||
export const useShapeStore = defineStore('shapes', () => {
|
||||
const shapes = ref<Record<string, Shape>>({})
|
||||
|
||||
actions: {
|
||||
addShape(shape: Shape) {
|
||||
this.$patch({ [shape.id]: shape } as Partial<Record<string, Shape>>)
|
||||
},
|
||||
|
||||
updateShape(id: string, updates: Partial<Shape>) {
|
||||
const existing = (this.$state as Record<string, Shape>)[id]
|
||||
if (existing) {
|
||||
this.$patch({
|
||||
[id]: { ...existing, ...updates, modified_at: Math.floor(Date.now() / 1000) }
|
||||
} as Partial<Record<string, Shape>>)
|
||||
// Helper methods
|
||||
const addShape = (shape: Shape) => {
|
||||
shapes.value[shape.id] = shape
|
||||
}
|
||||
},
|
||||
|
||||
removeShape(id: string) {
|
||||
this.$patch((state) => {
|
||||
delete (state as Record<string, Shape>)[id]
|
||||
})
|
||||
},
|
||||
|
||||
getShape(id: string): Shape | undefined {
|
||||
return (this.$state as Record<string, Shape>)[id]
|
||||
},
|
||||
|
||||
getAllShapes(): Shape[] {
|
||||
return Object.values(this.$state as Record<string, Shape>)
|
||||
},
|
||||
},
|
||||
const updateShape = (id: string, updates: Partial<Shape>) => {
|
||||
if (shapes.value[id]) {
|
||||
const updated = {
|
||||
...shapes.value[id],
|
||||
...updates,
|
||||
modified_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
// Replace the entire shape object to ensure arrays are replaced atomically
|
||||
shapes.value[id] = updated
|
||||
}
|
||||
}
|
||||
|
||||
const removeShape = (id: string) => {
|
||||
delete shapes.value[id]
|
||||
}
|
||||
|
||||
const getShape = (id: string): Shape | undefined => {
|
||||
return shapes.value[id]
|
||||
}
|
||||
|
||||
const getAllShapes = (): Shape[] => {
|
||||
return Object.values(shapes.value)
|
||||
}
|
||||
|
||||
return {
|
||||
shapes,
|
||||
addShape,
|
||||
updateShape,
|
||||
removeShape,
|
||||
getShape,
|
||||
getAllShapes
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface StrategyType {
|
||||
display_name: string
|
||||
@@ -7,11 +8,7 @@ export interface StrategyType {
|
||||
modified_at: number
|
||||
}
|
||||
|
||||
// Options API: flat $state matches backend initial state {}.
|
||||
// See indicatorTypes.ts for explanation of why composition API breaks $subscribe.
|
||||
export const useStrategyTypesStore = defineStore('strategy_types', {
|
||||
state: (): Record<string, StrategyType> => ({}),
|
||||
getters: {
|
||||
types: (state): Record<string, StrategyType> => state,
|
||||
},
|
||||
export const useStrategyTypesStore = defineStore('strategy_types', () => {
|
||||
const types = ref<Record<string, StrategyType>>({})
|
||||
return { types }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user