patch-rows.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. /**
  2. * The one walk over the rows a patch list introduces. Composition, ownership,
  3. * the package probe, and patch loading all read those rows and their group
  4. * children; sharing the walk keeps them reading the same tree.
  5. * @module @deepseek-ai/dsh-app-boot/patch-rows
  6. */
  7. import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
  8. import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
  9. /** How a patch introduced a row: inserted it, or set it as a group's `config`. */
  10. export type PatchRowSource = 'insert' | 'config'
  11. /**
  12. * Where a patch introduced a row. `target` is the id of the group the row is
  13. * declared under — the patch's target, or the enclosing group row for a nested
  14. * one — undefined for a root-level insert, and a private marker for a child of
  15. * a group without an id, which no later patch can address. `patch` is the
  16. * index of the patch in the list, so the same id twice in one config list can
  17. * be told from a restatement across patches.
  18. */
  19. export interface PatchRowPlace {
  20. readonly target: string | undefined
  21. readonly patch: number
  22. }
  23. /** The target of rows nested in a group that declares no id: nothing can restate them, so nothing may match it. */
  24. const ANONYMOUS_GROUP = '\u0000anonymous-group'
  25. /** Whether a config item is a row: a plain object naming a module. */
  26. function isRowLike(value: unknown): value is EntryOptions {
  27. return typeof value === 'object' && value !== null && !Array.isArray(value) && typeof (value as { name?: unknown }).name === 'string'
  28. }
  29. /**
  30. * Visit one row and, for a group with a list config, each of its children,
  31. * parents before children, in written order.
  32. * @param row - the row.
  33. * @param visit - called once per row.
  34. */
  35. export function visitRowTree(row: EntryOptions, visit: (row: EntryOptions) => void): void {
  36. visitPlacedRows(row, undefined, (entry) => { visit(entry) })
  37. }
  38. /** The walk behind {@link visitRowTree}, carrying the group each row is declared under. */
  39. function visitPlacedRows(
  40. row: EntryOptions, target: string | undefined, visit: (row: EntryOptions, target: string | undefined) => void,
  41. ): void {
  42. visit(row, target)
  43. if (row.group && Array.isArray(row.config)) {
  44. const own = typeof row.id === 'string' ? row.id : ANONYMOUS_GROUP
  45. for (const child of row.config as EntryOptions[]) visitPlacedRows(child, own, visit)
  46. }
  47. }
  48. /**
  49. * Visit every row a patch list introduces, at any depth, in written order:
  50. * the rows a patch inserts, and the rows an id-targeted patch sets as a
  51. * group's `config`, which mount as that group's children the same way an
  52. * insert's do.
  53. * @param patches - the patch list.
  54. * @param visit - called once per row, with how the patch introduced it and where.
  55. */
  56. export function visitPatchRows(
  57. patches: readonly PatchOptions[],
  58. visit: (row: EntryOptions, source: PatchRowSource, place: PatchRowPlace) => void,
  59. ): void {
  60. patches.forEach((patch, index) => {
  61. if (patch.insert !== undefined) {
  62. for (const row of patch.insert) visitPlacedRows(row, patch.id, (entry, target) => { visit(entry, 'insert', { target, patch: index }) })
  63. return
  64. }
  65. if (patch.id === undefined || !Array.isArray(patch.config)) return
  66. for (const item of patch.config as unknown[]) {
  67. if (isRowLike(item)) visitPlacedRows(item, patch.id, (entry, target) => { visit(entry, 'config', { target, patch: index }) })
  68. }
  69. })
  70. }
  71. /** One id-carrying row `visitIdentifiedRows` reports. */
  72. export interface IdentifiedRow {
  73. /** The row's id. */
  74. id: string
  75. /** The row as the patch wrote it. */
  76. row: EntryOptions
  77. /** How the patch introduced the row. */
  78. source: PatchRowSource
  79. /** Where the row mounts. */
  80. place: PatchRowPlace
  81. /** The ids the same patch listed before this row. */
  82. listed: ReadonlySet<string>
  83. }
  84. /**
  85. * Visit every row with an id a patch list introduces, in written order, with
  86. * the ids the same patch listed before it: a config override lists the
  87. * children it keeps, and an id it lists twice would mount as a rejected
  88. * duplicate, which each caller reports in its own terms.
  89. * @param patches - the patch list.
  90. * @param visit - called once per row that carries an id.
  91. */
  92. export function visitIdentifiedRows(patches: readonly PatchOptions[], visit: (visited: IdentifiedRow) => void): void {
  93. let listed = new Set<string>()
  94. let listIndex = -1
  95. visitPatchRows(patches, (row, source, place) => {
  96. if (typeof row.id !== 'string') return
  97. if (place.patch !== listIndex) {
  98. listIndex = place.patch
  99. listed = new Set()
  100. }
  101. visit({ id: row.id, row, source, place, listed })
  102. listed.add(row.id)
  103. })
  104. }