@homeostate/core 0.0.0 → 0.1.1

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.
@@ -1,136 +1,110 @@
1
- import { defaultSyncFilter } from './types.js';
2
- import { patchSharedType, patchState } from './patching.js';
1
+ import { defaultSyncFilter } from "./types.js";
2
+ import { patchState } from "./patching.js";
3
3
  /**
4
- * Creates a sync engine that manages bidirectional synchronization
5
- * between any store (via adapter) and a Yjs document.
4
+ * Creates a sync engine that keeps a store (via its adapter) and a CRDT backend
5
+ * in sync in both directions.
6
6
  *
7
- * This is the core abstraction that makes the sync logic state-manager agnostic.
7
+ * This is the core abstraction that makes the sync logic state-manager and
8
+ * CRDT-library agnostic.
9
+ *
10
+ * On `connect()` the two sides are reconciled per key over the filtered view: a key the
11
+ * backend holds wins over the store's value, and a synced key the backend lacks stays in
12
+ * the store and is seeded into the backend unless `seed` is `'never'`. Afterwards the
13
+ * backend owns the synced document: every write replaces it with the store's filtered
14
+ * state, and a key removed from the backend is removed from the store.
8
15
  *
9
16
  * @example
10
17
  * ```typescript
11
- * const doc = new Y.Doc();
18
+ * const backend = createYjsBackend(new Y.Doc(), 'shared');
12
19
  * const adapter = new ZustandAdapter(store);
13
- * const engine = createSyncEngine(doc, adapter, { name: 'shared' });
20
+ * const engine = createSyncEngine(backend, adapter);
14
21
  * engine.connect();
15
22
  * ```
16
23
  */
17
- export function createSyncEngine(doc, adapter, config) {
18
- const { name, filter = defaultSyncFilter } = config;
19
- // The root Y.Map that the store is written and read from
20
- const yMap = doc.getMap(name);
24
+ export function createSyncEngine(backend, adapter, config = {}) {
25
+ const { filter = defaultSyncFilter, seed = "if-empty" } = config;
21
26
  let connected = false;
22
27
  let storeUnsubscribe = null;
23
- let isUpdatingFromYjs = false;
24
- let isUpdatingFromStore = false;
25
- /**
26
- * Filter state to only include syncable properties
27
- */
28
+ let backendUnsubscribe = null;
29
+ let applyingRemote = false;
28
30
  const filterState = (state) => {
29
- if (typeof state !== 'object' || state === null) {
30
- return state;
31
- }
32
31
  const filtered = {};
33
32
  for (const [key, value] of Object.entries(state)) {
34
- if (filter(key, value)) {
33
+ if (filter(key, value))
35
34
  filtered[key] = value;
36
- }
37
35
  }
38
36
  return filtered;
39
37
  };
38
+ const readBackend = () => {
39
+ const value = backend.read();
40
+ return value !== null && typeof value === "object" ? value : {};
41
+ };
40
42
  /**
41
- * Sync store state to Yjs
43
+ * Builds the next store state from `remote`. With `keepLocalOnly`, a synced key the
44
+ * backend does not hold is left in place instead of being deleted; that is the
45
+ * connect-time reconciliation. Otherwise `remote` is the whole synced state.
42
46
  */
43
- const syncToYjs = () => {
44
- if (isUpdatingFromYjs)
45
- return;
46
- isUpdatingFromStore = true;
47
- try {
48
- const state = adapter.getState();
49
- const filteredState = filterState(state);
50
- doc.transact(() => {
51
- patchSharedType(yMap, filteredState);
52
- });
53
- }
54
- finally {
55
- isUpdatingFromStore = false;
47
+ const mergeStates = (current, remote, keepLocalOnly) => {
48
+ const synced = filterState(current);
49
+ const target = keepLocalOnly ? { ...synced, ...remote } : remote;
50
+ const patched = patchState(synced, target);
51
+ if (patched === synced)
52
+ return current;
53
+ const merged = { ...current, ...patched };
54
+ for (const key of Object.keys(synced)) {
55
+ if (!(key in patched))
56
+ delete merged[key];
56
57
  }
58
+ return merged;
57
59
  };
58
- /**
59
- * Sync Yjs state to store
60
- */
61
- const syncToStore = () => {
62
- if (isUpdatingFromStore)
60
+ const syncToBackend = () => {
61
+ if (applyingRemote)
63
62
  return;
64
- isUpdatingFromYjs = true;
63
+ backend.write(filterState(adapter.getState()));
64
+ };
65
+ const applyRemote = (remote, keepLocalOnly) => {
66
+ applyingRemote = true;
65
67
  try {
66
- const yjsState = yMap.toJSON();
67
- const currentState = adapter.getState();
68
- // Merge Yjs state with current state (preserving functions and non-synced props)
69
- const mergedState = mergeStates(currentState, yjsState);
70
- adapter.setState(mergedState, true);
68
+ const current = adapter.getState();
69
+ const merged = mergeStates(current, remote, keepLocalOnly);
70
+ if (merged !== current)
71
+ adapter.setState(merged);
71
72
  }
72
73
  finally {
73
- isUpdatingFromYjs = false;
74
- }
75
- };
76
- /**
77
- * Merge Yjs state with current store state, preserving non-syncable properties
78
- */
79
- const mergeStates = (current, fromYjs) => {
80
- if (typeof current !== 'object' || current === null) {
81
- return fromYjs;
82
- }
83
- const currentObj = current;
84
- const yjsObj = fromYjs;
85
- // Start with current state (preserves functions, etc.)
86
- const merged = { ...currentObj };
87
- // Apply Yjs state using patching for proper deep merging
88
- const patchedData = patchState({ ...merged }, yjsObj);
89
- // Restore non-syncable properties (like functions)
90
- for (const [key, value] of Object.entries(currentObj)) {
91
- if (!filter(key, value)) {
92
- patchedData[key] = value;
93
- }
74
+ applyingRemote = false;
94
75
  }
95
- return patchedData;
96
76
  };
97
- /**
98
- * Handler for Yjs changes
99
- */
100
- const handleYjsChange = () => {
101
- syncToStore();
77
+ const syncToStore = () => {
78
+ applyRemote(filterState(readBackend()), false);
102
79
  };
103
80
  return {
104
81
  connect: () => {
105
82
  if (connected)
106
83
  return;
107
- // Initialize Yjs with current store state
108
- const initialState = adapter.getInitialState();
109
- const filteredInitial = filterState(initialState);
110
- doc.transact(() => {
111
- patchSharedType(yMap, filteredInitial);
112
- });
113
- // Subscribe to Yjs changes
114
- yMap.observeDeep(handleYjsChange);
115
- // Subscribe to store changes
116
- storeUnsubscribe = adapter.subscribe(() => {
117
- syncToYjs();
118
- });
84
+ const remote = filterState(readBackend());
85
+ const local = filterState(adapter.getState());
86
+ const remoteKeys = new Set(Object.keys(remote));
87
+ const missing = Object.keys(local).filter((key) => !remoteKeys.has(key));
88
+ if (seed === "if-empty" && missing.length > 0) {
89
+ const seeded = { ...remote };
90
+ for (const key of missing)
91
+ seeded[key] = local[key];
92
+ backend.write(seeded);
93
+ }
94
+ applyRemote(remote, true);
95
+ backendUnsubscribe = backend.subscribe(syncToStore);
96
+ storeUnsubscribe = adapter.subscribe(syncToBackend);
119
97
  connected = true;
120
98
  },
121
99
  disconnect: () => {
122
100
  if (!connected)
123
101
  return;
124
- // Unsubscribe from Yjs
125
- yMap.unobserveDeep(handleYjsChange);
126
- // Unsubscribe from store
127
- if (storeUnsubscribe) {
128
- storeUnsubscribe();
129
- storeUnsubscribe = null;
130
- }
102
+ backendUnsubscribe?.();
103
+ backendUnsubscribe = null;
104
+ storeUnsubscribe?.();
105
+ storeUnsubscribe = null;
131
106
  connected = false;
132
107
  },
133
- getYMap: () => yMap,
134
108
  isConnected: () => connected,
135
109
  };
136
110
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../src/sync-engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../src/sync-engine.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Core types for the state-manager agnostic Yjs sync engine.
3
- * These interfaces allow any store manager to be connected to Yjs for P2P sync.
2
+ * Core types for the state-manager and CRDT-backend agnostic sync engine.
3
+ * These interfaces let any store be kept in sync through any replicated backend.
4
4
  */
5
5
  /**
6
6
  * Unsubscribe function returned by subscribe
@@ -9,47 +9,73 @@ export type Unsubscribe = () => void;
9
9
  /**
10
10
  * Adapter interface that bridges a specific state manager with the sync engine.
11
11
  * Implement this interface to add support for any state manager (Zustand, Redux, MobX, etc.)
12
+ *
13
+ * State must be plain JSON: objects, arrays, strings, numbers, booleans, and null.
14
+ * Functions are dropped by the default filter; other values such as Date, Map, or Set
15
+ * are neither diffed nor synced.
12
16
  */
13
- export interface StoreAdapter<S> {
17
+ export interface StoreAdapter<S extends object> {
14
18
  /** Get the current state from the store */
15
19
  getState: () => S;
16
20
  /**
17
- * Set state in the store.
18
- * @param state - New state to set
19
- * @param fromSync - If true, this update came from Yjs sync and should not trigger sync back
21
+ * Replace the store state. Called only for changes coming from the backend; the
22
+ * engine ignores store notifications raised while it runs, so the adapter needs no
23
+ * echo suppression of its own.
20
24
  */
21
- setState: (state: S, fromSync?: boolean) => void;
25
+ setState: (state: S) => void;
22
26
  /**
23
- * Subscribe to store changes that should trigger Yjs sync.
27
+ * Subscribe to store changes that should be written to the backend.
24
28
  * @returns Unsubscribe function
25
29
  */
26
30
  subscribe: (onStoreChange: () => void) => Unsubscribe;
27
- /** Get the initial state of the store */
28
- getInitialState: () => S;
29
31
  }
32
+ /**
33
+ * Backend that holds the synced subtree in a CRDT or any other replicated store.
34
+ * The engine only ever exchanges plain JSON with it.
35
+ */
36
+ export interface CrdtBackend {
37
+ /** Plain JSON snapshot of the synced subtree. Must not alias backend internals. */
38
+ read: () => unknown;
39
+ /**
40
+ * Make the backend equal to `next` in one atomic transaction.
41
+ * The backend decides the granularity of the operations; `getChanges` is exported for that.
42
+ */
43
+ write: (next: unknown) => void;
44
+ /**
45
+ * Notify about changes that did not come through this backend's own `write`.
46
+ * @returns Unsubscribe function
47
+ */
48
+ subscribe: (onRemoteChange: () => void) => Unsubscribe;
49
+ }
50
+ /**
51
+ * Whether `connect()` may write the store's synced keys that the backend does not hold.
52
+ * Reconciliation is per key over the filtered view: a key the backend holds always wins
53
+ * over the store's value, a key only the store holds always stays in the store.
54
+ * - `'if-empty'`: seed the keys the backend lacks, in one write (default)
55
+ * - `'never'`: leave seeding to the caller; the keys are written on the next local change
56
+ */
57
+ export type SeedStrategy = "if-empty" | "never";
30
58
  /**
31
59
  * Configuration options for the sync engine
32
60
  */
33
61
  export interface SyncEngineConfig {
34
- /** Name of the Y.Map in the Y.Doc to store the state */
35
- name: string;
36
62
  /**
37
- * Filter function to determine which state keys should be synced.
63
+ * Filter function to determine which state keys should be synced, in both directions.
38
64
  * Return true to sync the key, false to exclude it.
39
65
  * By default, functions are excluded from sync.
40
66
  */
41
67
  filter?: (key: string, value: unknown) => boolean;
68
+ /** Seeding strategy used by `connect()`; defaults to `'if-empty'` */
69
+ seed?: SeedStrategy;
42
70
  }
43
71
  /**
44
- * The sync engine interface - manages bidirectional sync between store and Yjs
72
+ * The sync engine interface - manages bidirectional sync between a store and a backend
45
73
  */
46
74
  export interface SyncEngine {
47
75
  /** Start synchronization */
48
76
  connect: () => void;
49
77
  /** Stop synchronization and cleanup */
50
78
  disconnect: () => void;
51
- /** Get the underlying Y.Map */
52
- getYMap: () => unknown;
53
79
  /** Check if engine is connected */
54
80
  isConnected: () => boolean;
55
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC;;;GAGG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC,CAAC;IAElB;;;;OAIG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAEjD;;;OAGG;IACH,SAAS,EAAE,CAAC,aAAa,EAAE,MAAM,IAAI,KAAK,WAAW,CAAC;IAEtD,yCAAyC;IACzC,eAAe,EAAE,MAAM,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,uCAAuC;IACvC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,mCAAmC;IACnC,WAAW,EAAE,MAAM,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,OAAO,OAAO,KAAG,OAEhE,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM;IAC5C,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC,CAAC;IAElB;;;;OAIG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAE7B;;;OAGG;IACH,SAAS,EAAE,CAAC,aAAa,EAAE,MAAM,IAAI,KAAK,WAAW,CAAC;CACvD;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,mFAAmF;IACnF,IAAI,EAAE,MAAM,OAAO,CAAC;IAEpB;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAE/B;;;OAGG;IACH,SAAS,EAAE,CAAC,cAAc,EAAE,MAAM,IAAI,KAAK,WAAW,CAAC;CACxD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,OAAO,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAClD,qEAAqE;IACrE,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,uCAAuC;IACvC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,mCAAmC;IACnC,WAAW,EAAE,MAAM,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,OAAO,OAAO,KAAG,OAEhE,CAAC"}
package/dist/types.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Core types for the state-manager agnostic Yjs sync engine.
3
- * These interfaces allow any store manager to be connected to Yjs for P2P sync.
2
+ * Core types for the state-manager and CRDT-backend agnostic sync engine.
3
+ * These interfaces let any store be kept in sync through any replicated backend.
4
4
  */
5
5
  /**
6
6
  * Default filter that excludes functions from sync
7
7
  */
8
8
  export const defaultSyncFilter = (_key, value) => {
9
- return typeof value !== 'function';
9
+ return typeof value !== "function";
10
10
  };
11
11
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4DH;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAY,EAAE,KAAc,EAAW,EAAE;IACzE,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC;AACrC,CAAC,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyFH;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAY,EAAE,KAAc,EAAW,EAAE;IACzE,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC;AACrC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@homeostate/core",
3
- "version": "0.0.0",
4
- "description": "State-manager agnostic sync engine between a store adapter and a Yjs document",
3
+ "version": "0.1.1",
4
+ "description": "State-manager and CRDT-backend agnostic sync engine between a store adapter and a CrdtBackend",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -14,7 +14,6 @@
14
14
  "url": "https://github.com/mixedrays/homeostate/issues"
15
15
  },
16
16
  "keywords": [
17
- "yjs",
18
17
  "crdt",
19
18
  "sync",
20
19
  "state-management",
@@ -32,17 +31,12 @@
32
31
  "dist",
33
32
  "README.md",
34
33
  "LICENSE",
35
- "THIRD_PARTY_NOTICES.md"
34
+ "THIRD_PARTY_NOTICES.md",
35
+ "CHANGELOG.md"
36
36
  ],
37
37
  "publishConfig": {
38
38
  "access": "public"
39
39
  },
40
- "peerDependencies": {
41
- "yjs": "^13.6.0"
42
- },
43
- "devDependencies": {
44
- "yjs": "^13.6.23"
45
- },
46
40
  "scripts": {
47
41
  "build": "tsc -b"
48
42
  }
package/dist/mapping.d.ts DELETED
@@ -1,19 +0,0 @@
1
- import * as Y from 'yjs';
2
- /**
3
- * Converts a normal JavaScript array to a YArray shared type. Any nested
4
- * objects or arrays are turned into YMaps and YArrays, respectively.
5
- *
6
- * @param array The array to transform into a YArray
7
- * @returns A YArray.
8
- */
9
- export declare const arrayToYArray: (array: unknown[]) => Y.Array<unknown>;
10
- /**
11
- * Converts a normal JavaScript object into a YMap shared type. Any nested
12
- * objects or arrays are turned into YMaps or YArrays, respectively.
13
- *
14
- * @param object The object to turn into a YMap shared type.
15
- * @returns A YMap.
16
- */
17
- export declare const objectToYMap: (object: Record<string, unknown>) => Y.Map<unknown>;
18
- export declare const stringToYText: (string: string) => Y.Text;
19
- //# sourceMappingURL=mapping.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,EAAE,KAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAW/D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAW3E,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,KAAG,CAAC,CAAC,IAA0B,CAAC"}
package/dist/mapping.js DELETED
@@ -1,45 +0,0 @@
1
- import * as Y from 'yjs';
2
- /**
3
- * Converts a normal JavaScript array to a YArray shared type. Any nested
4
- * objects or arrays are turned into YMaps and YArrays, respectively.
5
- *
6
- * @param array The array to transform into a YArray
7
- * @returns A YArray.
8
- */
9
- export const arrayToYArray = (array) => {
10
- const yarray = new Y.Array();
11
- array.forEach((value) => {
12
- if (value instanceof Array)
13
- yarray.push([arrayToYArray(value)]);
14
- else if (value !== null && typeof value === 'object')
15
- yarray.push([objectToYMap(value)]);
16
- else if (typeof value === 'string')
17
- yarray.push([stringToYText(value)]);
18
- else
19
- yarray.push([value]);
20
- });
21
- return yarray;
22
- };
23
- /**
24
- * Converts a normal JavaScript object into a YMap shared type. Any nested
25
- * objects or arrays are turned into YMaps or YArrays, respectively.
26
- *
27
- * @param object The object to turn into a YMap shared type.
28
- * @returns A YMap.
29
- */
30
- export const objectToYMap = (object) => {
31
- const ymap = new Y.Map();
32
- Object.entries(object).forEach(([property, value]) => {
33
- if (value instanceof Array)
34
- ymap.set(property, arrayToYArray(value));
35
- else if (value !== null && typeof value === 'object')
36
- ymap.set(property, objectToYMap(value));
37
- else if (typeof value === 'string')
38
- ymap.set(property, stringToYText(value));
39
- else
40
- ymap.set(property, value);
41
- });
42
- return ymap;
43
- };
44
- export const stringToYText = (string) => new Y.Text(string);
45
- //# sourceMappingURL=mapping.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mapping.js","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAgB,EAAoB,EAAE;IAClE,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;IAE7B,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QACtB,IAAI,KAAK,YAAY,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;aAC3D,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,KAAgC,CAAC,CAAC,CAAC,CAAC;aAC/G,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;;YACnE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAA+B,EAAkB,EAAE;IAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;IAEzB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE;QACnD,IAAI,KAAK,YAAY,KAAK;YAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;aAChE,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,KAAgC,CAAC,CAAC,CAAC;aACpH,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;;YACxE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC"}