So sánh Design: Nested Editor Config — Flow CŨ vs Flow MỚI

Nguồn: .kiro/specs/cyclic-editor-nesting/design.md (design chính thức) + nested-config-infinite-nesting-research.md (research/thảo luận ban đầu). Tài liệu này không lặp lại toàn bộ design, chỉ trực quan hóa 3 khía cạnh người đọc cần so sánh nhanh: (1) config MFE module, (2) config setting riêng theo use case cho mỗi nested editor, (3) cách start editor nested theo kiểu lazy.


0. Bất biến chốt (nhắc lại)


1. Config về MFE module (server: config.server)

1.1 CŨ — đệ quy subSpecs, inline toàn cây

editors() lặp mfeReq.specs; creator word/composer đọc settings.embedded, rồi với mỗi item đệ quy gọi lại creator của coninline toàn bộ kết quả (kể cả nestedCoordConf.edLookups của con) vào node cha. Với word ⊃ composer một chiều thì dừng được vì composer cố tình bỏ word khỏi embedded — nhưng đây chính là cái giới hạn cần bỏ.

[Diagram]

Đặc điểm:

1.2 MỚI — graph-closure, registry phẳng theo type

expandEditorsClosure dùng worklist + visited-set trên editor type (không phải trên vị trí trong cây). Mỗi type được xử lý đúng 1 lần; creator không còn đệ quy — chỉ emit node của chính mình + danh sách con theo tên. Con được enqueue vào worklist, không inline.

[Diagram]

Registry 2 tầng (điểm mấu chốt để so sánh):

[Diagram]
MỚI
Đơn vị payload 1 cây lồng sâu (impl + nestedCoordConf inline) 1 node / type reachable (module + childBindings 1 cấp)
Chặn chu trình Luật một chiều thủ công (composer bỏ word) visited trên type — tự nhiên hữu hạn, cho phép chu trình
settings.embedded thiếu for...of undefinedcrash safeEmbedded() → "không nhúng gì"
Cache/dedup theo type Không — con lặp lại bị inline nhiều lần Có — mỗi type đúng 1 node
Kích thước payload Tăng theo độ sâu cây (branching^depth) Chặn trên bởi số editor type (hằng số)

2. Config setting riêng cho mỗi use case của mỗi nested editor

Đây là phần dễ hiểu nhầm khi "phẳng hóa" config: câu hỏi cốt lõi là làm sao vẫn giữ được word → math (LOCAL) khác composer → math (GLOBAL) khi không còn inline theo cây.

2.1 CŨ — context nằm ngay trong node con (do bị inline theo cha)

Vì cây được inline hoàn toàn, "context" (cha nào đang nhúng) được thể hiện tự nhiên bằng vị trí trong cây — node con nằm dưới word mang settings khác node con (cùng type) nằm dưới composer, vì chúng là hai bản inline riêng biệt.

[Diagram]

→ Đúng về mặt ngữ nghĩa, nhưng cái giá phải trả là mỗi lần "math" xuất hiện dưới một cha khác, toàn bộ subtree bị nhân bản → đây chính là nguồn phình payload / đệ quy vô tận khi có chu trình.

2.2 MỚI — context nằm trên "cạnh" (edge), không nằm trên node con

Node math trong registry chỉ có 1 bản (defaults — context-độc-lập). Context-dependent settings (LOCAL vs GLOBAL) được lưu ở childBindings của node cha — tức là trên cạnh cha→con, không phải trên node con.

[Diagram]

Công thức resolve khi materialize (client, EditorConfigRegistry.resolveChildConfig):

settings := merge(registry[math].defaults,
                   registry[Parent].childBindings[math].settingsOverride)

word materialize math → merge với childBindings của word; composer materialize math → merge với childBindings của composer. Cùng 1 node math trong registry, nhưng 2 lần materialize (2 instance riêng, vì editor không share giữa các tầng) nhận 2 settings khác nhau — đúng yêu cầu, không cần nhân bản node.

[Diagram]
MỚI
Nơi lưu context settings Trên node con (do bị nhân bản theo cha lúc inline) Trên cạnh cha→con (childBindings của node cha)
Số bản của 1 type trong payload N bản (N = số cha khác nhau từng nhúng nó) 1 bản duy nhất (defaults) trong registry
Cách tính settings cuối Đã tính sẵn trong lúc server inline merge(child.defaults, parent.childBindings[child].override) — tính lúc client materialize
Module (remote) Lặp lại theo mỗi bản inline 1 bản (EditorModuleLookup), dùng chung cho mọi context — dedup theo Module Federation
Phạm vi context Không giới hạn rõ — ngầm định theo cây Rõ ràng: context = immediate parent type (1 cấp); sâu hơn (path-dependent) ngoài scope hiện tại

3. Cách start các editor nested theo kiểu lazy

3.1 CŨ — eager, start toàn bộ type hỗ trợ ngay khi initialize()

[Diagram]

Gateway khi thiếu channel (type chưa từng được add, hoặc lỗi thứ tự) thì:

[Diagram]

3.2 MỚI — lazy, resolve-on-access, construct đồng bộ

initialize() không còn loop tạo instance — chỉ đăng ký LazyEditorProvider + channel gateway. Instance chỉ được tạo khi thực sự bị truy cập (editorByType, routing lệnh, load/insert/paste).

[Diagram]

Truy cập lần đầu → construct đồng bộ → init/start nền:

[Diagram]

Không còn drop lệnh — gateway tự materialize khi thiếu channel:

[Diagram]

Đường bulk (load/paste cây dựng sẵn) — pre-scan rồi materialize tất cả trước khi apply:

[Diagram]

3.3 Bảng so sánh start/lazy

CŨ (eager) MỚI (lazy)
Khi nào tạo instance Ngay trong initialize(), cho mọi type hỗ trợ Khi truy cập lần đầu (access/route/load/insert/paste)
Có xét nội dung tài liệu? Không Có — chỉ type thực sự dùng mới materialize
Với chu trình type (word↔composer) 💥 construct dây chuyền vô tận Dừng ngay — construct không chain, nestedCoord con khởi tạo rỗng
Channel/deserializer sẵn sàng khi nào Cùng lúc với eager construct Ngay trong resolve(), trước khi return — vẫn đồng bộ
Lệnh đến cho type chưa có channel Drop im lặng → peer phân kỳ ensureChannel materialize đồng bộ → buffer tới start(), không drop
initialize()/start() của sub-editor Await tuần tự trong loop cha Dispatch nền, không await trong resolve(); ensureStarted() riêng cho nơi cần đợi
Double-start / double-init Không áp dụng (chỉ tạo 1 lần theo loop) Chặn bằng lifecycles: Map<Type, {initialized, started}> — memoized, mỗi type init/start đúng 1 lần
Toolbar Giả định mọi editor đã tồn tại, duyệt this.editors Dựng tăng dần theo event editor-added
Timeout an toàn cho caller Không có khái niệm này ensureStarted timeout 30s → reject, background init/start vẫn chạy tiếp nhưng caller đã abort

4. Sơ đồ tổng hợp: 1 request → mở document → nested access, CŨ vs MỚI

[Diagram]

5. Tham chiếu file (để đối chiếu code thật)

Khía cạnh File
Config closure (server) config.server/src/app.controller.ts (expandEditorsClosure), closure.helpers.ts (safeEmbedded, defaultEmbedSpec, indexSpecsByItem)
Data model 2 tầng config.server/src/app.model.ts (EditorRegistryNode, ChildBinding, EditorModuleLookup)
Module registry (client) editor.core/src/lib/registry/module.registry.ts (DefaultModuleRegistry)
Config registry (client, resolve theo context) editor.core/src/lib/registry/config.registry.ts (DefaultEditorConfigRegistry)
Lazy provider (construct đồng bộ, init/start nền) editor.nested/src/lib/coord/default.lazy.editor.provider.ts (DefaultLazyEditorProvider)
Nested coordinator (initialize không eager) editor.nested/src/lib/coord/nested.coordinator.ts
editorByType resolve-on-access editor.coordinator/common/src/base.coordinator.ts
Gateway ensureChannel (không drop) editor.coordinator/classroom/src/cmd/default.cmd.gateway.ts, editor.nested/.../cmds/nested.wrapping.cmd.gateway.ts
Bulk pre-scan (load/paste) editor.nested/src/lib/coord/bulk.prescan.ts
Design & requirements đầy đủ .kiro/specs/cyclic-editor-nesting/{design.md,requirements.md,tasks.md}
Research/thảo luận ban đầu document/config.server/nested-config-infinite-nesting-research.md