gloomberb 0.9.9 → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.9.9",
3
+ "version": "0.9.10",
4
4
  "description": "Market research and portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
package/src/app.tsx CHANGED
@@ -268,6 +268,7 @@ function AppInner({
268
268
 
269
269
  useCloudSyncRuntime({
270
270
  state,
271
+ getState: getRemoteState,
271
272
  dispatch,
272
273
  tickerRepository,
273
274
  pluginRegistry,
@@ -1,8 +1,24 @@
1
1
  import { expect, test } from "bun:test";
2
- import type { AppState } from "../core/state/app/state";
2
+ import {
3
+ appReducer,
4
+ createInitialState,
5
+ type AppAction,
6
+ type AppState,
7
+ } from "../core/state/app/state";
3
8
  import type { TickerRepository } from "../data/ticker-repository";
9
+ import { createDefaultConfig } from "../types/config";
10
+ import {
11
+ __syncContributorInternalsForTests,
12
+ coreConfigSyncContributor,
13
+ } from "./core-contributors";
4
14
  import { CloudSyncController } from "./controller";
5
- import type { SyncTransport } from "./types";
15
+ import {
16
+ SYNC_SNAPSHOT_SCHEMA_VERSION,
17
+ type SyncContributor,
18
+ type SyncSnapshot,
19
+ type SyncSnapshotResponse,
20
+ type SyncTransport,
21
+ } from "./types";
6
22
 
7
23
  test("does not push local state when the initial pull fails", async () => {
8
24
  let pushes = 0;
@@ -17,7 +33,7 @@ test("does not push local state when the initial pull fails", async () => {
17
33
  };
18
34
  const controller = new CloudSyncController();
19
35
  controller.setRuntime({
20
- state: {} as AppState,
36
+ getState: () => ({} as AppState),
21
37
  dispatch: () => {},
22
38
  tickerRepository: {} as TickerRepository,
23
39
  getContributors: () => [],
@@ -29,3 +45,107 @@ test("does not push local state when the initial pull fails", async () => {
29
45
  expect(pushes).toBe(0);
30
46
  expect(controller.getStatus()).toMatchObject({ phase: "error", error: "pull failed" });
31
47
  });
48
+
49
+ test("keeps startup layout changes while serializing pull and push", async () => {
50
+ let state = createInitialState(createDefaultConfig("/tmp/gloomberb-sync-controller-test"));
51
+ const dispatch = (action: AppAction) => {
52
+ state = appReducer(state, action);
53
+ };
54
+ let resolvePull!: (response: SyncSnapshotResponse) => void;
55
+ const deferredPull = new Promise<SyncSnapshotResponse>((resolve) => {
56
+ resolvePull = resolve;
57
+ });
58
+ let pulls = 0;
59
+ const pushes: Array<{
60
+ snapshot: SyncSnapshot;
61
+ options?: { baseRevision?: number | null };
62
+ }> = [];
63
+ const transport: SyncTransport = {
64
+ id: "deferred-pull",
65
+ isAvailable: () => true,
66
+ pullSnapshot: () => {
67
+ pulls += 1;
68
+ return deferredPull;
69
+ },
70
+ pushSnapshot: async (snapshot, options) => {
71
+ pushes.push({ snapshot, options });
72
+ return { revision: 8, updatedAt: "2026-07-13T10:00:08.000Z" };
73
+ },
74
+ };
75
+ const contributor: SyncContributor = {
76
+ ...coreConfigSyncContributor,
77
+ apply: (payload, context) => {
78
+ const config = __syncContributorInternalsForTests.mergeConfigPayload(
79
+ context.state.config,
80
+ payload,
81
+ context.baselineState.config,
82
+ );
83
+ if (config) context.dispatch({ type: "SET_CONFIG", config });
84
+ },
85
+ };
86
+ const controller = new CloudSyncController();
87
+ controller.setRuntime({
88
+ getState: () => state,
89
+ dispatch,
90
+ tickerRepository: {} as TickerRepository,
91
+ getContributors: () => [{ pluginId: "test", contributor }],
92
+ getTransport: () => ({ pluginId: "test", transport }),
93
+ });
94
+
95
+ const startupSync = controller.requestSync({ reason: "startup" });
96
+ const startupPane = {
97
+ instanceId: "help:startup",
98
+ paneId: "help",
99
+ binding: { kind: "none" as const },
100
+ };
101
+ const localLayout = {
102
+ ...state.config.layout,
103
+ instances: [...state.config.layout.instances, startupPane],
104
+ floating: [
105
+ ...state.config.layout.floating,
106
+ { instanceId: startupPane.instanceId, x: 4, y: 3, width: 60, height: 20 },
107
+ ],
108
+ };
109
+ dispatch({
110
+ type: "SET_CONFIG",
111
+ config: {
112
+ ...state.config,
113
+ layout: localLayout,
114
+ layouts: state.config.layouts.map((saved, index) => (
115
+ index === state.config.activeLayoutIndex ? { ...saved, layout: localLayout } : saved
116
+ )),
117
+ },
118
+ });
119
+ const queuedPush = controller.requestSync({ reason: "state-change" });
120
+
121
+ const remoteConfig = createDefaultConfig("/remote/path-is-not-synced");
122
+ remoteConfig.theme = "green";
123
+ resolvePull({
124
+ snapshot: {
125
+ schemaVersion: SYNC_SNAPSHOT_SCHEMA_VERSION,
126
+ appId: "gloomberb",
127
+ clientId: "remote-client",
128
+ createdAt: "2026-07-13T10:00:07.000Z",
129
+ contributors: {
130
+ "core.config": {
131
+ schemaVersion: 1,
132
+ updatedAt: "2026-07-13T10:00:07.000Z",
133
+ payload: __syncContributorInternalsForTests.collectCoreConfigPayload(remoteConfig),
134
+ },
135
+ },
136
+ },
137
+ revision: 7,
138
+ updatedAt: "2026-07-13T10:00:07.000Z",
139
+ });
140
+ await Promise.all([startupSync, queuedPush]);
141
+
142
+ const pushedConfig = pushes[0]?.snapshot.contributors["core.config"]?.payload as {
143
+ layout: AppState["config"]["layout"];
144
+ };
145
+ expect(pulls).toBe(1);
146
+ expect(pushes).toHaveLength(1);
147
+ expect(pushes[0]?.options).toEqual({ baseRevision: 7 });
148
+ expect(state.config.theme).toBe("green");
149
+ expect(state.config.layout.instances).toContainEqual(startupPane);
150
+ expect(pushedConfig.layout.instances).toContainEqual(startupPane);
151
+ });
@@ -23,7 +23,7 @@ export interface CloudSyncStatus {
23
23
  }
24
24
 
25
25
  interface SyncRuntime {
26
- state: AppState;
26
+ getState: () => AppState;
27
27
  dispatch: Dispatch<AppAction>;
28
28
  tickerRepository: TickerRepository;
29
29
  getContributors: () => RegisteredSyncContributor[];
@@ -32,7 +32,6 @@ interface SyncRuntime {
32
32
 
33
33
  const CLIENT_ID_STORAGE_KEY = "gloomberb.sync.clientId";
34
34
  const PUSH_DEBOUNCE_MS = 2500;
35
- const PULL_MIN_INTERVAL_MS = 60_000;
36
35
 
37
36
  function nowIso(): string {
38
37
  return new Date().toISOString();
@@ -47,6 +46,15 @@ function resolveClientId(): string {
47
46
  return id;
48
47
  }
49
48
 
49
+ function snapshotContentSignature(snapshot: SyncSnapshot): string {
50
+ return stableStringify(Object.fromEntries(
51
+ Object.entries(snapshot.contributors).map(([id, contributor]) => [
52
+ id,
53
+ { schemaVersion: contributor.schemaVersion, payload: contributor.payload },
54
+ ]),
55
+ ));
56
+ }
57
+
50
58
  export class CloudSyncController {
51
59
  private runtime: SyncRuntime | null = null;
52
60
  private contributors = new Map<string, RegisteredSyncContributor>();
@@ -57,7 +65,6 @@ export class CloudSyncController {
57
65
  private syncQueued = false;
58
66
  private clientId: string | null = null;
59
67
  private lastSignature: string | null = null;
60
- private lastPulledAtMs = 0;
61
68
  private hasPulledForTransport = new Set<string>();
62
69
  private status: CloudSyncStatus = {
63
70
  phase: "idle",
@@ -100,16 +107,20 @@ export class CloudSyncController {
100
107
  return [...this.transports.values()];
101
108
  }
102
109
 
103
- setRuntime(runtime: SyncRuntime): void {
104
- this.runtime = runtime;
105
- this.updateAvailability();
110
+ setRuntime(runtime: SyncRuntime): () => void {
111
+ if (this.runtime !== runtime) {
112
+ this.resetOperations();
113
+ this.runtime = runtime;
114
+ this.updateAvailability();
115
+ }
116
+ return () => this.clearRuntime(runtime);
106
117
  }
107
118
 
108
- clearRuntime(): void {
119
+ clearRuntime(runtime?: SyncRuntime): void {
120
+ if (runtime && this.runtime !== runtime) return;
109
121
  this.runtime = null;
110
- if (this.pushTimer) clearTimeout(this.pushTimer);
111
- this.pushTimer = null;
112
- this.syncQueued = false;
122
+ this.resetOperations();
123
+ this.setStatus({ phase: "disabled", transportId: null, error: null });
113
124
  }
114
125
 
115
126
  subscribe(listener: () => void): () => void {
@@ -124,11 +135,11 @@ export class CloudSyncController {
124
135
  schedulePush(reason: string): void {
125
136
  const runtime = this.runtime;
126
137
  if (!runtime) return;
127
- const transport = runtime.getTransport();
128
- if (!transport?.transport.isAvailable()) {
138
+ const registration = runtime.getTransport();
139
+ if (!registration?.transport.isAvailable()) {
129
140
  this.setStatus({
130
141
  phase: "disabled",
131
- transportId: transport?.transport.id ?? null,
142
+ transportId: registration?.transport.id ?? null,
132
143
  error: null,
133
144
  });
134
145
  return;
@@ -146,6 +157,7 @@ export class CloudSyncController {
146
157
  return this.inFlight;
147
158
  }
148
159
  const run = this.syncOnce(options).finally(async () => {
160
+ if (this.inFlight !== run) return;
149
161
  this.inFlight = null;
150
162
  if (!this.syncQueued) return;
151
163
  this.syncQueued = false;
@@ -155,42 +167,36 @@ export class CloudSyncController {
155
167
  return run;
156
168
  }
157
169
 
158
- async pullLatest(options: { force?: boolean } = {}): Promise<void> {
159
- const runtime = this.runtime;
160
- if (!runtime) return;
161
- const transport = runtime.getTransport();
162
- if (!transport?.transport.isAvailable()) {
163
- this.setStatus({ phase: "disabled", transportId: transport?.transport.id ?? null, error: null });
164
- return;
165
- }
166
- const currentMs = Date.now();
167
- if (!options.force && currentMs - this.lastPulledAtMs < PULL_MIN_INTERVAL_MS) return;
168
- if (await this.runPull(runtime, transport.transport)) {
169
- this.lastPulledAtMs = currentMs;
170
- }
171
- }
172
-
173
170
  private async syncOnce(options: { reason?: string; force?: boolean }): Promise<void> {
174
171
  const runtime = this.runtime;
175
172
  if (!runtime) return;
176
- const transportRegistration = runtime.getTransport();
177
- if (!transportRegistration?.transport.isAvailable()) {
178
- this.setStatus({ phase: "disabled", transportId: transportRegistration?.transport.id ?? null, error: null });
173
+ const registration = runtime.getTransport();
174
+ if (!registration?.transport.isAvailable()) {
175
+ this.setStatus({
176
+ phase: "disabled",
177
+ transportId: registration?.transport.id ?? null,
178
+ error: null,
179
+ });
179
180
  return;
180
181
  }
181
- const transport = transportRegistration.transport;
182
+ const transport = registration.transport;
183
+
182
184
  if (!this.hasPulledForTransport.has(transport.id)) {
185
+ this.lastSignature = null;
183
186
  if (!await this.runPull(runtime, transport)) return;
187
+ if (!this.isCurrent(runtime, transport)) return;
184
188
  this.hasPulledForTransport.add(transport.id);
185
189
  }
186
190
 
187
191
  const snapshot = await this.assembleSnapshot(runtime);
188
- const signature = stableStringify(snapshot.contributors);
192
+ if (!this.isCurrent(runtime, transport)) return;
193
+ const signature = snapshotContentSignature(snapshot);
189
194
  if (!options.force && signature === this.lastSignature) return;
190
195
 
191
196
  this.setStatus({ phase: "syncing", transportId: transport.id, error: null });
192
197
  try {
193
198
  const result = await transport.pushSnapshot(snapshot, { baseRevision: this.status.revision });
199
+ if (!this.isCurrent(runtime, transport)) return;
194
200
  this.lastSignature = signature;
195
201
  this.setStatus({
196
202
  phase: "synced",
@@ -200,6 +206,7 @@ export class CloudSyncController {
200
206
  error: null,
201
207
  });
202
208
  } catch (error) {
209
+ if (!this.isCurrent(runtime, transport)) return;
203
210
  this.setStatus({
204
211
  phase: "error",
205
212
  transportId: transport.id,
@@ -209,9 +216,30 @@ export class CloudSyncController {
209
216
  }
210
217
 
211
218
  private async runPull(runtime: SyncRuntime, transport: SyncTransport): Promise<boolean> {
219
+ const baselineState = runtime.getState();
212
220
  this.setStatus({ phase: "syncing", transportId: transport.id, error: null });
213
221
  try {
214
222
  const response = await transport.pullSnapshot();
223
+ if (!this.isCurrent(runtime, transport)) return false;
224
+
225
+ if (response.snapshot) {
226
+ for (const entry of runtime.getContributors()) {
227
+ if (!this.isCurrent(runtime, transport)) return false;
228
+ const payload = response.snapshot.contributors[entry.contributor.id]?.payload;
229
+ if (payload === undefined || !entry.contributor.apply) continue;
230
+ await entry.contributor.apply(payload, {
231
+ snapshot: response.snapshot,
232
+ baselineState,
233
+ state: runtime.getState(),
234
+ getState: runtime.getState,
235
+ isCurrent: () => this.isCurrent(runtime, transport),
236
+ dispatch: runtime.dispatch,
237
+ tickerRepository: runtime.tickerRepository,
238
+ });
239
+ }
240
+ }
241
+
242
+ if (!this.isCurrent(runtime, transport)) return false;
215
243
  this.setStatus({
216
244
  phase: response.snapshot ? "synced" : "idle",
217
245
  transportId: transport.id,
@@ -220,19 +248,9 @@ export class CloudSyncController {
220
248
  lastSyncAt: response.updatedAt,
221
249
  error: null,
222
250
  });
223
- if (!response.snapshot) return true;
224
- for (const entry of runtime.getContributors()) {
225
- const payload = response.snapshot.contributors[entry.contributor.id]?.payload;
226
- if (payload === undefined || !entry.contributor.apply) continue;
227
- await entry.contributor.apply(payload, {
228
- snapshot: response.snapshot,
229
- state: runtime.state,
230
- dispatch: runtime.dispatch,
231
- tickerRepository: runtime.tickerRepository,
232
- });
233
- }
234
251
  return true;
235
252
  } catch (error) {
253
+ if (!this.isCurrent(runtime, transport)) return false;
236
254
  this.setStatus({
237
255
  phase: "error",
238
256
  transportId: transport.id,
@@ -247,7 +265,7 @@ export class CloudSyncController {
247
265
  const createdAt = nowIso();
248
266
  const payloads: SyncSnapshot["contributors"] = {};
249
267
  for (const entry of contributors) {
250
- const payload = await entry.contributor.collect({ state: runtime.state });
268
+ const payload = await entry.contributor.collect({ state: runtime.getState() });
251
269
  payloads[entry.contributor.id] = {
252
270
  schemaVersion: entry.contributor.schemaVersion,
253
271
  updatedAt: createdAt,
@@ -263,6 +281,19 @@ export class CloudSyncController {
263
281
  };
264
282
  }
265
283
 
284
+ private isCurrent(runtime: SyncRuntime, transport: SyncTransport): boolean {
285
+ return this.runtime === runtime && runtime.getTransport()?.transport === transport;
286
+ }
287
+
288
+ private resetOperations(): void {
289
+ if (this.pushTimer) clearTimeout(this.pushTimer);
290
+ this.pushTimer = null;
291
+ this.inFlight = null;
292
+ this.syncQueued = false;
293
+ this.hasPulledForTransport.clear();
294
+ this.lastSignature = null;
295
+ }
296
+
266
297
  private updateAvailability(): void {
267
298
  const transport = this.runtime?.getTransport() ?? null;
268
299
  if (!transport) {
@@ -1,4 +1,5 @@
1
1
  import { scheduleConfigSave } from "../state/config-save-scheduler";
2
+ import { stableStringify } from "../remote/revision";
2
3
  import type { AppConfig, BrokerInstanceConfig } from "../types/config";
3
4
  import type { PricePoint, TickerFinancials } from "../types/financials";
4
5
  import type { Portfolio, TickerMetadata, TickerPosition, TickerRecord, Watchlist } from "../types/ticker";
@@ -295,20 +296,30 @@ function collectCoreCollectionsPayload(
295
296
  };
296
297
  }
297
298
 
298
- function mergeConfigPayload(config: AppConfig, payload: unknown): AppConfig | null {
299
+ function valuesEqual(left: unknown, right: unknown): boolean {
300
+ return stableStringify(left) === stableStringify(right);
301
+ }
302
+
303
+ function mergeConfigPayload(
304
+ config: AppConfig,
305
+ payload: unknown,
306
+ baselineConfig: AppConfig = config,
307
+ ): AppConfig | null {
299
308
  if (!isPlainObject(payload)) return null;
300
309
  const next: AppConfig = { ...config };
310
+ const canApply = <K extends keyof AppConfig>(key: K) => (
311
+ valuesEqual(config[key], baselineConfig[key])
312
+ );
301
313
  const assign = <K extends keyof AppConfig>(key: K) => {
302
- if (key in payload) {
314
+ if (key in payload && canApply(key)) {
303
315
  next[key] = payload[key as string] as AppConfig[K];
304
316
  }
305
317
  };
306
318
 
307
319
  assign("baseCurrency");
308
320
  assign("refreshIntervalMinutes");
309
- assign("layout");
310
- assign("layouts");
311
- assign("activeLayoutIndex");
321
+ assign("portfolios");
322
+ assign("watchlists");
312
323
  assign("disabledPlugins");
313
324
  assign("disabledSources");
314
325
  assign("pluginConfig");
@@ -318,9 +329,21 @@ function mergeConfigPayload(config: AppConfig, payload: unknown): AppConfig | nu
318
329
  assign("recentTickers");
319
330
  assign("onboardingComplete");
320
331
 
321
- if (Array.isArray(payload.portfolios)) next.portfolios = payload.portfolios as Portfolio[];
322
- if (Array.isArray(payload.watchlists)) next.watchlists = payload.watchlists as Watchlist[];
323
- if (Array.isArray(payload.brokerInstances)) {
332
+ if (
333
+ ["layout", "layouts", "activeLayoutIndex"].every((key) => (
334
+ key in payload &&
335
+ valuesEqual(
336
+ config[key as keyof AppConfig],
337
+ baselineConfig[key as keyof AppConfig],
338
+ )
339
+ ))
340
+ ) {
341
+ next.layout = payload.layout as unknown as AppConfig["layout"];
342
+ next.layouts = payload.layouts as AppConfig["layouts"];
343
+ next.activeLayoutIndex = payload.activeLayoutIndex as number;
344
+ }
345
+
346
+ if (Array.isArray(payload.brokerInstances) && canApply("brokerInstances")) {
324
347
  const incoming = payload.brokerInstances as Array<Partial<BrokerInstanceConfig>>;
325
348
  const existingById = new Map(config.brokerInstances.map((instance) => [instance.id, instance]));
326
349
  next.brokerInstances = incoming.map((instance) => {
@@ -343,9 +366,9 @@ export const coreConfigSyncContributor: SyncContributor = {
343
366
  id: "core.config",
344
367
  schemaVersion: 1,
345
368
  collect: ({ state }) => collectCoreConfigPayload(state.config),
346
- apply: (payload, { state, dispatch }) => {
347
- const nextConfig = mergeConfigPayload(state.config, payload);
348
- if (!nextConfig) return;
369
+ apply: (payload, { baselineState, state, dispatch }) => {
370
+ const nextConfig = mergeConfigPayload(state.config, payload, baselineState.config);
371
+ if (!nextConfig || valuesEqual(nextConfig, state.config)) return;
349
372
  dispatch({ type: "SET_CONFIG", config: nextConfig });
350
373
  scheduleConfigSave(nextConfig);
351
374
  },
@@ -360,18 +383,15 @@ export const coreCollectionsSyncContributor: SyncContributor = {
360
383
  state.financials,
361
384
  state.exchangeRates,
362
385
  ),
363
- apply: async (payload, { state, dispatch, tickerRepository }) => {
386
+ apply: async (payload, { getState, isCurrent, dispatch, tickerRepository }) => {
364
387
  if (!isPlainObject(payload)) return;
365
- const nextConfig = { ...state.config };
366
- if (Array.isArray(payload.portfolios)) nextConfig.portfolios = payload.portfolios as Portfolio[];
367
- if (Array.isArray(payload.watchlists)) nextConfig.watchlists = payload.watchlists as Watchlist[];
368
-
369
- const nextTickers = new Map(state.tickers);
388
+ const incomingRecords: TickerRecord[] = [];
370
389
  const rawTickers = Array.isArray(payload.tickers) ? payload.tickers : [];
371
390
  for (const rawTicker of rawTickers) {
391
+ if (!isCurrent()) return;
372
392
  if (!isPlainObject(rawTicker)) continue;
373
393
  const current = typeof rawTicker.ticker === "string"
374
- ? nextTickers.get(rawTicker.ticker)
394
+ ? getState().tickers.get(rawTicker.ticker)
375
395
  : null;
376
396
  const metadata = hydrateTickerMetadata({
377
397
  ...current?.metadata,
@@ -380,12 +400,15 @@ export const coreCollectionsSyncContributor: SyncContributor = {
380
400
  });
381
401
  const record: TickerRecord = { metadata };
382
402
  await tickerRepository.saveTicker(record);
383
- nextTickers.set(metadata.ticker, record);
403
+ incomingRecords.push(record);
384
404
  }
385
405
 
386
- dispatch({ type: "SET_CONFIG", config: nextConfig });
406
+ if (!isCurrent() || incomingRecords.length === 0) return;
407
+ const nextTickers = new Map(getState().tickers);
408
+ for (const record of incomingRecords) {
409
+ nextTickers.set(record.metadata.ticker, record);
410
+ }
387
411
  dispatch({ type: "SET_TICKERS", tickers: nextTickers });
388
- scheduleConfigSave(nextConfig);
389
412
  },
390
413
  };
391
414
 
@@ -397,4 +420,5 @@ export const __syncContributorInternalsForTests = {
397
420
  sanitizeUnknown,
398
421
  collectCoreConfigPayload,
399
422
  collectCoreCollectionsPayload,
423
+ mergeConfigPayload,
400
424
  };
package/src/sync/react.ts CHANGED
@@ -6,6 +6,7 @@ import { cloudSyncController } from "./controller";
6
6
 
7
7
  interface CloudSyncRuntimeOptions {
8
8
  state: AppState;
9
+ getState: () => AppState;
9
10
  dispatch: Dispatch<AppAction>;
10
11
  tickerRepository: TickerRepository;
11
12
  pluginRegistry: PluginRegistry;
@@ -14,28 +15,25 @@ interface CloudSyncRuntimeOptions {
14
15
 
15
16
  export function useCloudSyncRuntime({
16
17
  state,
18
+ getState,
17
19
  dispatch,
18
20
  tickerRepository,
19
21
  pluginRegistry,
20
22
  initialized,
21
23
  }: CloudSyncRuntimeOptions): void {
22
24
  useEffect(() => {
23
- cloudSyncController.setRuntime({
24
- state,
25
+ return cloudSyncController.setRuntime({
26
+ getState,
25
27
  dispatch,
26
28
  tickerRepository,
27
29
  getContributors: () => pluginRegistry.getEnabledSyncContributors(),
28
30
  getTransport: () => pluginRegistry.getActiveSyncTransport(),
29
31
  });
30
- }, [dispatch, pluginRegistry, state, tickerRepository]);
31
-
32
- useEffect(() => () => {
33
- cloudSyncController.clearRuntime();
34
- }, []);
32
+ }, [dispatch, getState, pluginRegistry, tickerRepository]);
35
33
 
36
34
  useEffect(() => {
37
35
  if (!initialized) return;
38
- void cloudSyncController.pullLatest();
36
+ void cloudSyncController.requestSync({ reason: "startup" });
39
37
  }, [initialized, pluginRegistry]);
40
38
 
41
39
  useEffect(() => {
package/src/sync/types.ts CHANGED
@@ -25,7 +25,10 @@ export interface SyncCollectContext {
25
25
 
26
26
  export interface SyncApplyContext {
27
27
  snapshot: SyncSnapshot;
28
+ baselineState: AppState;
28
29
  state: AppState;
30
+ getState: () => AppState;
31
+ isCurrent: () => boolean;
29
32
  dispatch: Dispatch<AppAction>;
30
33
  tickerRepository: TickerRepository;
31
34
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.9.9";
1
+ export const VERSION = "0.9.10";