store sync bugfix
This commit is contained in:
@@ -91,7 +91,7 @@ export const DEFAULT_STORES: StoreConfig[] = [
|
|||||||
{
|
{
|
||||||
name: 'shapes',
|
name: 'shapes',
|
||||||
persistent: true,
|
persistent: true,
|
||||||
initialState: () => ({}),
|
initialState: () => ({ shapes: {} }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'indicators',
|
name: 'indicators',
|
||||||
@@ -285,7 +285,7 @@ export interface Shape {
|
|||||||
/**
|
/**
|
||||||
* Shapes store - persistent, stores TradingView drawings and annotations.
|
* 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.
|
* Parameter schema entry for a custom indicator.
|
||||||
|
|||||||
@@ -90,7 +90,14 @@ export class WorkspaceManager {
|
|||||||
|
|
||||||
const states = await this.containerSync.loadAllStores(persistentStores);
|
const states = await this.containerSync.loadAllStores(persistentStores);
|
||||||
for (const [storeName, state] of states) {
|
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');
|
this.logger.debug({ store: storeName }, 'Loaded persistent store');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import type { BackendMessage, FrontendMessage, HelloMessage, PatchMessage } from
|
|||||||
import { wsManager } from './useWebSocket';
|
import { wsManager } from './useWebSocket';
|
||||||
|
|
||||||
function deepReplace(target: Record<string, any>, source: Record<string, any>) {
|
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)) {
|
for (const key of Object.keys(target)) {
|
||||||
if (!(key in source)) {
|
if (!(key in source)) {
|
||||||
delete target[key]
|
delete target[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (const [key, value] of Object.entries(source)) {
|
for (const [key, value] of Object.entries(source)) {
|
||||||
if (
|
if (
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ export function useTradingViewShapes(tvWidget: IChartingLibraryWidget) {
|
|||||||
function setupStoreWatchers() {
|
function setupStoreWatchers() {
|
||||||
// Watch for shape store changes and apply to TradingView
|
// Watch for shape store changes and apply to TradingView
|
||||||
watch(
|
watch(
|
||||||
() => shapeStore.$state,
|
() => shapeStore.shapes,
|
||||||
async (newShapes, oldShapes) => {
|
async (newShapes, oldShapes) => {
|
||||||
if (isUpdatingStore || !isChartReady) return
|
if (isUpdatingStore || !isChartReady) return
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
import type { CustomIndicatorMetadata } from './indicators'
|
import type { CustomIndicatorMetadata } from './indicators'
|
||||||
|
|
||||||
export interface CustomIndicatorType {
|
export interface CustomIndicatorType {
|
||||||
@@ -10,12 +11,7 @@ export interface CustomIndicatorType {
|
|||||||
modified_at: number
|
modified_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options API: flat $state = Record<string, CustomIndicatorType> matches backend initial state {}.
|
export const useIndicatorTypesStore = defineStore('indicator_types', () => {
|
||||||
// Composition API would produce $state = { types: {} }, causing deepReplace to delete 'types' on
|
const types = ref<Record<string, CustomIndicatorType>>({})
|
||||||
// the initial empty snapshot from the backend, which breaks $subscribe reactivity tracking.
|
return { types }
|
||||||
export const useIndicatorTypesStore = defineStore('indicator_types', {
|
|
||||||
state: (): Record<string, CustomIndicatorType> => ({}),
|
|
||||||
getters: {
|
|
||||||
types: (state): Record<string, CustomIndicatorType> => state,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export interface CustomIndicatorParam {
|
export interface CustomIndicatorParam {
|
||||||
type: 'int' | 'float' | 'bool' | 'string'
|
type: 'int' | 'float' | 'bool' | 'string'
|
||||||
@@ -75,48 +76,48 @@ export interface IndicatorInstance {
|
|||||||
custom_metadata?: CustomIndicatorMetadata
|
custom_metadata?: CustomIndicatorMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options API: flat $state = Record<string, IndicatorInstance> matches backend initial state {}.
|
export const useIndicatorStore = defineStore('indicators', () => {
|
||||||
// Composition API would produce $state = { indicators: {} }, causing deepReplace to delete
|
const indicators = ref<Record<string, IndicatorInstance>>({})
|
||||||
// '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> => ({}),
|
|
||||||
|
|
||||||
getters: {
|
// Helper methods
|
||||||
indicators: (state): Record<string, IndicatorInstance> => state,
|
const addIndicator = (indicator: IndicatorInstance) => {
|
||||||
},
|
indicators.value[indicator.id] = indicator
|
||||||
|
|
||||||
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>>)
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
removeIndicator(id: string) {
|
const updateIndicator = (id: string, updates: Partial<IndicatorInstance>) => {
|
||||||
this.$patch((state) => {
|
if (indicators.value[id]) {
|
||||||
delete (state as Record<string, IndicatorInstance>)[id]
|
const updated = {
|
||||||
})
|
...indicators.value[id],
|
||||||
},
|
...updates,
|
||||||
|
modified_at: Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
indicators.value[id] = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getIndicator(id: string): IndicatorInstance | undefined {
|
const removeIndicator = (id: string) => {
|
||||||
return (this.$state as Record<string, IndicatorInstance>)[id]
|
delete indicators.value[id]
|
||||||
},
|
}
|
||||||
|
|
||||||
getAllIndicators(): IndicatorInstance[] {
|
const getIndicator = (id: string): IndicatorInstance | undefined => {
|
||||||
return Object.values(this.$state as Record<string, IndicatorInstance>)
|
return indicators.value[id]
|
||||||
},
|
}
|
||||||
|
|
||||||
getIndicatorsBySymbol(symbol: string): IndicatorInstance[] {
|
const getAllIndicators = (): IndicatorInstance[] => {
|
||||||
return Object.values(this.$state as Record<string, IndicatorInstance>)
|
return Object.values(indicators.value)
|
||||||
.filter(ind => ind.symbol === symbol)
|
}
|
||||||
},
|
|
||||||
},
|
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 { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export interface ResearchType {
|
export interface ResearchType {
|
||||||
display_name: string
|
display_name: string
|
||||||
@@ -7,11 +8,7 @@ export interface ResearchType {
|
|||||||
modified_at: number
|
modified_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options API: flat $state matches backend initial state {}.
|
export const useResearchTypesStore = defineStore('research_types', () => {
|
||||||
// See indicatorTypes.ts for explanation of why composition API breaks $subscribe.
|
const types = ref<Record<string, ResearchType>>({})
|
||||||
export const useResearchTypesStore = defineStore('research_types', {
|
return { types }
|
||||||
state: (): Record<string, ResearchType> => ({}),
|
|
||||||
getters: {
|
|
||||||
types: (state): Record<string, ResearchType> => state,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export interface ControlPoint {
|
export interface ControlPoint {
|
||||||
time: number
|
time: number
|
||||||
@@ -20,38 +21,44 @@ export interface Shape {
|
|||||||
original_id?: string // Original ID from backend/agent before TradingView assigns its own ID
|
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.
|
export const useShapeStore = defineStore('shapes', () => {
|
||||||
// Composition API would produce $state = { shapes: {} } (extra nesting), which causes deepReplace in
|
const shapes = ref<Record<string, Shape>>({})
|
||||||
// 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>),
|
|
||||||
|
|
||||||
actions: {
|
// Helper methods
|
||||||
addShape(shape: Shape) {
|
const addShape = (shape: Shape) => {
|
||||||
this.$patch({ [shape.id]: shape } as Partial<Record<string, Shape>>)
|
shapes.value[shape.id] = 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>>)
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
removeShape(id: string) {
|
const updateShape = (id: string, updates: Partial<Shape>) => {
|
||||||
this.$patch((state) => {
|
if (shapes.value[id]) {
|
||||||
delete (state as Record<string, Shape>)[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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getShape(id: string): Shape | undefined {
|
const removeShape = (id: string) => {
|
||||||
return (this.$state as Record<string, Shape>)[id]
|
delete shapes.value[id]
|
||||||
},
|
}
|
||||||
|
|
||||||
getAllShapes(): Shape[] {
|
const getShape = (id: string): Shape | undefined => {
|
||||||
return Object.values(this.$state as Record<string, Shape>)
|
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 { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export interface StrategyType {
|
export interface StrategyType {
|
||||||
display_name: string
|
display_name: string
|
||||||
@@ -7,11 +8,7 @@ export interface StrategyType {
|
|||||||
modified_at: number
|
modified_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options API: flat $state matches backend initial state {}.
|
export const useStrategyTypesStore = defineStore('strategy_types', () => {
|
||||||
// See indicatorTypes.ts for explanation of why composition API breaks $subscribe.
|
const types = ref<Record<string, StrategyType>>({})
|
||||||
export const useStrategyTypesStore = defineStore('strategy_types', {
|
return { types }
|
||||||
state: (): Record<string, StrategyType> => ({}),
|
|
||||||
getters: {
|
|
||||||
types: (state): Record<string, StrategyType> => state,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user