@jimka/typescript-ui 0.3.0 → 0.4.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.
Files changed (238) hide show
  1. package/dist/lib/{AbstractBooleanInput-C3O4kv7_.js → AbstractBooleanInput-DE5NNJT0.js} +2 -2
  2. package/dist/lib/{AbstractBooleanInput-C3O4kv7_.js.map → AbstractBooleanInput-DE5NNJT0.js.map} +1 -1
  3. package/dist/lib/{AbstractInput-DFWniHqq.js → AbstractInput-BxfrmD2W.js} +2 -2
  4. package/dist/lib/{AbstractInput-DFWniHqq.js.map → AbstractInput-BxfrmD2W.js.map} +1 -1
  5. package/dist/lib/{AnchorConstraints-CSUcj8Wx.js → AnchorConstraints-DdDYSDkf.js} +2 -2
  6. package/dist/lib/{AnchorConstraints-CSUcj8Wx.js.map → AnchorConstraints-DdDYSDkf.js.map} +1 -1
  7. package/dist/lib/{AnimatedDropdown-e9urCXK1.js → AnimatedDropdown-QLNasu_Y.js} +2 -2
  8. package/dist/lib/{AnimatedDropdown-e9urCXK1.js.map → AnimatedDropdown-QLNasu_Y.js.map} +1 -1
  9. package/dist/lib/Border-CKKUk5G9.js +2 -0
  10. package/dist/lib/Border-CKKUk5G9.js.map +1 -0
  11. package/dist/lib/Button-MZDYJOMC.js +2 -0
  12. package/dist/lib/{Button-B36rBMKC.js.map → Button-MZDYJOMC.js.map} +1 -1
  13. package/dist/lib/{Card-P8iUH2-C.js → Card-a4dzYrbX.js} +2 -2
  14. package/dist/lib/{Card-P8iUH2-C.js.map → Card-a4dzYrbX.js.map} +1 -1
  15. package/dist/lib/ComboBox-C04SfXAR.js +2 -0
  16. package/dist/lib/ComboBox-C04SfXAR.js.map +1 -0
  17. package/dist/lib/Component-fRQvgaEY.js +2 -0
  18. package/dist/lib/Component-fRQvgaEY.js.map +1 -0
  19. package/dist/lib/Container-0bsG2Q8Z.js +2 -0
  20. package/dist/lib/{Container-xUbHk6l_.js.map → Container-0bsG2Q8Z.js.map} +1 -1
  21. package/dist/lib/{DOM-BeR95tE8.js → DOM-67StIm5G.js} +2 -2
  22. package/dist/lib/DOM-67StIm5G.js.map +1 -0
  23. package/dist/lib/DragChain-CYGuT7OA.js +2 -0
  24. package/dist/lib/DragChain-CYGuT7OA.js.map +1 -0
  25. package/dist/lib/DragManager-DRor_-1x.js +2 -0
  26. package/dist/lib/DragManager-DRor_-1x.js.map +1 -0
  27. package/dist/lib/{FieldDecorator-BaTGfKiW.js → FieldDecorator-Bpx8J8n9.js} +2 -2
  28. package/dist/lib/{FieldDecorator-BaTGfKiW.js.map → FieldDecorator-Bpx8J8n9.js.map} +1 -1
  29. package/dist/lib/{Fit-E-EIZv1k.js → Fit-CL1m1nIZ.js} +2 -2
  30. package/dist/lib/{Fit-E-EIZv1k.js.map → Fit-CL1m1nIZ.js.map} +1 -1
  31. package/dist/lib/Glyph-wN7bk7mg.js +2 -0
  32. package/dist/lib/Glyph-wN7bk7mg.js.map +1 -0
  33. package/dist/lib/{Grid-BXS2T8MT.js → Grid-DHHmaB-Y.js} +2 -2
  34. package/dist/lib/{Grid-BXS2T8MT.js.map → Grid-DHHmaB-Y.js.map} +1 -1
  35. package/dist/lib/GridConstraints-DVc1BmlP.js +2 -0
  36. package/dist/lib/GridConstraints-DVc1BmlP.js.map +1 -0
  37. package/dist/lib/Header-2hKwQo_u.js +2 -0
  38. package/dist/lib/Header-2hKwQo_u.js.map +1 -0
  39. package/dist/lib/IconText-DC7Xe34b.js +2 -0
  40. package/dist/lib/IconText-DC7Xe34b.js.map +1 -0
  41. package/dist/lib/LayoutConstraints-LIHYYd2F.js +2 -0
  42. package/dist/lib/LayoutConstraints-LIHYYd2F.js.map +1 -0
  43. package/dist/lib/{LayoutSerialization-D1OHmxSB.js → LayoutSerialization-D5bBas4W.js} +2 -2
  44. package/dist/lib/{LayoutSerialization-D1OHmxSB.js.map → LayoutSerialization-D5bBas4W.js.map} +1 -1
  45. package/dist/lib/LayoutSizes-qNaURyiw.js +2 -0
  46. package/dist/lib/LayoutSizes-qNaURyiw.js.map +1 -0
  47. package/dist/lib/List-CG4NvwuV.js +2 -0
  48. package/dist/lib/List-CG4NvwuV.js.map +1 -0
  49. package/dist/lib/MemoryStore-DT6iWWco.js.map +1 -1
  50. package/dist/lib/Menu-CrZy2u0g.js +2 -0
  51. package/dist/lib/Menu-CrZy2u0g.js.map +1 -0
  52. package/dist/lib/MenuButton-wy_0RskC.js +2 -0
  53. package/dist/lib/MenuButton-wy_0RskC.js.map +1 -0
  54. package/dist/lib/{Panel-D9lTLGqP.js → Panel-Rd9zkNXx.js} +2 -2
  55. package/dist/lib/{Panel-D9lTLGqP.js.map → Panel-Rd9zkNXx.js.map} +1 -1
  56. package/dist/lib/Position-Ck_KD4cR.js +2 -0
  57. package/dist/lib/Position-Ck_KD4cR.js.map +1 -0
  58. package/dist/lib/{ProgressSpinner-glv-29eK.js → ProgressSpinner-yt-VXyCb.js} +2 -2
  59. package/dist/lib/{ProgressSpinner-glv-29eK.js.map → ProgressSpinner-yt-VXyCb.js.map} +1 -1
  60. package/dist/lib/{RadioButton-R4S3c9WD.js → RadioButton-D23rId-V.js} +2 -2
  61. package/dist/lib/{RadioButton-R4S3c9WD.js.map → RadioButton-D23rId-V.js.map} +1 -1
  62. package/dist/lib/{RovingTabIndex-Dgt01LOE.js → RovingTabIndex-DFJyoveq.js} +2 -2
  63. package/dist/lib/{RovingTabIndex-Dgt01LOE.js.map → RovingTabIndex-DFJyoveq.js.map} +1 -1
  64. package/dist/lib/Scrollbar-SOj9psiP.js +2 -0
  65. package/dist/lib/Scrollbar-SOj9psiP.js.map +1 -0
  66. package/dist/lib/Slider-DBnAZ-F7.js +2 -0
  67. package/dist/lib/Slider-DBnAZ-F7.js.map +1 -0
  68. package/dist/lib/{Spacer-B57i2-_G.js → Spacer-qalSEJ5i.js} +2 -2
  69. package/dist/lib/{Spacer-B57i2-_G.js.map → Spacer-qalSEJ5i.js.map} +1 -1
  70. package/dist/lib/TabButton-DKECTTk1.js +2 -0
  71. package/dist/lib/TabButton-DKECTTk1.js.map +1 -0
  72. package/dist/lib/{Text-Bni4icFY.js → Text-KB5LksJl.js} +2 -2
  73. package/dist/lib/{Text-Bni4icFY.js.map → Text-KB5LksJl.js.map} +1 -1
  74. package/dist/lib/Tooltip-Dro4DOZs.js +3 -0
  75. package/dist/lib/{Tooltip-BUy6uFKN.js.map → Tooltip-Dro4DOZs.js.map} +1 -1
  76. package/dist/lib/VBox-P3WFGkPc.js +2 -0
  77. package/dist/lib/VBox-P3WFGkPc.js.map +1 -0
  78. package/dist/lib/VirtualScroller-Bv4KXGKW.js +2 -0
  79. package/dist/lib/VirtualScroller-Bv4KXGKW.js.map +1 -0
  80. package/dist/lib/component/button.es.js +1 -1
  81. package/dist/lib/component/button.es.js.map +1 -1
  82. package/dist/lib/component/chart.es.js +1 -1
  83. package/dist/lib/component/chart.es.js.map +1 -1
  84. package/dist/lib/component/container.es.js +1 -1
  85. package/dist/lib/component/container.es.js.map +1 -1
  86. package/dist/lib/component/diagram.es.js +1 -1
  87. package/dist/lib/component/diagram.es.js.map +1 -1
  88. package/dist/lib/component/display.es.js +1 -1
  89. package/dist/lib/component/display.es.js.map +1 -1
  90. package/dist/lib/component/editor.es.js +2 -2
  91. package/dist/lib/component/editor.es.js.map +1 -1
  92. package/dist/lib/component/input.es.js +1 -1
  93. package/dist/lib/component/input.es.js.map +1 -1
  94. package/dist/lib/component/list.es.js +1 -1
  95. package/dist/lib/component/list.es.js.map +1 -1
  96. package/dist/lib/component/menubar.es.js +1 -1
  97. package/dist/lib/component/menubar.es.js.map +1 -1
  98. package/dist/lib/component/table.es.js +2 -2
  99. package/dist/lib/component/table.es.js.map +1 -1
  100. package/dist/lib/component/tree.es.js +1 -1
  101. package/dist/lib/component/tree.es.js.map +1 -1
  102. package/dist/lib/core.es.js +1 -1
  103. package/dist/lib/core.es.js.map +1 -1
  104. package/dist/lib/layout.es.js +1 -1
  105. package/dist/lib/layout.es.js.map +1 -1
  106. package/dist/lib/overlay.es.js +1 -1
  107. package/dist/lib/overlay.es.js.map +1 -1
  108. package/dist/lib/primitive.es.js +1 -1
  109. package/dist/lib/router.es.js +1 -1
  110. package/dist/lib/selectionsEqual-Ci3_uflt.js +2 -0
  111. package/dist/lib/selectionsEqual-Ci3_uflt.js.map +1 -0
  112. package/dist/lib/types/component/button/MenuButton.d.ts +1 -0
  113. package/dist/lib/types/component/button/SplitButton.d.ts +1 -0
  114. package/dist/lib/types/component/button/TabButton.d.ts +1 -0
  115. package/dist/lib/types/component/button/TabCloseButton.d.ts +1 -1
  116. package/dist/lib/types/component/chart/ChartLegend.d.ts +0 -1
  117. package/dist/lib/types/component/container/CollapseButton.d.ts +0 -1
  118. package/dist/lib/types/component/container/DialogBackdrop.d.ts +1 -1
  119. package/dist/lib/types/component/container/MenuItem.d.ts +4 -0
  120. package/dist/lib/types/component/container/MenuSeparator.d.ts +1 -1
  121. package/dist/lib/types/component/container/Scrollbar.d.ts +1 -0
  122. package/dist/lib/types/component/container/SplitGutter.d.ts +2 -2
  123. package/dist/lib/types/component/container/StatusBar.d.ts +1 -1
  124. package/dist/lib/types/component/container/TabPanel.d.ts +3 -0
  125. package/dist/lib/types/component/container/VirtualScroller.d.ts +1 -0
  126. package/dist/lib/types/component/container/WindowBorder.d.ts +1 -1
  127. package/dist/lib/types/component/diagram/DiagramNode.d.ts +1 -1
  128. package/dist/lib/types/component/display/Canvas.d.ts +8 -2
  129. package/dist/lib/types/component/display/Glyph.d.ts +2 -1
  130. package/dist/lib/types/component/display/Header.d.ts +1 -1
  131. package/dist/lib/types/component/display/IconLabel.d.ts +1 -1
  132. package/dist/lib/types/component/display/IconText.d.ts +1 -1
  133. package/dist/lib/types/component/display/Image.d.ts +1 -1
  134. package/dist/lib/types/component/display/Video.d.ts +1 -1
  135. package/dist/lib/types/component/display/WebGLCanvas.d.ts +8 -2
  136. package/dist/lib/types/component/editor/CodeEditor.d.ts +1 -1
  137. package/dist/lib/types/component/input/AbstractPickerField.d.ts +1 -1
  138. package/dist/lib/types/component/input/AutoCompleteDropdown.d.ts +1 -1
  139. package/dist/lib/types/component/input/Link.d.ts +1 -2
  140. package/dist/lib/types/component/input/NumberSpinner.d.ts +1 -1
  141. package/dist/lib/types/component/input/PasswordField.d.ts +1 -1
  142. package/dist/lib/types/component/input/SpinButton.d.ts +1 -1
  143. package/dist/lib/types/component/input/TextArea.d.ts +1 -1
  144. package/dist/lib/types/component/input/TextField.d.ts +3 -1
  145. package/dist/lib/types/component/input/UsernameField.d.ts +1 -1
  146. package/dist/lib/types/component/list/AbstractMarkerList.d.ts +9 -1
  147. package/dist/lib/types/component/list/BulletedList.d.ts +1 -0
  148. package/dist/lib/types/component/list/ListItem.d.ts +10 -4
  149. package/dist/lib/types/component/list/NumberedList.d.ts +1 -0
  150. package/dist/lib/types/component/menubar/MenuBar.d.ts +1 -0
  151. package/dist/lib/types/component/menubar/MenuBarButton.d.ts +1 -2
  152. package/dist/lib/types/component/menubar/ToolBar.d.ts +2 -1
  153. package/dist/lib/types/component/menubar/ToolBarSeparator.d.ts +1 -1
  154. package/dist/lib/types/component/shared/VirtualRowView.d.ts +13 -1
  155. package/dist/lib/types/component/table/Body.d.ts +15 -2
  156. package/dist/lib/types/component/table/CellGeometry.d.ts +6 -0
  157. package/dist/lib/types/component/table/Column.d.ts +4 -0
  158. package/dist/lib/types/component/table/ColumnConfig.d.ts +3 -0
  159. package/dist/lib/types/component/table/Header.d.ts +24 -2
  160. package/dist/lib/types/component/table/Row.d.ts +13 -2
  161. package/dist/lib/types/component/table/Table.d.ts +26 -4
  162. package/dist/lib/types/component/table/TableExporter.d.ts +1 -1
  163. package/dist/lib/types/component/table/TablePanel.d.ts +2 -1
  164. package/dist/lib/types/component/table/cell/Cell.d.ts +1 -1
  165. package/dist/lib/types/component/table/cell/Header.d.ts +3 -0
  166. package/dist/lib/types/component/table/index.d.ts +1 -1
  167. package/dist/lib/types/component/tree/Tree.d.ts +1 -0
  168. package/dist/lib/types/core/Aria.d.ts +1 -1
  169. package/dist/lib/types/core/Body.d.ts +8 -2
  170. package/dist/lib/types/core/BorderWidths.d.ts +12 -0
  171. package/dist/lib/types/core/Component.d.ts +7 -0
  172. package/dist/lib/types/core/DOM.d.ts +4 -0
  173. package/dist/lib/types/core/DragChain.d.ts +3 -0
  174. package/dist/lib/types/core/Event.d.ts +2 -0
  175. package/dist/lib/types/core/Favicon.d.ts +7 -0
  176. package/dist/lib/types/core/FirstLayoutGate.d.ts +5 -0
  177. package/dist/lib/types/core/Theme.d.ts +1 -0
  178. package/dist/lib/types/core/Util.d.ts +1 -0
  179. package/dist/lib/types/core/index.d.ts +2 -0
  180. package/dist/lib/types/layout/Accordion.d.ts +0 -1
  181. package/dist/lib/types/layout/FlowLayout.d.ts +4 -0
  182. package/dist/lib/types/layout/HFlow.d.ts +3 -0
  183. package/dist/lib/types/layout/LayoutConstraints.d.ts +1 -0
  184. package/dist/lib/types/layout/LayoutSerialization.d.ts +3 -0
  185. package/dist/lib/types/layout/VFlow.d.ts +1 -0
  186. package/dist/lib/types/overlay/AbstractWindow.d.ts +1 -1
  187. package/dist/lib/types/overlay/Dock.d.ts +2 -0
  188. package/dist/lib/types/overlay/DropZoneOverlay.d.ts +1 -0
  189. package/dist/lib/types/overlay/Popover.d.ts +1 -1
  190. package/dist/lib/validation.es.js +1 -1
  191. package/llms.txt +1 -0
  192. package/package.json +2 -2
  193. package/dist/lib/Border-Dq6dXkmS.js +0 -2
  194. package/dist/lib/Border-Dq6dXkmS.js.map +0 -1
  195. package/dist/lib/Button-B36rBMKC.js +0 -2
  196. package/dist/lib/ComboBox-BjAhXXkY.js +0 -2
  197. package/dist/lib/ComboBox-BjAhXXkY.js.map +0 -1
  198. package/dist/lib/Component-CHTzJxEf.js +0 -2
  199. package/dist/lib/Component-CHTzJxEf.js.map +0 -1
  200. package/dist/lib/Container-xUbHk6l_.js +0 -2
  201. package/dist/lib/DOM-BeR95tE8.js.map +0 -1
  202. package/dist/lib/DragManager-T5Oljd0F.js +0 -2
  203. package/dist/lib/DragManager-T5Oljd0F.js.map +0 -1
  204. package/dist/lib/Glyph-CnNslL95.js +0 -2
  205. package/dist/lib/Glyph-CnNslL95.js.map +0 -1
  206. package/dist/lib/GridConstraints-D_kbd-h2.js +0 -2
  207. package/dist/lib/GridConstraints-D_kbd-h2.js.map +0 -1
  208. package/dist/lib/HBox-Dm3xgsDE.js +0 -2
  209. package/dist/lib/HBox-Dm3xgsDE.js.map +0 -1
  210. package/dist/lib/Header-CqFc2OUx.js +0 -2
  211. package/dist/lib/Header-CqFc2OUx.js.map +0 -1
  212. package/dist/lib/IconText-CTM2r0Tv.js +0 -2
  213. package/dist/lib/IconText-CTM2r0Tv.js.map +0 -1
  214. package/dist/lib/LayoutConstraints-DkctJZXB.js +0 -2
  215. package/dist/lib/LayoutConstraints-DkctJZXB.js.map +0 -1
  216. package/dist/lib/LayoutSizes-CvMmMgHJ.js +0 -2
  217. package/dist/lib/LayoutSizes-CvMmMgHJ.js.map +0 -1
  218. package/dist/lib/List-DHeOPAK2.js +0 -2
  219. package/dist/lib/List-DHeOPAK2.js.map +0 -1
  220. package/dist/lib/Menu-B6ippw-p.js +0 -2
  221. package/dist/lib/Menu-B6ippw-p.js.map +0 -1
  222. package/dist/lib/MenuButton-Bjm1OyB-.js +0 -2
  223. package/dist/lib/MenuButton-Bjm1OyB-.js.map +0 -1
  224. package/dist/lib/Position-DRSlWn4Z.js +0 -2
  225. package/dist/lib/Position-DRSlWn4Z.js.map +0 -1
  226. package/dist/lib/Scrollbar-CiVV3axj.js +0 -2
  227. package/dist/lib/Scrollbar-CiVV3axj.js.map +0 -1
  228. package/dist/lib/Slider-_JVvuQ8M.js +0 -2
  229. package/dist/lib/Slider-_JVvuQ8M.js.map +0 -1
  230. package/dist/lib/TabButton-DEypdlZf.js +0 -2
  231. package/dist/lib/TabButton-DEypdlZf.js.map +0 -1
  232. package/dist/lib/Tooltip-BUy6uFKN.js +0 -3
  233. package/dist/lib/VBox-Dr7Ge9M6.js +0 -2
  234. package/dist/lib/VBox-Dr7Ge9M6.js.map +0 -1
  235. package/dist/lib/VirtualScroller-BPy6KmSX.js +0 -2
  236. package/dist/lib/VirtualScroller-BPy6KmSX.js.map +0 -1
  237. package/dist/lib/selectionsEqual-2PJrcimg.js +0 -2
  238. package/dist/lib/selectionsEqual-2PJrcimg.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"MemoryStore-DT6iWWco.js","names":[],"sources":["../../src/typescript/lib/data/Field.ts","../../src/typescript/lib/data/FilterDescriptor.ts","../../src/typescript/lib/data/StoreWorkerClient.ts","../../src/typescript/lib/data/compareValues.ts","../../src/typescript/lib/data/AbstractStore.ts","../../src/typescript/lib/data/Store.ts","../../src/typescript/lib/data/ModelRecord.ts","../../src/typescript/lib/data/Association.ts","../../src/typescript/lib/data/AbstractModel.ts","../../src/typescript/lib/data/Model.ts","../../src/typescript/lib/data/proxy/Proxy.ts","../../src/typescript/lib/data/proxy/MemoryProxy.ts","../../src/typescript/lib/data/MemoryStore.ts"],"sourcesContent":["// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { ValidationRule } from '~/validation/ValidationRule.js';\n\n/**\n * Built-in field types supported by {@link Model} and {@link AbstractModel}.\n *\n * @category Data\n */\nexport type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'glyph' | 'auto';\n\n/**\n * Construction-time options for a {@link Field}.\n * Can be passed directly to `AbstractModel.fields` or used to construct a `Field` instance.\n *\n * @category Data\n */\nexport interface FieldOptions {\n name: string;\n type?: FieldType;\n defaultValue?: any;\n mapping?: string;\n description?: string;\n order?: number;\n /** Custom raw-to-typed coercion; wins over the built-in `type` conversion. */\n convert?: (raw: any, sourceRecord?: Record<string, any>) => any;\n /** Field-level validation rules, evaluated by {@link ModelRecord} (pull-based). */\n validators?: ValidationRule[];\n}\n\n/**\n * @deprecated Use {@link FieldOptions}.\n */\nexport type FieldConfig = FieldOptions;\n\n/**\n * Represents a single typed field in a model's schema.\n * Encapsulates the field's name, type, default value, raw-data mapping, description, and display order.\n *\n * @category Data\n */\nexport class Field {\n\n private _name: string;\n private _type: FieldType;\n private _defaultValue: any;\n private _mapping: string;\n private _description: string | undefined;\n private _order: number | undefined;\n private _convert: ((raw: any, sourceRecord?: Record<string, any>) => any) | undefined;\n private _validators: ValidationRule[];\n\n /**\n * Constructs a Field from a FieldOptions object.\n *\n * @param options - The options object describing the field's properties.\n */\n constructor(options: FieldOptions) {\n this._name = options.name;\n this._type = options.type ?? 'auto';\n this._defaultValue = options.defaultValue;\n this._mapping = options.mapping ?? options.name;\n this._description = options.description;\n this._order = options.order;\n this._convert = options.convert;\n this._validators = options.validators ?? [];\n }\n\n /**\n * Returns the field's logical name used as the record property key.\n *\n * @returns The logical name string for this field.\n */\n getName(): string {\n return this._name;\n }\n\n /**\n * Returns the field's data type.\n *\n * @returns The FieldType value for this field.\n */\n getType(): FieldType {\n return this._type;\n }\n\n /**\n * Returns the value used when raw data does not contain this field.\n *\n * @returns The configured default value, or undefined if none was specified.\n */\n getDefaultValue(): any {\n return this._defaultValue;\n }\n\n /**\n * Returns the raw-data property name that maps to this field.\n *\n * @returns The mapping key string; defaults to the field name when not explicitly configured.\n */\n getMapping(): string {\n return this._mapping;\n }\n\n /**\n * Returns the human-readable description, falling back to the field name.\n *\n * @returns The description string if configured, otherwise the field name.\n */\n getDescription(): string {\n return this._description ?? this._name;\n }\n\n /**\n * Returns the display order index; -1 means unspecified.\n *\n * @returns The configured order value, or -1 if no order was specified.\n */\n getOrder(): number {\n return this._order ?? -1;\n }\n\n /**\n * Coerces a raw value to this field's type, or runs the custom `convert` hook.\n *\n * @remarks\n * A configured `convert` callback wins over the built-in type conversion. Otherwise\n * `null`/`undefined` short-circuit to themselves (an absent value never becomes `NaN`,\n * `\"null\"`, or an Invalid Date), and any other value is routed through the type switch.\n *\n * @param raw - The raw value to coerce.\n * @param sourceRecord - Optional. The full mapped source object, so a custom `convert`\n * can derive this field from sibling raw values.\n *\n * @returns The coerced value typed according to this field's `type`.\n */\n convertValue(raw: any, sourceRecord?: Record<string, any>): any {\n if (this._convert) {\n return this._convert(raw, sourceRecord);\n }\n\n if (raw === null || raw === undefined) {\n return raw;\n }\n\n return this.convertByType(raw);\n }\n\n /**\n * Coerces a non-null raw value according to this field's built-in `type`.\n *\n * @param raw - The raw value to coerce; guaranteed non-null by the caller.\n *\n * @returns The coerced value, or undefined when the value cannot be coerced to the type.\n */\n private convertByType(raw: any): any {\n switch (this._type) {\n case 'number': {\n if (raw === '') {\n return undefined;\n }\n\n const num = Number(raw);\n\n return num;\n }\n\n case 'boolean': {\n return this.convertBoolean(raw);\n }\n\n case 'date':\n case 'datetime':\n case 'time': {\n if (raw instanceof Date) {\n return raw;\n }\n\n const date = new Date(raw);\n\n return isNaN(date.getTime()) ? undefined : date;\n }\n\n case 'string': {\n return String(raw);\n }\n\n default: {\n return raw;\n }\n }\n }\n\n /**\n * Coerces a non-null raw value to a boolean, honouring the common truthy / falsy\n * string and numeric spellings before falling back to `Boolean(raw)`.\n *\n * @param raw - The raw value to coerce; guaranteed non-null by the caller.\n *\n * @returns The coerced boolean value.\n */\n private convertBoolean(raw: any): boolean {\n const truthy = [true, 1, 'true', '1', 'yes'];\n const falsy = [false, 0, 'false', '0', 'no', ''];\n\n if (truthy.includes(raw)) {\n return true;\n }\n\n if (falsy.includes(raw)) {\n return false;\n }\n\n return Boolean(raw);\n }\n\n /**\n * Returns the configured validation rules, or an empty array.\n *\n * @remarks\n * Evaluated by [`ModelRecord`](/api/data/classes/ModelRecord) on demand; see its\n * pull-based `isValid` / `getErrors` / `validateField` API.\n *\n * @returns The field's validation rules; empty when none were configured.\n */\n getValidators(): ValidationRule[] {\n return this._validators;\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\n/**\n * Serializable filter algebra for {@link AbstractStore}. Descriptors are plain objects so\n * they can cross the worker boundary via structured clone (unlike arbitrary filter\n * functions, which can't). The same evaluator runs on either side.\n *\n * @category Data\n */\nexport type FilterDescriptor =\n | { type: 'eq'; field: string; value: any }\n | { type: 'neq'; field: string; value: any }\n | { type: 'contains'; field: string; value: string; caseSensitive?: boolean }\n | { type: 'startsWith'; field: string; value: string; caseSensitive?: boolean }\n | { type: 'gt'; field: string; value: number | string | Date }\n | { type: 'gte'; field: string; value: number | string | Date }\n | { type: 'lt'; field: string; value: number | string | Date }\n | { type: 'lte'; field: string; value: number | string | Date }\n | { type: 'in'; field: string; values: any[] }\n | { type: 'and'; filters: FilterDescriptor[] }\n | { type: 'or'; filters: FilterDescriptor[] }\n | { type: 'not'; filter: FilterDescriptor };\n\n/**\n * Looks up a field's value from either a plain data object or a ModelRecord-like\n * object that exposes a `get(field)` method. Lets the same matcher run on both\n * sides of the worker boundary.\n */\nfunction readField(record: any, field: string): any {\n if (record && typeof record.get === 'function') {\n return record.get(field);\n }\n return record ? record[field] : undefined;\n}\n\n/**\n * Evaluates a FilterDescriptor against a record. Returns true if the record\n * matches the filter. Works with both plain data objects (worker side) and\n * ModelRecord instances (main thread side).\n */\nexport function matchesFilter(record: any, descriptor: FilterDescriptor): boolean {\n switch (descriptor.type) {\n case 'eq':\n return readField(record, descriptor.field) === descriptor.value;\n\n case 'neq':\n return readField(record, descriptor.field) !== descriptor.value;\n\n case 'contains': {\n const raw = readField(record, descriptor.field);\n if (raw == null) return false;\n const haystack = descriptor.caseSensitive ? String(raw) : String(raw).toLowerCase();\n const needle = descriptor.caseSensitive ? descriptor.value : descriptor.value.toLowerCase();\n return haystack.indexOf(needle) !== -1;\n }\n\n case 'startsWith': {\n const raw = readField(record, descriptor.field);\n if (raw == null) return false;\n const haystack = descriptor.caseSensitive ? String(raw) : String(raw).toLowerCase();\n const needle = descriptor.caseSensitive ? descriptor.value : descriptor.value.toLowerCase();\n return haystack.indexOf(needle) === 0;\n }\n\n case 'gt':\n return readField(record, descriptor.field) > descriptor.value;\n\n case 'gte':\n return readField(record, descriptor.field) >= descriptor.value;\n\n case 'lt':\n return readField(record, descriptor.field) < descriptor.value;\n\n case 'lte':\n return readField(record, descriptor.field) <= descriptor.value;\n\n case 'in':\n return descriptor.values.indexOf(readField(record, descriptor.field)) !== -1;\n\n case 'and':\n for (const f of descriptor.filters) {\n if (!matchesFilter(record, f)) return false;\n }\n return true;\n\n case 'or':\n for (const f of descriptor.filters) {\n if (matchesFilter(record, f)) return true;\n }\n return false;\n\n case 'not':\n return !matchesFilter(record, descriptor.filter);\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// Main-thread client for the StoreWorker. Lazily constructs a single Worker\n// instance shared across all stores; routes requests by requestId so concurrent\n// stores don't crosstalk. Falls back gracefully if Worker isn't available\n// (test environment, server-side, etc.) — the AbstractStore caller checks\n// `isAvailable()` before dispatching.\n\nimport { FilterDescriptor } from \"~/data/FilterDescriptor.js\";\nimport type { FieldType } from \"~/data/Field.js\";\n\n// Vite-specific worker import. The `?worker` suffix tells Vite to bundle the\n// module as a Web Worker entry. The default export is the Worker constructor.\n// @ts-ignore — Vite resolves this at build time; tsc on its own can't.\nimport StoreWorkerCtor from \"~/data/StoreWorker.js?worker\";\n\ntype Direction = \"asc\" | \"desc\";\n\ntype Response = { requestId: number; indices?: number[]; error?: string };\n\ninterface Pending {\n resolve: (indices: number[] | undefined) => void;\n reject: (err: Error) => void;\n}\n\nlet worker: Worker | null = null;\nlet nextRequestId = 1;\nconst pending: Map<number, Pending> = new Map();\n\nfunction ensureWorker(): Worker | null {\n if (worker) return worker;\n if (typeof Worker === \"undefined\") return null;\n\n try {\n worker = new (StoreWorkerCtor as any)() as Worker;\n } catch {\n worker = null;\n return null;\n }\n\n worker.onmessage = (e: MessageEvent<Response>) => {\n const { requestId, indices, error } = e.data;\n const p = pending.get(requestId);\n if (!p) return;\n\n pending.delete(requestId);\n\n if (error) {\n p.reject(new Error(error));\n } else {\n p.resolve(indices);\n }\n };\n\n return worker;\n}\n\nfunction send(message: any): Promise<number[] | undefined> {\n const w = ensureWorker();\n if (!w) {\n return Promise.reject(new Error(\"Worker unavailable\"));\n }\n\n const requestId = nextRequestId++;\n message.requestId = requestId;\n\n return new Promise((resolve, reject) => {\n pending.set(requestId, { resolve, reject });\n w.postMessage(message);\n });\n}\n\nexport const StoreWorkerClient = {\n /**\n * @returns true when a worker can be constructed in this runtime.\n */\n isAvailable(): boolean {\n return ensureWorker() !== null;\n },\n\n /**\n * Ships a fresh snapshot of plain record data to the worker for the given storeId.\n * Subsequent sort/filter requests run against this snapshot until replaced.\n */\n snapshot(storeId: string, records: Array<Record<string, any>>): Promise<void> {\n return send({ type: \"snapshot\", storeId, records }).then(() => undefined);\n },\n\n /**\n * Combined filter + sort in a single round-trip. Either spec may be omitted.\n * The sort spec carries the field's `fieldType` so the worker's comparator\n * stays in parity with the main thread's (locale-aware strings, timestamp\n * dates).\n */\n sortFilter(\n storeId: string,\n sort?: { field: string; direction: Direction; fieldType?: FieldType },\n filter?: FilterDescriptor,\n ): Promise<number[]> {\n return send({ type: \"sortFilter\", storeId, sort, filter }).then(idx => idx ?? []);\n },\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { FieldType } from '~/data/Field.js';\n\n/**\n * Native ordering of two non-null values via `<` / `>`, returning the\n * ascending-sense sign. Used for numeric (and any non-string, non-date) fields.\n *\n * @param av - The left operand.\n * @param bv - The right operand.\n *\n * @returns A negative, zero, or positive number in ascending sense.\n */\nfunction nativeCompare(av: any, bv: any): number {\n return av < bv ? -1 : av > bv ? 1 : 0;\n}\n\n/**\n * Type-aware, locale-aware comparison of two raw field values, returning a\n * negative / zero / positive number in **ascending** sense. This is the single\n * comparator shared by the main thread ({@link AbstractStore}) and the\n * `StoreWorker`, so the two sort paths can never drift.\n *\n * @param av - The left value.\n * @param bv - The right value.\n * @param type - Optional. The field's {@link FieldType}, which selects the\n * comparison strategy (string → locale, date/time → timestamp, else native).\n * When omitted, two string operands still use the locale path.\n *\n * @returns The ascending-sense comparison result.\n *\n * @remarks\n * `null`/`undefined` sort **last**: both null compare equal (`0`), and a single\n * null returns the sign that places it after a non-null value. Callers must\n * leave a null-involving result un-negated (apply sort direction only to a\n * non-null comparison) so nulls stay last regardless of `'asc'`/`'desc'`.\n *\n * String fields (or two string operands when `type` is unknown) use\n * `localeCompare`, so `'Ä'` orders between `'a'` and `'Z'` rather than after\n * `'Z'` by code point. Date/time fields compare by `getTime()` when both\n * operands are `Date`, falling through to native comparison otherwise.\n *\n * @category Data\n */\nexport function compareValues(av: any, bv: any, type?: FieldType): number {\n if (av == null && bv == null) {\n return 0;\n }\n\n if (av == null) {\n return 1;\n }\n\n if (bv == null) {\n return -1;\n }\n\n if (type === 'string' || (type === undefined && typeof av === 'string' && typeof bv === 'string')) {\n return av.localeCompare(bv);\n }\n\n if (type === 'date' || type === 'datetime' || type === 'time') {\n if (av instanceof Date && bv instanceof Date) {\n return nativeCompare(av.getTime(), bv.getTime());\n }\n }\n\n return nativeCompare(av, bv);\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { AbstractModel } from '~/data/AbstractModel.js';\nimport { ModelRecord, type FieldChange } from '~/data/ModelRecord.js';\nimport { Proxy, ReadParams } from '~/data/proxy/Proxy.js';\nimport { FilterDescriptor, matchesFilter } from '~/data/FilterDescriptor.js';\nimport { StoreWorkerClient } from '~/data/StoreWorkerClient.js';\nimport { compareValues } from '~/data/compareValues.js';\nimport { ListenerBag } from '~/core/ListenerBag.js';\n\n/**\n * Datasets above this size are sorted/filtered on a Web Worker so the main\n * thread stays responsive. Below the threshold, the round-trip overhead exceeds\n * the work, so we run synchronously in-process.\n */\nconst WORKER_THRESHOLD = 1000;\nlet nextStoreId = 1;\n\n/**\n * Callback fired when a store event ({@link StoreEvent}) is emitted.\n *\n * @category Data\n */\nexport type StoreListener<T = any> = (payload: T) => void;\n/**\n * Names of the events fired by an {@link AbstractStore}.\n *\n * @category Data\n */\nexport type StoreEvent = 'load' | 'beforeload' | 'datachange' | 'add' | 'remove' | 'clear' | 'beforesync' | 'sync' | 'exception' | 'loadingchange' | 'pagechange' | 'pagechangeblocked' | 'sortchange' | 'filterchange' | 'update' | 'groupchange';\n\n/**\n * The proxy operation that failed in a {@link StoreExceptionEvent}.\n *\n * @category Data\n */\nexport type StoreOperation = 'read' | 'create' | 'update' | 'destroy';\n\n/**\n * Payload for the `'exception'` event, fired when a `load()` read or a `sync()`\n * create/update/destroy fails.\n *\n * @remarks\n * `operation` disambiguates which proxy op failed; `records` carries the\n * offending record(s) — the batch or single record whose op failed — and is\n * empty for a `read` failure. `error` is the raw thrown value.\n *\n * @category Data\n */\nexport interface StoreExceptionEvent {\n operation: StoreOperation;\n records : ModelRecord[];\n error : unknown;\n}\n\n/**\n * Payload for the `'clear'` event fired by {@link AbstractStore.removeAll}.\n *\n * @category Data\n */\nexport interface StoreClearEvent {\n removed: ModelRecord[];\n}\n\n/**\n * Payload for the `'filterchange'` event fired when the active filter list is\n * replaced or cleared.\n *\n * @category Data\n */\nexport interface StoreFilterChangeEvent {\n filters: FilterDescriptor[];\n}\n\n/**\n * Payload for the `'update'` event fired by {@link AbstractStore.notifyRecordChanged}.\n *\n * @category Data\n */\nexport interface StoreUpdateEvent {\n record: ModelRecord;\n /**\n * Field-level diff of the change, keyed by field name. Carried by both the\n * single-`set()` auto-notify and a record edit-batch commit; absent only when\n * a caller invokes {@link AbstractStore.notifyRecordChanged} with no diff.\n */\n changes?: Record<string, FieldChange>;\n}\n\n/**\n * Summary payload for the `'sync'` event: the per-op failures recorded during\n * the just-finished `sync()`, empty when every operation succeeded.\n *\n * @category Data\n */\nexport interface StoreSyncEvent {\n failures: StoreExceptionEvent[];\n}\n\n/**\n * Payload for the `'groupchange'` event fired when {@link AbstractStore.setGroupField}\n * changes the active group field.\n *\n * @category Data\n */\nexport interface StoreGroupChangeEvent {\n groupField: string | null;\n}\n\n/**\n * Describes one column's contribution to a multi-column sort.\n *\n * @category Data\n */\nexport interface SortDescriptor {\n field: string;\n dir : 'asc' | 'desc';\n /**\n * Optional custom comparator returning the ascending-sense ordering of two\n * records. Main-thread only: a function cannot cross the structured-clone\n * boundary, so a sorter carrying a `sorterFn` forces {@link AbstractStore}'s\n * in-process sort path even for datasets above the worker threshold.\n */\n sorterFn?: (a: ModelRecord, b: ModelRecord) => number;\n}\n\n/**\n * Construction-time options shared by every {@link AbstractStore} subclass.\n *\n * @remarks Concrete stores extend this interface with the fields specific to\n * their model/proxy wiring (e.g. {@link StoreOptions}, {@link MemoryStoreOptions},\n * {@link AjaxStoreOptions}).\n *\n * @category Data\n */\nexport interface AbstractStoreOptions {\n pageSize?: number;\n page?: number;\n sorters?: SortDescriptor[];\n filters?: FilterDescriptor[];\n remoteSort?: boolean;\n remoteFilter?: boolean;\n autoLoad?: boolean;\n syncErrorPolicy?: 'stop' | 'continue';\n groupField?: string;\n cascadeSync?: boolean;\n listeners?: Partial<Record<StoreEvent, StoreListener>>;\n}\n\n/**\n * Abstract base class for all data stores.\n * Manages a collection of ModelRecord instances with support for loading, CRUD mutations,\n * filtering, sorting, and event notification.\n *\n * @remarks\n * The store maintains two parallel arrays: `allRecords` (the master list) and `records`\n * (the filtered and sorted view). Mutations always target `allRecords` and then rebuild\n * the view by calling `applyView()`. Consumers should read from `getRecords()` or\n * `getAt()` rather than accessing the raw arrays directly.\n *\n * @category Data\n */\nexport abstract class AbstractStore {\n\n abstract readonly model: AbstractModel;\n abstract readonly proxy: Proxy | undefined;\n\n private _allRecords: ModelRecord[] = [];\n private _records: ModelRecord[] = [];\n // Whether the most recent applyView() offloaded to the worker (so `_records`\n // is populated only when its promise resolves, not synchronously). Read right\n // after an ingestRaw() to decide whether the 'load' emit must wait for the\n // view. See applyView / loadData.\n private _viewAsync: boolean = false;\n private _pendingRemoved: ModelRecord[] = [];\n private _activeFilters: FilterDescriptor[] = [];\n private _activeSorters: SortDescriptor[] = [];\n private _listeners: ListenerBag<StoreEvent> = new ListenerBag<StoreEvent>();\n\n // id → record index over `_allRecords`, rebuilt by `rebuildIdIndex()` on\n // every `applyView()` so `getById()` is O(1). Stays empty (so getById returns\n // undefined) when the model has no primary key.\n private _idIndex: Map<any, ModelRecord> = new Map();\n\n // Active single-level group field, or null when grouping is off. Read by\n // `getGroupString()` / `getGroups()`; set via `setGroupField()`.\n private _groupField: string | null = null;\n\n // Worker-offload state. Each store gets a unique id so the shared worker can\n // keep snapshots from different stores apart. `snapshotDirty` flags whether\n // the worker's copy of allRecords is stale (re-shipped on the next applyView\n // when the dataset is over the threshold).\n private _storeId: string = 'store-' + (nextStoreId++);\n private _snapshotDirty: boolean = true;\n private _loading: boolean = false;\n\n // Store-level edit-batch flag. While set, owned records suppress their own\n // auto-notify (consulted through their back-ref by ModelRecord.set()); the\n // matching commitEdit() fires a single coalesced 'datachange'.\n private _batching: boolean = false;\n\n // ── Server-side pagination state ─────────────────────────────────────────\n // `_pageSize` is undefined until `setPageSize(n)` is called; while undefined,\n // `load()` calls `proxy.read()` with no arguments and the store behaves\n // identically to its unpaginated form.\n private _page: number = 1;\n private _pageSize: number | undefined = undefined;\n private _totalCount: number | undefined = undefined;\n\n // ── Remote sort/filter + load concurrency state ──────────────────────────\n // When set, active sorters/filters are serialized into ReadParams and a\n // mutation triggers a reload, instead of being applied locally by applyView.\n private _remoteSort: boolean = false;\n private _remoteFilter: boolean = false;\n\n // Controls what sync() does after an op fails: 'stop' aborts the remaining\n // sync (already-committed records stay committed; failed/untouched records\n // remain pending), 'continue' records the failure and proceeds.\n private _syncErrorPolicy: 'stop' | 'continue' = 'stop';\n\n // When true (default), sync() walks each parent record's materialised hasMany\n // child stores after the parent creates/updates resolve, stamping the parent\n // foreign key and cascading the child store's own sync(). Set false to opt a\n // store out of the cascade.\n private _cascadeSync: boolean = true;\n\n // `_loadSeq` is bumped on every load() so a stale in-flight response (whose\n // captured seq no longer matches) is ignored. `_loadAbort` cancels the\n // previous HTTP read when a newer load starts.\n private _loadSeq: number = 0;\n private _loadAbort: AbortController | undefined = undefined;\n\n /**\n * Applies an {@link AbstractStoreOptions} bag to this store. Subclasses\n * should call this from their constructor (after `model` and `proxy` are\n * assigned) so that pagination, sort, filter, and listener defaults are\n * dispatched to the existing setters.\n *\n * @param options - The options bag carrying the values to apply.\n *\n * @remarks Listener registrations and filters are applied first so that an\n * `autoLoad: true` flag triggers a `load()` whose result fires the\n * already-registered `'load'` listener.\n */\n protected applyOptions(options: AbstractStoreOptions): void {\n if (options.listeners !== undefined) {\n for (const event of Object.keys(options.listeners) as StoreEvent[]) {\n const listener = options.listeners[event];\n\n if (listener !== undefined) {\n this.on(event, listener);\n }\n }\n }\n\n if (options.pageSize !== undefined) {\n this.setPageSize(options.pageSize);\n }\n\n if (options.page !== undefined) {\n this._page = options.page;\n }\n\n if (options.sorters !== undefined && options.sorters.length > 0) {\n this._activeSorters = options.sorters.slice();\n }\n\n if (options.filters !== undefined && options.filters.length > 0) {\n this._activeFilters = options.filters.slice();\n }\n\n if (options.remoteSort !== undefined) {\n this._remoteSort = options.remoteSort;\n }\n\n if (options.remoteFilter !== undefined) {\n this._remoteFilter = options.remoteFilter;\n }\n\n if (options.syncErrorPolicy !== undefined) {\n this._syncErrorPolicy = options.syncErrorPolicy;\n }\n\n if (options.cascadeSync !== undefined) {\n this._cascadeSync = options.cascadeSync;\n }\n\n if (options.groupField !== undefined) {\n this.setGroupField(options.groupField);\n }\n\n if (options.autoLoad === true) {\n void this.load();\n }\n }\n\n // ── Loading ──────────────────────────────────────────────────────────────\n\n /**\n * Fetches data through the proxy, replaces all records, and fires the 'load' event.\n *\n * @returns A promise that resolves when the data has been loaded and the view rebuilt.\n *\n * @remarks\n * Throws an `Error` if no proxy is configured. Any existing records (including pending\n * removals) are discarded when new data is ingested.\n *\n * Fires `'beforeload'` before the proxy read. A read failure emits an\n * `'exception'` event ({@link StoreExceptionEvent} with `operation: 'read'`\n * and empty `records`) and then re-throws so existing `await store.load()`\n * call sites still observe the rejection. An aborted or superseded load is a\n * silent no-op and emits neither `'exception'` nor `'load'`.\n */\n async load(): Promise<void> {\n if (!this.proxy) {\n throw new Error('Store.load() called but no proxy is configured');\n }\n\n this.emit('beforeload', {});\n\n const seq = ++this._loadSeq;\n\n this._loadAbort?.abort();\n\n const controller = new AbortController();\n this._loadAbort = controller;\n\n this.setLoading(true);\n\n try {\n const params = this.buildReadParams(controller.signal);\n const raw = await this.proxy.read(params);\n\n if (seq !== this._loadSeq) {\n return;\n }\n\n // Await the view build so a worker-offloaded sort/filter has\n // populated `_records` before 'load' fires; below the worker\n // threshold this resolves synchronously. load() is already async, so\n // there is no caller-visible timing change.\n await this.ingestRaw(raw);\n\n this._totalCount = this.proxy.getLastTotalCount();\n\n this.emit('load', { records: this._records });\n } catch (err) {\n // An aborted fetch or a superseded load is a silent no-op; only a\n // genuine failure from the current load emits 'exception' and\n // propagates to the awaiter.\n if ((err as Error).name === 'AbortError' || seq !== this._loadSeq) {\n return;\n }\n\n this.emit('exception', { operation: 'read', records: [], error: err });\n\n throw err;\n } finally {\n if (seq === this._loadSeq) {\n this.setLoading(false);\n }\n }\n }\n\n /**\n * Builds the {@link ReadParams} for a load: pagination when enabled, plus\n * active sorters/filters when `remoteSort`/`remoteFilter` are on, plus the\n * abort signal.\n *\n * @param signal - The abort signal for the in-flight HTTP read.\n *\n * @returns A ReadParams object, or undefined when nothing applies — so an\n * unpaginated client-side store still calls `read()` with no arguments and\n * pagination-unaware proxies keep ignoring it.\n */\n private buildReadParams(signal: AbortSignal): ReadParams | undefined {\n const params: ReadParams = {};\n\n if (this._pageSize != null) {\n params.page = this._page;\n params.pageSize = this._pageSize;\n }\n\n if (this._remoteSort && this._activeSorters.length > 0) {\n params.sorters = this.getActiveSorters();\n }\n\n if (this._remoteFilter && this._activeFilters.length > 0) {\n params.filters = this.getActiveFilters();\n }\n\n if (params.page == null && params.sorters == null && params.filters == null) {\n return undefined;\n }\n\n params.signal = signal;\n\n return params;\n }\n\n /**\n * Returns whether the store is currently loading data.\n *\n * @returns True while `load()` is in-flight.\n */\n isLoading(): boolean {\n return this._loading;\n }\n\n /**\n * Sets the loading flag and fires `'loadingchange'` only when the value actually changes.\n *\n * @param value - The new loading state.\n */\n private setLoading(value: boolean): void {\n if (this._loading === value) {\n return;\n }\n\n this._loading = value;\n this.emit('loadingchange', { loading: value });\n }\n\n /**\n * Loads raw data directly without going through the proxy, then fires 'load'.\n *\n * @param data - An array of plain objects to convert into ModelRecords.\n */\n loadData(data: any[]): void {\n const pending = this.ingestRaw(data);\n\n // When applyView built the view synchronously (below the worker\n // threshold, or no worker available) `_records` is already populated, so\n // emit 'load' synchronously — consumers and tests rely on that timing.\n // When it offloaded to the worker, `_records` is not ready yet; defer the\n // emit until the worker resolves so listeners never render an empty view.\n if (this._viewAsync) {\n void pending.then(() => this.emit('load', { records: this._records }));\n } else {\n this.emit('load', { records: this._records });\n }\n }\n\n // ── Pagination ───────────────────────────────────────────────────────────\n\n /**\n * Enables server-side pagination at the given page size and resets to page 1.\n *\n * @param n - The number of records to request per page. Must be a positive integer.\n *\n * @remarks\n * Calling this method opts the store into the paginated `load()` path, where\n * [`ReadParams`](/api/data/interfaces/ReadParams) are forwarded to the proxy. Paginated mode also causes\n * `sort()` and `clearFilter()` to reset to page 1 and re-fetch from the proxy.\n * Fires `'pagechange'`.\n */\n setPageSize(n: number): this {\n this._pageSize = n;\n this._page = 1;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n\n return this;\n }\n\n /**\n * Returns the configured page size, or undefined if pagination is disabled.\n *\n * @returns The page size set via {@link setPageSize}, or undefined.\n */\n getPageSize(): number | undefined {\n return this._pageSize;\n }\n\n /**\n * Returns the current 1-based page number.\n *\n * @returns The current page (defaults to 1 even when pagination is disabled).\n */\n getPage(): number {\n return this._page;\n }\n\n /**\n * Returns the total record count reported by the most recent paginated load.\n *\n * @returns The total count from the proxy, or undefined when no paginated\n * load has occurred or the proxy did not report one.\n */\n getTotalCount(): number | undefined {\n return this._totalCount;\n }\n\n /**\n * Returns the total number of pages, derived from the page size and total count.\n *\n * @returns The number of pages, or undefined when either piece of information\n * is missing.\n */\n getTotalPages(): number | undefined {\n if (this._pageSize == null || this._totalCount == null) {\n return undefined;\n }\n\n return Math.max(1, Math.ceil(this._totalCount / this._pageSize));\n }\n\n /**\n * Advances to the next page and reloads, unless already on the last page.\n *\n * @remarks\n * No-op when pagination is disabled or the current page equals the total\n * page count. When the store has pending unsynced changes, the navigation\n * is blocked and `'pagechangeblocked'` is emitted instead, so the user can\n * sync or reject before leaving the page. Otherwise fires `'pagechange'`\n * and triggers a fire-and-forget reload.\n */\n nextPage(): void {\n if (this._pageSize == null) {\n return;\n }\n\n const total = this.getTotalPages();\n if (total != null && this._page >= total) {\n return;\n }\n\n if (this.hasPendingChanges()) {\n this.emit('pagechangeblocked', { from: this._page, to: this._page + 1 });\n return;\n }\n\n this._page++;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n void this.load();\n }\n\n /**\n * Returns to the previous page and reloads, unless already on page 1.\n *\n * @remarks\n * Blocked when the store has pending changes — emits `'pagechangeblocked'`\n * instead of firing `'pagechange'`.\n */\n prevPage(): void {\n if (this._pageSize == null || this._page <= 1) {\n return;\n }\n\n if (this.hasPendingChanges()) {\n this.emit('pagechangeblocked', { from: this._page, to: this._page - 1 });\n return;\n }\n\n this._page--;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n void this.load();\n }\n\n /**\n * Jumps to the given 1-based page and reloads.\n *\n * @param n - The target page number; clamped to `[1, totalPages]` when total is known.\n *\n * @remarks\n * No-op when pagination is disabled. Blocked when the store has pending\n * changes — emits `'pagechangeblocked'` instead.\n */\n goToPage(n: number): this {\n if (this._pageSize == null) {\n return this;\n }\n\n const total = this.getTotalPages();\n const upper = total ?? n;\n const target = Math.max(1, Math.min(n, upper));\n\n if (target === this._page) {\n return this;\n }\n\n if (this.hasPendingChanges()) {\n this.emit('pagechangeblocked', { from: this._page, to: target });\n return this;\n }\n\n this._page = target;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n void this.load();\n\n return this;\n }\n\n /**\n * Converts raw objects to ModelRecords and rebuilds the filtered/sorted view.\n *\n * @param data - An array of plain objects to convert via the model's `createRecord`.\n *\n * @remarks\n * A reload replaces the store's contents with a fresh authoritative snapshot,\n * so any removal queued against the previous snapshot is discarded — its target\n * may not exist in the new data, and letting it fire on the next `sync()` would\n * destroy a server row the user never asked to delete. The queued records were\n * already ownership-released by `remove()`, so clearing the array suffices.\n */\n private ingestRaw(data: any[]): Promise<void> {\n this.setOwnership(this._allRecords, false);\n this._allRecords = data.map(item => this.model.createRecord(item));\n this.setOwnership(this._allRecords, true);\n this._pendingRemoved = [];\n this._snapshotDirty = true;\n\n return this.applyView();\n }\n\n // ── Access ───────────────────────────────────────────────────────────────\n\n /**\n * Returns a copy of the currently filtered and sorted records.\n *\n * @returns A shallow-copy array of the records in the active view.\n */\n getRecords(): ModelRecord[] {\n return this._records.slice();\n }\n\n /**\n * Returns a copy of all records, bypassing any active filters or sorting.\n *\n * @returns A shallow-copy array of every record in the store.\n */\n getAll(): ModelRecord[] {\n return this._allRecords.slice();\n }\n\n /**\n * Returns the number of records in the current filtered view.\n *\n * @returns The count of visible records after filters are applied.\n *\n * @remarks This is the store's **count aggregate** — the number of rows the\n * view exposes after filtering. There is no separate `count()` method;\n * `getCount()` fills that role, consistent with {@link sum} / {@link average}\n * / {@link min} / {@link max} operating over the same filtered view.\n */\n getCount(): number {\n return this._records.length;\n }\n\n /**\n * Returns the record at the given index in the filtered view, or undefined.\n *\n * @param index - The zero-based position in the filtered and sorted view.\n *\n * @returns The ModelRecord at that position, or undefined if the index is out of range.\n */\n getAt(index: number): ModelRecord | undefined {\n return this._records[index];\n }\n\n /**\n * Finds a record by its primary-key value, searching all records (ignoring filters).\n *\n * @param id - The primary key value to search for.\n *\n * @returns The matching ModelRecord, or undefined if not found or no primary key is defined.\n *\n * @remarks O(1): backed by an internal id→record index that is refreshed on\n * every `applyView()`, so it tracks every mutation of the master list.\n * Returns undefined when the model defines no primary key (the index stays\n * empty).\n */\n getById(id: any): ModelRecord | undefined {\n return this._idIndex.get(id);\n }\n\n /**\n * Returns the position of a record within the filtered view.\n *\n * @param record - The record to locate.\n *\n * @returns The zero-based index in the view, or -1 when the record is not\n * in the current view (filtered out or absent).\n */\n indexOf(record: ModelRecord): number {\n return this._records.indexOf(record);\n }\n\n /**\n * Returns an inclusive slice of the filtered view between two indices.\n *\n * @param start - The first index to include; clamped up to 0.\n * @param end - The last index to include; clamped down to the final index.\n *\n * @returns A shallow-copy array of the records in `[start, end]`; empty when\n * the clamped range is empty.\n */\n getRange(start: number, end: number): ModelRecord[] {\n const lo = Math.max(0, start);\n const hi = Math.min(end, this._records.length - 1);\n\n if (hi < lo) {\n return [];\n }\n\n return this._records.slice(lo, hi + 1);\n }\n\n /**\n * Returns the first record in the filtered view.\n *\n * @returns The record at view index 0, or undefined when the view is empty.\n */\n first(): ModelRecord | undefined {\n return this._records[0];\n }\n\n /**\n * Returns the last record in the filtered view.\n *\n * @returns The record at the final view index, or undefined when the view is empty.\n */\n last(): ModelRecord | undefined {\n return this._records[this._records.length - 1];\n }\n\n /**\n * Invokes a callback for each record in the filtered view, in view order.\n *\n * @param fn - The callback applied to each record and its view index.\n */\n each(fn: (record: ModelRecord, index: number) => void): void {\n this._records.forEach((record, index) => fn(record, index));\n }\n\n /**\n * Returns whether a record is present in the filtered view.\n *\n * @param record - The record to test for membership.\n *\n * @returns True when the record is in the current view.\n */\n contains(record: ModelRecord): boolean {\n return this._records.includes(record);\n }\n\n /**\n * Returns the first record in the filtered view where property equals value.\n *\n * @param property - The field name to match against.\n * @param value - The value to compare using strict equality.\n *\n * @returns The first matching ModelRecord, or undefined if none is found.\n */\n find(property: string, value: any): ModelRecord | undefined {\n return this._records.find(r => r.get(property) === value);\n }\n\n /**\n * Returns all records in the filtered view where property equals value.\n *\n * @param property - The field name to match against.\n * @param value - The value to compare using strict equality.\n *\n * @returns An array of all matching ModelRecords; empty if none match.\n */\n findAll(property: string, value: any): ModelRecord[] {\n return this._records.filter(r => r.get(property) === value);\n }\n\n // ── Mutation ─────────────────────────────────────────────────────────────\n\n /**\n * Adds one or more records (marked as new), updates the view, and fires 'add'/'datachange'.\n *\n * @param data - A single plain object or an array of plain objects to add.\n *\n * @returns An array of the newly created ModelRecord instances.\n */\n add(data: any | any[]): ModelRecord[] {\n return this.insertAt(null, data);\n }\n\n /**\n * Inserts one or more records (marked as new) into the master list at a\n * clamped position, rebuilds the view, and fires 'add'/'datachange'.\n *\n * @param index - The target position in the master list; clamped to\n * `[0, allRecords.length]`.\n * @param data - A single plain object or an array of plain objects to insert.\n *\n * @returns An array of the newly created ModelRecord instances.\n *\n * @remarks\n * Mirrors {@link add} but splices at `index` instead of appending. The\n * insertion position is into the master list; the visible position in the\n * view still depends on any active sort and filter.\n */\n insert(index: number, data: any | any[]): ModelRecord[] {\n return this.insertAt(index, data);\n }\n\n /**\n * Shared body of {@link add} and {@link insert}: creates records marked new,\n * places them in the master list, rebuilds the view, and fires 'add' then\n * 'datachange'.\n *\n * @param index - `null` appends to the master list; a number splices at the\n * position clamped to `[0, allRecords.length]`.\n * @param data - A single plain object or an array of plain objects.\n *\n * @returns An array of the newly created ModelRecord instances.\n */\n private insertAt(index: number | null, data: any | any[]): ModelRecord[] {\n const items = Array.isArray(data) ? data : [data];\n const added = items.map(item => {\n const record = this.model.createRecord(item);\n\n record.markAsNew();\n\n return record;\n });\n\n if (index === null) {\n this._allRecords.push(...added);\n } else {\n const at = Math.max(0, Math.min(index, this._allRecords.length));\n\n this._allRecords.splice(at, 0, ...added);\n }\n\n this.setOwnership(added, true);\n this._snapshotDirty = true;\n this.applyView();\n\n this.emit('add', { records: added });\n this.emit('datachange', {});\n\n return added;\n }\n\n /**\n * Removes a record from the store, queuing it for deletion on the next sync.\n *\n * @param record - The ModelRecord to remove.\n *\n * @remarks\n * New records (never synced) are discarded immediately without being queued.\n * Records that have been persisted are added to `pendingRemoved` and sent to\n * the proxy during the next call to `sync()`.\n */\n remove(record: ModelRecord): this {\n const allIdx = this._allRecords.indexOf(record);\n if (allIdx === -1) {\n return this;\n }\n\n this._allRecords.splice(allIdx, 1);\n this.setOwnership([record], false);\n this._snapshotDirty = true;\n\n if (!record.isNew()) {\n this._pendingRemoved.push(record);\n }\n\n this.applyView();\n\n this.emit('remove', { record });\n this.emit('datachange', {});\n\n return this;\n }\n\n /**\n * Removes all records, queuing existing (non-new) ones for deletion on the next sync.\n *\n * @remarks\n * Only persisted records are queued for removal; records that are still marked\n * as new are simply discarded.\n */\n removeAll(): this {\n const removed = this._allRecords.slice();\n\n this._pendingRemoved.push(...this._allRecords.filter(r => !r.isNew()));\n\n this.setOwnership(removed, false);\n this._allRecords = [];\n this._snapshotDirty = true;\n this.applyView();\n\n this.emit('clear', { removed });\n this.emit('datachange', {});\n\n return this;\n }\n\n /**\n * Appends already-committed records to the master list and rebuilds the\n * view, without marking them new or firing `'add'`.\n *\n * @param records - The records to append; they keep their committed state.\n *\n * @remarks\n * The lazy-load seam for {@link TreeStore}: children fetched on a node\n * expand are server-backed (not pending inserts), so they bypass the\n * new-record path of {@link add}. This is the only sanctioned way for a\n * subclass to grow the master list with committed records.\n */\n protected appendRecords(records: ModelRecord[]): void {\n this._allRecords.push(...records);\n this.setOwnership(records, true);\n this._snapshotDirty = true;\n this.applyView();\n }\n\n /**\n * Signals that a record's fields were mutated outside the store's own\n * mutation methods (e.g. an in-cell edit in a Table).\n *\n * @param record - The record that was edited; carried on the `'update'`\n * event so listeners can identify it.\n * @param changes - Optional. The field-level diff of the change, carried on\n * the `'update'` event for listeners that want per-field granularity.\n *\n * @remarks\n * Fires `'update'` ({@link StoreUpdateEvent}) followed by `'datachange'` so\n * listeners (toolbars, pagination bars, etc.) re-evaluate state such as\n * {@link hasPendingChanges}. A store-owned record calls this automatically\n * from `set()`; it remains public for the standalone/manual case (an unowned\n * record, or forcing a refresh).\n */\n notifyRecordChanged(record: ModelRecord, changes?: Record<string, FieldChange>): void {\n this.emit('update', { record, changes });\n this.emit('datachange', {});\n }\n\n /**\n * Opens a store-level edit batch: owned records suppress their own\n * auto-notify until the matching {@link commitEdit}.\n *\n * @returns This store, for method chaining.\n *\n * @remarks\n * The coarse counterpart to a record edit batch — use it to mutate many\n * records and refresh bound views once. Records reach this flag through their\n * back-ref. Not nested by any framework caller, so the flag is a plain\n * boolean rather than a depth counter.\n */\n beginEdit(): this {\n this._batching = true;\n\n return this;\n }\n\n /**\n * Closes a store-level edit batch and fires a single `'datachange'` so bound\n * views refresh once for the whole batch.\n *\n * @returns This store, for method chaining.\n *\n * @remarks\n * Deliberately emits only `'datachange'`, not per-record `'update'`s:\n * replaying every record's update would defeat the coalescing the batch\n * exists to provide. Use a record edit batch when per-record granularity is\n * needed.\n */\n commitEdit(): this {\n this._batching = false;\n\n this.emit('datachange', {});\n\n return this;\n }\n\n /**\n * Reports whether a store-level edit batch is currently open.\n *\n * @returns True while a {@link beginEdit} batch is open.\n *\n * @internal Framework wiring; consulted by an owned record's `set()` through\n * its back-ref to decide whether to suppress its auto-notify.\n */\n isBatching(): boolean {\n return this._batching;\n }\n\n /**\n * Stamps or clears the owning-store back-ref on a set of records as they\n * enter or leave this store's master list.\n *\n * @param records - The records joining or leaving the master list.\n * @param owned - True to adopt the records into this store, false to release.\n *\n * @remarks\n * The single seam that keeps {@link ModelRecord}'s auto-notify back-ref in\n * step with `_allRecords` membership. Called at every site that grows or\n * shrinks the master list.\n */\n private setOwnership(records: ModelRecord[], owned: boolean): void {\n for (const record of records) {\n if (owned) {\n record.adoptedBy(this);\n } else {\n record.released();\n }\n }\n }\n\n /**\n * Returns whether the store holds any unsynced state.\n *\n * @returns True if any record is dirty, any record is new, or there are\n * queued removals waiting to be synced.\n *\n * @remarks\n * Used by the pagination guard to prevent navigation that would silently\n * discard in-memory edits. Also useful for \"unsaved changes\" prompts.\n */\n hasPendingChanges(): boolean {\n if (this._pendingRemoved.length > 0) {\n return true;\n }\n\n for (const record of this._allRecords) {\n if (record.isNew() || record.isDirty()) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Discards all unsynced changes — reverts dirty records, drops new ones,\n * and restores pending removals — then fires 'datachange'.\n *\n * @remarks\n * Pending removals are pushed back into `allRecords` (in their original\n * order is not preserved; they are appended) so the user can recover from\n * an accidental removal. Dirty records are restored to their last\n * committed values. New records are dropped outright since they were never\n * persisted.\n */\n reject(): void {\n const previouslyOwned = this._allRecords;\n const survivors: ModelRecord[] = [];\n\n for (const record of this._allRecords) {\n if (record.isNew()) {\n continue;\n }\n\n if (record.isDirty()) {\n record.reject();\n }\n\n survivors.push(record);\n }\n\n if (this._pendingRemoved.length > 0) {\n survivors.push(...this._pendingRemoved);\n this._pendingRemoved = [];\n }\n\n this._allRecords = survivors;\n\n // Release everyone who was owned (survivors + dropped new records), then\n // re-adopt the final set: dropped new records stay released, restored\n // pending-removals (released by remove()) are re-adopted.\n this.setOwnership(previouslyOwned, false);\n this.setOwnership(survivors, true);\n\n this._snapshotDirty = true;\n this.applyView();\n\n this.emit('datachange', {});\n }\n\n /**\n * Persists new, dirty, and removed records via the proxy, then fires 'sync'/'datachange'.\n *\n * @returns A promise that resolves when the sync run has settled.\n *\n * @remarks\n * Sync is a no-op when no proxy is configured. Operations run in order:\n * creates first, then updates, then deletes — and use the proxy's optional\n * batch hooks ({@link Proxy.createBatch}/{@link Proxy.updateBatch}/{@link Proxy.destroyBatch})\n * when present, falling back to one request per record otherwise. Each record\n * is committed only after its own op succeeds, so a record never appears\n * committed unless the server accepted it.\n *\n * **Contract change:** `sync()` no longer rejects on a transport failure. A\n * failed op emits an `'exception'` event ({@link StoreExceptionEvent}) and is\n * recorded; `sync()` always resolves and fires `'sync'` with a\n * {@link StoreSyncEvent} listing the failures. The `syncErrorPolicy` option\n * controls what happens after the first failure: `'stop'` (default) aborts\n * the remaining sync (already-committed records stay committed; failed and\n * untouched records remain pending for the next `sync()`), `'continue'`\n * proceeds through every record/batch. Callers that previously relied on a\n * rejected promise should switch to the `'exception'` event or the `'sync'`\n * payload's `failures`.\n */\n async sync(): Promise<void> {\n const proxy = this.proxy;\n\n if (!proxy) {\n return;\n }\n\n this.emit('beforesync', {});\n\n const failures: StoreExceptionEvent[] = [];\n\n // Each phase returns true when the run should stop early ('stop' policy\n // after a failure); the || chain then short-circuits the later phases.\n // Under 'continue' every phase returns false and all phases run. The\n // hasMany cascade runs after creates/updates resolve (so parents carry\n // their server ids) and before deletes (children sync before the parent\n // is removed); a stopped run skips it like any later phase.\n const stopped = await this.syncCreates(proxy, failures)\n || await this.syncUpdates(proxy, failures);\n\n if (!stopped) {\n await this.syncCascade();\n\n await this.syncDeletes(proxy, failures);\n }\n\n this.emit('sync', { failures });\n this.emit('datachange', {});\n }\n\n /**\n * Cascades persistence into each parent record's materialised hasMany child\n * stores: stamps the parent foreign key onto every child, then runs the\n * child store's own `sync()`.\n *\n * @returns A promise that resolves when every cascaded child sync has settled.\n *\n * @remarks\n * No-op when `cascadeSync` is disabled. Only associations whose child store\n * was actually built (via {@link ModelRecord.getAssociated}) are walked, so a\n * loaded-but-untouched parent costs nothing. Reusing the child store's own\n * `sync()` means child `'exception'` events, `syncErrorPolicy`, and batch\n * behaviour all apply to the cascade unchanged; failures surface on the child\n * store's own event surface, not the parent's `'sync'` payload.\n */\n private async syncCascade(): Promise<void> {\n if (!this._cascadeSync) {\n return;\n }\n\n for (const parent of this._allRecords) {\n for (const association of parent.getModel().getAssociations()) {\n if (association.kind !== 'hasMany' || !parent.hasChildStore(association.getAccessor())) {\n continue;\n }\n\n const child = parent.getAssociated(association.getAccessor());\n\n this.stampForeignKeys(child, association.getForeignKey(), parent);\n\n await child.sync();\n }\n }\n }\n\n /**\n * Stamps the parent's id onto the foreign-key field of every record in a\n * hasMany child store, so a child created against a brand-new parent picks up\n * the real server id before it is itself persisted.\n *\n * @param child - The parent-scoped child store.\n * @param foreignKey - The child field that holds the owner's id.\n * @param parent - The owning record, whose `getId()` supplies the FK value.\n *\n * @remarks\n * `set()` is a no-op when the value already matches, so a child added to an\n * already-persisted parent (already carrying the correct FK) is untouched.\n * When the parent still has no id (its create failed or was skipped), the\n * stamp writes `undefined` and the child is not falsely re-keyed.\n */\n private stampForeignKeys(child: AbstractStore, foreignKey: string, parent: ModelRecord): void {\n const parentId = parent.getId();\n\n if (parentId === undefined) {\n return;\n }\n\n for (const record of child.getAll()) {\n record.setSilent(foreignKey, parentId);\n }\n }\n\n /**\n * Persists the new records, by batch when the proxy advertises\n * {@link Proxy.createBatch}, else one {@link Proxy.create} per record.\n *\n * @param proxy - The configured proxy.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when the run should stop early (policy `'stop'` and this\n * phase recorded a failure).\n */\n private async syncCreates(proxy: Proxy, failures: StoreExceptionEvent[]): Promise<boolean> {\n const created = this._allRecords.filter(r => r.isNew());\n\n if (created.length === 0) {\n return false;\n }\n\n if (proxy.createBatch) {\n return this.runBatch('create', created, failures, records => proxy.createBatch!(records));\n }\n\n return this.runPerRecord('create', created, failures, record => proxy.create(record));\n }\n\n /**\n * Persists the dirty (non-new) records, by batch when the proxy advertises\n * {@link Proxy.updateBatch}, else one {@link Proxy.update} per record.\n *\n * @param proxy - The configured proxy.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when the run should stop early.\n */\n private async syncUpdates(proxy: Proxy, failures: StoreExceptionEvent[]): Promise<boolean> {\n const dirty = this._allRecords.filter(r => r.isDirty() && !r.isNew());\n\n if (dirty.length === 0) {\n return false;\n }\n\n if (proxy.updateBatch) {\n return this.runBatch('update', dirty, failures, records => proxy.updateBatch!(records));\n }\n\n return this.runPerRecord('update', dirty, failures, record => proxy.update(record));\n }\n\n /**\n * Destroys the pending-removed records, by batch when the proxy advertises\n * {@link Proxy.destroyBatch}, else one {@link Proxy.destroy} per record.\n * Only successfully-destroyed records are cleared from the pending queue.\n *\n * @param proxy - The configured proxy.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when the run should stop early.\n */\n private async syncDeletes(proxy: Proxy, failures: StoreExceptionEvent[]): Promise<boolean> {\n // Snapshot the queue so a failure's 'exception' payload holds an immutable\n // copy (matching the create/update phases, whose filter() returns a fresh\n // array) rather than aliasing the live _pendingRemoved array.\n const removed = this._pendingRemoved.slice();\n\n if (removed.length === 0) {\n return false;\n }\n\n if (proxy.destroyBatch) {\n try {\n await proxy.destroyBatch(removed);\n this._pendingRemoved = [];\n\n return false;\n } catch (err) {\n return this.recordFailure('destroy', removed, err, failures);\n }\n }\n\n const survivors: ModelRecord[] = [];\n let stopped = false;\n\n for (const record of removed) {\n if (stopped) {\n survivors.push(record);\n continue;\n }\n\n try {\n await proxy.destroy(record);\n } catch (err) {\n survivors.push(record);\n stopped = this.recordFailure('destroy', [record], err, failures);\n }\n }\n\n this._pendingRemoved = survivors;\n\n return stopped;\n }\n\n /**\n * Runs a create/update batch op, committing every record positionally from\n * the server response, or recording one failure for the whole batch.\n *\n * @param operation - The op kind, for the failure payload.\n * @param records - The batch's records, in request order.\n * @param failures - The accumulator that collects this run's op failures.\n * @param call - Issues the batch request and resolves to per-record server data in input order.\n *\n * @returns True when the run should stop early.\n */\n private async runBatch(operation: 'create' | 'update', records: ModelRecord[], failures: StoreExceptionEvent[], call: (records: ModelRecord[]) => Promise<Record<string, any>[]>): Promise<boolean> {\n try {\n const serverData = await call(records);\n\n records.forEach((record, i) => {\n this.commitFromServerData(record, serverData[i] ?? {});\n });\n\n return false;\n } catch (err) {\n return this.recordFailure(operation, records, err, failures);\n }\n }\n\n /**\n * Runs a create/update op one record at a time, committing each on success\n * and recording a per-record failure otherwise.\n *\n * @param operation - The op kind, for the failure payload.\n * @param records - The records to persist, in order.\n * @param failures - The accumulator that collects this run's op failures.\n * @param call - Issues a single-record request and resolves to that record's server data.\n *\n * @returns True when the run should stop early.\n */\n private async runPerRecord(operation: 'create' | 'update', records: ModelRecord[], failures: StoreExceptionEvent[], call: (record: ModelRecord) => Promise<Record<string, any>>): Promise<boolean> {\n for (const record of records) {\n try {\n const serverData = await call(record);\n\n this.commitFromServerData(record, serverData);\n } catch (err) {\n if (this.recordFailure(operation, [record], err, failures)) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Applies a server response onto a record and commits it, so the record\n * drops out of subsequent sync cycles.\n *\n * @param record - The record that was persisted.\n * @param serverData - The server's representation of the record.\n */\n private commitFromServerData(record: ModelRecord, serverData: Record<string, any>): void {\n for (const [k, v] of Object.entries(serverData)) {\n record.setSilent(k, v);\n }\n\n record.commit();\n }\n\n /**\n * Records an op failure: pushes a {@link StoreExceptionEvent} onto the run's\n * accumulator, emits `'exception'`, and reports whether the run should stop.\n *\n * @param operation - The op kind that failed.\n * @param records - The offending record(s).\n * @param error - The raw thrown value.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when `syncErrorPolicy` is `'stop'` (so the caller halts).\n */\n private recordFailure(operation: StoreOperation, records: ModelRecord[], error: unknown, failures: StoreExceptionEvent[]): boolean {\n const failure: StoreExceptionEvent = { operation, records, error };\n\n failures.push(failure);\n this.emit('exception', failure);\n\n return this._syncErrorPolicy === 'stop';\n }\n\n // ── Sort ─────────────────────────────────────────────────────────────────\n\n /**\n * Sorts the view by a single property in the given direction.\n *\n * @param field - The field name to sort by.\n * @param dir - Optional. The sort direction; defaults to 'asc'.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n *\n * @remarks\n * When `remoteSort` is enabled, or when server-side pagination is enabled\n * (the legacy trigger), this method also resets the current page to 1 and\n * triggers a fire-and-forget reload. With `remoteSort` on, the active\n * sorters are serialized into [`ReadParams`](/api/data/interfaces/ReadParams) so the proxy receives the new\n * ordering; without it, a paginated reload still fires but sends only\n * `{page, pageSize}`. Fires `'sortchange'` and `'datachange'`.\n */\n sort(field: string, dir?: 'asc' | 'desc'): Promise<void>;\n /**\n * Applies a multi-column sort. Pass an empty array to clear all sorters.\n *\n * @param descriptors - The ordered list of sort descriptors. Earlier\n * descriptors take priority.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n *\n * @remarks\n * Mirrors the single-column overload's reload side effects when `remoteSort`\n * or server-side pagination is enabled.\n */\n sort(descriptors: SortDescriptor[]): Promise<void>;\n sort(fieldOrDescriptors: string | SortDescriptor[], dir: 'asc' | 'desc' = 'asc'): Promise<void> {\n if (typeof fieldOrDescriptors === 'string') {\n this._activeSorters = [{ field: fieldOrDescriptors, dir }];\n } else {\n this._activeSorters = fieldOrDescriptors.slice();\n }\n\n const reload = this._remoteSort || this._pageSize != null;\n\n if (reload) {\n this._page = 1;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n }\n\n return this.applyView().then(() => {\n this.emit('sortchange', { sorters: this.getActiveSorters() });\n this.emit('datachange', {});\n\n if (reload) {\n void this.load();\n }\n });\n }\n\n /**\n * Returns a copy of all active sort descriptors in priority order.\n *\n * @returns A shallow-copy array of the active sort descriptors; empty when no sort is active.\n */\n getActiveSorters(): SortDescriptor[] {\n return this._activeSorters.map(s => ({ ...s }));\n }\n\n /**\n * Returns a copy of all active filter descriptors.\n *\n * @returns A shallow-copy array of the active filter descriptors; empty when no filter is active.\n */\n getActiveFilters(): FilterDescriptor[] {\n return this._activeFilters.map(f => ({ ...f }));\n }\n\n /**\n * Returns a copy of the primary active sorter config, or null if no sort is active.\n *\n * @returns The first active sorter mapped to the legacy `{ property, direction }` shape, or null.\n *\n * @deprecated Use {@link getActiveSorters} instead.\n */\n getActiveSorter(): { property: string; direction: 'asc' | 'desc' } | null {\n const first = this._activeSorters[0];\n\n return first ? { property: first.field, direction: first.dir } : null;\n }\n\n /**\n * Removes any active sort and restores insertion order, firing 'sortchange' and 'datachange'.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n */\n clearSort(): Promise<void> {\n this._activeSorters = [];\n\n return this.applyView().then(() => {\n this.emit('sortchange', { sorters: [] });\n this.emit('datachange', {});\n });\n }\n\n // ── Filter ───────────────────────────────────────────────────────────────\n\n /**\n * Adds an equality filter on a property and fires 'datachange'.\n *\n * @param property - The field name to filter on.\n * @param value - The value a record's field must equal to pass the filter.\n *\n * @remarks\n * When `remoteFilter` is enabled, or when server-side pagination is enabled\n * (the legacy trigger), this also resets to page 1 and reloads. With\n * `remoteFilter` on, the active filters are serialized into [`ReadParams`](/api/data/interfaces/ReadParams) so\n * the proxy filters the result set.\n */\n filter(property: string, value: any): Promise<void> {\n this._activeFilters.push({ type: 'eq', field: property, value: value });\n\n return this.applyFilterChange();\n }\n\n /**\n * Adds a filter described by a serializable {@link FilterDescriptor}. Descriptors\n * cross the worker boundary cleanly (unlike arbitrary predicate functions), so\n * the same call works for in-process and worker-offloaded evaluation.\n *\n * @param descriptor - The filter descriptor to apply.\n *\n * @remarks\n * Mirrors {@link filter}'s reload side effects when `remoteFilter` or\n * server-side pagination is enabled.\n */\n filterBy(descriptor: FilterDescriptor): Promise<void> {\n this._activeFilters.push(descriptor);\n\n return this.applyFilterChange();\n }\n\n /**\n * Rebuilds the view after a filter mutation, fires `'filterchange'` (with the\n * active filters) plus `'datachange'`, and, when `remoteFilter` or\n * pagination is enabled, resets to page 1 and triggers a reload.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n */\n private applyFilterChange(): Promise<void> {\n const reload = this._remoteFilter || this._pageSize != null;\n\n if (reload) {\n this._page = 1;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n }\n\n return this.applyView().then(() => {\n this.emit('filterchange', { filters: this.getActiveFilters() });\n this.emit('datachange', {});\n\n if (reload) {\n void this.load();\n }\n });\n }\n\n /**\n * Removes all active filters and fires 'datachange'.\n *\n * @remarks\n * When `remoteFilter` is enabled, or when server-side pagination is enabled\n * (the legacy trigger), this method also resets the current page to 1 and\n * triggers a fire-and-forget reload so the proxy is queried without filter\n * context.\n */\n clearFilter(): Promise<void> {\n this._activeFilters = [];\n\n return this.applyFilterChange();\n }\n\n // ── Aggregation ────────────────────────────────────────────────────────────\n\n /**\n * Collects the numeric, non-null values of a field across the filtered view.\n *\n * @param field - The field to read from each visible record.\n *\n * @returns The coerced numbers, skipping null/undefined and any value that\n * does not coerce to a finite number.\n *\n * @remarks Shared by {@link sum} / {@link average} / {@link min} / {@link max};\n * `null`/`undefined` are skipped (never coerced to `0`) so an absent value\n * never distorts the result.\n */\n private numericValues(field: string): number[] {\n const values: number[] = [];\n\n for (const record of this._records) {\n const raw = record.get(field);\n\n if (raw == null) {\n continue;\n }\n\n const value = Number(raw);\n\n if (!Number.isNaN(value)) {\n values.push(value);\n }\n }\n\n return values;\n }\n\n /**\n * Sums a numeric field across the filtered view.\n *\n * @param field - The field to total.\n *\n * @returns The sum of the field's numeric values; `0` over an empty or\n * all-null view.\n */\n sum(field: string): number {\n return this.numericValues(field).reduce((total, value) => total + value, 0);\n }\n\n /**\n * Averages a numeric field across the filtered view.\n *\n * @param field - The field to average.\n *\n * @returns The mean of the field's numeric values; `0` over an empty or\n * all-null view.\n */\n average(field: string): number {\n const values = this.numericValues(field);\n\n if (values.length === 0) {\n return 0;\n }\n\n return values.reduce((total, value) => total + value, 0) / values.length;\n }\n\n /**\n * Returns the smallest value of a numeric field across the filtered view.\n *\n * @param field - The field to minimise.\n *\n * @returns The minimum numeric value, or undefined over an empty or\n * all-null view.\n */\n min(field: string): number | undefined {\n const values = this.numericValues(field);\n\n if (values.length === 0) {\n return undefined;\n }\n\n return values.reduce((lowest, value) => value < lowest ? value : lowest);\n }\n\n /**\n * Returns the largest value of a numeric field across the filtered view.\n *\n * @param field - The field to maximise.\n *\n * @returns The maximum numeric value, or undefined over an empty or\n * all-null view.\n */\n max(field: string): number | undefined {\n const values = this.numericValues(field);\n\n if (values.length === 0) {\n return undefined;\n }\n\n return values.reduce((highest, value) => value > highest ? value : highest);\n }\n\n /**\n * Collects the distinct values of a field across the filtered view, in\n * first-encounter (view) order.\n *\n * @param field - The field to collect distinct values from.\n *\n * @returns An array of unique values (by strict `===` identity), preserving\n * the order in which they first appear in the view.\n *\n * @remarks The type-agnostic companion to the numeric aggregates: useful for\n * building a distinct-value filter list. Values are de-duplicated by strict\n * equality, so distinct object references are treated as distinct values.\n */\n collect(field: string): any[] {\n const seen = new Set<any>();\n const result: any[] = [];\n\n for (const record of this._records) {\n const value = record.get(field);\n\n if (!seen.has(value)) {\n seen.add(value);\n result.push(value);\n }\n }\n\n return result;\n }\n\n // ── Grouping ───────────────────────────────────────────────────────────────\n\n /**\n * Sets the single-level group field, firing 'groupchange' only on a real change.\n *\n * @param field - The field to group by, or null to disable grouping.\n *\n * @returns This store, for method chaining.\n *\n * @remarks\n * Grouping is a pure read over the existing view ({@link getGroups}), so\n * changing the group field does **not** rebuild the view or fire\n * `'datachange'`; it fires only `'groupchange'` ({@link StoreGroupChangeEvent}).\n */\n setGroupField(field: string | null): this {\n if (this._groupField === field) {\n return this;\n }\n\n this._groupField = field;\n this.emit('groupchange', { groupField: field });\n\n return this;\n }\n\n /**\n * Returns the active group field, or null when grouping is disabled.\n *\n * @returns The field set via {@link setGroupField}, or null.\n */\n getGroupField(): string | null {\n return this._groupField;\n }\n\n /**\n * Returns the group-bucket key for a record under the active group field.\n *\n * @param record - The record to derive a group key for.\n *\n * @returns `String(record.get(groupField))`, or `''` when no group field is\n * set or the record's value is null/undefined.\n */\n getGroupString(record: ModelRecord): string {\n if (this._groupField == null) {\n return '';\n }\n\n const value = record.get(this._groupField);\n\n return value == null ? '' : String(value);\n }\n\n /**\n * Buckets the filtered view by the active group field.\n *\n * @returns A `Map` from group key ({@link getGroupString}) to the records in\n * that group. Groups appear in first-encounter order, and records within a\n * group keep view order. When no group field is set, every record falls\n * under the single `''` key.\n */\n getGroups(): Map<string, ModelRecord[]> {\n const groups = new Map<string, ModelRecord[]>();\n\n for (const record of this._records) {\n const key = this.getGroupString(record);\n const bucket = groups.get(key);\n\n if (bucket) {\n bucket.push(record);\n } else {\n groups.set(key, [record]);\n }\n }\n\n return groups;\n }\n\n // ── Events ───────────────────────────────────────────────────────────────\n\n /**\n * Subscribes a listener to a store event. Listeners are invoked in\n * registration order when the matching event is emitted.\n *\n * @param event - The name of the store event to listen for.\n * @param listener - The callback function to invoke when the event fires.\n *\n * @returns This store, for method chaining.\n */\n on(event: StoreEvent, listener: StoreListener): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered store event listener. No-op if the\n * listener was never registered for the given event.\n *\n * @param event - The name of the store event the listener was registered for.\n * @param listener - The exact callback reference to remove.\n *\n * @returns This store, for method chaining.\n */\n off(event: StoreEvent, listener: StoreListener): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Notifies all listeners registered for an event, in registration order.\n *\n * @param event - The name of the event to emit.\n * @param payload - The data object passed to each listener.\n */\n protected emit(event: StoreEvent, payload: any): void {\n this._listeners.fire(event, payload);\n }\n\n // ── Internal ─────────────────────────────────────────────────────────────\n\n /**\n * Rebuilds the visible records slice by applying all active filters and the active sorter.\n *\n * @remarks\n * Null values sort to the end regardless of sort direction. All active filter\n * predicates must pass for a record to be included in the view.\n */\n /**\n * Recomputes the filtered/sorted view from `allRecords`. Returns a Promise so a\n * future worker-offload path can resolve after the worker round-trip completes;\n * the current implementation runs synchronously and resolves immediately.\n */\n protected applyView(): Promise<void> {\n this.rebuildIdIndex();\n\n if (this._allRecords.length >= WORKER_THRESHOLD && StoreWorkerClient.isAvailable() && !this.hasCustomSorter()) {\n this._viewAsync = true;\n\n return this.applyViewOnWorker();\n }\n\n this._viewAsync = false;\n\n let view = this._allRecords.slice();\n\n for (const descriptor of this._activeFilters) {\n view = view.filter(r => matchesFilter(r, descriptor));\n }\n\n if (this._activeSorters.length > 0) {\n view.sort((a, b) => {\n for (const sorter of this._activeSorters) {\n const cmp = this.compareBySorter(a, b, sorter);\n\n if (cmp !== 0) {\n return cmp;\n }\n }\n\n return 0;\n });\n }\n\n this._records = view;\n\n return Promise.resolve();\n }\n\n /**\n * Compares two records under one sorter, applying its direction. A sorter\n * with a `sorterFn` delegates to it; otherwise the shared, type-aware\n * {@link compareValues} runs against the field values.\n *\n * @param a - The left record.\n * @param b - The right record.\n * @param sorter - The sorter whose `field`/`dir`/`sorterFn` drive the compare.\n *\n * @returns The final ordering: negative if `a` precedes `b`, positive if it\n * follows, `0` if equal under this sorter.\n *\n * @remarks\n * Nulls sort last regardless of direction — when either field value is\n * null/undefined, the (un-negated) {@link compareValues} result is returned\n * so direction applies only to a non-null comparison, matching the worker's\n * `sortIndices`.\n */\n private compareBySorter(a: ModelRecord, b: ModelRecord, sorter: SortDescriptor): number {\n if (sorter.sorterFn) {\n const cmp = sorter.sorterFn(a, b);\n\n return sorter.dir === 'asc' ? cmp : -cmp;\n }\n\n const av = a.get(sorter.field);\n const bv = b.get(sorter.field);\n const cmp = compareValues(av, bv, this.model.getField(sorter.field)?.getType());\n\n if (av == null || bv == null) {\n return cmp;\n }\n\n return sorter.dir === 'asc' ? cmp : -cmp;\n }\n\n /**\n * Reports whether any active sorter carries a custom `sorterFn`.\n *\n * @returns True when at least one sorter has a `sorterFn`, which forces the\n * in-process sort path (a function cannot cross the worker boundary).\n */\n private hasCustomSorter(): boolean {\n return this._activeSorters.some(sorter => sorter.sorterFn !== undefined);\n }\n\n /**\n * Rebuilds the id→record index from the master list so {@link getById} is\n * O(1). The index stays empty when the model defines no primary key.\n */\n private rebuildIdIndex(): void {\n this._idIndex.clear();\n\n if (!this.model.getPrimaryKeyField()) {\n return;\n }\n\n for (const record of this._allRecords) {\n this._idIndex.set(record.getId(), record);\n }\n }\n\n /**\n * Worker-offloaded view rebuild for stores above WORKER_THRESHOLD. Ships a fresh\n * snapshot when allRecords has changed since the last dispatch, then asks the\n * worker for sorted/filtered indices into the snapshot, and maps those indices\n * back to the local ModelRecord array. The worker returns indices (not records)\n * because ModelRecord instances can't survive structured clone.\n *\n * @remarks\n * The worker protocol currently accepts only a single sorter, so on the\n * worker path multi-sort degrades to the primary (first) sorter. Datasets\n * below {@link WORKER_THRESHOLD} run in-process and apply the full\n * multi-key comparator.\n */\n private applyViewOnWorker(): Promise<void> {\n const snapshot = this._snapshotDirty\n ? StoreWorkerClient.snapshot(this._storeId, this._allRecords.map(r => r.getData()))\n : Promise.resolve();\n\n if (this._snapshotDirty) {\n this._snapshotDirty = false;\n }\n\n const allRecordsRef = this._allRecords;\n const primary = this._activeSorters[0];\n\n return snapshot\n .then(() => StoreWorkerClient.sortFilter(\n this._storeId,\n primary\n ? { field: primary.field, direction: primary.dir, fieldType: this.model.getField(primary.field)?.getType() }\n : undefined,\n this._activeFilters.length > 0\n ? (this._activeFilters.length === 1\n ? this._activeFilters[0]\n : { type: 'and', filters: this._activeFilters })\n : undefined,\n ))\n .then(indices => {\n // Guard against allRecords having been replaced while the worker ran.\n // If so, the indices reference stale data; trigger a fresh applyView.\n if (allRecordsRef !== this._allRecords) {\n return this.applyView();\n }\n\n this._records = indices.map(i => this._allRecords[i]);\n return undefined;\n });\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { Model } from '~/data/Model.js';\nimport { AbstractModel } from '~/data/AbstractModel.js';\nimport { Proxy } from '~/data/proxy/Proxy.js';\nimport { AbstractStore, AbstractStoreOptions } from '~/data/AbstractStore.js';\n\n/**\n * Construction-time options for {@link Store}. May be passed as the first\n * argument in place of the positional `(model, proxy)` form, in which case\n * `model` and optional `proxy` come from the bag.\n *\n * @category Data\n */\nexport interface StoreOptions extends AbstractStoreOptions {\n model: Model;\n proxy?: Proxy;\n}\n\n/**\n * A general-purpose concrete store that pairs a Model with an optional Proxy.\n * Use this class when you do not need a dedicated store subclass.\n *\n * @category Data\n */\nexport class Store extends AbstractStore {\n\n readonly model: Model;\n readonly proxy: Proxy | undefined;\n\n /**\n * Constructs a Store with the given model and an optional proxy.\n *\n * @param modelOrOptions - The {@link Model} that defines the record schema, or a {@link StoreOptions} bag.\n * @param proxy - Optional. The Proxy used to load and persist records. Ignored when the first argument is a {@link StoreOptions} bag.\n */\n constructor(modelOrOptions: Model | StoreOptions, proxy?: Proxy) {\n super();\n\n if (modelOrOptions instanceof AbstractModel) {\n this.model = modelOrOptions;\n this.proxy = proxy;\n } else {\n this.model = modelOrOptions.model;\n this.proxy = modelOrOptions.proxy;\n\n this.applyOptions(modelOrOptions);\n }\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { AbstractModel } from '~/data/AbstractModel.js';\nimport { AbstractStore } from '~/data/AbstractStore.js';\nimport type { Association } from '~/data/Association.js';\nimport { Field } from '~/data/Field.js';\nimport { Store } from '~/data/Store.js';\nimport type { Model } from '~/data/Model.js';\nimport { applyRule } from '~/validation/Validator.js';\n\n// Monotonic per-session id source for client-side row keys; never leaves the client,\n// so a plain counter suffices (no UUID / collision concern across sessions).\nlet nextInternalId = 1;\n\n// Recursion cap for deep value equality. Model field values are JSON-shaped and\n// acyclic in practice; this bound stops a pathologically deep or accidentally\n// cyclic structure from overflowing the stack. Beyond it we fall back to `===`\n// for that sub-comparison rather than tracking a visited-set on every set().\nconst MAX_EQUALITY_DEPTH = 100;\n\n/**\n * A single field's before / after values, as returned by\n * {@link ModelRecord.getChanges} and {@link ModelRecord.getModified}.\n *\n * @category Data\n */\nexport interface FieldChange {\n old: any;\n new: any;\n}\n\n/**\n * A single data record managed by a store.\n *\n * Tracks current field values, dirty state, and new / committed status.\n *\n * @remarks\n * On construction the data snapshot is also stored as `original` so that\n * `reject()` can restore the record to its last committed state without\n * requiring a round-trip to the server.\n *\n * @example\n * ```typescript\n * const record = store.getAt(0);\n * record?.set('age', 31);\n * console.log(record?.isDirty()); // true\n * record?.commit(); // clears dirty flag\n * // record?.reject(); // reverts to last committed snapshot\n * ```\n *\n * @category Data\n */\nexport class ModelRecord {\n\n private _model: AbstractModel;\n private _data: Record<string, any>;\n private _original: Record<string, any>;\n private _dirty: boolean = false;\n private _isNew: boolean = false;\n private _internalId: number;\n\n // Embedded child rows captured from the parent payload at createRecord time,\n // keyed by association accessor. Kept out of `_data` so getData() / the proxy\n // writers never see them; consumed once when the child store is first built.\n private _associatedSeed: Record<string, any[]>;\n\n // Lazily-built per-accessor child stores, so repeated getAssociated() calls\n // return the same instance (stable identity for listeners and the collection\n // id-index). Allocated on first access.\n private _childStores: Map<string, AbstractStore> | undefined;\n\n // Back-ref to the owning store, stamped by adoptedBy() when the record enters\n // a store's record set and cleared by released() when it leaves. Null for an\n // un-adopted record (mid-construction, a freestanding new ModelRecord, a\n // clone), which is what keeps set() silent until the store takes ownership.\n private _store: AbstractStore | null = null;\n\n // Record-level edit-batch state, depth-counted so nested batches (e.g.\n // setMany inside a consumer's beginEdit) collapse to a single snapshot and a\n // single notify: the snapshot is taken at the 0->1 transition and the diff +\n // notify happen at the 1->0 transition.\n private _editDepth: number = 0;\n private _editSnapshot: Record<string, any> | null = null;\n\n /**\n * Constructs a ModelRecord with the given model schema and initial data.\n *\n * @param model - The AbstractModel that describes this record's field schema.\n * @param data - The initial field values keyed by field name.\n * @param associatedSeed - Optional. Embedded child rows from the parent\n * payload, keyed by association accessor, used to seed child stores on\n * first access. Never enters `_data` and is never serialised.\n */\n constructor(model: AbstractModel, data: Record<string, any>, associatedSeed: Record<string, any[]> = {}) {\n this._model = model;\n this._data = { ...data };\n this._original = { ...data };\n this._internalId = nextInternalId++;\n this._associatedSeed = associatedSeed;\n }\n\n /**\n * Records the store that now owns this record, so a subsequent `set()` can\n * notify it.\n *\n * @param store - The store taking ownership of this record.\n *\n * @internal Framework wiring; called by AbstractStore when it adopts this\n * record into its record set. Not part of the consumer API.\n */\n adoptedBy(store: AbstractStore): void {\n this._store = store;\n }\n\n /**\n * Clears the owning-store back-ref, so a `set()` on the now-detached record\n * notifies nobody.\n *\n * @internal Framework wiring; called by AbstractStore when this record leaves\n * its record set. Not part of the consumer API.\n */\n released(): void {\n this._store = null;\n }\n\n /**\n * Returns the value of a field by name.\n *\n * @param field - The logical name of the field to retrieve.\n *\n * @returns The current value of the field, or undefined if the field is not present.\n */\n get(field: string): any {\n return this._data[field];\n }\n\n /**\n * Sets a field value, marks the record dirty, and—when the record is\n * store-owned—notifies that store so bound views refresh.\n *\n * @remarks\n * A no-op assignment (the converted value already equals the current one) is\n * short-circuited and notifies nobody. The notify is suppressed while an edit\n * batch is open (see {@link beginEdit}) or while the owning store is batching,\n * and is absent entirely for an un-adopted record. Use {@link setSilent} to\n * mutate without ever notifying.\n *\n * @param field - The logical name of the field to update.\n * @param value - The new value to assign to the field.\n *\n * @returns This record, for method chaining.\n */\n set(field: string, value: any): this {\n const old = this._data[field];\n\n if (this.applySet(field, value)) {\n this.notifyStore({ [field]: { old, new: this._data[field] } });\n }\n\n return this;\n }\n\n /**\n * Sets a field value and marks the record dirty without ever notifying the\n * owning store.\n *\n * @remarks\n * The silent counterpart to {@link set}, for framework-internal writes whose\n * surrounding operation emits its own events (so a per-field notify would be\n * redundant). Conversion, the no-op short-circuit, and dirty tracking are\n * identical to {@link set}.\n *\n * @param field - The logical name of the field to update.\n * @param value - The new value to assign to the field.\n *\n * @returns This record, for method chaining.\n */\n setSilent(field: string, value: any): this {\n this.applySet(field, value);\n\n return this;\n }\n\n /**\n * Sets several fields, then notifies the owning store once for the whole\n * group.\n *\n * @remarks\n * Implemented over the record edit batch: it opens a batch, assigns each\n * field (so no intermediate notify fires), then commits for a single\n * coalesced notify carrying every change. Because the batch is depth-counted,\n * calling `setMany` inside a consumer's own {@link beginEdit} nests safely.\n *\n * @param values - A map of field name to new value.\n *\n * @returns This record, for method chaining.\n */\n setMany(values: Record<string, any>): this {\n this.beginEdit();\n\n for (const [field, value] of Object.entries(values)) {\n this.set(field, value);\n }\n\n this.commitEdit();\n\n return this;\n }\n\n /**\n * Opens a record-level edit batch, suppressing per-`set()` notifications\n * until the matching {@link commitEdit}.\n *\n * @remarks\n * Batches are depth-counted: the pre-edit snapshot is captured only on the\n * outermost `beginEdit`, so a nested batch (including the implicit one inside\n * {@link setMany}) collapses into the outer one rather than re-snapshotting.\n *\n * @returns This record, for method chaining.\n */\n beginEdit(): this {\n if (this._editDepth === 0) {\n this._editSnapshot = { ...this._data };\n }\n\n this._editDepth++;\n\n return this;\n }\n\n /**\n * Closes a record-level edit batch; on the outermost close, fires one notify\n * carrying every field changed since {@link beginEdit}.\n *\n * @remarks\n * A no-op when no batch is open. While nesting remains, the close is deferred\n * to the outermost {@link commitEdit}. The notify is skipped when the batch\n * produced no net change.\n *\n * @returns This record, for method chaining.\n */\n commitEdit(): this {\n if (this._editDepth === 0) {\n return this;\n }\n\n this._editDepth--;\n\n if (this._editDepth > 0) {\n return this;\n }\n\n const snapshot = this._editSnapshot ?? {};\n const changes: Record<string, FieldChange> = {};\n\n for (const key of Object.keys(this._data)) {\n if (!ModelRecord.isEqual(this._data[key], snapshot[key])) {\n changes[key] = { old: snapshot[key], new: this._data[key] };\n }\n }\n\n this._editSnapshot = null;\n\n if (Object.keys(changes).length > 0) {\n this.notifyStore(changes);\n }\n\n return this;\n }\n\n /**\n * Discards an open edit batch: reverts the fields to the pre-edit snapshot\n * and fires nothing.\n *\n * @remarks\n * A no-op when no batch is open. A cancel from anywhere inside a nested batch\n * collapses the whole stack and reverts to the outermost snapshot, since the\n * batch shares a single baseline.\n *\n * @returns This record, for method chaining.\n */\n cancelEdit(): this {\n if (this._editDepth === 0) {\n return this;\n }\n\n this._editDepth = 0;\n\n if (this._editSnapshot !== null) {\n this._data = { ...this._editSnapshot };\n this._editSnapshot = null;\n\n this.recomputeDirty();\n }\n\n return this;\n }\n\n /**\n * Converts, equality-gates, and applies a field write, updating dirty state.\n *\n * @remarks\n * The shared body of {@link set} and {@link setSilent}; the two differ only in\n * whether they notify afterward. Returns whether the write was a real change\n * so the caller knows whether a notify is warranted.\n *\n * @param field - The logical name of the field to update.\n * @param value - The new value to assign to the field.\n *\n * @returns True when the value actually changed; false for a no-op write.\n */\n private applySet(field: string, value: any): boolean {\n const modelField = this._model.getField(field);\n const converted = modelField ? modelField.convertValue(value, this._data) : value;\n\n if (ModelRecord.isEqual(this._data[field], converted)) {\n return false;\n }\n\n this._data[field] = converted;\n\n this.recomputeDirty();\n\n return true;\n }\n\n /**\n * Recomputes `_dirty` by comparing every current field against its committed\n * baseline. A new record is always dirty; otherwise any field whose value\n * differs from the original marks the record dirty.\n *\n * @remarks\n * Iterates `_data` keys — a superset of `_original` keys — so a field added\n * after construction (e.g. `set('newField', v)`) counts as a change, keeping\n * dirty tracking in agreement with `getChanges`.\n */\n private recomputeDirty(): void {\n this._dirty = this._isNew || Object.keys(this._data)\n .some(k => !ModelRecord.isEqual(this._data[k], this._original[k]));\n }\n\n /**\n * Notifies the owning store of a change, unless suppressed.\n *\n * @remarks\n * Fires only when the record is store-owned, no record edit batch is open,\n * and the owning store is not itself batching — the three layers that\n * coalesce or silence auto-notify.\n *\n * @param changes - The field-level diff to carry on the `'update'` event.\n */\n private notifyStore(changes: Record<string, FieldChange>): void {\n if (this._store === null || this._editDepth > 0 || this._store.isBatching()) {\n return;\n }\n\n this._store.notifyRecordChanged(this, changes);\n }\n\n /**\n * Structural value equality used for dirty-tracking: primitives by SameValueZero,\n * `Date` by time, arrays and plain objects deep, class instances by reference.\n *\n * @param a - The first value to compare.\n * @param b - The second value to compare.\n *\n * @returns True when the two values are structurally equal for dirty-tracking purposes.\n */\n private static isEqual(a: any, b: any): boolean {\n return ModelRecord.isEqualAtDepth(a, b, 0);\n }\n\n /**\n * Recursive core of {@link ModelRecord.isEqual}, carrying the depth budget.\n *\n * @param a - The first value to compare.\n * @param b - The second value to compare.\n * @param depth - The current recursion depth, capped by `MAX_EQUALITY_DEPTH`.\n *\n * @returns True when the two values are structurally equal at this depth.\n */\n private static isEqualAtDepth(a: any, b: any, depth: number): boolean {\n if (a === b) {\n return true;\n }\n\n if (a !== a && b !== b) {\n return true;\n }\n\n if (a === null || a === undefined || b === null || b === undefined) {\n return false;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n if (depth >= MAX_EQUALITY_DEPTH) {\n return a === b;\n }\n\n if (Array.isArray(a) || Array.isArray(b)) {\n return Array.isArray(a) && Array.isArray(b) && ModelRecord.arraysEqual(a, b, depth);\n }\n\n if (ModelRecord.isPlainObject(a) && ModelRecord.isPlainObject(b)) {\n return ModelRecord.plainObjectsEqual(a, b, depth);\n }\n\n return false;\n }\n\n /**\n * Compares two arrays element-wise, recursing one level deeper per element.\n *\n * @param a - The first array.\n * @param b - The second array.\n * @param depth - The current recursion depth.\n *\n * @returns True when both arrays have equal length and structurally-equal elements.\n */\n private static arraysEqual(a: any[], b: any[], depth: number): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n for (let i = 0; i < a.length; i++) {\n if (!ModelRecord.isEqualAtDepth(a[i], b[i], depth + 1)) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Compares two plain objects by own-enumerable keys, recursing one level deeper per key.\n *\n * @param a - The first plain object.\n * @param b - The second plain object.\n * @param depth - The current recursion depth.\n *\n * @returns True when both objects share the same key set and structurally-equal values.\n */\n private static plainObjectsEqual(a: Record<string, any>, b: Record<string, any>, depth: number): boolean {\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n\n if (aKeys.length !== bKeys.length) {\n return false;\n }\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) {\n return false;\n }\n\n if (!ModelRecord.isEqualAtDepth(a[key], b[key], depth + 1)) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Reports whether a value is a plain object (prototype is `Object.prototype` or `null`).\n *\n * @param value - The value to test.\n *\n * @returns True for plain data objects; false for class instances, arrays, and primitives.\n */\n private static isPlainObject(value: any): boolean {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const proto = Object.getPrototypeOf(value);\n\n return proto === Object.prototype || proto === null;\n }\n\n /**\n * Returns a shallow copy of all field data.\n *\n * @returns A plain object containing all current field values keyed by field name.\n */\n getData(): Record<string, any> {\n return { ...this._data };\n }\n\n /**\n * Returns the changed-field new values for a dirty-only update body, always\n * including the primary-key field so a batch update (no id in the URL) stays\n * identifiable.\n *\n * @remarks\n * Empty of changes only when the record is clean, in which case only the\n * primary-key entry (if any) is returned.\n *\n * @returns A plain object of changed field name to its new value, plus the\n * primary-key field when the model defines one.\n */\n getChangedData(): Record<string, any> {\n const data: Record<string, any> = {};\n\n for (const [field, change] of Object.entries(this.getChanges())) {\n data[field] = change.new;\n }\n\n const pkField = this._model.getPrimaryKeyField();\n\n if (pkField) {\n data[pkField.getName()] = this._data[pkField.getName()];\n }\n\n return data;\n }\n\n /**\n * Returns true if any field has been changed since the last commit.\n *\n * @returns True if the record has uncommitted changes, false otherwise.\n */\n isDirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Returns true if this record has not yet been persisted (added via store.add).\n *\n * @returns True if the record is new and has not been synced to the server.\n */\n isNew(): boolean {\n return this._isNew;\n }\n\n /**\n * Marks the record as newly created and not yet synced to the server.\n */\n markAsNew(): void {\n this._isNew = true;\n }\n\n /**\n * Accepts current field values as the new baseline, clearing dirty and new flags.\n *\n * @remarks\n * Called automatically by `AbstractStore.sync()` after a successful create or update\n * so that the record no longer appears in subsequent sync cycles.\n */\n commit(): this {\n this._original = { ...this._data };\n this._dirty = false;\n this._isNew = false;\n\n return this;\n }\n\n /**\n * Reverts all field values to the last committed state.\n *\n * @remarks\n * The dirty flag is cleared but the new flag is not changed; a new record that has\n * been rejected remains new until it is committed or removed from the store.\n */\n reject(): void {\n this._data = { ...this._original };\n this._dirty = false;\n }\n\n /**\n * Returns the value of the model's primary-key field, or undefined if none is defined.\n *\n * @returns The primary key value, or undefined if the model has no primary key configured.\n */\n getId(): any {\n const pkField = this._model.getPrimaryKeyField();\n\n return pkField ? this._data[pkField.getName()] : undefined;\n }\n\n /**\n * Returns the AbstractModel that describes this record's schema.\n *\n * @returns The model instance associated with this record.\n */\n getModel(): AbstractModel {\n return this._model;\n }\n\n /**\n * Returns the stable client-side id assigned at construction.\n *\n * @remarks\n * Unlike `getId()` (the primary-key value, which is `undefined` until the server\n * replies), this id exists immediately and is unique within the session, so UI can\n * use it as a key for unsynced rows. It is never serialised and never sent to the server.\n *\n * @returns The monotonic per-session internal id.\n */\n getInternalId(): number {\n return this._internalId;\n }\n\n /**\n * Returns the cached, parent-scoped child {@link AbstractStore} for an\n * association accessor, building it on first access.\n *\n * @remarks\n * Repeated calls for the same accessor return the **same** store instance, so\n * listeners and the collection id-index stay stable. For a hasMany association\n * the store is seeded from an embedded child array when the parent payload\n * carried one (eager), otherwise it is configured to load through the target\n * model's proxy filtered on the parent's foreign key (lazy). For a belongsTo\n * association the store is filtered to the single owner record.\n *\n * @param accessor - The association accessor declared on this record's model.\n *\n * @returns The cached child store for that association.\n *\n * @throws Error when no association with that accessor exists (programmer error).\n */\n getAssociated(accessor: string): AbstractStore {\n const cached = this._childStores?.get(accessor);\n\n if (cached) {\n return cached;\n }\n\n const association = this._model.getAssociation(accessor);\n\n if (!association) {\n throw new Error(`ModelRecord.getAssociated: no association '${accessor}'`);\n }\n\n const store = this.buildChildStore(association);\n\n (this._childStores ??= new Map()).set(accessor, store);\n\n return store;\n }\n\n /**\n * Builds the parent-scoped child store for an association.\n *\n * @remarks\n * The target model is resolved through the association's memoised thunk. A\n * hasMany association with an embedded seed loads those rows directly (each\n * committed, not new); otherwise it carries a `remoteFilter` on the parent\n * foreign key for the lazy load path. A belongsTo association filters the\n * target store to the owner via the foreign-key value. The association's\n * {@link Association.resolveProxy} feeds the child store's transport, so the\n * lazy/owner load reaches a real proxy.\n *\n * @param association - The association whose child store is built.\n *\n * @returns A freshly-constructed {@link Store} for the target model.\n */\n private buildChildStore(association: Association): AbstractStore {\n const targetModel = association.resolveTarget() as Model;\n const proxy = association.resolveProxy();\n\n if (association.kind === 'belongsTo') {\n const pkName = targetModel.getPrimaryKeyField()?.getName() ?? association.getForeignKey();\n\n return new Store({\n model: targetModel,\n proxy,\n remoteFilter: true,\n filters: [{ type: 'eq', field: pkName, value: this.get(association.getForeignKey()) }],\n });\n }\n\n const seed = this._associatedSeed[association.getAccessor()];\n\n if (seed) {\n // The seed branch calls loadData() and never load(); the proxy is\n // inert here unless a consumer later load()s or sync()s this store.\n const store = new Store({ model: targetModel, proxy });\n\n store.loadData(seed);\n\n return store;\n }\n\n return new Store({\n model: targetModel,\n proxy,\n remoteFilter: true,\n filters: [{ type: 'eq', field: association.getForeignKey(), value: this.getId() }],\n });\n }\n\n /**\n * Reports whether a child store for an association has already been built.\n *\n * @remarks\n * Used by the parent store's cascade sync to skip associations whose child\n * store was never materialised — a loaded-but-untouched parent then costs\n * nothing.\n *\n * @param accessor - The association accessor to test.\n *\n * @returns True when {@link getAssociated} has already built that store.\n */\n hasChildStore(accessor: string): boolean {\n return this._childStores?.has(accessor) ?? false;\n }\n\n /**\n * Returns the raw foreign-key value for a belongsTo accessor, without loading.\n *\n * @param accessor - The belongsTo association accessor.\n *\n * @returns The foreign-key field value held on this record.\n *\n * @throws Error when no association with that accessor exists (programmer error).\n */\n getForeignKeyValue(accessor: string): any {\n const association = this._model.getAssociation(accessor);\n\n if (!association) {\n throw new Error(`ModelRecord.getForeignKeyValue: no association '${accessor}'`);\n }\n\n return this.get(association.getForeignKey());\n }\n\n /**\n * Returns this record's data augmented with embedded children for every\n * `'nested'`-persist association whose child store was materialised.\n *\n * @remarks\n * Unlike {@link getData}, which never carries children, this is the hook a\n * nested-aware writer reads to serialise children inside the parent's write\n * body. Each materialised `'nested'` association contributes\n * `{ [nestedKey]: childRecordsData }`; `'proxy'`-persist associations and\n * unbuilt stores are omitted (they persist through their own proxy).\n *\n * @returns A plain object: the field data plus nested child-record arrays.\n */\n getDataWithNested(): Record<string, any> {\n const data = this.getData();\n\n for (const association of this._model.getAssociations()) {\n if (association.kind !== 'hasMany' || association.getPersist() !== 'nested') {\n continue;\n }\n\n if (!this.hasChildStore(association.getAccessor())) {\n continue;\n }\n\n const child = this.getAssociated(association.getAccessor());\n\n data[association.getNestedKey()] = child.getAll().map(r => r.getData());\n }\n\n return data;\n }\n\n /**\n * Returns the fields whose current value differs from the last committed baseline.\n *\n * @remarks\n * Comparison uses the same structural equality as dirty tracking, so plain object /\n * array field values compare deeply; class instances still compare by reference.\n *\n * @returns A map of changed field name to its `{ old, new }` values; empty when clean.\n */\n getChanges(): Record<string, FieldChange> {\n const changes: Record<string, FieldChange> = {};\n\n for (const key of Object.keys(this._data)) {\n if (!ModelRecord.isEqual(this._data[key], this._original[key])) {\n changes[key] = { old: this._original[key], new: this._data[key] };\n }\n }\n\n return changes;\n }\n\n /**\n * Returns the modified fields as a `{ old, new }` map.\n *\n * @remarks\n * Alias of {@link ModelRecord.getChanges}; both names are provided for caller intent.\n *\n * @returns A map of modified field name to its `{ old, new }` values; empty when clean.\n */\n getModified(): Record<string, FieldChange> {\n return this.getChanges();\n }\n\n /**\n * Returns a copy of this record carrying a fresh internal id, marked new and dirty.\n *\n * @remarks\n * A clone is a distinct row, so it receives its own `internalId` and is flagged as a\n * new, dirty insert. The data is shallow-copied (object / array field values are shared\n * with the source, matching the shallow contract elsewhere in this class).\n *\n * @returns A new {@link ModelRecord} for the same model with copied field data.\n */\n clone(): ModelRecord {\n const copy = new ModelRecord(this._model, { ...this._data });\n\n copy.markAsNew();\n copy._dirty = true;\n\n return copy;\n }\n\n /**\n * Returns true when every field passes its implicit type check and explicit validators.\n *\n * @returns True when no field reports an error, false otherwise.\n */\n isValid(): boolean {\n return Object.keys(this.getErrors()).length === 0;\n }\n\n /**\n * Returns the first failing message for each currently-invalid field.\n *\n * @returns A map of field name to its first error message; empty when the record is valid.\n */\n getErrors(): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const field of this._model.getFields()) {\n const message = this.validateField(field.getName());\n\n if (message) {\n errors[field.getName()] = message;\n }\n }\n\n return errors;\n }\n\n /**\n * Validates a single field, returning its first failing message.\n *\n * @remarks\n * An implicit type check runs first: a non-null value on a typed (non-`auto`/`glyph`)\n * field that fails coercion reports a type error before the explicit `validators` run.\n *\n * @param name - The logical name of the field to validate.\n *\n * @returns The first error message, or `''` when the field is valid or is not a model field.\n */\n validateField(name: string): string {\n const field = this._model.getField(name);\n\n if (!field) {\n return '';\n }\n\n const value = this._data[name];\n const typeError = this.checkType(field, value);\n\n if (typeError) {\n return typeError;\n }\n\n for (const rule of field.getValidators()) {\n const result = applyRule(rule, value);\n\n if (!result.valid) {\n return result.message;\n }\n }\n\n return '';\n }\n\n /**\n * Runs the implicit, conversion-derived type check for a field value.\n *\n * @remarks\n * Skips `auto` / `glyph` fields (no type to enforce) and `null` / `undefined` values\n * (absence is governed by a `required` rule, not the type check). For every other typed\n * field, a stored value that re-coerces to `undefined` (a `number` holding `NaN`, a date\n * holding an Invalid Date) is reported as a type error.\n *\n * @param field - The field whose declared type is enforced.\n * @param value - The current stored value for that field.\n *\n * @returns The type-error message, or `''` when the value satisfies the field's type.\n */\n private checkType(field: Field, value: any): string {\n const type = field.getType();\n\n if (type === 'auto' || type === 'glyph' || value === null || value === undefined) {\n return '';\n }\n\n const coerced = field.convertValue(value);\n const failed = coerced === undefined\n || (typeof coerced === 'number' && isNaN(coerced));\n\n if (failed) {\n return `Value is not a valid ${type}.`;\n }\n\n return '';\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { AbstractModel } from '~/data/AbstractModel.js';\nimport type { Proxy } from '~/data/proxy/Proxy.js';\n\n/**\n * Per-association persistence strategy applied during cascade sync.\n *\n * @remarks\n * `'proxy'` (the default) persists children through the child store's own\n * proxy — the cascade simply calls the child store's `sync()`. `'nested'`\n * serialises children inside the parent's write body under the association's\n * nested key, via {@link ModelRecord.getDataWithNested}; the child store is not\n * synced independently.\n *\n * @category Data\n */\nexport type AssociationPersist = 'nested' | 'proxy';\n\n/**\n * Construction-time options shared by all association kinds.\n *\n * @category Data\n */\nexport interface AssociationOptions {\n /** Accessor name exposed on the record (e.g. `'employees'`). */\n accessor: string;\n /** Thunk returning the target model — a thunk to break declaration cycles. */\n target: () => AbstractModel;\n /**\n * The child field holding the owner's id (hasMany) / the owner's primary\n * key this record points at (belongsTo).\n */\n foreignKey: string;\n /** Raw-payload key carrying an embedded child array for eager hydration. Defaults to `accessor`. */\n nestedKey?: string;\n /** Cascade persistence strategy; defaults to `'proxy'`. */\n persist?: AssociationPersist;\n /**\n * Discriminant selecting the concrete association kind when a plain options\n * object is promoted by {@link AbstractModel}. Ignored when an\n * {@link Association} instance is supplied directly.\n */\n kind?: 'hasMany' | 'belongsTo';\n /**\n * Proxy used to load (and persist) the target model's records for this\n * association's parent-scoped child {@link Store}. Required for the lazy\n * fetch path: without it, the lazily-built child store has no transport and\n * `load()` throws.\n *\n * @remarks\n * A direct {@link Proxy} reference, not a thunk like `target`. The `target`\n * thunk exists only to break the module-initialisation cycle between two\n * mutually-referential models; a proxy is a plain runtime object holding no\n * back-reference to the association or the owning model, so referencing it\n * directly creates no cycle. Do not \"fix\" this into a thunk by analogy with\n * `target`.\n */\n proxy?: Proxy;\n}\n\n/**\n * Declarative association descriptor; behaviour lives in\n * {@link AbstractModel} / {@link ModelRecord} / {@link AbstractStore}.\n *\n * @remarks\n * Mirrors {@link Field}: a passive schema object holding name/target/foreignKey\n * and pure getters, with the runtime logic (hydrate, lazy-load, cascade) living\n * in the model, record, and store. The `target` thunk is called at most once and\n * its result memoised, so mutually-referential models can declare each other\n * without a module-initialisation cycle.\n *\n * @category Data\n */\nexport abstract class Association {\n\n private _accessor: string;\n private _target: () => AbstractModel;\n private _foreignKey: string;\n private _nestedKey: string | undefined;\n private _persist: AssociationPersist;\n private _proxy: Proxy | undefined;\n private _resolvedTarget: AbstractModel | undefined;\n\n /** The concrete kind of this association. */\n abstract readonly kind: 'hasMany' | 'belongsTo';\n\n /**\n * Constructs an Association from an {@link AssociationOptions} object.\n *\n * @param options - The options describing the accessor, target, and foreign key.\n */\n constructor(options: AssociationOptions) {\n this._accessor = options.accessor;\n this._target = options.target;\n this._foreignKey = options.foreignKey;\n this._nestedKey = options.nestedKey;\n this._persist = options.persist ?? 'proxy';\n this._proxy = options.proxy;\n }\n\n /**\n * Returns the accessor name exposed on the record.\n *\n * @returns The accessor string (e.g. `'employees'`).\n */\n getAccessor(): string {\n return this._accessor;\n }\n\n /**\n * Returns the foreign-key field name.\n *\n * @returns The child field holding the owner's id (hasMany), or the owner's\n * primary key this record points at (belongsTo).\n */\n getForeignKey(): string {\n return this._foreignKey;\n }\n\n /**\n * Returns the raw-payload key carrying an embedded child array.\n *\n * @returns The configured nested key, or the accessor name when none was set.\n */\n getNestedKey(): string {\n return this._nestedKey ?? this._accessor;\n }\n\n /**\n * Returns the cascade persistence strategy.\n *\n * @returns `'nested'` or `'proxy'` (the default).\n */\n getPersist(): AssociationPersist {\n return this._persist;\n }\n\n /**\n * Resolves and memoises the target model via the configured thunk.\n *\n * @returns The target {@link AbstractModel} instance; the thunk is invoked at\n * most once across the association's lifetime.\n */\n resolveTarget(): AbstractModel {\n if (this._resolvedTarget === undefined) {\n this._resolvedTarget = this._target();\n }\n\n return this._resolvedTarget;\n }\n\n /**\n * Returns the proxy for this association's parent-scoped child store.\n *\n * @remarks\n * Mirrors {@link resolveTarget} so both child-store kinds read the target's\n * transport identically. Unlike `target` there is no thunk to invoke and no\n * memoisation: the stored {@link Proxy} reference is returned verbatim.\n *\n * @returns The configured target-model proxy, or undefined when none was set\n * (the lazy fetch path then has no transport and `load()` will throw).\n */\n resolveProxy(): Proxy | undefined {\n return this._proxy;\n }\n}\n\n/**\n * Parent owns many child records, surfaced as a parent-scoped child\n * {@link Store} via {@link ModelRecord.getAssociated}.\n *\n * @category Data\n */\nexport class HasManyAssociation extends Association {\n\n readonly kind = 'hasMany' as const;\n}\n\n/**\n * Record references a single owner via its foreign key.\n *\n * @remarks\n * Like {@link HasManyAssociation}, {@link ModelRecord.getAssociated} returns a\n * parent-scoped child {@link Store} (filtered to the owner) whose first record is\n * the owner; {@link ModelRecord.getForeignKeyValue} reads the raw FK without\n * loading.\n *\n * @category Data\n */\nexport class BelongsToAssociation extends Association {\n\n readonly kind = 'belongsTo' as const;\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Field, FieldOptions } from '~/data/Field.js';\nimport { ModelRecord } from '~/data/ModelRecord.js';\nimport { Association, AssociationOptions, HasManyAssociation, BelongsToAssociation } from '~/data/Association.js';\n\n/**\n * Base class for all data models.\n * Defines the field schema used to create and validate ModelRecord instances.\n *\n * @remarks\n * Subclasses must declare the `fields` array. Field resolution and the name-to-field\n * index are built lazily on first access and cached for subsequent calls.\n *\n * @category Data\n */\nexport abstract class AbstractModel {\n\n abstract readonly fields: (Field | FieldOptions)[];\n\n /**\n * Optional association schema, mirroring `fields`. Declaring an association\n * surfaces a parent-scoped child {@link Association} accessor on records via\n * {@link ModelRecord.getAssociated}. Defaults to none, so every model that\n * omits it compiles and behaves unchanged.\n *\n * @remarks\n * Set once at construction (subclasses assign it, like `fields`); treated as\n * read-only schema thereafter. It is not `readonly` only so the {@link Model}\n * runtime subclass can assign it from its options bag.\n */\n associations?: (Association | AssociationOptions)[];\n\n protected _primaryKey: string | undefined;\n\n private _resolvedFields: Field[] | undefined;\n private _fieldsByName: Map<string, Field> | undefined;\n private _resolvedAssociations: Association[] | undefined;\n private _associationsByAccessor: Map<string, Association> | undefined;\n\n /**\n * Lazily builds the resolved fields list and name-to-field index on first access.\n *\n * @remarks\n * Plain [`FieldConfig`](/api/data/type-aliases/FieldConfig) objects in the `fields` array are promoted to [`Field`](/api/data/classes/Field) instances\n * on the first call; subsequent calls return immediately.\n */\n private ensureIndex(): void {\n if (this._resolvedFields) {\n return;\n }\n\n this._resolvedFields = this.fields.map(f => f instanceof Field ? f : new Field(f));\n this._fieldsByName = new Map();\n\n for (const field of this._resolvedFields) {\n this._fieldsByName.set(field.getName(), field);\n }\n\n this._resolvedAssociations = (this.associations ?? []).map(a => AbstractModel.promoteAssociation(a));\n this._associationsByAccessor = new Map();\n\n for (const association of this._resolvedAssociations) {\n this._associationsByAccessor.set(association.getAccessor(), association);\n\n this.assertNestedKeyFree(association);\n }\n }\n\n /**\n * Promotes a plain {@link AssociationOptions} object to the concrete\n * {@link Association} subclass named by its `kind` discriminant.\n *\n * @param association - An already-built association, or an options object.\n *\n * @returns The supplied {@link Association}, or a freshly-built\n * {@link HasManyAssociation} / {@link BelongsToAssociation}.\n */\n private static promoteAssociation(association: Association | AssociationOptions): Association {\n if (association instanceof Association) {\n return association;\n }\n\n return association.kind === 'belongsTo'\n ? new BelongsToAssociation(association)\n : new HasManyAssociation(association);\n }\n\n /**\n * Asserts that an association's nested key does not collide with any field's\n * raw-data mapping, which would let an embedded child array be mis-read by\n * the field-mapping loop in {@link createRecord}.\n *\n * @param association - The association whose nested key is checked.\n */\n private assertNestedKeyFree(association: Association): void {\n const nestedKey = association.getNestedKey();\n\n for (const field of this._resolvedFields!) {\n if (field.getMapping() === nestedKey) {\n throw new Error(`AbstractModel: association '${association.getAccessor()}' nested key '${nestedKey}' collides with field '${field.getName()}' mapping`);\n }\n }\n }\n\n /**\n * Returns the Field designated as the primary key, or undefined if none is set.\n *\n * @returns The primary key Field, or undefined if no primary key has been configured.\n */\n getPrimaryKeyField(): Field | undefined {\n if (!this._primaryKey) {\n return undefined;\n }\n\n return this.getField(this._primaryKey);\n }\n\n /**\n * Returns all resolved Field instances for this model.\n *\n * @returns An array of all Field instances defined on this model.\n */\n getFields(): Field[] {\n this.ensureIndex();\n\n return this._resolvedFields!;\n }\n\n /**\n * Returns the Field with the given name, or undefined if not found.\n *\n * @param name - The logical name of the field to look up.\n *\n * @returns The matching Field, or undefined if no field with that name exists.\n */\n getField(name: string): Field | undefined {\n this.ensureIndex();\n\n return this._fieldsByName!.get(name);\n }\n\n /**\n * Returns true if the model contains a field with the given name.\n *\n * @param name - The logical name of the field to check.\n *\n * @returns True if a field with the given name exists, false otherwise.\n */\n hasField(name: string): boolean {\n this.ensureIndex();\n\n return this._fieldsByName!.has(name);\n }\n\n /**\n * Returns all resolved {@link Association} instances for this model.\n *\n * @returns An array of every association defined on this model; empty when none.\n */\n getAssociations(): Association[] {\n this.ensureIndex();\n\n return this._resolvedAssociations!;\n }\n\n /**\n * Returns the {@link Association} with the given accessor, or undefined.\n *\n * @param accessor - The accessor name to look up.\n *\n * @returns The matching association, or undefined when none is declared.\n */\n getAssociation(accessor: string): Association | undefined {\n this.ensureIndex();\n\n return this._associationsByAccessor!.get(accessor);\n }\n\n /**\n * Creates a ModelRecord from a plain object or positional array, applying field mappings and defaults.\n *\n * @param data - Optional. The source data as a key/value object or a positional array.\n * When an array is provided, values are assigned to fields ordered by their `order` property.\n *\n * @returns A new ModelRecord populated with mapped and defaulted field values.\n *\n * @remarks\n * When `data` is an array, fields are sorted by their `order` value before being matched\n * by position. Fields absent from `data` receive the value from `field.getDefaultValue()`.\n */\n createRecord(data: Record<string, any> | any[] = {}): ModelRecord {\n this.ensureIndex();\n\n let source: Record<string, any>;\n\n if (Array.isArray(data)) {\n const sorted = this._resolvedFields!.slice().sort((a, b) => a.getOrder() - b.getOrder());\n\n source = {};\n\n sorted.forEach((field, i) => {\n source[field.getMapping()] = data[i];\n });\n } else {\n source = data;\n }\n\n const mapped: Record<string, any> = {};\n\n for (const field of this._resolvedFields!) {\n const raw = source[field.getMapping()];\n const value = raw !== undefined ? raw : field.getDefaultValue();\n\n mapped[field.getName()] = field.convertValue(value, source);\n }\n\n const seed: Record<string, any[]> = {};\n\n for (const association of this._resolvedAssociations!) {\n const raw = source[association.getNestedKey()];\n\n if (association.kind === 'hasMany' && Array.isArray(raw)) {\n seed[association.getAccessor()] = raw;\n }\n }\n\n return new ModelRecord(this, mapped, seed);\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Field, FieldOptions } from '~/data/Field.js';\nimport type { Association, AssociationOptions } from '~/data/Association.js';\nimport { AbstractModel } from '~/data/AbstractModel.js';\n\n/**\n * Construction-time options for {@link Model}.\n *\n * @category Data\n */\nexport interface ModelOptions {\n fields: Array<Field | FieldOptions>;\n primaryKey?: string;\n associations?: Array<Association | AssociationOptions>;\n}\n\n/**\n * A concrete, configurable model created at runtime from a field array.\n * Use this class when you do not need a dedicated model subclass.\n *\n * @category Data\n */\nexport class Model extends AbstractModel {\n\n readonly fields: (Field | FieldOptions)[];\n\n /**\n * Constructs a Model with the specified fields and an optional primary key.\n *\n * @param fields - An array of Field instances or FieldOptions objects that define the schema, or a {@link ModelOptions} bag.\n * @param primaryKey - Optional. The name of the field to use as the primary key. Ignored when the first argument is a {@link ModelOptions} bag.\n */\n constructor(fields: Array<Field | FieldOptions> | ModelOptions, primaryKey?: string) {\n super();\n\n if (Array.isArray(fields)) {\n this.fields = fields;\n this._primaryKey = primaryKey;\n } else {\n this.fields = fields.fields;\n this._primaryKey = fields.primaryKey;\n this.associations = fields.associations;\n }\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { ModelRecord } from '~/data/ModelRecord.js';\n// Type-only imports: erased at compile time, so they introduce no runtime\n// dependency edge back to AbstractStore/FilterDescriptor and cannot form a cycle.\nimport type { SortDescriptor } from '~/data/AbstractStore.js';\nimport type { FilterDescriptor } from '~/data/FilterDescriptor.js';\n\n/**\n * Optional parameters passed to {@link Proxy.read} when the store opts in to\n * server-side pagination or remote sort/filter.\n *\n * @remarks\n * When `AbstractStore.setPageSize(n)` has been called, `AbstractStore.load()`\n * builds a [`ReadParams`](/api/data/interfaces/ReadParams) object describing the desired page and forwards it to\n * the proxy. Proxies that do not understand pagination (e.g. {@link MemoryProxy})\n * are free to ignore the argument.\n *\n * When the store sets `remoteSort`/`remoteFilter`, the active `sorters`/`filters`\n * descriptors ride along here so the proxy can encode them for its transport.\n * `signal` lets the store abort a superseded HTTP read.\n *\n * @category Data\n */\nexport interface ReadParams {\n page? : number;\n pageSize?: number;\n sorters? : SortDescriptor[];\n filters? : FilterDescriptor[];\n signal? : AbortSignal;\n}\n\n/**\n * Abstract base class for all data proxies.\n * Defines the four CRUD operations that every proxy implementation must provide.\n *\n * @remarks\n * AbstractStore calls these methods during `load()` and `sync()`. Each method\n * receives or returns plain data objects so that the proxy layer remains\n * decoupled from the store's record management logic.\n *\n * @category Data\n */\nexport abstract class Proxy {\n\n /**\n * Fetches records from the data source.\n *\n * @param params - Optional. Pagination parameters from the store.\n * Proxies that do not support pagination may ignore this argument.\n *\n * @returns A promise that resolves to an array of raw data objects.\n */\n abstract read(params?: ReadParams): Promise<any[]>;\n\n /**\n * Persists a new record to the data source.\n *\n * @param record - The new ModelRecord to create.\n *\n * @returns A promise that resolves to the server-side representation of the created record,\n * which may include server-assigned values such as a generated primary key.\n */\n abstract create(record: ModelRecord): Promise<Record<string, any>>;\n\n /**\n * Updates an existing record in the data source.\n *\n * @param record - The dirty ModelRecord to persist.\n *\n * @returns A promise that resolves to the server-side representation of the updated record.\n */\n abstract update(record: ModelRecord): Promise<Record<string, any>>;\n\n /**\n * Removes a record from the data source.\n *\n * @param record - The ModelRecord to delete.\n *\n * @returns A promise that resolves when the deletion is complete.\n */\n abstract destroy(record: ModelRecord): Promise<void>;\n\n /**\n * Batch-creates records in a single request, when the transport supports it.\n *\n * @param records - The new ModelRecords to create, in order.\n *\n * @returns A promise resolving to the per-record server data in the same\n * order as `records`, so the store can commit each positionally.\n *\n * @remarks\n * Optional. When absent, {@link AbstractStore.sync} falls back to issuing one\n * {@link create} per record.\n */\n createBatch?(records: ModelRecord[]): Promise<Record<string, any>[]>;\n\n /**\n * Batch-updates records in a single request, when the transport supports it.\n *\n * @param records - The dirty ModelRecords to update, in order.\n *\n * @returns A promise resolving to the per-record server data in the same\n * order as `records`, so the store can commit each positionally.\n *\n * @remarks\n * Optional. When absent, {@link AbstractStore.sync} falls back to issuing one\n * {@link update} per record.\n */\n updateBatch?(records: ModelRecord[]): Promise<Record<string, any>[]>;\n\n /**\n * Batch-destroys records in a single request, when the transport supports it.\n *\n * @param records - The ModelRecords to delete, in order.\n *\n * @returns A promise that resolves when the batch deletion is complete.\n *\n * @remarks\n * Optional. When absent, {@link AbstractStore.sync} falls back to issuing one\n * {@link destroy} per record.\n */\n destroyBatch?(records: ModelRecord[]): Promise<void>;\n\n /**\n * Returns the total record count reported by the most recent paginated read.\n *\n * @returns The total count from the last paginated response, or undefined if\n * the proxy does not support pagination or no paginated read has occurred.\n *\n * @remarks\n * Default implementation returns undefined. Pagination-aware proxies (such as\n * {@link AjaxProxy}) override this to return the `total` value parsed from\n * the server envelope.\n */\n getLastTotalCount(): number | undefined {\n return undefined;\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { ModelRecord } from '~/data/ModelRecord.js';\nimport { Proxy, ReadParams } from '~/data/proxy/Proxy.js';\n\n/**\n * Construction-time options for {@link MemoryProxy}.\n *\n * @category Data\n */\nexport interface MemoryProxyOptions {\n data: any[];\n}\n\n/**\n * @deprecated Use {@link MemoryProxyOptions}.\n */\nexport type MemoryProxyConfig = MemoryProxyOptions;\n\n/**\n * An in-memory proxy that stores data as a plain JavaScript array.\n * All operations resolve synchronously via `Promise.resolve`.\n *\n * @remarks\n * Because this proxy holds no external state, data is lost when the page is\n * refreshed or the proxy instance is discarded. It is primarily intended for\n * testing and for stores that manage transient, client-only data.\n *\n * @category Data\n */\nexport class MemoryProxy extends Proxy {\n\n private _data: any[];\n\n /**\n * Constructs a MemoryProxy, optionally pre-populated with an initial data array.\n *\n * @param options - Optional. Options object containing the initial data array.\n */\n constructor(options: MemoryProxyOptions = { data: [] }) {\n // Proxy has no options bag; fields are read from options directly below.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this._data = options.data.slice();\n }\n\n /**\n * Replaces the in-memory data array used by this proxy.\n *\n * @param data - The new data array; a shallow copy is stored internally.\n */\n setData(data: any[]): void {\n this._data = data.slice();\n }\n\n /**\n * Returns a copy of the in-memory data array.\n *\n * @param _params - Optional. Pagination parameters; ignored by this proxy.\n *\n * @returns A promise that resolves to a shallow copy of the current data array.\n */\n read(_params?: ReadParams): Promise<any[]> {\n return Promise.resolve(this._data.slice());\n }\n\n /**\n * Appends a copy of the record's data to the in-memory array.\n *\n * @param record - The new ModelRecord to store.\n *\n * @returns A promise that resolves to the stored copy of the record's data.\n */\n create(record: ModelRecord): Promise<Record<string, any>> {\n const copy = { ...record.getData() };\n\n this._data.push(copy);\n\n return Promise.resolve(copy);\n }\n\n /**\n * Updates the matching entry in the in-memory array by primary key.\n *\n * @param record - The dirty ModelRecord whose data should replace the existing entry.\n *\n * @returns A promise that resolves to the updated copy of the record's data.\n *\n * @remarks\n * If the model has no primary key or the record is not found in the array, the\n * array is left unchanged and the method still resolves successfully.\n */\n update(record: ModelRecord): Promise<Record<string, any>> {\n const copy = { ...record.getData() };\n const pkName = record.getModel().getPrimaryKeyField()?.getName();\n\n if (pkName !== undefined) {\n const id = record.getId();\n const idx = this._data.findIndex(d => d[pkName] === id);\n\n if (idx !== -1) {\n this._data[idx] = copy;\n }\n }\n\n return Promise.resolve(copy);\n }\n\n /**\n * Removes the matching entry from the in-memory array by primary key.\n *\n * @param record - The ModelRecord to remove.\n *\n * @returns A promise that resolves when the removal is complete.\n *\n * @remarks\n * If the model has no primary key or the record is not found, the array is\n * left unchanged and the method still resolves successfully.\n */\n destroy(record: ModelRecord): Promise<void> {\n const pkName = record.getModel().getPrimaryKeyField()?.getName();\n\n if (pkName !== undefined) {\n const id = record.getId();\n const idx = this._data.findIndex(d => d[pkName] === id);\n\n if (idx !== -1) {\n this._data.splice(idx, 1);\n }\n }\n\n return Promise.resolve();\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { AbstractStore, AbstractStoreOptions } from '~/data/AbstractStore.js';\nimport { Model } from '~/data/Model.js';\nimport { MemoryProxy } from '~/data/proxy/MemoryProxy.js';\n\n/**\n * Construction-time options for {@link MemoryStore}.\n *\n * @category Data\n */\nexport interface MemoryStoreOptions extends AbstractStoreOptions {\n model: Model;\n data?: any[];\n}\n\n/**\n * A store backed entirely by in-memory data.\n * Useful for testing or for static datasets that do not require server persistence.\n *\n * @category Data\n */\nexport class MemoryStore extends AbstractStore {\n\n readonly model: Model;\n readonly proxy: MemoryProxy = new MemoryProxy();\n\n /**\n * Constructs a MemoryStore with the given model and an optional initial data array.\n *\n * @param modelOrOptions - The Model that defines the record schema, or a {@link MemoryStoreOptions} bag.\n * @param data - Optional. The initial data records to load into the store. Ignored when the first argument is a {@link MemoryStoreOptions} bag.\n */\n constructor(modelOrOptions: Model | MemoryStoreOptions, data: any[] = []) {\n super();\n\n if (modelOrOptions instanceof Model) {\n this.model = modelOrOptions;\n this.proxy.setData(data);\n } else {\n this.model = modelOrOptions.model;\n this.proxy.setData(modelOrOptions.data ?? []);\n\n this.applyOptions(modelOrOptions);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"MemoryStore-DT6iWWco.js","names":[],"sources":["../../src/typescript/lib/data/Field.ts","../../src/typescript/lib/data/FilterDescriptor.ts","../../src/typescript/lib/data/StoreWorkerClient.ts","../../src/typescript/lib/data/compareValues.ts","../../src/typescript/lib/data/AbstractStore.ts","../../src/typescript/lib/data/Store.ts","../../src/typescript/lib/data/ModelRecord.ts","../../src/typescript/lib/data/Association.ts","../../src/typescript/lib/data/AbstractModel.ts","../../src/typescript/lib/data/Model.ts","../../src/typescript/lib/data/proxy/Proxy.ts","../../src/typescript/lib/data/proxy/MemoryProxy.ts","../../src/typescript/lib/data/MemoryStore.ts"],"sourcesContent":["// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { ValidationRule } from '~/validation/ValidationRule.js';\n\n/**\n * Built-in field types supported by {@link Model} and {@link AbstractModel}.\n *\n * @category Data\n */\nexport type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'glyph' | 'auto';\n\n/**\n * Construction-time options for a {@link Field}.\n * Can be passed directly to `AbstractModel.fields` or used to construct a `Field` instance.\n *\n * @category Data\n */\nexport interface FieldOptions {\n name: string;\n type?: FieldType;\n defaultValue?: any;\n mapping?: string;\n description?: string;\n order?: number;\n /** Custom raw-to-typed coercion; wins over the built-in `type` conversion. */\n convert?: (raw: any, sourceRecord?: Record<string, any>) => any;\n /** Field-level validation rules, evaluated by {@link ModelRecord} (pull-based). */\n validators?: ValidationRule[];\n}\n\n/**\n * @deprecated Use {@link FieldOptions}.\n */\nexport type FieldConfig = FieldOptions;\n\n/**\n * Represents a single typed field in a model's schema.\n * Encapsulates the field's name, type, default value, raw-data mapping, description, and display order.\n *\n * @category Data\n */\nexport class Field {\n\n private _name: string;\n private _type: FieldType;\n private _defaultValue: any;\n private _mapping: string;\n private _description: string | undefined;\n private _order: number | undefined;\n private _convert: ((raw: any, sourceRecord?: Record<string, any>) => any) | undefined;\n private _validators: ValidationRule[];\n\n /**\n * Constructs a Field from a FieldOptions object.\n *\n * @param options - The options object describing the field's properties.\n */\n constructor(options: FieldOptions) {\n this._name = options.name;\n this._type = options.type ?? 'auto';\n this._defaultValue = options.defaultValue;\n this._mapping = options.mapping ?? options.name;\n this._description = options.description;\n this._order = options.order;\n this._convert = options.convert;\n this._validators = options.validators ?? [];\n }\n\n /**\n * Returns the field's logical name used as the record property key.\n *\n * @returns The logical name string for this field.\n */\n getName(): string {\n return this._name;\n }\n\n /**\n * Returns the field's data type.\n *\n * @returns The FieldType value for this field.\n */\n getType(): FieldType {\n return this._type;\n }\n\n /**\n * Returns the value used when raw data does not contain this field.\n *\n * @returns The configured default value, or undefined if none was specified.\n */\n getDefaultValue(): any {\n return this._defaultValue;\n }\n\n /**\n * Returns the raw-data property name that maps to this field.\n *\n * @returns The mapping key string; defaults to the field name when not explicitly configured.\n */\n getMapping(): string {\n return this._mapping;\n }\n\n /**\n * Returns the human-readable description, falling back to the field name.\n *\n * @returns The description string if configured, otherwise the field name.\n */\n getDescription(): string {\n return this._description ?? this._name;\n }\n\n /**\n * Returns the display order index; -1 means unspecified.\n *\n * @remarks\n * The value is a sort key over a field list that is already in declaration\n * order, and nothing more. Two consequences are worth knowing:\n *\n * - **A field that declares no `order` sorts as though it declared -1**, so\n * it lands ahead of every field declaring 0 or more, and behind one\n * declaring a lower negative. Declaring `order` on some fields and not\n * others therefore moves the undeclared ones to the front rather than\n * leaving them where they were. Declare it on all of them or none.\n * - **When no field declares an `order` every comparison ties.** `Array`\n * sorts are stable, so the list keeps its declaration order — which is\n * the intent. But it also means this value cannot be used to *re-derive*\n * a position that has already been computed. Sorting rendered cells or\n * components by it is a no-op either way: on tied orders because the sort\n * is stable, and on declared ones because everything the framework\n * derives from the field list is already in this order. Order those from\n * the assignment that placed them.\n *\n * @returns The configured order value, or -1 if no order was specified.\n */\n getOrder(): number {\n return this._order ?? -1;\n }\n\n /**\n * Coerces a raw value to this field's type, or runs the custom `convert` hook.\n *\n * @remarks\n * A configured `convert` callback wins over the built-in type conversion. Otherwise\n * `null`/`undefined` short-circuit to themselves (an absent value never becomes `NaN`,\n * `\"null\"`, or an Invalid Date), and any other value is routed through the type switch.\n *\n * @param raw - The raw value to coerce.\n * @param sourceRecord - Optional. The full mapped source object, so a custom `convert`\n * can derive this field from sibling raw values.\n *\n * @returns The coerced value typed according to this field's `type`.\n */\n convertValue(raw: any, sourceRecord?: Record<string, any>): any {\n if (this._convert) {\n return this._convert(raw, sourceRecord);\n }\n\n if (raw === null || raw === undefined) {\n return raw;\n }\n\n return this.convertByType(raw);\n }\n\n /**\n * Coerces a non-null raw value according to this field's built-in `type`.\n *\n * @param raw - The raw value to coerce; guaranteed non-null by the caller.\n *\n * @returns The coerced value, or undefined when the value cannot be coerced to the type.\n */\n private convertByType(raw: any): any {\n switch (this._type) {\n case 'number': {\n if (raw === '') {\n return undefined;\n }\n\n const num = Number(raw);\n\n return num;\n }\n\n case 'boolean': {\n return this.convertBoolean(raw);\n }\n\n case 'date':\n case 'datetime':\n case 'time': {\n if (raw instanceof Date) {\n return raw;\n }\n\n const date = new Date(raw);\n\n return isNaN(date.getTime()) ? undefined : date;\n }\n\n case 'string': {\n return String(raw);\n }\n\n default: {\n return raw;\n }\n }\n }\n\n /**\n * Coerces a non-null raw value to a boolean, honouring the common truthy / falsy\n * string and numeric spellings before falling back to `Boolean(raw)`.\n *\n * @param raw - The raw value to coerce; guaranteed non-null by the caller.\n *\n * @returns The coerced boolean value.\n */\n private convertBoolean(raw: any): boolean {\n const truthy = [true, 1, 'true', '1', 'yes'];\n const falsy = [false, 0, 'false', '0', 'no', ''];\n\n if (truthy.includes(raw)) {\n return true;\n }\n\n if (falsy.includes(raw)) {\n return false;\n }\n\n return Boolean(raw);\n }\n\n /**\n * Returns the configured validation rules, or an empty array.\n *\n * @remarks\n * Evaluated by [`ModelRecord`](/api/data/classes/ModelRecord) on demand; see its\n * pull-based `isValid` / `getErrors` / `validateField` API.\n *\n * @returns The field's validation rules; empty when none were configured.\n */\n getValidators(): ValidationRule[] {\n return this._validators;\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\n/**\n * Serializable filter algebra for {@link AbstractStore}. Descriptors are plain objects so\n * they can cross the worker boundary via structured clone (unlike arbitrary filter\n * functions, which can't). The same evaluator runs on either side.\n *\n * @category Data\n */\nexport type FilterDescriptor =\n | { type: 'eq'; field: string; value: any }\n | { type: 'neq'; field: string; value: any }\n | { type: 'contains'; field: string; value: string; caseSensitive?: boolean }\n | { type: 'startsWith'; field: string; value: string; caseSensitive?: boolean }\n | { type: 'gt'; field: string; value: number | string | Date }\n | { type: 'gte'; field: string; value: number | string | Date }\n | { type: 'lt'; field: string; value: number | string | Date }\n | { type: 'lte'; field: string; value: number | string | Date }\n | { type: 'in'; field: string; values: any[] }\n | { type: 'and'; filters: FilterDescriptor[] }\n | { type: 'or'; filters: FilterDescriptor[] }\n | { type: 'not'; filter: FilterDescriptor };\n\n/**\n * Looks up a field's value from either a plain data object or a ModelRecord-like\n * object that exposes a `get(field)` method. Lets the same matcher run on both\n * sides of the worker boundary.\n */\nfunction readField(record: any, field: string): any {\n if (record && typeof record.get === 'function') {\n return record.get(field);\n }\n return record ? record[field] : undefined;\n}\n\n/**\n * Evaluates a FilterDescriptor against a record. Returns true if the record\n * matches the filter. Works with both plain data objects (worker side) and\n * ModelRecord instances (main thread side).\n */\nexport function matchesFilter(record: any, descriptor: FilterDescriptor): boolean {\n switch (descriptor.type) {\n case 'eq':\n return readField(record, descriptor.field) === descriptor.value;\n\n case 'neq':\n return readField(record, descriptor.field) !== descriptor.value;\n\n case 'contains': {\n const raw = readField(record, descriptor.field);\n if (raw == null) return false;\n const haystack = descriptor.caseSensitive ? String(raw) : String(raw).toLowerCase();\n const needle = descriptor.caseSensitive ? descriptor.value : descriptor.value.toLowerCase();\n return haystack.indexOf(needle) !== -1;\n }\n\n case 'startsWith': {\n const raw = readField(record, descriptor.field);\n if (raw == null) return false;\n const haystack = descriptor.caseSensitive ? String(raw) : String(raw).toLowerCase();\n const needle = descriptor.caseSensitive ? descriptor.value : descriptor.value.toLowerCase();\n return haystack.indexOf(needle) === 0;\n }\n\n case 'gt':\n return readField(record, descriptor.field) > descriptor.value;\n\n case 'gte':\n return readField(record, descriptor.field) >= descriptor.value;\n\n case 'lt':\n return readField(record, descriptor.field) < descriptor.value;\n\n case 'lte':\n return readField(record, descriptor.field) <= descriptor.value;\n\n case 'in':\n return descriptor.values.indexOf(readField(record, descriptor.field)) !== -1;\n\n case 'and':\n for (const f of descriptor.filters) {\n if (!matchesFilter(record, f)) return false;\n }\n return true;\n\n case 'or':\n for (const f of descriptor.filters) {\n if (matchesFilter(record, f)) return true;\n }\n return false;\n\n case 'not':\n return !matchesFilter(record, descriptor.filter);\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n//\n// Main-thread client for the StoreWorker. Lazily constructs a single Worker\n// instance shared across all stores; routes requests by requestId so concurrent\n// stores don't crosstalk. Falls back gracefully if Worker isn't available\n// (test environment, server-side, etc.) — the AbstractStore caller checks\n// `isAvailable()` before dispatching.\n\nimport { FilterDescriptor } from \"~/data/FilterDescriptor.js\";\nimport type { FieldType } from \"~/data/Field.js\";\n\n// Vite-specific worker import. The `?worker` suffix tells Vite to bundle the\n// module as a Web Worker entry. The default export is the Worker constructor.\n// @ts-ignore — Vite resolves this at build time; tsc on its own can't.\nimport StoreWorkerCtor from \"~/data/StoreWorker.js?worker\";\n\ntype Direction = \"asc\" | \"desc\";\n\ntype Response = { requestId: number; indices?: number[]; error?: string };\n\ninterface Pending {\n resolve: (indices: number[] | undefined) => void;\n reject: (err: Error) => void;\n}\n\nlet worker: Worker | null = null;\nlet nextRequestId = 1;\nconst pending: Map<number, Pending> = new Map();\n\nfunction ensureWorker(): Worker | null {\n if (worker) return worker;\n if (typeof Worker === \"undefined\") return null;\n\n try {\n worker = new (StoreWorkerCtor as any)() as Worker;\n } catch {\n worker = null;\n return null;\n }\n\n worker.onmessage = (e: MessageEvent<Response>) => {\n const { requestId, indices, error } = e.data;\n const p = pending.get(requestId);\n if (!p) return;\n\n pending.delete(requestId);\n\n if (error) {\n p.reject(new Error(error));\n } else {\n p.resolve(indices);\n }\n };\n\n return worker;\n}\n\nfunction send(message: any): Promise<number[] | undefined> {\n const w = ensureWorker();\n if (!w) {\n return Promise.reject(new Error(\"Worker unavailable\"));\n }\n\n const requestId = nextRequestId++;\n message.requestId = requestId;\n\n return new Promise((resolve, reject) => {\n pending.set(requestId, { resolve, reject });\n w.postMessage(message);\n });\n}\n\nexport const StoreWorkerClient = {\n /**\n * @returns true when a worker can be constructed in this runtime.\n */\n isAvailable(): boolean {\n return ensureWorker() !== null;\n },\n\n /**\n * Ships a fresh snapshot of plain record data to the worker for the given storeId.\n * Subsequent sort/filter requests run against this snapshot until replaced.\n */\n snapshot(storeId: string, records: Array<Record<string, any>>): Promise<void> {\n return send({ type: \"snapshot\", storeId, records }).then(() => undefined);\n },\n\n /**\n * Combined filter + sort in a single round-trip. Either spec may be omitted.\n * The sort spec carries the field's `fieldType` so the worker's comparator\n * stays in parity with the main thread's (locale-aware strings, timestamp\n * dates).\n */\n sortFilter(\n storeId: string,\n sort?: { field: string; direction: Direction; fieldType?: FieldType },\n filter?: FilterDescriptor,\n ): Promise<number[]> {\n return send({ type: \"sortFilter\", storeId, sort, filter }).then(idx => idx ?? []);\n },\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { FieldType } from '~/data/Field.js';\n\n/**\n * Native ordering of two non-null values via `<` / `>`, returning the\n * ascending-sense sign. Used for numeric (and any non-string, non-date) fields.\n *\n * @param av - The left operand.\n * @param bv - The right operand.\n *\n * @returns A negative, zero, or positive number in ascending sense.\n */\nfunction nativeCompare(av: any, bv: any): number {\n return av < bv ? -1 : av > bv ? 1 : 0;\n}\n\n/**\n * Type-aware, locale-aware comparison of two raw field values, returning a\n * negative / zero / positive number in **ascending** sense. This is the single\n * comparator shared by the main thread ({@link AbstractStore}) and the\n * `StoreWorker`, so the two sort paths can never drift.\n *\n * @param av - The left value.\n * @param bv - The right value.\n * @param type - Optional. The field's {@link FieldType}, which selects the\n * comparison strategy (string → locale, date/time → timestamp, else native).\n * When omitted, two string operands still use the locale path.\n *\n * @returns The ascending-sense comparison result.\n *\n * @remarks\n * `null`/`undefined` sort **last**: both null compare equal (`0`), and a single\n * null returns the sign that places it after a non-null value. Callers must\n * leave a null-involving result un-negated (apply sort direction only to a\n * non-null comparison) so nulls stay last regardless of `'asc'`/`'desc'`.\n *\n * String fields (or two string operands when `type` is unknown) use\n * `localeCompare`, so `'Ä'` orders between `'a'` and `'Z'` rather than after\n * `'Z'` by code point. Date/time fields compare by `getTime()` when both\n * operands are `Date`, falling through to native comparison otherwise.\n *\n * @category Data\n */\nexport function compareValues(av: any, bv: any, type?: FieldType): number {\n if (av == null && bv == null) {\n return 0;\n }\n\n if (av == null) {\n return 1;\n }\n\n if (bv == null) {\n return -1;\n }\n\n if (type === 'string' || (type === undefined && typeof av === 'string' && typeof bv === 'string')) {\n return av.localeCompare(bv);\n }\n\n if (type === 'date' || type === 'datetime' || type === 'time') {\n if (av instanceof Date && bv instanceof Date) {\n return nativeCompare(av.getTime(), bv.getTime());\n }\n }\n\n return nativeCompare(av, bv);\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { AbstractModel } from '~/data/AbstractModel.js';\nimport { ModelRecord, type FieldChange } from '~/data/ModelRecord.js';\nimport { Proxy, ReadParams } from '~/data/proxy/Proxy.js';\nimport { FilterDescriptor, matchesFilter } from '~/data/FilterDescriptor.js';\nimport { StoreWorkerClient } from '~/data/StoreWorkerClient.js';\nimport { compareValues } from '~/data/compareValues.js';\nimport { ListenerBag } from '~/core/ListenerBag.js';\n\n/**\n * Datasets above this size are sorted/filtered on a Web Worker so the main\n * thread stays responsive. Below the threshold, the round-trip overhead exceeds\n * the work, so we run synchronously in-process.\n */\nconst WORKER_THRESHOLD = 1000;\nlet nextStoreId = 1;\n\n/**\n * Callback fired when a store event ({@link StoreEvent}) is emitted.\n *\n * @category Data\n */\nexport type StoreListener<T = any> = (payload: T) => void;\n/**\n * Names of the events fired by an {@link AbstractStore}.\n *\n * @category Data\n */\nexport type StoreEvent = 'load' | 'beforeload' | 'datachange' | 'add' | 'remove' | 'clear' | 'beforesync' | 'sync' | 'exception' | 'loadingchange' | 'pagechange' | 'pagechangeblocked' | 'sortchange' | 'filterchange' | 'update' | 'groupchange';\n\n/**\n * The proxy operation that failed in a {@link StoreExceptionEvent}.\n *\n * @category Data\n */\nexport type StoreOperation = 'read' | 'create' | 'update' | 'destroy';\n\n/**\n * Payload for the `'exception'` event, fired when a `load()` read or a `sync()`\n * create/update/destroy fails.\n *\n * @remarks\n * `operation` disambiguates which proxy op failed; `records` carries the\n * offending record(s) — the batch or single record whose op failed — and is\n * empty for a `read` failure. `error` is the raw thrown value.\n *\n * @category Data\n */\nexport interface StoreExceptionEvent {\n operation: StoreOperation;\n records : ModelRecord[];\n error : unknown;\n}\n\n/**\n * Payload for the `'clear'` event fired by {@link AbstractStore.removeAll}.\n *\n * @category Data\n */\nexport interface StoreClearEvent {\n removed: ModelRecord[];\n}\n\n/**\n * Payload for the `'filterchange'` event fired when the active filter list is\n * replaced or cleared.\n *\n * @category Data\n */\nexport interface StoreFilterChangeEvent {\n filters: FilterDescriptor[];\n}\n\n/**\n * Payload for the `'update'` event fired by {@link AbstractStore.notifyRecordChanged}.\n *\n * @category Data\n */\nexport interface StoreUpdateEvent {\n record: ModelRecord;\n /**\n * Field-level diff of the change, keyed by field name. Carried by both the\n * single-`set()` auto-notify and a record edit-batch commit; absent only when\n * a caller invokes {@link AbstractStore.notifyRecordChanged} with no diff.\n */\n changes?: Record<string, FieldChange>;\n}\n\n/**\n * Summary payload for the `'sync'` event: the per-op failures recorded during\n * the just-finished `sync()`, empty when every operation succeeded.\n *\n * @category Data\n */\nexport interface StoreSyncEvent {\n failures: StoreExceptionEvent[];\n}\n\n/**\n * Payload for the `'groupchange'` event fired when {@link AbstractStore.setGroupField}\n * changes the active group field.\n *\n * @category Data\n */\nexport interface StoreGroupChangeEvent {\n groupField: string | null;\n}\n\n/**\n * Describes one column's contribution to a multi-column sort.\n *\n * @category Data\n */\nexport interface SortDescriptor {\n field: string;\n dir : 'asc' | 'desc';\n /**\n * Optional custom comparator returning the ascending-sense ordering of two\n * records. Main-thread only: a function cannot cross the structured-clone\n * boundary, so a sorter carrying a `sorterFn` forces {@link AbstractStore}'s\n * in-process sort path even for datasets above the worker threshold.\n */\n sorterFn?: (a: ModelRecord, b: ModelRecord) => number;\n}\n\n/**\n * Construction-time options shared by every {@link AbstractStore} subclass.\n *\n * @remarks Concrete stores extend this interface with the fields specific to\n * their model/proxy wiring (e.g. {@link StoreOptions}, {@link MemoryStoreOptions},\n * {@link AjaxStoreOptions}).\n *\n * @category Data\n */\nexport interface AbstractStoreOptions {\n pageSize?: number;\n page?: number;\n sorters?: SortDescriptor[];\n filters?: FilterDescriptor[];\n remoteSort?: boolean;\n remoteFilter?: boolean;\n autoLoad?: boolean;\n syncErrorPolicy?: 'stop' | 'continue';\n groupField?: string;\n cascadeSync?: boolean;\n listeners?: Partial<Record<StoreEvent, StoreListener>>;\n}\n\n/**\n * Abstract base class for all data stores.\n * Manages a collection of ModelRecord instances with support for loading, CRUD mutations,\n * filtering, sorting, and event notification.\n *\n * @remarks\n * The store maintains two parallel arrays: `allRecords` (the master list) and `records`\n * (the filtered and sorted view). Mutations always target `allRecords` and then rebuild\n * the view by calling `applyView()`. Consumers should read from `getRecords()` or\n * `getAt()` rather than accessing the raw arrays directly.\n *\n * @category Data\n */\nexport abstract class AbstractStore {\n\n abstract readonly model: AbstractModel;\n abstract readonly proxy: Proxy | undefined;\n\n private _allRecords: ModelRecord[] = [];\n private _records: ModelRecord[] = [];\n // Whether the most recent applyView() offloaded to the worker (so `_records`\n // is populated only when its promise resolves, not synchronously). Read right\n // after an ingestRaw() to decide whether the 'load' emit must wait for the\n // view. See applyView / loadData.\n private _viewAsync: boolean = false;\n private _pendingRemoved: ModelRecord[] = [];\n private _activeFilters: FilterDescriptor[] = [];\n private _activeSorters: SortDescriptor[] = [];\n private _listeners: ListenerBag<StoreEvent> = new ListenerBag<StoreEvent>();\n\n // id → record index over `_allRecords`, rebuilt by `rebuildIdIndex()` on\n // every `applyView()` so `getById()` is O(1). Stays empty (so getById returns\n // undefined) when the model has no primary key.\n private _idIndex: Map<any, ModelRecord> = new Map();\n\n // Active single-level group field, or null when grouping is off. Read by\n // `getGroupString()` / `getGroups()`; set via `setGroupField()`.\n private _groupField: string | null = null;\n\n // Worker-offload state. Each store gets a unique id so the shared worker can\n // keep snapshots from different stores apart. `snapshotDirty` flags whether\n // the worker's copy of allRecords is stale (re-shipped on the next applyView\n // when the dataset is over the threshold).\n private _storeId: string = 'store-' + (nextStoreId++);\n private _snapshotDirty: boolean = true;\n private _loading: boolean = false;\n\n // Store-level edit-batch flag. While set, owned records suppress their own\n // auto-notify (consulted through their back-ref by ModelRecord.set()); the\n // matching commitEdit() fires a single coalesced 'datachange'.\n private _batching: boolean = false;\n\n // ── Server-side pagination state ─────────────────────────────────────────\n // `_pageSize` is undefined until `setPageSize(n)` is called; while undefined,\n // `load()` calls `proxy.read()` with no arguments and the store behaves\n // identically to its unpaginated form.\n private _page: number = 1;\n private _pageSize: number | undefined = undefined;\n private _totalCount: number | undefined = undefined;\n\n // ── Remote sort/filter + load concurrency state ──────────────────────────\n // When set, active sorters/filters are serialized into ReadParams and a\n // mutation triggers a reload, instead of being applied locally by applyView.\n private _remoteSort: boolean = false;\n private _remoteFilter: boolean = false;\n\n // Controls what sync() does after an op fails: 'stop' aborts the remaining\n // sync (already-committed records stay committed; failed/untouched records\n // remain pending), 'continue' records the failure and proceeds.\n private _syncErrorPolicy: 'stop' | 'continue' = 'stop';\n\n // When true (default), sync() walks each parent record's materialised hasMany\n // child stores after the parent creates/updates resolve, stamping the parent\n // foreign key and cascading the child store's own sync(). Set false to opt a\n // store out of the cascade.\n private _cascadeSync: boolean = true;\n\n // `_loadSeq` is bumped on every load() so a stale in-flight response (whose\n // captured seq no longer matches) is ignored. `_loadAbort` cancels the\n // previous HTTP read when a newer load starts.\n private _loadSeq: number = 0;\n private _loadAbort: AbortController | undefined = undefined;\n\n /**\n * Applies an {@link AbstractStoreOptions} bag to this store. Subclasses\n * should call this from their constructor (after `model` and `proxy` are\n * assigned) so that pagination, sort, filter, and listener defaults are\n * dispatched to the existing setters.\n *\n * @param options - The options bag carrying the values to apply.\n *\n * @remarks Listener registrations and filters are applied first so that an\n * `autoLoad: true` flag triggers a `load()` whose result fires the\n * already-registered `'load'` listener.\n */\n protected applyOptions(options: AbstractStoreOptions): void {\n if (options.listeners !== undefined) {\n for (const event of Object.keys(options.listeners) as StoreEvent[]) {\n const listener = options.listeners[event];\n\n if (listener !== undefined) {\n this.on(event, listener);\n }\n }\n }\n\n if (options.pageSize !== undefined) {\n this.setPageSize(options.pageSize);\n }\n\n if (options.page !== undefined) {\n this._page = options.page;\n }\n\n if (options.sorters !== undefined && options.sorters.length > 0) {\n this._activeSorters = options.sorters.slice();\n }\n\n if (options.filters !== undefined && options.filters.length > 0) {\n this._activeFilters = options.filters.slice();\n }\n\n if (options.remoteSort !== undefined) {\n this._remoteSort = options.remoteSort;\n }\n\n if (options.remoteFilter !== undefined) {\n this._remoteFilter = options.remoteFilter;\n }\n\n if (options.syncErrorPolicy !== undefined) {\n this._syncErrorPolicy = options.syncErrorPolicy;\n }\n\n if (options.cascadeSync !== undefined) {\n this._cascadeSync = options.cascadeSync;\n }\n\n if (options.groupField !== undefined) {\n this.setGroupField(options.groupField);\n }\n\n if (options.autoLoad === true) {\n void this.load();\n }\n }\n\n // ── Loading ──────────────────────────────────────────────────────────────\n\n /**\n * Fetches data through the proxy, replaces all records, and fires the 'load' event.\n *\n * @returns A promise that resolves when the data has been loaded and the view rebuilt.\n *\n * @remarks\n * Throws an `Error` if no proxy is configured. Any existing records (including pending\n * removals) are discarded when new data is ingested.\n *\n * Fires `'beforeload'` before the proxy read. A read failure emits an\n * `'exception'` event ({@link StoreExceptionEvent} with `operation: 'read'`\n * and empty `records`) and then re-throws so existing `await store.load()`\n * call sites still observe the rejection. An aborted or superseded load is a\n * silent no-op and emits neither `'exception'` nor `'load'`.\n */\n async load(): Promise<void> {\n if (!this.proxy) {\n throw new Error('Store.load() called but no proxy is configured');\n }\n\n this.emit('beforeload', {});\n\n const seq = ++this._loadSeq;\n\n this._loadAbort?.abort();\n\n const controller = new AbortController();\n this._loadAbort = controller;\n\n this.setLoading(true);\n\n try {\n const params = this.buildReadParams(controller.signal);\n const raw = await this.proxy.read(params);\n\n if (seq !== this._loadSeq) {\n return;\n }\n\n // Await the view build so a worker-offloaded sort/filter has\n // populated `_records` before 'load' fires; below the worker\n // threshold this resolves synchronously. load() is already async, so\n // there is no caller-visible timing change.\n await this.ingestRaw(raw);\n\n this._totalCount = this.proxy.getLastTotalCount();\n\n this.emit('load', { records: this._records });\n } catch (err) {\n // An aborted fetch or a superseded load is a silent no-op; only a\n // genuine failure from the current load emits 'exception' and\n // propagates to the awaiter.\n if ((err as Error).name === 'AbortError' || seq !== this._loadSeq) {\n return;\n }\n\n this.emit('exception', { operation: 'read', records: [], error: err });\n\n throw err;\n } finally {\n if (seq === this._loadSeq) {\n this.setLoading(false);\n }\n }\n }\n\n /**\n * Builds the {@link ReadParams} for a load: pagination when enabled, plus\n * active sorters/filters when `remoteSort`/`remoteFilter` are on, plus the\n * abort signal.\n *\n * @param signal - The abort signal for the in-flight HTTP read.\n *\n * @returns A ReadParams object, or undefined when nothing applies — so an\n * unpaginated client-side store still calls `read()` with no arguments and\n * pagination-unaware proxies keep ignoring it.\n */\n private buildReadParams(signal: AbortSignal): ReadParams | undefined {\n const params: ReadParams = {};\n\n if (this._pageSize != null) {\n params.page = this._page;\n params.pageSize = this._pageSize;\n }\n\n if (this._remoteSort && this._activeSorters.length > 0) {\n params.sorters = this.getActiveSorters();\n }\n\n if (this._remoteFilter && this._activeFilters.length > 0) {\n params.filters = this.getActiveFilters();\n }\n\n if (params.page == null && params.sorters == null && params.filters == null) {\n return undefined;\n }\n\n params.signal = signal;\n\n return params;\n }\n\n /**\n * Returns whether the store is currently loading data.\n *\n * @returns True while `load()` is in-flight.\n */\n isLoading(): boolean {\n return this._loading;\n }\n\n /**\n * Sets the loading flag and fires `'loadingchange'` only when the value actually changes.\n *\n * @param value - The new loading state.\n */\n private setLoading(value: boolean): void {\n if (this._loading === value) {\n return;\n }\n\n this._loading = value;\n this.emit('loadingchange', { loading: value });\n }\n\n /**\n * Loads raw data directly without going through the proxy, then fires 'load'.\n *\n * @param data - An array of plain objects to convert into ModelRecords.\n */\n loadData(data: any[]): void {\n const pending = this.ingestRaw(data);\n\n // When applyView built the view synchronously (below the worker\n // threshold, or no worker available) `_records` is already populated, so\n // emit 'load' synchronously — consumers and tests rely on that timing.\n // When it offloaded to the worker, `_records` is not ready yet; defer the\n // emit until the worker resolves so listeners never render an empty view.\n if (this._viewAsync) {\n void pending.then(() => this.emit('load', { records: this._records }));\n } else {\n this.emit('load', { records: this._records });\n }\n }\n\n // ── Pagination ───────────────────────────────────────────────────────────\n\n /**\n * Enables server-side pagination at the given page size and resets to page 1.\n *\n * @param n - The number of records to request per page. Must be a positive integer.\n *\n * @remarks\n * Calling this method opts the store into the paginated `load()` path, where\n * [`ReadParams`](/api/data/interfaces/ReadParams) are forwarded to the proxy. Paginated mode also causes\n * `sort()` and `clearFilter()` to reset to page 1 and re-fetch from the proxy.\n * Fires `'pagechange'`.\n */\n setPageSize(n: number): this {\n this._pageSize = n;\n this._page = 1;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n\n return this;\n }\n\n /**\n * Returns the configured page size, or undefined if pagination is disabled.\n *\n * @returns The page size set via {@link setPageSize}, or undefined.\n */\n getPageSize(): number | undefined {\n return this._pageSize;\n }\n\n /**\n * Returns the current 1-based page number.\n *\n * @returns The current page (defaults to 1 even when pagination is disabled).\n */\n getPage(): number {\n return this._page;\n }\n\n /**\n * Returns the total record count reported by the most recent paginated load.\n *\n * @returns The total count from the proxy, or undefined when no paginated\n * load has occurred or the proxy did not report one.\n */\n getTotalCount(): number | undefined {\n return this._totalCount;\n }\n\n /**\n * Returns the total number of pages, derived from the page size and total count.\n *\n * @returns The number of pages, or undefined when either piece of information\n * is missing.\n */\n getTotalPages(): number | undefined {\n if (this._pageSize == null || this._totalCount == null) {\n return undefined;\n }\n\n return Math.max(1, Math.ceil(this._totalCount / this._pageSize));\n }\n\n /**\n * Advances to the next page and reloads, unless already on the last page.\n *\n * @remarks\n * No-op when pagination is disabled or the current page equals the total\n * page count. When the store has pending unsynced changes, the navigation\n * is blocked and `'pagechangeblocked'` is emitted instead, so the user can\n * sync or reject before leaving the page. Otherwise fires `'pagechange'`\n * and triggers a fire-and-forget reload.\n */\n nextPage(): void {\n if (this._pageSize == null) {\n return;\n }\n\n const total = this.getTotalPages();\n if (total != null && this._page >= total) {\n return;\n }\n\n if (this.hasPendingChanges()) {\n this.emit('pagechangeblocked', { from: this._page, to: this._page + 1 });\n return;\n }\n\n this._page++;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n void this.load();\n }\n\n /**\n * Returns to the previous page and reloads, unless already on page 1.\n *\n * @remarks\n * Blocked when the store has pending changes — emits `'pagechangeblocked'`\n * instead of firing `'pagechange'`.\n */\n prevPage(): void {\n if (this._pageSize == null || this._page <= 1) {\n return;\n }\n\n if (this.hasPendingChanges()) {\n this.emit('pagechangeblocked', { from: this._page, to: this._page - 1 });\n return;\n }\n\n this._page--;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n void this.load();\n }\n\n /**\n * Jumps to the given 1-based page and reloads.\n *\n * @param n - The target page number; clamped to `[1, totalPages]` when total is known.\n *\n * @remarks\n * No-op when pagination is disabled. Blocked when the store has pending\n * changes — emits `'pagechangeblocked'` instead.\n */\n goToPage(n: number): this {\n if (this._pageSize == null) {\n return this;\n }\n\n const total = this.getTotalPages();\n const upper = total ?? n;\n const target = Math.max(1, Math.min(n, upper));\n\n if (target === this._page) {\n return this;\n }\n\n if (this.hasPendingChanges()) {\n this.emit('pagechangeblocked', { from: this._page, to: target });\n return this;\n }\n\n this._page = target;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n void this.load();\n\n return this;\n }\n\n /**\n * Converts raw objects to ModelRecords and rebuilds the filtered/sorted view.\n *\n * @param data - An array of plain objects to convert via the model's `createRecord`.\n *\n * @remarks\n * A reload replaces the store's contents with a fresh authoritative snapshot,\n * so any removal queued against the previous snapshot is discarded — its target\n * may not exist in the new data, and letting it fire on the next `sync()` would\n * destroy a server row the user never asked to delete. The queued records were\n * already ownership-released by `remove()`, so clearing the array suffices.\n */\n private ingestRaw(data: any[]): Promise<void> {\n this.setOwnership(this._allRecords, false);\n this._allRecords = data.map(item => this.model.createRecord(item));\n this.setOwnership(this._allRecords, true);\n this._pendingRemoved = [];\n this._snapshotDirty = true;\n\n return this.applyView();\n }\n\n // ── Access ───────────────────────────────────────────────────────────────\n\n /**\n * Returns a copy of the currently filtered and sorted records.\n *\n * @returns A shallow-copy array of the records in the active view.\n */\n getRecords(): ModelRecord[] {\n return this._records.slice();\n }\n\n /**\n * Returns a copy of all records, bypassing any active filters or sorting.\n *\n * @returns A shallow-copy array of every record in the store.\n */\n getAll(): ModelRecord[] {\n return this._allRecords.slice();\n }\n\n /**\n * Returns the number of records in the current filtered view.\n *\n * @returns The count of visible records after filters are applied.\n *\n * @remarks This is the store's **count aggregate** — the number of rows the\n * view exposes after filtering. There is no separate `count()` method;\n * `getCount()` fills that role, consistent with {@link sum} / {@link average}\n * / {@link min} / {@link max} operating over the same filtered view.\n */\n getCount(): number {\n return this._records.length;\n }\n\n /**\n * Returns the record at the given index in the filtered view, or undefined.\n *\n * @param index - The zero-based position in the filtered and sorted view.\n *\n * @returns The ModelRecord at that position, or undefined if the index is out of range.\n */\n getAt(index: number): ModelRecord | undefined {\n return this._records[index];\n }\n\n /**\n * Finds a record by its primary-key value, searching all records (ignoring filters).\n *\n * @param id - The primary key value to search for.\n *\n * @returns The matching ModelRecord, or undefined if not found or no primary key is defined.\n *\n * @remarks O(1): backed by an internal id→record index that is refreshed on\n * every `applyView()`, so it tracks every mutation of the master list.\n * Returns undefined when the model defines no primary key (the index stays\n * empty).\n */\n getById(id: any): ModelRecord | undefined {\n return this._idIndex.get(id);\n }\n\n /**\n * Returns the position of a record within the filtered view.\n *\n * @param record - The record to locate.\n *\n * @returns The zero-based index in the view, or -1 when the record is not\n * in the current view (filtered out or absent).\n */\n indexOf(record: ModelRecord): number {\n return this._records.indexOf(record);\n }\n\n /**\n * Returns an inclusive slice of the filtered view between two indices.\n *\n * @param start - The first index to include; clamped up to 0.\n * @param end - The last index to include; clamped down to the final index.\n *\n * @returns A shallow-copy array of the records in `[start, end]`; empty when\n * the clamped range is empty.\n */\n getRange(start: number, end: number): ModelRecord[] {\n const lo = Math.max(0, start);\n const hi = Math.min(end, this._records.length - 1);\n\n if (hi < lo) {\n return [];\n }\n\n return this._records.slice(lo, hi + 1);\n }\n\n /**\n * Returns the first record in the filtered view.\n *\n * @returns The record at view index 0, or undefined when the view is empty.\n */\n first(): ModelRecord | undefined {\n return this._records[0];\n }\n\n /**\n * Returns the last record in the filtered view.\n *\n * @returns The record at the final view index, or undefined when the view is empty.\n */\n last(): ModelRecord | undefined {\n return this._records[this._records.length - 1];\n }\n\n /**\n * Invokes a callback for each record in the filtered view, in view order.\n *\n * @param fn - The callback applied to each record and its view index.\n */\n each(fn: (record: ModelRecord, index: number) => void): void {\n this._records.forEach((record, index) => fn(record, index));\n }\n\n /**\n * Returns whether a record is present in the filtered view.\n *\n * @param record - The record to test for membership.\n *\n * @returns True when the record is in the current view.\n */\n contains(record: ModelRecord): boolean {\n return this._records.includes(record);\n }\n\n /**\n * Returns the first record in the filtered view where property equals value.\n *\n * @param property - The field name to match against.\n * @param value - The value to compare using strict equality.\n *\n * @returns The first matching ModelRecord, or undefined if none is found.\n */\n find(property: string, value: any): ModelRecord | undefined {\n return this._records.find(r => r.get(property) === value);\n }\n\n /**\n * Returns all records in the filtered view where property equals value.\n *\n * @param property - The field name to match against.\n * @param value - The value to compare using strict equality.\n *\n * @returns An array of all matching ModelRecords; empty if none match.\n */\n findAll(property: string, value: any): ModelRecord[] {\n return this._records.filter(r => r.get(property) === value);\n }\n\n // ── Mutation ─────────────────────────────────────────────────────────────\n\n /**\n * Adds one or more records (marked as new), updates the view, and fires 'add'/'datachange'.\n *\n * @param data - A single plain object or an array of plain objects to add.\n *\n * @returns An array of the newly created ModelRecord instances.\n */\n add(data: any | any[]): ModelRecord[] {\n return this.insertAt(null, data);\n }\n\n /**\n * Inserts one or more records (marked as new) into the master list at a\n * clamped position, rebuilds the view, and fires 'add'/'datachange'.\n *\n * @param index - The target position in the master list; clamped to\n * `[0, allRecords.length]`.\n * @param data - A single plain object or an array of plain objects to insert.\n *\n * @returns An array of the newly created ModelRecord instances.\n *\n * @remarks\n * Mirrors {@link add} but splices at `index` instead of appending. The\n * insertion position is into the master list; the visible position in the\n * view still depends on any active sort and filter.\n */\n insert(index: number, data: any | any[]): ModelRecord[] {\n return this.insertAt(index, data);\n }\n\n /**\n * Shared body of {@link add} and {@link insert}: creates records marked new,\n * places them in the master list, rebuilds the view, and fires 'add' then\n * 'datachange'.\n *\n * @param index - `null` appends to the master list; a number splices at the\n * position clamped to `[0, allRecords.length]`.\n * @param data - A single plain object or an array of plain objects.\n *\n * @returns An array of the newly created ModelRecord instances.\n */\n private insertAt(index: number | null, data: any | any[]): ModelRecord[] {\n const items = Array.isArray(data) ? data : [data];\n const added = items.map(item => {\n const record = this.model.createRecord(item);\n\n record.markAsNew();\n\n return record;\n });\n\n if (index === null) {\n this._allRecords.push(...added);\n } else {\n const at = Math.max(0, Math.min(index, this._allRecords.length));\n\n this._allRecords.splice(at, 0, ...added);\n }\n\n this.setOwnership(added, true);\n this._snapshotDirty = true;\n this.applyView();\n\n this.emit('add', { records: added });\n this.emit('datachange', {});\n\n return added;\n }\n\n /**\n * Removes a record from the store, queuing it for deletion on the next sync.\n *\n * @param record - The ModelRecord to remove.\n *\n * @remarks\n * New records (never synced) are discarded immediately without being queued.\n * Records that have been persisted are added to `pendingRemoved` and sent to\n * the proxy during the next call to `sync()`.\n */\n remove(record: ModelRecord): this {\n const allIdx = this._allRecords.indexOf(record);\n if (allIdx === -1) {\n return this;\n }\n\n this._allRecords.splice(allIdx, 1);\n this.setOwnership([record], false);\n this._snapshotDirty = true;\n\n if (!record.isNew()) {\n this._pendingRemoved.push(record);\n }\n\n this.applyView();\n\n this.emit('remove', { record });\n this.emit('datachange', {});\n\n return this;\n }\n\n /**\n * Removes all records, queuing existing (non-new) ones for deletion on the next sync.\n *\n * @remarks\n * Only persisted records are queued for removal; records that are still marked\n * as new are simply discarded.\n */\n removeAll(): this {\n const removed = this._allRecords.slice();\n\n this._pendingRemoved.push(...this._allRecords.filter(r => !r.isNew()));\n\n this.setOwnership(removed, false);\n this._allRecords = [];\n this._snapshotDirty = true;\n this.applyView();\n\n this.emit('clear', { removed });\n this.emit('datachange', {});\n\n return this;\n }\n\n /**\n * Appends already-committed records to the master list and rebuilds the\n * view, without marking them new or firing `'add'`.\n *\n * @param records - The records to append; they keep their committed state.\n *\n * @remarks\n * The lazy-load seam for {@link TreeStore}: children fetched on a node\n * expand are server-backed (not pending inserts), so they bypass the\n * new-record path of {@link add}. This is the only sanctioned way for a\n * subclass to grow the master list with committed records.\n */\n protected appendRecords(records: ModelRecord[]): void {\n this._allRecords.push(...records);\n this.setOwnership(records, true);\n this._snapshotDirty = true;\n this.applyView();\n }\n\n /**\n * Signals that a record's fields were mutated outside the store's own\n * mutation methods (e.g. an in-cell edit in a Table).\n *\n * @param record - The record that was edited; carried on the `'update'`\n * event so listeners can identify it.\n * @param changes - Optional. The field-level diff of the change, carried on\n * the `'update'` event for listeners that want per-field granularity.\n *\n * @remarks\n * Fires `'update'` ({@link StoreUpdateEvent}) followed by `'datachange'` so\n * listeners (toolbars, pagination bars, etc.) re-evaluate state such as\n * {@link hasPendingChanges}. A store-owned record calls this automatically\n * from `set()`; it remains public for the standalone/manual case (an unowned\n * record, or forcing a refresh).\n */\n notifyRecordChanged(record: ModelRecord, changes?: Record<string, FieldChange>): void {\n this.emit('update', { record, changes });\n this.emit('datachange', {});\n }\n\n /**\n * Opens a store-level edit batch: owned records suppress their own\n * auto-notify until the matching {@link commitEdit}.\n *\n * @returns This store, for method chaining.\n *\n * @remarks\n * The coarse counterpart to a record edit batch — use it to mutate many\n * records and refresh bound views once. Records reach this flag through their\n * back-ref. Not nested by any framework caller, so the flag is a plain\n * boolean rather than a depth counter.\n */\n beginEdit(): this {\n this._batching = true;\n\n return this;\n }\n\n /**\n * Closes a store-level edit batch and fires a single `'datachange'` so bound\n * views refresh once for the whole batch.\n *\n * @returns This store, for method chaining.\n *\n * @remarks\n * Deliberately emits only `'datachange'`, not per-record `'update'`s:\n * replaying every record's update would defeat the coalescing the batch\n * exists to provide. Use a record edit batch when per-record granularity is\n * needed.\n */\n commitEdit(): this {\n this._batching = false;\n\n this.emit('datachange', {});\n\n return this;\n }\n\n /**\n * Reports whether a store-level edit batch is currently open.\n *\n * @returns True while a {@link beginEdit} batch is open.\n *\n * @internal Framework wiring; consulted by an owned record's `set()` through\n * its back-ref to decide whether to suppress its auto-notify.\n */\n isBatching(): boolean {\n return this._batching;\n }\n\n /**\n * Stamps or clears the owning-store back-ref on a set of records as they\n * enter or leave this store's master list.\n *\n * @param records - The records joining or leaving the master list.\n * @param owned - True to adopt the records into this store, false to release.\n *\n * @remarks\n * The single seam that keeps {@link ModelRecord}'s auto-notify back-ref in\n * step with `_allRecords` membership. Called at every site that grows or\n * shrinks the master list.\n */\n private setOwnership(records: ModelRecord[], owned: boolean): void {\n for (const record of records) {\n if (owned) {\n record.adoptedBy(this);\n } else {\n record.released();\n }\n }\n }\n\n /**\n * Returns whether the store holds any unsynced state.\n *\n * @returns True if any record is dirty, any record is new, or there are\n * queued removals waiting to be synced.\n *\n * @remarks\n * Used by the pagination guard to prevent navigation that would silently\n * discard in-memory edits. Also useful for \"unsaved changes\" prompts.\n */\n hasPendingChanges(): boolean {\n if (this._pendingRemoved.length > 0) {\n return true;\n }\n\n for (const record of this._allRecords) {\n if (record.isNew() || record.isDirty()) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Discards all unsynced changes — reverts dirty records, drops new ones,\n * and restores pending removals — then fires 'datachange'.\n *\n * @remarks\n * Pending removals are pushed back into `allRecords` (in their original\n * order is not preserved; they are appended) so the user can recover from\n * an accidental removal. Dirty records are restored to their last\n * committed values. New records are dropped outright since they were never\n * persisted.\n */\n reject(): void {\n const previouslyOwned = this._allRecords;\n const survivors: ModelRecord[] = [];\n\n for (const record of this._allRecords) {\n if (record.isNew()) {\n continue;\n }\n\n if (record.isDirty()) {\n record.reject();\n }\n\n survivors.push(record);\n }\n\n if (this._pendingRemoved.length > 0) {\n survivors.push(...this._pendingRemoved);\n this._pendingRemoved = [];\n }\n\n this._allRecords = survivors;\n\n // Release everyone who was owned (survivors + dropped new records), then\n // re-adopt the final set: dropped new records stay released, restored\n // pending-removals (released by remove()) are re-adopted.\n this.setOwnership(previouslyOwned, false);\n this.setOwnership(survivors, true);\n\n this._snapshotDirty = true;\n this.applyView();\n\n this.emit('datachange', {});\n }\n\n /**\n * Persists new, dirty, and removed records via the proxy, then fires 'sync'/'datachange'.\n *\n * @returns A promise that resolves when the sync run has settled.\n *\n * @remarks\n * Sync is a no-op when no proxy is configured. Operations run in order:\n * creates first, then updates, then deletes — and use the proxy's optional\n * batch hooks ({@link Proxy.createBatch}/{@link Proxy.updateBatch}/{@link Proxy.destroyBatch})\n * when present, falling back to one request per record otherwise. Each record\n * is committed only after its own op succeeds, so a record never appears\n * committed unless the server accepted it.\n *\n * **Contract change:** `sync()` no longer rejects on a transport failure. A\n * failed op emits an `'exception'` event ({@link StoreExceptionEvent}) and is\n * recorded; `sync()` always resolves and fires `'sync'` with a\n * {@link StoreSyncEvent} listing the failures. The `syncErrorPolicy` option\n * controls what happens after the first failure: `'stop'` (default) aborts\n * the remaining sync (already-committed records stay committed; failed and\n * untouched records remain pending for the next `sync()`), `'continue'`\n * proceeds through every record/batch. Callers that previously relied on a\n * rejected promise should switch to the `'exception'` event or the `'sync'`\n * payload's `failures`.\n */\n async sync(): Promise<void> {\n const proxy = this.proxy;\n\n if (!proxy) {\n return;\n }\n\n this.emit('beforesync', {});\n\n const failures: StoreExceptionEvent[] = [];\n\n // Each phase returns true when the run should stop early ('stop' policy\n // after a failure); the || chain then short-circuits the later phases.\n // Under 'continue' every phase returns false and all phases run. The\n // hasMany cascade runs after creates/updates resolve (so parents carry\n // their server ids) and before deletes (children sync before the parent\n // is removed); a stopped run skips it like any later phase.\n const stopped = await this.syncCreates(proxy, failures)\n || await this.syncUpdates(proxy, failures);\n\n if (!stopped) {\n await this.syncCascade();\n\n await this.syncDeletes(proxy, failures);\n }\n\n this.emit('sync', { failures });\n this.emit('datachange', {});\n }\n\n /**\n * Cascades persistence into each parent record's materialised hasMany child\n * stores: stamps the parent foreign key onto every child, then runs the\n * child store's own `sync()`.\n *\n * @returns A promise that resolves when every cascaded child sync has settled.\n *\n * @remarks\n * No-op when `cascadeSync` is disabled. Only associations whose child store\n * was actually built (via {@link ModelRecord.getAssociated}) are walked, so a\n * loaded-but-untouched parent costs nothing. Reusing the child store's own\n * `sync()` means child `'exception'` events, `syncErrorPolicy`, and batch\n * behaviour all apply to the cascade unchanged; failures surface on the child\n * store's own event surface, not the parent's `'sync'` payload.\n */\n private async syncCascade(): Promise<void> {\n if (!this._cascadeSync) {\n return;\n }\n\n for (const parent of this._allRecords) {\n for (const association of parent.getModel().getAssociations()) {\n if (association.kind !== 'hasMany' || !parent.hasChildStore(association.getAccessor())) {\n continue;\n }\n\n const child = parent.getAssociated(association.getAccessor());\n\n this.stampForeignKeys(child, association.getForeignKey(), parent);\n\n await child.sync();\n }\n }\n }\n\n /**\n * Stamps the parent's id onto the foreign-key field of every record in a\n * hasMany child store, so a child created against a brand-new parent picks up\n * the real server id before it is itself persisted.\n *\n * @param child - The parent-scoped child store.\n * @param foreignKey - The child field that holds the owner's id.\n * @param parent - The owning record, whose `getId()` supplies the FK value.\n *\n * @remarks\n * `set()` is a no-op when the value already matches, so a child added to an\n * already-persisted parent (already carrying the correct FK) is untouched.\n * When the parent still has no id (its create failed or was skipped), the\n * stamp writes `undefined` and the child is not falsely re-keyed.\n */\n private stampForeignKeys(child: AbstractStore, foreignKey: string, parent: ModelRecord): void {\n const parentId = parent.getId();\n\n if (parentId === undefined) {\n return;\n }\n\n for (const record of child.getAll()) {\n record.setSilent(foreignKey, parentId);\n }\n }\n\n /**\n * Persists the new records, by batch when the proxy advertises\n * {@link Proxy.createBatch}, else one {@link Proxy.create} per record.\n *\n * @param proxy - The configured proxy.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when the run should stop early (policy `'stop'` and this\n * phase recorded a failure).\n */\n private async syncCreates(proxy: Proxy, failures: StoreExceptionEvent[]): Promise<boolean> {\n const created = this._allRecords.filter(r => r.isNew());\n\n if (created.length === 0) {\n return false;\n }\n\n if (proxy.createBatch) {\n return this.runBatch('create', created, failures, records => proxy.createBatch!(records));\n }\n\n return this.runPerRecord('create', created, failures, record => proxy.create(record));\n }\n\n /**\n * Persists the dirty (non-new) records, by batch when the proxy advertises\n * {@link Proxy.updateBatch}, else one {@link Proxy.update} per record.\n *\n * @param proxy - The configured proxy.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when the run should stop early.\n */\n private async syncUpdates(proxy: Proxy, failures: StoreExceptionEvent[]): Promise<boolean> {\n const dirty = this._allRecords.filter(r => r.isDirty() && !r.isNew());\n\n if (dirty.length === 0) {\n return false;\n }\n\n if (proxy.updateBatch) {\n return this.runBatch('update', dirty, failures, records => proxy.updateBatch!(records));\n }\n\n return this.runPerRecord('update', dirty, failures, record => proxy.update(record));\n }\n\n /**\n * Destroys the pending-removed records, by batch when the proxy advertises\n * {@link Proxy.destroyBatch}, else one {@link Proxy.destroy} per record.\n * Only successfully-destroyed records are cleared from the pending queue.\n *\n * @param proxy - The configured proxy.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when the run should stop early.\n */\n private async syncDeletes(proxy: Proxy, failures: StoreExceptionEvent[]): Promise<boolean> {\n // Snapshot the queue so a failure's 'exception' payload holds an immutable\n // copy (matching the create/update phases, whose filter() returns a fresh\n // array) rather than aliasing the live _pendingRemoved array.\n const removed = this._pendingRemoved.slice();\n\n if (removed.length === 0) {\n return false;\n }\n\n if (proxy.destroyBatch) {\n try {\n await proxy.destroyBatch(removed);\n this._pendingRemoved = [];\n\n return false;\n } catch (err) {\n return this.recordFailure('destroy', removed, err, failures);\n }\n }\n\n const survivors: ModelRecord[] = [];\n let stopped = false;\n\n for (const record of removed) {\n if (stopped) {\n survivors.push(record);\n continue;\n }\n\n try {\n await proxy.destroy(record);\n } catch (err) {\n survivors.push(record);\n stopped = this.recordFailure('destroy', [record], err, failures);\n }\n }\n\n this._pendingRemoved = survivors;\n\n return stopped;\n }\n\n /**\n * Runs a create/update batch op, committing every record positionally from\n * the server response, or recording one failure for the whole batch.\n *\n * @param operation - The op kind, for the failure payload.\n * @param records - The batch's records, in request order.\n * @param failures - The accumulator that collects this run's op failures.\n * @param call - Issues the batch request and resolves to per-record server data in input order.\n *\n * @returns True when the run should stop early.\n */\n private async runBatch(operation: 'create' | 'update', records: ModelRecord[], failures: StoreExceptionEvent[], call: (records: ModelRecord[]) => Promise<Record<string, any>[]>): Promise<boolean> {\n try {\n const serverData = await call(records);\n\n records.forEach((record, i) => {\n this.commitFromServerData(record, serverData[i] ?? {});\n });\n\n return false;\n } catch (err) {\n return this.recordFailure(operation, records, err, failures);\n }\n }\n\n /**\n * Runs a create/update op one record at a time, committing each on success\n * and recording a per-record failure otherwise.\n *\n * @param operation - The op kind, for the failure payload.\n * @param records - The records to persist, in order.\n * @param failures - The accumulator that collects this run's op failures.\n * @param call - Issues a single-record request and resolves to that record's server data.\n *\n * @returns True when the run should stop early.\n */\n private async runPerRecord(operation: 'create' | 'update', records: ModelRecord[], failures: StoreExceptionEvent[], call: (record: ModelRecord) => Promise<Record<string, any>>): Promise<boolean> {\n for (const record of records) {\n try {\n const serverData = await call(record);\n\n this.commitFromServerData(record, serverData);\n } catch (err) {\n if (this.recordFailure(operation, [record], err, failures)) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Applies a server response onto a record and commits it, so the record\n * drops out of subsequent sync cycles.\n *\n * @param record - The record that was persisted.\n * @param serverData - The server's representation of the record.\n */\n private commitFromServerData(record: ModelRecord, serverData: Record<string, any>): void {\n for (const [k, v] of Object.entries(serverData)) {\n record.setSilent(k, v);\n }\n\n record.commit();\n }\n\n /**\n * Records an op failure: pushes a {@link StoreExceptionEvent} onto the run's\n * accumulator, emits `'exception'`, and reports whether the run should stop.\n *\n * @param operation - The op kind that failed.\n * @param records - The offending record(s).\n * @param error - The raw thrown value.\n * @param failures - The accumulator that collects this run's op failures.\n *\n * @returns True when `syncErrorPolicy` is `'stop'` (so the caller halts).\n */\n private recordFailure(operation: StoreOperation, records: ModelRecord[], error: unknown, failures: StoreExceptionEvent[]): boolean {\n const failure: StoreExceptionEvent = { operation, records, error };\n\n failures.push(failure);\n this.emit('exception', failure);\n\n return this._syncErrorPolicy === 'stop';\n }\n\n // ── Sort ─────────────────────────────────────────────────────────────────\n\n /**\n * Sorts the view by a single property in the given direction.\n *\n * @param field - The field name to sort by.\n * @param dir - Optional. The sort direction; defaults to 'asc'.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n *\n * @remarks\n * When `remoteSort` is enabled, or when server-side pagination is enabled\n * (the legacy trigger), this method also resets the current page to 1 and\n * triggers a fire-and-forget reload. With `remoteSort` on, the active\n * sorters are serialized into [`ReadParams`](/api/data/interfaces/ReadParams) so the proxy receives the new\n * ordering; without it, a paginated reload still fires but sends only\n * `{page, pageSize}`. Fires `'sortchange'` and `'datachange'`.\n */\n sort(field: string, dir?: 'asc' | 'desc'): Promise<void>;\n /**\n * Applies a multi-column sort. Pass an empty array to clear all sorters.\n *\n * @param descriptors - The ordered list of sort descriptors. Earlier\n * descriptors take priority.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n *\n * @remarks\n * Mirrors the single-column overload's reload side effects when `remoteSort`\n * or server-side pagination is enabled.\n */\n sort(descriptors: SortDescriptor[]): Promise<void>;\n sort(fieldOrDescriptors: string | SortDescriptor[], dir: 'asc' | 'desc' = 'asc'): Promise<void> {\n if (typeof fieldOrDescriptors === 'string') {\n this._activeSorters = [{ field: fieldOrDescriptors, dir }];\n } else {\n this._activeSorters = fieldOrDescriptors.slice();\n }\n\n const reload = this._remoteSort || this._pageSize != null;\n\n if (reload) {\n this._page = 1;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n }\n\n return this.applyView().then(() => {\n this.emit('sortchange', { sorters: this.getActiveSorters() });\n this.emit('datachange', {});\n\n if (reload) {\n void this.load();\n }\n });\n }\n\n /**\n * Returns a copy of all active sort descriptors in priority order.\n *\n * @returns A shallow-copy array of the active sort descriptors; empty when no sort is active.\n */\n getActiveSorters(): SortDescriptor[] {\n return this._activeSorters.map(s => ({ ...s }));\n }\n\n /**\n * Returns a copy of all active filter descriptors.\n *\n * @returns A shallow-copy array of the active filter descriptors; empty when no filter is active.\n */\n getActiveFilters(): FilterDescriptor[] {\n return this._activeFilters.map(f => ({ ...f }));\n }\n\n /**\n * Returns a copy of the primary active sorter config, or null if no sort is active.\n *\n * @returns The first active sorter mapped to the legacy `{ property, direction }` shape, or null.\n *\n * @deprecated Use {@link getActiveSorters} instead.\n */\n getActiveSorter(): { property: string; direction: 'asc' | 'desc' } | null {\n const first = this._activeSorters[0];\n\n return first ? { property: first.field, direction: first.dir } : null;\n }\n\n /**\n * Removes any active sort and restores insertion order, firing 'sortchange' and 'datachange'.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n */\n clearSort(): Promise<void> {\n this._activeSorters = [];\n\n return this.applyView().then(() => {\n this.emit('sortchange', { sorters: [] });\n this.emit('datachange', {});\n });\n }\n\n // ── Filter ───────────────────────────────────────────────────────────────\n\n /**\n * Adds an equality filter on a property and fires 'datachange'.\n *\n * @param property - The field name to filter on.\n * @param value - The value a record's field must equal to pass the filter.\n *\n * @remarks\n * When `remoteFilter` is enabled, or when server-side pagination is enabled\n * (the legacy trigger), this also resets to page 1 and reloads. With\n * `remoteFilter` on, the active filters are serialized into [`ReadParams`](/api/data/interfaces/ReadParams) so\n * the proxy filters the result set.\n */\n filter(property: string, value: any): Promise<void> {\n this._activeFilters.push({ type: 'eq', field: property, value: value });\n\n return this.applyFilterChange();\n }\n\n /**\n * Adds a filter described by a serializable {@link FilterDescriptor}. Descriptors\n * cross the worker boundary cleanly (unlike arbitrary predicate functions), so\n * the same call works for in-process and worker-offloaded evaluation.\n *\n * @param descriptor - The filter descriptor to apply.\n *\n * @remarks\n * Mirrors {@link filter}'s reload side effects when `remoteFilter` or\n * server-side pagination is enabled.\n */\n filterBy(descriptor: FilterDescriptor): Promise<void> {\n this._activeFilters.push(descriptor);\n\n return this.applyFilterChange();\n }\n\n /**\n * Rebuilds the view after a filter mutation, fires `'filterchange'` (with the\n * active filters) plus `'datachange'`, and, when `remoteFilter` or\n * pagination is enabled, resets to page 1 and triggers a reload.\n *\n * @returns A promise that resolves once the local view has been rebuilt.\n */\n private applyFilterChange(): Promise<void> {\n const reload = this._remoteFilter || this._pageSize != null;\n\n if (reload) {\n this._page = 1;\n this.emit('pagechange', { page: this._page, pageSize: this._pageSize });\n }\n\n return this.applyView().then(() => {\n this.emit('filterchange', { filters: this.getActiveFilters() });\n this.emit('datachange', {});\n\n if (reload) {\n void this.load();\n }\n });\n }\n\n /**\n * Removes all active filters and fires 'datachange'.\n *\n * @remarks\n * When `remoteFilter` is enabled, or when server-side pagination is enabled\n * (the legacy trigger), this method also resets the current page to 1 and\n * triggers a fire-and-forget reload so the proxy is queried without filter\n * context.\n */\n clearFilter(): Promise<void> {\n this._activeFilters = [];\n\n return this.applyFilterChange();\n }\n\n // ── Aggregation ────────────────────────────────────────────────────────────\n\n /**\n * Collects the numeric, non-null values of a field across the filtered view.\n *\n * @param field - The field to read from each visible record.\n *\n * @returns The coerced numbers, skipping null/undefined and any value that\n * does not coerce to a finite number.\n *\n * @remarks Shared by {@link sum} / {@link average} / {@link min} / {@link max};\n * `null`/`undefined` are skipped (never coerced to `0`) so an absent value\n * never distorts the result.\n */\n private numericValues(field: string): number[] {\n const values: number[] = [];\n\n for (const record of this._records) {\n const raw = record.get(field);\n\n if (raw == null) {\n continue;\n }\n\n const value = Number(raw);\n\n if (!Number.isNaN(value)) {\n values.push(value);\n }\n }\n\n return values;\n }\n\n /**\n * Sums a numeric field across the filtered view.\n *\n * @param field - The field to total.\n *\n * @returns The sum of the field's numeric values; `0` over an empty or\n * all-null view.\n */\n sum(field: string): number {\n return this.numericValues(field).reduce((total, value) => total + value, 0);\n }\n\n /**\n * Averages a numeric field across the filtered view.\n *\n * @param field - The field to average.\n *\n * @returns The mean of the field's numeric values; `0` over an empty or\n * all-null view.\n */\n average(field: string): number {\n const values = this.numericValues(field);\n\n if (values.length === 0) {\n return 0;\n }\n\n return values.reduce((total, value) => total + value, 0) / values.length;\n }\n\n /**\n * Returns the smallest value of a numeric field across the filtered view.\n *\n * @param field - The field to minimise.\n *\n * @returns The minimum numeric value, or undefined over an empty or\n * all-null view.\n */\n min(field: string): number | undefined {\n const values = this.numericValues(field);\n\n if (values.length === 0) {\n return undefined;\n }\n\n return values.reduce((lowest, value) => value < lowest ? value : lowest);\n }\n\n /**\n * Returns the largest value of a numeric field across the filtered view.\n *\n * @param field - The field to maximise.\n *\n * @returns The maximum numeric value, or undefined over an empty or\n * all-null view.\n */\n max(field: string): number | undefined {\n const values = this.numericValues(field);\n\n if (values.length === 0) {\n return undefined;\n }\n\n return values.reduce((highest, value) => value > highest ? value : highest);\n }\n\n /**\n * Collects the distinct values of a field across the filtered view, in\n * first-encounter (view) order.\n *\n * @param field - The field to collect distinct values from.\n *\n * @returns An array of unique values (by strict `===` identity), preserving\n * the order in which they first appear in the view.\n *\n * @remarks The type-agnostic companion to the numeric aggregates: useful for\n * building a distinct-value filter list. Values are de-duplicated by strict\n * equality, so distinct object references are treated as distinct values.\n */\n collect(field: string): any[] {\n const seen = new Set<any>();\n const result: any[] = [];\n\n for (const record of this._records) {\n const value = record.get(field);\n\n if (!seen.has(value)) {\n seen.add(value);\n result.push(value);\n }\n }\n\n return result;\n }\n\n // ── Grouping ───────────────────────────────────────────────────────────────\n\n /**\n * Sets the single-level group field, firing 'groupchange' only on a real change.\n *\n * @param field - The field to group by, or null to disable grouping.\n *\n * @returns This store, for method chaining.\n *\n * @remarks\n * Grouping is a pure read over the existing view ({@link getGroups}), so\n * changing the group field does **not** rebuild the view or fire\n * `'datachange'`; it fires only `'groupchange'` ({@link StoreGroupChangeEvent}).\n */\n setGroupField(field: string | null): this {\n if (this._groupField === field) {\n return this;\n }\n\n this._groupField = field;\n this.emit('groupchange', { groupField: field });\n\n return this;\n }\n\n /**\n * Returns the active group field, or null when grouping is disabled.\n *\n * @returns The field set via {@link setGroupField}, or null.\n */\n getGroupField(): string | null {\n return this._groupField;\n }\n\n /**\n * Returns the group-bucket key for a record under the active group field.\n *\n * @param record - The record to derive a group key for.\n *\n * @returns `String(record.get(groupField))`, or `''` when no group field is\n * set or the record's value is null/undefined.\n */\n getGroupString(record: ModelRecord): string {\n if (this._groupField == null) {\n return '';\n }\n\n const value = record.get(this._groupField);\n\n return value == null ? '' : String(value);\n }\n\n /**\n * Buckets the filtered view by the active group field.\n *\n * @returns A `Map` from group key ({@link getGroupString}) to the records in\n * that group. Groups appear in first-encounter order, and records within a\n * group keep view order. When no group field is set, every record falls\n * under the single `''` key.\n */\n getGroups(): Map<string, ModelRecord[]> {\n const groups = new Map<string, ModelRecord[]>();\n\n for (const record of this._records) {\n const key = this.getGroupString(record);\n const bucket = groups.get(key);\n\n if (bucket) {\n bucket.push(record);\n } else {\n groups.set(key, [record]);\n }\n }\n\n return groups;\n }\n\n // ── Events ───────────────────────────────────────────────────────────────\n\n /**\n * Subscribes a listener to a store event. Listeners are invoked in\n * registration order when the matching event is emitted.\n *\n * @param event - The name of the store event to listen for.\n * @param listener - The callback function to invoke when the event fires.\n *\n * @returns This store, for method chaining.\n */\n on(event: StoreEvent, listener: StoreListener): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered store event listener. No-op if the\n * listener was never registered for the given event.\n *\n * @param event - The name of the store event the listener was registered for.\n * @param listener - The exact callback reference to remove.\n *\n * @returns This store, for method chaining.\n */\n off(event: StoreEvent, listener: StoreListener): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Notifies all listeners registered for an event, in registration order.\n *\n * @param event - The name of the event to emit.\n * @param payload - The data object passed to each listener.\n */\n protected emit(event: StoreEvent, payload: any): void {\n this._listeners.fire(event, payload);\n }\n\n // ── Internal ─────────────────────────────────────────────────────────────\n\n /**\n * Rebuilds the visible records slice by applying all active filters and the active sorter.\n *\n * @remarks\n * Null values sort to the end regardless of sort direction. All active filter\n * predicates must pass for a record to be included in the view.\n */\n /**\n * Recomputes the filtered/sorted view from `allRecords`. Returns a Promise so a\n * future worker-offload path can resolve after the worker round-trip completes;\n * the current implementation runs synchronously and resolves immediately.\n */\n protected applyView(): Promise<void> {\n this.rebuildIdIndex();\n\n if (this._allRecords.length >= WORKER_THRESHOLD && StoreWorkerClient.isAvailable() && !this.hasCustomSorter()) {\n this._viewAsync = true;\n\n return this.applyViewOnWorker();\n }\n\n this._viewAsync = false;\n\n let view = this._allRecords.slice();\n\n for (const descriptor of this._activeFilters) {\n view = view.filter(r => matchesFilter(r, descriptor));\n }\n\n if (this._activeSorters.length > 0) {\n view.sort((a, b) => {\n for (const sorter of this._activeSorters) {\n const cmp = this.compareBySorter(a, b, sorter);\n\n if (cmp !== 0) {\n return cmp;\n }\n }\n\n return 0;\n });\n }\n\n this._records = view;\n\n return Promise.resolve();\n }\n\n /**\n * Compares two records under one sorter, applying its direction. A sorter\n * with a `sorterFn` delegates to it; otherwise the shared, type-aware\n * {@link compareValues} runs against the field values.\n *\n * @param a - The left record.\n * @param b - The right record.\n * @param sorter - The sorter whose `field`/`dir`/`sorterFn` drive the compare.\n *\n * @returns The final ordering: negative if `a` precedes `b`, positive if it\n * follows, `0` if equal under this sorter.\n *\n * @remarks\n * Nulls sort last regardless of direction — when either field value is\n * null/undefined, the (un-negated) {@link compareValues} result is returned\n * so direction applies only to a non-null comparison, matching the worker's\n * `sortIndices`.\n */\n private compareBySorter(a: ModelRecord, b: ModelRecord, sorter: SortDescriptor): number {\n if (sorter.sorterFn) {\n const cmp = sorter.sorterFn(a, b);\n\n return sorter.dir === 'asc' ? cmp : -cmp;\n }\n\n const av = a.get(sorter.field);\n const bv = b.get(sorter.field);\n const cmp = compareValues(av, bv, this.model.getField(sorter.field)?.getType());\n\n if (av == null || bv == null) {\n return cmp;\n }\n\n return sorter.dir === 'asc' ? cmp : -cmp;\n }\n\n /**\n * Reports whether any active sorter carries a custom `sorterFn`.\n *\n * @returns True when at least one sorter has a `sorterFn`, which forces the\n * in-process sort path (a function cannot cross the worker boundary).\n */\n private hasCustomSorter(): boolean {\n return this._activeSorters.some(sorter => sorter.sorterFn !== undefined);\n }\n\n /**\n * Rebuilds the id→record index from the master list so {@link getById} is\n * O(1). The index stays empty when the model defines no primary key.\n */\n private rebuildIdIndex(): void {\n this._idIndex.clear();\n\n if (!this.model.getPrimaryKeyField()) {\n return;\n }\n\n for (const record of this._allRecords) {\n this._idIndex.set(record.getId(), record);\n }\n }\n\n /**\n * Worker-offloaded view rebuild for stores above WORKER_THRESHOLD. Ships a fresh\n * snapshot when allRecords has changed since the last dispatch, then asks the\n * worker for sorted/filtered indices into the snapshot, and maps those indices\n * back to the local ModelRecord array. The worker returns indices (not records)\n * because ModelRecord instances can't survive structured clone.\n *\n * @remarks\n * The worker protocol currently accepts only a single sorter, so on the\n * worker path multi-sort degrades to the primary (first) sorter. Datasets\n * below {@link WORKER_THRESHOLD} run in-process and apply the full\n * multi-key comparator.\n */\n private applyViewOnWorker(): Promise<void> {\n const snapshot = this._snapshotDirty\n ? StoreWorkerClient.snapshot(this._storeId, this._allRecords.map(r => r.getData()))\n : Promise.resolve();\n\n if (this._snapshotDirty) {\n this._snapshotDirty = false;\n }\n\n const allRecordsRef = this._allRecords;\n const primary = this._activeSorters[0];\n\n return snapshot\n .then(() => StoreWorkerClient.sortFilter(\n this._storeId,\n primary\n ? { field: primary.field, direction: primary.dir, fieldType: this.model.getField(primary.field)?.getType() }\n : undefined,\n this._activeFilters.length > 0\n ? (this._activeFilters.length === 1\n ? this._activeFilters[0]\n : { type: 'and', filters: this._activeFilters })\n : undefined,\n ))\n .then(indices => {\n // Guard against allRecords having been replaced while the worker ran.\n // If so, the indices reference stale data; trigger a fresh applyView.\n if (allRecordsRef !== this._allRecords) {\n return this.applyView();\n }\n\n this._records = indices.map(i => this._allRecords[i]);\n return undefined;\n });\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { Model } from '~/data/Model.js';\nimport { AbstractModel } from '~/data/AbstractModel.js';\nimport { Proxy } from '~/data/proxy/Proxy.js';\nimport { AbstractStore, AbstractStoreOptions } from '~/data/AbstractStore.js';\n\n/**\n * Construction-time options for {@link Store}. May be passed as the first\n * argument in place of the positional `(model, proxy)` form, in which case\n * `model` and optional `proxy` come from the bag.\n *\n * @category Data\n */\nexport interface StoreOptions extends AbstractStoreOptions {\n model: Model;\n proxy?: Proxy;\n}\n\n/**\n * A general-purpose concrete store that pairs a Model with an optional Proxy.\n * Use this class when you do not need a dedicated store subclass.\n *\n * @category Data\n */\nexport class Store extends AbstractStore {\n\n readonly model: Model;\n readonly proxy: Proxy | undefined;\n\n /**\n * Constructs a Store with the given model and an optional proxy.\n *\n * @param modelOrOptions - The {@link Model} that defines the record schema, or a {@link StoreOptions} bag.\n * @param proxy - Optional. The Proxy used to load and persist records. Ignored when the first argument is a {@link StoreOptions} bag.\n */\n constructor(modelOrOptions: Model | StoreOptions, proxy?: Proxy) {\n super();\n\n if (modelOrOptions instanceof AbstractModel) {\n this.model = modelOrOptions;\n this.proxy = proxy;\n } else {\n this.model = modelOrOptions.model;\n this.proxy = modelOrOptions.proxy;\n\n this.applyOptions(modelOrOptions);\n }\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { AbstractModel } from '~/data/AbstractModel.js';\nimport { AbstractStore } from '~/data/AbstractStore.js';\nimport type { Association } from '~/data/Association.js';\nimport { Field } from '~/data/Field.js';\nimport { Store } from '~/data/Store.js';\nimport type { Model } from '~/data/Model.js';\nimport { applyRule } from '~/validation/Validator.js';\n\n// Monotonic per-session id source for client-side row keys; never leaves the client,\n// so a plain counter suffices (no UUID / collision concern across sessions).\nlet nextInternalId = 1;\n\n// Recursion cap for deep value equality. Model field values are JSON-shaped and\n// acyclic in practice; this bound stops a pathologically deep or accidentally\n// cyclic structure from overflowing the stack. Beyond it we fall back to `===`\n// for that sub-comparison rather than tracking a visited-set on every set().\nconst MAX_EQUALITY_DEPTH = 100;\n\n/**\n * A single field's before / after values, as returned by\n * {@link ModelRecord.getChanges} and {@link ModelRecord.getModified}.\n *\n * @category Data\n */\nexport interface FieldChange {\n old: any;\n new: any;\n}\n\n/**\n * A single data record managed by a store.\n *\n * Tracks current field values, dirty state, and new / committed status.\n *\n * @remarks\n * On construction the data snapshot is also stored as `original` so that\n * `reject()` can restore the record to its last committed state without\n * requiring a round-trip to the server.\n *\n * @example\n * ```typescript\n * const record = store.getAt(0);\n * record?.set('age', 31);\n * console.log(record?.isDirty()); // true\n * record?.commit(); // clears dirty flag\n * // record?.reject(); // reverts to last committed snapshot\n * ```\n *\n * @category Data\n */\nexport class ModelRecord {\n\n private _model: AbstractModel;\n private _data: Record<string, any>;\n private _original: Record<string, any>;\n private _dirty: boolean = false;\n private _isNew: boolean = false;\n private _internalId: number;\n\n // Embedded child rows captured from the parent payload at createRecord time,\n // keyed by association accessor. Kept out of `_data` so getData() / the proxy\n // writers never see them; consumed once when the child store is first built.\n private _associatedSeed: Record<string, any[]>;\n\n // Lazily-built per-accessor child stores, so repeated getAssociated() calls\n // return the same instance (stable identity for listeners and the collection\n // id-index). Allocated on first access.\n private _childStores: Map<string, AbstractStore> | undefined;\n\n // Back-ref to the owning store, stamped by adoptedBy() when the record enters\n // a store's record set and cleared by released() when it leaves. Null for an\n // un-adopted record (mid-construction, a freestanding new ModelRecord, a\n // clone), which is what keeps set() silent until the store takes ownership.\n private _store: AbstractStore | null = null;\n\n // Record-level edit-batch state, depth-counted so nested batches (e.g.\n // setMany inside a consumer's beginEdit) collapse to a single snapshot and a\n // single notify: the snapshot is taken at the 0->1 transition and the diff +\n // notify happen at the 1->0 transition.\n private _editDepth: number = 0;\n private _editSnapshot: Record<string, any> | null = null;\n\n /**\n * Constructs a ModelRecord with the given model schema and initial data.\n *\n * @param model - The AbstractModel that describes this record's field schema.\n * @param data - The initial field values keyed by field name.\n * @param associatedSeed - Optional. Embedded child rows from the parent\n * payload, keyed by association accessor, used to seed child stores on\n * first access. Never enters `_data` and is never serialised.\n */\n constructor(model: AbstractModel, data: Record<string, any>, associatedSeed: Record<string, any[]> = {}) {\n this._model = model;\n this._data = { ...data };\n this._original = { ...data };\n this._internalId = nextInternalId++;\n this._associatedSeed = associatedSeed;\n }\n\n /**\n * Records the store that now owns this record, so a subsequent `set()` can\n * notify it.\n *\n * @param store - The store taking ownership of this record.\n *\n * @internal Framework wiring; called by AbstractStore when it adopts this\n * record into its record set. Not part of the consumer API.\n */\n adoptedBy(store: AbstractStore): void {\n this._store = store;\n }\n\n /**\n * Clears the owning-store back-ref, so a `set()` on the now-detached record\n * notifies nobody.\n *\n * @internal Framework wiring; called by AbstractStore when this record leaves\n * its record set. Not part of the consumer API.\n */\n released(): void {\n this._store = null;\n }\n\n /**\n * Returns the value of a field by name.\n *\n * @param field - The logical name of the field to retrieve.\n *\n * @returns The current value of the field, or undefined if the field is not present.\n */\n get(field: string): any {\n return this._data[field];\n }\n\n /**\n * Sets a field value, marks the record dirty, and—when the record is\n * store-owned—notifies that store so bound views refresh.\n *\n * @remarks\n * A no-op assignment (the converted value already equals the current one) is\n * short-circuited and notifies nobody. The notify is suppressed while an edit\n * batch is open (see {@link beginEdit}) or while the owning store is batching,\n * and is absent entirely for an un-adopted record. Use {@link setSilent} to\n * mutate without ever notifying.\n *\n * @param field - The logical name of the field to update.\n * @param value - The new value to assign to the field.\n *\n * @returns This record, for method chaining.\n */\n set(field: string, value: any): this {\n const old = this._data[field];\n\n if (this.applySet(field, value)) {\n this.notifyStore({ [field]: { old, new: this._data[field] } });\n }\n\n return this;\n }\n\n /**\n * Sets a field value and marks the record dirty without ever notifying the\n * owning store.\n *\n * @remarks\n * The silent counterpart to {@link set}, for framework-internal writes whose\n * surrounding operation emits its own events (so a per-field notify would be\n * redundant). Conversion, the no-op short-circuit, and dirty tracking are\n * identical to {@link set}.\n *\n * @param field - The logical name of the field to update.\n * @param value - The new value to assign to the field.\n *\n * @returns This record, for method chaining.\n */\n setSilent(field: string, value: any): this {\n this.applySet(field, value);\n\n return this;\n }\n\n /**\n * Sets several fields, then notifies the owning store once for the whole\n * group.\n *\n * @remarks\n * Implemented over the record edit batch: it opens a batch, assigns each\n * field (so no intermediate notify fires), then commits for a single\n * coalesced notify carrying every change. Because the batch is depth-counted,\n * calling `setMany` inside a consumer's own {@link beginEdit} nests safely.\n *\n * @param values - A map of field name to new value.\n *\n * @returns This record, for method chaining.\n */\n setMany(values: Record<string, any>): this {\n this.beginEdit();\n\n for (const [field, value] of Object.entries(values)) {\n this.set(field, value);\n }\n\n this.commitEdit();\n\n return this;\n }\n\n /**\n * Opens a record-level edit batch, suppressing per-`set()` notifications\n * until the matching {@link commitEdit}.\n *\n * @remarks\n * Batches are depth-counted: the pre-edit snapshot is captured only on the\n * outermost `beginEdit`, so a nested batch (including the implicit one inside\n * {@link setMany}) collapses into the outer one rather than re-snapshotting.\n *\n * @returns This record, for method chaining.\n */\n beginEdit(): this {\n if (this._editDepth === 0) {\n this._editSnapshot = { ...this._data };\n }\n\n this._editDepth++;\n\n return this;\n }\n\n /**\n * Closes a record-level edit batch; on the outermost close, fires one notify\n * carrying every field changed since {@link beginEdit}.\n *\n * @remarks\n * A no-op when no batch is open. While nesting remains, the close is deferred\n * to the outermost {@link commitEdit}. The notify is skipped when the batch\n * produced no net change.\n *\n * @returns This record, for method chaining.\n */\n commitEdit(): this {\n if (this._editDepth === 0) {\n return this;\n }\n\n this._editDepth--;\n\n if (this._editDepth > 0) {\n return this;\n }\n\n const snapshot = this._editSnapshot ?? {};\n const changes: Record<string, FieldChange> = {};\n\n for (const key of Object.keys(this._data)) {\n if (!ModelRecord.isEqual(this._data[key], snapshot[key])) {\n changes[key] = { old: snapshot[key], new: this._data[key] };\n }\n }\n\n this._editSnapshot = null;\n\n if (Object.keys(changes).length > 0) {\n this.notifyStore(changes);\n }\n\n return this;\n }\n\n /**\n * Discards an open edit batch: reverts the fields to the pre-edit snapshot\n * and fires nothing.\n *\n * @remarks\n * A no-op when no batch is open. A cancel from anywhere inside a nested batch\n * collapses the whole stack and reverts to the outermost snapshot, since the\n * batch shares a single baseline.\n *\n * @returns This record, for method chaining.\n */\n cancelEdit(): this {\n if (this._editDepth === 0) {\n return this;\n }\n\n this._editDepth = 0;\n\n if (this._editSnapshot !== null) {\n this._data = { ...this._editSnapshot };\n this._editSnapshot = null;\n\n this.recomputeDirty();\n }\n\n return this;\n }\n\n /**\n * Converts, equality-gates, and applies a field write, updating dirty state.\n *\n * @remarks\n * The shared body of {@link set} and {@link setSilent}; the two differ only in\n * whether they notify afterward. Returns whether the write was a real change\n * so the caller knows whether a notify is warranted.\n *\n * @param field - The logical name of the field to update.\n * @param value - The new value to assign to the field.\n *\n * @returns True when the value actually changed; false for a no-op write.\n */\n private applySet(field: string, value: any): boolean {\n const modelField = this._model.getField(field);\n const converted = modelField ? modelField.convertValue(value, this._data) : value;\n\n if (ModelRecord.isEqual(this._data[field], converted)) {\n return false;\n }\n\n this._data[field] = converted;\n\n this.recomputeDirty();\n\n return true;\n }\n\n /**\n * Recomputes `_dirty` by comparing every current field against its committed\n * baseline. A new record is always dirty; otherwise any field whose value\n * differs from the original marks the record dirty.\n *\n * @remarks\n * Iterates `_data` keys — a superset of `_original` keys — so a field added\n * after construction (e.g. `set('newField', v)`) counts as a change, keeping\n * dirty tracking in agreement with `getChanges`.\n */\n private recomputeDirty(): void {\n this._dirty = this._isNew || Object.keys(this._data)\n .some(k => !ModelRecord.isEqual(this._data[k], this._original[k]));\n }\n\n /**\n * Notifies the owning store of a change, unless suppressed.\n *\n * @remarks\n * Fires only when the record is store-owned, no record edit batch is open,\n * and the owning store is not itself batching — the three layers that\n * coalesce or silence auto-notify.\n *\n * @param changes - The field-level diff to carry on the `'update'` event.\n */\n private notifyStore(changes: Record<string, FieldChange>): void {\n if (this._store === null || this._editDepth > 0 || this._store.isBatching()) {\n return;\n }\n\n this._store.notifyRecordChanged(this, changes);\n }\n\n /**\n * Structural value equality used for dirty-tracking: primitives by SameValueZero,\n * `Date` by time, arrays and plain objects deep, class instances by reference.\n *\n * @param a - The first value to compare.\n * @param b - The second value to compare.\n *\n * @returns True when the two values are structurally equal for dirty-tracking purposes.\n */\n private static isEqual(a: any, b: any): boolean {\n return ModelRecord.isEqualAtDepth(a, b, 0);\n }\n\n /**\n * Recursive core of {@link ModelRecord.isEqual}, carrying the depth budget.\n *\n * @param a - The first value to compare.\n * @param b - The second value to compare.\n * @param depth - The current recursion depth, capped by `MAX_EQUALITY_DEPTH`.\n *\n * @returns True when the two values are structurally equal at this depth.\n */\n private static isEqualAtDepth(a: any, b: any, depth: number): boolean {\n if (a === b) {\n return true;\n }\n\n if (a !== a && b !== b) {\n return true;\n }\n\n if (a === null || a === undefined || b === null || b === undefined) {\n return false;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n if (depth >= MAX_EQUALITY_DEPTH) {\n return a === b;\n }\n\n if (Array.isArray(a) || Array.isArray(b)) {\n return Array.isArray(a) && Array.isArray(b) && ModelRecord.arraysEqual(a, b, depth);\n }\n\n if (ModelRecord.isPlainObject(a) && ModelRecord.isPlainObject(b)) {\n return ModelRecord.plainObjectsEqual(a, b, depth);\n }\n\n return false;\n }\n\n /**\n * Compares two arrays element-wise, recursing one level deeper per element.\n *\n * @param a - The first array.\n * @param b - The second array.\n * @param depth - The current recursion depth.\n *\n * @returns True when both arrays have equal length and structurally-equal elements.\n */\n private static arraysEqual(a: any[], b: any[], depth: number): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n for (let i = 0; i < a.length; i++) {\n if (!ModelRecord.isEqualAtDepth(a[i], b[i], depth + 1)) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Compares two plain objects by own-enumerable keys, recursing one level deeper per key.\n *\n * @param a - The first plain object.\n * @param b - The second plain object.\n * @param depth - The current recursion depth.\n *\n * @returns True when both objects share the same key set and structurally-equal values.\n */\n private static plainObjectsEqual(a: Record<string, any>, b: Record<string, any>, depth: number): boolean {\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n\n if (aKeys.length !== bKeys.length) {\n return false;\n }\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) {\n return false;\n }\n\n if (!ModelRecord.isEqualAtDepth(a[key], b[key], depth + 1)) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Reports whether a value is a plain object (prototype is `Object.prototype` or `null`).\n *\n * @param value - The value to test.\n *\n * @returns True for plain data objects; false for class instances, arrays, and primitives.\n */\n private static isPlainObject(value: any): boolean {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const proto = Object.getPrototypeOf(value);\n\n return proto === Object.prototype || proto === null;\n }\n\n /**\n * Returns a shallow copy of all field data.\n *\n * @returns A plain object containing all current field values keyed by field name.\n */\n getData(): Record<string, any> {\n return { ...this._data };\n }\n\n /**\n * Returns the changed-field new values for a dirty-only update body, always\n * including the primary-key field so a batch update (no id in the URL) stays\n * identifiable.\n *\n * @remarks\n * Empty of changes only when the record is clean, in which case only the\n * primary-key entry (if any) is returned.\n *\n * @returns A plain object of changed field name to its new value, plus the\n * primary-key field when the model defines one.\n */\n getChangedData(): Record<string, any> {\n const data: Record<string, any> = {};\n\n for (const [field, change] of Object.entries(this.getChanges())) {\n data[field] = change.new;\n }\n\n const pkField = this._model.getPrimaryKeyField();\n\n if (pkField) {\n data[pkField.getName()] = this._data[pkField.getName()];\n }\n\n return data;\n }\n\n /**\n * Returns true if any field has been changed since the last commit.\n *\n * @returns True if the record has uncommitted changes, false otherwise.\n */\n isDirty(): boolean {\n return this._dirty;\n }\n\n /**\n * Returns true if this record has not yet been persisted (added via store.add).\n *\n * @returns True if the record is new and has not been synced to the server.\n */\n isNew(): boolean {\n return this._isNew;\n }\n\n /**\n * Marks the record as newly created and not yet synced to the server.\n */\n markAsNew(): void {\n this._isNew = true;\n }\n\n /**\n * Accepts current field values as the new baseline, clearing dirty and new flags.\n *\n * @remarks\n * Called automatically by `AbstractStore.sync()` after a successful create or update\n * so that the record no longer appears in subsequent sync cycles.\n */\n commit(): this {\n this._original = { ...this._data };\n this._dirty = false;\n this._isNew = false;\n\n return this;\n }\n\n /**\n * Reverts all field values to the last committed state.\n *\n * @remarks\n * The dirty flag is cleared but the new flag is not changed; a new record that has\n * been rejected remains new until it is committed or removed from the store.\n */\n reject(): void {\n this._data = { ...this._original };\n this._dirty = false;\n }\n\n /**\n * Returns the value of the model's primary-key field, or undefined if none is defined.\n *\n * @returns The primary key value, or undefined if the model has no primary key configured.\n */\n getId(): any {\n const pkField = this._model.getPrimaryKeyField();\n\n return pkField ? this._data[pkField.getName()] : undefined;\n }\n\n /**\n * Returns the AbstractModel that describes this record's schema.\n *\n * @returns The model instance associated with this record.\n */\n getModel(): AbstractModel {\n return this._model;\n }\n\n /**\n * Returns the stable client-side id assigned at construction.\n *\n * @remarks\n * Unlike `getId()` (the primary-key value, which is `undefined` until the server\n * replies), this id exists immediately and is unique within the session, so UI can\n * use it as a key for unsynced rows. It is never serialised and never sent to the server.\n *\n * @returns The monotonic per-session internal id.\n */\n getInternalId(): number {\n return this._internalId;\n }\n\n /**\n * Returns the cached, parent-scoped child {@link AbstractStore} for an\n * association accessor, building it on first access.\n *\n * @remarks\n * Repeated calls for the same accessor return the **same** store instance, so\n * listeners and the collection id-index stay stable. For a hasMany association\n * the store is seeded from an embedded child array when the parent payload\n * carried one (eager), otherwise it is configured to load through the target\n * model's proxy filtered on the parent's foreign key (lazy). For a belongsTo\n * association the store is filtered to the single owner record.\n *\n * @param accessor - The association accessor declared on this record's model.\n *\n * @returns The cached child store for that association.\n *\n * @throws Error when no association with that accessor exists (programmer error).\n */\n getAssociated(accessor: string): AbstractStore {\n const cached = this._childStores?.get(accessor);\n\n if (cached) {\n return cached;\n }\n\n const association = this._model.getAssociation(accessor);\n\n if (!association) {\n throw new Error(`ModelRecord.getAssociated: no association '${accessor}'`);\n }\n\n const store = this.buildChildStore(association);\n\n (this._childStores ??= new Map()).set(accessor, store);\n\n return store;\n }\n\n /**\n * Builds the parent-scoped child store for an association.\n *\n * @remarks\n * The target model is resolved through the association's memoised thunk. A\n * hasMany association with an embedded seed loads those rows directly (each\n * committed, not new); otherwise it carries a `remoteFilter` on the parent\n * foreign key for the lazy load path. A belongsTo association filters the\n * target store to the owner via the foreign-key value. The association's\n * {@link Association.resolveProxy} feeds the child store's transport, so the\n * lazy/owner load reaches a real proxy.\n *\n * @param association - The association whose child store is built.\n *\n * @returns A freshly-constructed {@link Store} for the target model.\n */\n private buildChildStore(association: Association): AbstractStore {\n const targetModel = association.resolveTarget() as Model;\n const proxy = association.resolveProxy();\n\n if (association.kind === 'belongsTo') {\n const pkName = targetModel.getPrimaryKeyField()?.getName() ?? association.getForeignKey();\n\n return new Store({\n model: targetModel,\n proxy,\n remoteFilter: true,\n filters: [{ type: 'eq', field: pkName, value: this.get(association.getForeignKey()) }],\n });\n }\n\n const seed = this._associatedSeed[association.getAccessor()];\n\n if (seed) {\n // The seed branch calls loadData() and never load(); the proxy is\n // inert here unless a consumer later load()s or sync()s this store.\n const store = new Store({ model: targetModel, proxy });\n\n store.loadData(seed);\n\n return store;\n }\n\n return new Store({\n model: targetModel,\n proxy,\n remoteFilter: true,\n filters: [{ type: 'eq', field: association.getForeignKey(), value: this.getId() }],\n });\n }\n\n /**\n * Reports whether a child store for an association has already been built.\n *\n * @remarks\n * Used by the parent store's cascade sync to skip associations whose child\n * store was never materialised — a loaded-but-untouched parent then costs\n * nothing.\n *\n * @param accessor - The association accessor to test.\n *\n * @returns True when {@link getAssociated} has already built that store.\n */\n hasChildStore(accessor: string): boolean {\n return this._childStores?.has(accessor) ?? false;\n }\n\n /**\n * Returns the raw foreign-key value for a belongsTo accessor, without loading.\n *\n * @param accessor - The belongsTo association accessor.\n *\n * @returns The foreign-key field value held on this record.\n *\n * @throws Error when no association with that accessor exists (programmer error).\n */\n getForeignKeyValue(accessor: string): any {\n const association = this._model.getAssociation(accessor);\n\n if (!association) {\n throw new Error(`ModelRecord.getForeignKeyValue: no association '${accessor}'`);\n }\n\n return this.get(association.getForeignKey());\n }\n\n /**\n * Returns this record's data augmented with embedded children for every\n * `'nested'`-persist association whose child store was materialised.\n *\n * @remarks\n * Unlike {@link getData}, which never carries children, this is the hook a\n * nested-aware writer reads to serialise children inside the parent's write\n * body. Each materialised `'nested'` association contributes\n * `{ [nestedKey]: childRecordsData }`; `'proxy'`-persist associations and\n * unbuilt stores are omitted (they persist through their own proxy).\n *\n * @returns A plain object: the field data plus nested child-record arrays.\n */\n getDataWithNested(): Record<string, any> {\n const data = this.getData();\n\n for (const association of this._model.getAssociations()) {\n if (association.kind !== 'hasMany' || association.getPersist() !== 'nested') {\n continue;\n }\n\n if (!this.hasChildStore(association.getAccessor())) {\n continue;\n }\n\n const child = this.getAssociated(association.getAccessor());\n\n data[association.getNestedKey()] = child.getAll().map(r => r.getData());\n }\n\n return data;\n }\n\n /**\n * Returns the fields whose current value differs from the last committed baseline.\n *\n * @remarks\n * Comparison uses the same structural equality as dirty tracking, so plain object /\n * array field values compare deeply; class instances still compare by reference.\n *\n * @returns A map of changed field name to its `{ old, new }` values; empty when clean.\n */\n getChanges(): Record<string, FieldChange> {\n const changes: Record<string, FieldChange> = {};\n\n for (const key of Object.keys(this._data)) {\n if (!ModelRecord.isEqual(this._data[key], this._original[key])) {\n changes[key] = { old: this._original[key], new: this._data[key] };\n }\n }\n\n return changes;\n }\n\n /**\n * Returns the modified fields as a `{ old, new }` map.\n *\n * @remarks\n * Alias of {@link ModelRecord.getChanges}; both names are provided for caller intent.\n *\n * @returns A map of modified field name to its `{ old, new }` values; empty when clean.\n */\n getModified(): Record<string, FieldChange> {\n return this.getChanges();\n }\n\n /**\n * Returns a copy of this record carrying a fresh internal id, marked new and dirty.\n *\n * @remarks\n * A clone is a distinct row, so it receives its own `internalId` and is flagged as a\n * new, dirty insert. The data is shallow-copied (object / array field values are shared\n * with the source, matching the shallow contract elsewhere in this class).\n *\n * @returns A new {@link ModelRecord} for the same model with copied field data.\n */\n clone(): ModelRecord {\n const copy = new ModelRecord(this._model, { ...this._data });\n\n copy.markAsNew();\n copy._dirty = true;\n\n return copy;\n }\n\n /**\n * Returns true when every field passes its implicit type check and explicit validators.\n *\n * @returns True when no field reports an error, false otherwise.\n */\n isValid(): boolean {\n return Object.keys(this.getErrors()).length === 0;\n }\n\n /**\n * Returns the first failing message for each currently-invalid field.\n *\n * @returns A map of field name to its first error message; empty when the record is valid.\n */\n getErrors(): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const field of this._model.getFields()) {\n const message = this.validateField(field.getName());\n\n if (message) {\n errors[field.getName()] = message;\n }\n }\n\n return errors;\n }\n\n /**\n * Validates a single field, returning its first failing message.\n *\n * @remarks\n * An implicit type check runs first: a non-null value on a typed (non-`auto`/`glyph`)\n * field that fails coercion reports a type error before the explicit `validators` run.\n *\n * @param name - The logical name of the field to validate.\n *\n * @returns The first error message, or `''` when the field is valid or is not a model field.\n */\n validateField(name: string): string {\n const field = this._model.getField(name);\n\n if (!field) {\n return '';\n }\n\n const value = this._data[name];\n const typeError = this.checkType(field, value);\n\n if (typeError) {\n return typeError;\n }\n\n for (const rule of field.getValidators()) {\n const result = applyRule(rule, value);\n\n if (!result.valid) {\n return result.message;\n }\n }\n\n return '';\n }\n\n /**\n * Runs the implicit, conversion-derived type check for a field value.\n *\n * @remarks\n * Skips `auto` / `glyph` fields (no type to enforce) and `null` / `undefined` values\n * (absence is governed by a `required` rule, not the type check). For every other typed\n * field, a stored value that re-coerces to `undefined` (a `number` holding `NaN`, a date\n * holding an Invalid Date) is reported as a type error.\n *\n * @param field - The field whose declared type is enforced.\n * @param value - The current stored value for that field.\n *\n * @returns The type-error message, or `''` when the value satisfies the field's type.\n */\n private checkType(field: Field, value: any): string {\n const type = field.getType();\n\n if (type === 'auto' || type === 'glyph' || value === null || value === undefined) {\n return '';\n }\n\n const coerced = field.convertValue(value);\n const failed = coerced === undefined\n || (typeof coerced === 'number' && isNaN(coerced));\n\n if (failed) {\n return `Value is not a valid ${type}.`;\n }\n\n return '';\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { AbstractModel } from '~/data/AbstractModel.js';\nimport type { Proxy } from '~/data/proxy/Proxy.js';\n\n/**\n * Per-association persistence strategy applied during cascade sync.\n *\n * @remarks\n * `'proxy'` (the default) persists children through the child store's own\n * proxy — the cascade simply calls the child store's `sync()`. `'nested'`\n * serialises children inside the parent's write body under the association's\n * nested key, via {@link ModelRecord.getDataWithNested}; the child store is not\n * synced independently.\n *\n * @category Data\n */\nexport type AssociationPersist = 'nested' | 'proxy';\n\n/**\n * Construction-time options shared by all association kinds.\n *\n * @category Data\n */\nexport interface AssociationOptions {\n /** Accessor name exposed on the record (e.g. `'employees'`). */\n accessor: string;\n /** Thunk returning the target model — a thunk to break declaration cycles. */\n target: () => AbstractModel;\n /**\n * The child field holding the owner's id (hasMany) / the owner's primary\n * key this record points at (belongsTo).\n */\n foreignKey: string;\n /** Raw-payload key carrying an embedded child array for eager hydration. Defaults to `accessor`. */\n nestedKey?: string;\n /** Cascade persistence strategy; defaults to `'proxy'`. */\n persist?: AssociationPersist;\n /**\n * Discriminant selecting the concrete association kind when a plain options\n * object is promoted by {@link AbstractModel}. Ignored when an\n * {@link Association} instance is supplied directly.\n */\n kind?: 'hasMany' | 'belongsTo';\n /**\n * Proxy used to load (and persist) the target model's records for this\n * association's parent-scoped child {@link Store}. Required for the lazy\n * fetch path: without it, the lazily-built child store has no transport and\n * `load()` throws.\n *\n * @remarks\n * A direct {@link Proxy} reference, not a thunk like `target`. The `target`\n * thunk exists only to break the module-initialisation cycle between two\n * mutually-referential models; a proxy is a plain runtime object holding no\n * back-reference to the association or the owning model, so referencing it\n * directly creates no cycle. Do not \"fix\" this into a thunk by analogy with\n * `target`.\n */\n proxy?: Proxy;\n}\n\n/**\n * Declarative association descriptor; behaviour lives in\n * {@link AbstractModel} / {@link ModelRecord} / {@link AbstractStore}.\n *\n * @remarks\n * Mirrors {@link Field}: a passive schema object holding name/target/foreignKey\n * and pure getters, with the runtime logic (hydrate, lazy-load, cascade) living\n * in the model, record, and store. The `target` thunk is called at most once and\n * its result memoised, so mutually-referential models can declare each other\n * without a module-initialisation cycle.\n *\n * @category Data\n */\nexport abstract class Association {\n\n private _accessor: string;\n private _target: () => AbstractModel;\n private _foreignKey: string;\n private _nestedKey: string | undefined;\n private _persist: AssociationPersist;\n private _proxy: Proxy | undefined;\n private _resolvedTarget: AbstractModel | undefined;\n\n /** The concrete kind of this association. */\n abstract readonly kind: 'hasMany' | 'belongsTo';\n\n /**\n * Constructs an Association from an {@link AssociationOptions} object.\n *\n * @param options - The options describing the accessor, target, and foreign key.\n */\n constructor(options: AssociationOptions) {\n this._accessor = options.accessor;\n this._target = options.target;\n this._foreignKey = options.foreignKey;\n this._nestedKey = options.nestedKey;\n this._persist = options.persist ?? 'proxy';\n this._proxy = options.proxy;\n }\n\n /**\n * Returns the accessor name exposed on the record.\n *\n * @returns The accessor string (e.g. `'employees'`).\n */\n getAccessor(): string {\n return this._accessor;\n }\n\n /**\n * Returns the foreign-key field name.\n *\n * @returns The child field holding the owner's id (hasMany), or the owner's\n * primary key this record points at (belongsTo).\n */\n getForeignKey(): string {\n return this._foreignKey;\n }\n\n /**\n * Returns the raw-payload key carrying an embedded child array.\n *\n * @returns The configured nested key, or the accessor name when none was set.\n */\n getNestedKey(): string {\n return this._nestedKey ?? this._accessor;\n }\n\n /**\n * Returns the cascade persistence strategy.\n *\n * @returns `'nested'` or `'proxy'` (the default).\n */\n getPersist(): AssociationPersist {\n return this._persist;\n }\n\n /**\n * Resolves and memoises the target model via the configured thunk.\n *\n * @returns The target {@link AbstractModel} instance; the thunk is invoked at\n * most once across the association's lifetime.\n */\n resolveTarget(): AbstractModel {\n if (this._resolvedTarget === undefined) {\n this._resolvedTarget = this._target();\n }\n\n return this._resolvedTarget;\n }\n\n /**\n * Returns the proxy for this association's parent-scoped child store.\n *\n * @remarks\n * Mirrors {@link resolveTarget} so both child-store kinds read the target's\n * transport identically. Unlike `target` there is no thunk to invoke and no\n * memoisation: the stored {@link Proxy} reference is returned verbatim.\n *\n * @returns The configured target-model proxy, or undefined when none was set\n * (the lazy fetch path then has no transport and `load()` will throw).\n */\n resolveProxy(): Proxy | undefined {\n return this._proxy;\n }\n}\n\n/**\n * Parent owns many child records, surfaced as a parent-scoped child\n * {@link Store} via {@link ModelRecord.getAssociated}.\n *\n * @category Data\n */\nexport class HasManyAssociation extends Association {\n\n readonly kind = 'hasMany' as const;\n}\n\n/**\n * Record references a single owner via its foreign key.\n *\n * @remarks\n * Like {@link HasManyAssociation}, {@link ModelRecord.getAssociated} returns a\n * parent-scoped child {@link Store} (filtered to the owner) whose first record is\n * the owner; {@link ModelRecord.getForeignKeyValue} reads the raw FK without\n * loading.\n *\n * @category Data\n */\nexport class BelongsToAssociation extends Association {\n\n readonly kind = 'belongsTo' as const;\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Field, FieldOptions } from '~/data/Field.js';\nimport { ModelRecord } from '~/data/ModelRecord.js';\nimport { Association, AssociationOptions, HasManyAssociation, BelongsToAssociation } from '~/data/Association.js';\n\n/**\n * Base class for all data models.\n * Defines the field schema used to create and validate ModelRecord instances.\n *\n * @remarks\n * Subclasses must declare the `fields` array. Field resolution and the name-to-field\n * index are built lazily on first access and cached for subsequent calls.\n *\n * @category Data\n */\nexport abstract class AbstractModel {\n\n abstract readonly fields: (Field | FieldOptions)[];\n\n /**\n * Optional association schema, mirroring `fields`. Declaring an association\n * surfaces a parent-scoped child {@link Association} accessor on records via\n * {@link ModelRecord.getAssociated}. Defaults to none, so every model that\n * omits it compiles and behaves unchanged.\n *\n * @remarks\n * Set once at construction (subclasses assign it, like `fields`); treated as\n * read-only schema thereafter. It is not `readonly` only so the {@link Model}\n * runtime subclass can assign it from its options bag.\n */\n associations?: (Association | AssociationOptions)[];\n\n protected _primaryKey: string | undefined;\n\n private _resolvedFields: Field[] | undefined;\n private _fieldsByName: Map<string, Field> | undefined;\n private _resolvedAssociations: Association[] | undefined;\n private _associationsByAccessor: Map<string, Association> | undefined;\n\n /**\n * Lazily builds the resolved fields list and name-to-field index on first access.\n *\n * @remarks\n * Plain [`FieldConfig`](/api/data/type-aliases/FieldConfig) objects in the `fields` array are promoted to [`Field`](/api/data/classes/Field) instances\n * on the first call; subsequent calls return immediately.\n */\n private ensureIndex(): void {\n if (this._resolvedFields) {\n return;\n }\n\n this._resolvedFields = this.fields.map(f => f instanceof Field ? f : new Field(f));\n this._fieldsByName = new Map();\n\n for (const field of this._resolvedFields) {\n this._fieldsByName.set(field.getName(), field);\n }\n\n this._resolvedAssociations = (this.associations ?? []).map(a => AbstractModel.promoteAssociation(a));\n this._associationsByAccessor = new Map();\n\n for (const association of this._resolvedAssociations) {\n this._associationsByAccessor.set(association.getAccessor(), association);\n\n this.assertNestedKeyFree(association);\n }\n }\n\n /**\n * Promotes a plain {@link AssociationOptions} object to the concrete\n * {@link Association} subclass named by its `kind` discriminant.\n *\n * @param association - An already-built association, or an options object.\n *\n * @returns The supplied {@link Association}, or a freshly-built\n * {@link HasManyAssociation} / {@link BelongsToAssociation}.\n */\n private static promoteAssociation(association: Association | AssociationOptions): Association {\n if (association instanceof Association) {\n return association;\n }\n\n return association.kind === 'belongsTo'\n ? new BelongsToAssociation(association)\n : new HasManyAssociation(association);\n }\n\n /**\n * Asserts that an association's nested key does not collide with any field's\n * raw-data mapping, which would let an embedded child array be mis-read by\n * the field-mapping loop in {@link createRecord}.\n *\n * @param association - The association whose nested key is checked.\n */\n private assertNestedKeyFree(association: Association): void {\n const nestedKey = association.getNestedKey();\n\n for (const field of this._resolvedFields!) {\n if (field.getMapping() === nestedKey) {\n throw new Error(`AbstractModel: association '${association.getAccessor()}' nested key '${nestedKey}' collides with field '${field.getName()}' mapping`);\n }\n }\n }\n\n /**\n * Returns the Field designated as the primary key, or undefined if none is set.\n *\n * @returns The primary key Field, or undefined if no primary key has been configured.\n */\n getPrimaryKeyField(): Field | undefined {\n if (!this._primaryKey) {\n return undefined;\n }\n\n return this.getField(this._primaryKey);\n }\n\n /**\n * Returns all resolved Field instances for this model.\n *\n * @returns An array of all Field instances defined on this model.\n */\n getFields(): Field[] {\n this.ensureIndex();\n\n return this._resolvedFields!;\n }\n\n /**\n * Returns the Field with the given name, or undefined if not found.\n *\n * @param name - The logical name of the field to look up.\n *\n * @returns The matching Field, or undefined if no field with that name exists.\n */\n getField(name: string): Field | undefined {\n this.ensureIndex();\n\n return this._fieldsByName!.get(name);\n }\n\n /**\n * Returns true if the model contains a field with the given name.\n *\n * @param name - The logical name of the field to check.\n *\n * @returns True if a field with the given name exists, false otherwise.\n */\n hasField(name: string): boolean {\n this.ensureIndex();\n\n return this._fieldsByName!.has(name);\n }\n\n /**\n * Returns all resolved {@link Association} instances for this model.\n *\n * @returns An array of every association defined on this model; empty when none.\n */\n getAssociations(): Association[] {\n this.ensureIndex();\n\n return this._resolvedAssociations!;\n }\n\n /**\n * Returns the {@link Association} with the given accessor, or undefined.\n *\n * @param accessor - The accessor name to look up.\n *\n * @returns The matching association, or undefined when none is declared.\n */\n getAssociation(accessor: string): Association | undefined {\n this.ensureIndex();\n\n return this._associationsByAccessor!.get(accessor);\n }\n\n /**\n * Creates a ModelRecord from a plain object or positional array, applying field mappings and defaults.\n *\n * @param data - Optional. The source data as a key/value object or a positional array.\n * When an array is provided, values are assigned to fields ordered by their `order` property.\n *\n * @returns A new ModelRecord populated with mapped and defaulted field values.\n *\n * @remarks\n * When `data` is an array, fields are sorted by their `order` value before being matched\n * by position. Fields absent from `data` receive the value from `field.getDefaultValue()`.\n */\n createRecord(data: Record<string, any> | any[] = {}): ModelRecord {\n this.ensureIndex();\n\n let source: Record<string, any>;\n\n if (Array.isArray(data)) {\n const sorted = this._resolvedFields!.slice().sort((a, b) => a.getOrder() - b.getOrder());\n\n source = {};\n\n sorted.forEach((field, i) => {\n source[field.getMapping()] = data[i];\n });\n } else {\n source = data;\n }\n\n const mapped: Record<string, any> = {};\n\n for (const field of this._resolvedFields!) {\n const raw = source[field.getMapping()];\n const value = raw !== undefined ? raw : field.getDefaultValue();\n\n mapped[field.getName()] = field.convertValue(value, source);\n }\n\n const seed: Record<string, any[]> = {};\n\n for (const association of this._resolvedAssociations!) {\n const raw = source[association.getNestedKey()];\n\n if (association.kind === 'hasMany' && Array.isArray(raw)) {\n seed[association.getAccessor()] = raw;\n }\n }\n\n return new ModelRecord(this, mapped, seed);\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Field, FieldOptions } from '~/data/Field.js';\nimport type { Association, AssociationOptions } from '~/data/Association.js';\nimport { AbstractModel } from '~/data/AbstractModel.js';\n\n/**\n * Construction-time options for {@link Model}.\n *\n * @category Data\n */\nexport interface ModelOptions {\n fields: Array<Field | FieldOptions>;\n primaryKey?: string;\n associations?: Array<Association | AssociationOptions>;\n}\n\n/**\n * A concrete, configurable model created at runtime from a field array.\n * Use this class when you do not need a dedicated model subclass.\n *\n * @category Data\n */\nexport class Model extends AbstractModel {\n\n readonly fields: (Field | FieldOptions)[];\n\n /**\n * Constructs a Model with the specified fields and an optional primary key.\n *\n * @param fields - An array of Field instances or FieldOptions objects that define the schema, or a {@link ModelOptions} bag.\n * @param primaryKey - Optional. The name of the field to use as the primary key. Ignored when the first argument is a {@link ModelOptions} bag.\n */\n constructor(fields: Array<Field | FieldOptions> | ModelOptions, primaryKey?: string) {\n super();\n\n if (Array.isArray(fields)) {\n this.fields = fields;\n this._primaryKey = primaryKey;\n } else {\n this.fields = fields.fields;\n this._primaryKey = fields.primaryKey;\n this.associations = fields.associations;\n }\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { ModelRecord } from '~/data/ModelRecord.js';\n// Type-only imports: erased at compile time, so they introduce no runtime\n// dependency edge back to AbstractStore/FilterDescriptor and cannot form a cycle.\nimport type { SortDescriptor } from '~/data/AbstractStore.js';\nimport type { FilterDescriptor } from '~/data/FilterDescriptor.js';\n\n/**\n * Optional parameters passed to {@link Proxy.read} when the store opts in to\n * server-side pagination or remote sort/filter.\n *\n * @remarks\n * When `AbstractStore.setPageSize(n)` has been called, `AbstractStore.load()`\n * builds a [`ReadParams`](/api/data/interfaces/ReadParams) object describing the desired page and forwards it to\n * the proxy. Proxies that do not understand pagination (e.g. {@link MemoryProxy})\n * are free to ignore the argument.\n *\n * When the store sets `remoteSort`/`remoteFilter`, the active `sorters`/`filters`\n * descriptors ride along here so the proxy can encode them for its transport.\n * `signal` lets the store abort a superseded HTTP read.\n *\n * @category Data\n */\nexport interface ReadParams {\n page? : number;\n pageSize?: number;\n sorters? : SortDescriptor[];\n filters? : FilterDescriptor[];\n signal? : AbortSignal;\n}\n\n/**\n * Abstract base class for all data proxies.\n * Defines the four CRUD operations that every proxy implementation must provide.\n *\n * @remarks\n * AbstractStore calls these methods during `load()` and `sync()`. Each method\n * receives or returns plain data objects so that the proxy layer remains\n * decoupled from the store's record management logic.\n *\n * @category Data\n */\nexport abstract class Proxy {\n\n /**\n * Fetches records from the data source.\n *\n * @param params - Optional. Pagination parameters from the store.\n * Proxies that do not support pagination may ignore this argument.\n *\n * @returns A promise that resolves to an array of raw data objects.\n */\n abstract read(params?: ReadParams): Promise<any[]>;\n\n /**\n * Persists a new record to the data source.\n *\n * @param record - The new ModelRecord to create.\n *\n * @returns A promise that resolves to the server-side representation of the created record,\n * which may include server-assigned values such as a generated primary key.\n */\n abstract create(record: ModelRecord): Promise<Record<string, any>>;\n\n /**\n * Updates an existing record in the data source.\n *\n * @param record - The dirty ModelRecord to persist.\n *\n * @returns A promise that resolves to the server-side representation of the updated record.\n */\n abstract update(record: ModelRecord): Promise<Record<string, any>>;\n\n /**\n * Removes a record from the data source.\n *\n * @param record - The ModelRecord to delete.\n *\n * @returns A promise that resolves when the deletion is complete.\n */\n abstract destroy(record: ModelRecord): Promise<void>;\n\n /**\n * Batch-creates records in a single request, when the transport supports it.\n *\n * @param records - The new ModelRecords to create, in order.\n *\n * @returns A promise resolving to the per-record server data in the same\n * order as `records`, so the store can commit each positionally.\n *\n * @remarks\n * Optional. When absent, {@link AbstractStore.sync} falls back to issuing one\n * {@link create} per record.\n */\n createBatch?(records: ModelRecord[]): Promise<Record<string, any>[]>;\n\n /**\n * Batch-updates records in a single request, when the transport supports it.\n *\n * @param records - The dirty ModelRecords to update, in order.\n *\n * @returns A promise resolving to the per-record server data in the same\n * order as `records`, so the store can commit each positionally.\n *\n * @remarks\n * Optional. When absent, {@link AbstractStore.sync} falls back to issuing one\n * {@link update} per record.\n */\n updateBatch?(records: ModelRecord[]): Promise<Record<string, any>[]>;\n\n /**\n * Batch-destroys records in a single request, when the transport supports it.\n *\n * @param records - The ModelRecords to delete, in order.\n *\n * @returns A promise that resolves when the batch deletion is complete.\n *\n * @remarks\n * Optional. When absent, {@link AbstractStore.sync} falls back to issuing one\n * {@link destroy} per record.\n */\n destroyBatch?(records: ModelRecord[]): Promise<void>;\n\n /**\n * Returns the total record count reported by the most recent paginated read.\n *\n * @returns The total count from the last paginated response, or undefined if\n * the proxy does not support pagination or no paginated read has occurred.\n *\n * @remarks\n * Default implementation returns undefined. Pagination-aware proxies (such as\n * {@link AjaxProxy}) override this to return the `total` value parsed from\n * the server envelope.\n */\n getLastTotalCount(): number | undefined {\n return undefined;\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { ModelRecord } from '~/data/ModelRecord.js';\nimport { Proxy, ReadParams } from '~/data/proxy/Proxy.js';\n\n/**\n * Construction-time options for {@link MemoryProxy}.\n *\n * @category Data\n */\nexport interface MemoryProxyOptions {\n data: any[];\n}\n\n/**\n * @deprecated Use {@link MemoryProxyOptions}.\n */\nexport type MemoryProxyConfig = MemoryProxyOptions;\n\n/**\n * An in-memory proxy that stores data as a plain JavaScript array.\n * All operations resolve synchronously via `Promise.resolve`.\n *\n * @remarks\n * Because this proxy holds no external state, data is lost when the page is\n * refreshed or the proxy instance is discarded. It is primarily intended for\n * testing and for stores that manage transient, client-only data.\n *\n * @category Data\n */\nexport class MemoryProxy extends Proxy {\n\n private _data: any[];\n\n /**\n * Constructs a MemoryProxy, optionally pre-populated with an initial data array.\n *\n * @param options - Optional. Options object containing the initial data array.\n */\n constructor(options: MemoryProxyOptions = { data: [] }) {\n // Proxy has no options bag; fields are read from options directly below.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this._data = options.data.slice();\n }\n\n /**\n * Replaces the in-memory data array used by this proxy.\n *\n * @param data - The new data array; a shallow copy is stored internally.\n */\n setData(data: any[]): void {\n this._data = data.slice();\n }\n\n /**\n * Returns a copy of the in-memory data array.\n *\n * @param _params - Optional. Pagination parameters; ignored by this proxy.\n *\n * @returns A promise that resolves to a shallow copy of the current data array.\n */\n read(_params?: ReadParams): Promise<any[]> {\n return Promise.resolve(this._data.slice());\n }\n\n /**\n * Appends a copy of the record's data to the in-memory array.\n *\n * @param record - The new ModelRecord to store.\n *\n * @returns A promise that resolves to the stored copy of the record's data.\n */\n create(record: ModelRecord): Promise<Record<string, any>> {\n const copy = { ...record.getData() };\n\n this._data.push(copy);\n\n return Promise.resolve(copy);\n }\n\n /**\n * Updates the matching entry in the in-memory array by primary key.\n *\n * @param record - The dirty ModelRecord whose data should replace the existing entry.\n *\n * @returns A promise that resolves to the updated copy of the record's data.\n *\n * @remarks\n * If the model has no primary key or the record is not found in the array, the\n * array is left unchanged and the method still resolves successfully.\n */\n update(record: ModelRecord): Promise<Record<string, any>> {\n const copy = { ...record.getData() };\n const pkName = record.getModel().getPrimaryKeyField()?.getName();\n\n if (pkName !== undefined) {\n const id = record.getId();\n const idx = this._data.findIndex(d => d[pkName] === id);\n\n if (idx !== -1) {\n this._data[idx] = copy;\n }\n }\n\n return Promise.resolve(copy);\n }\n\n /**\n * Removes the matching entry from the in-memory array by primary key.\n *\n * @param record - The ModelRecord to remove.\n *\n * @returns A promise that resolves when the removal is complete.\n *\n * @remarks\n * If the model has no primary key or the record is not found, the array is\n * left unchanged and the method still resolves successfully.\n */\n destroy(record: ModelRecord): Promise<void> {\n const pkName = record.getModel().getPrimaryKeyField()?.getName();\n\n if (pkName !== undefined) {\n const id = record.getId();\n const idx = this._data.findIndex(d => d[pkName] === id);\n\n if (idx !== -1) {\n this._data.splice(idx, 1);\n }\n }\n\n return Promise.resolve();\n }\n}\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { AbstractStore, AbstractStoreOptions } from '~/data/AbstractStore.js';\nimport { Model } from '~/data/Model.js';\nimport { MemoryProxy } from '~/data/proxy/MemoryProxy.js';\n\n/**\n * Construction-time options for {@link MemoryStore}.\n *\n * @category Data\n */\nexport interface MemoryStoreOptions extends AbstractStoreOptions {\n model: Model;\n data?: any[];\n}\n\n/**\n * A store backed entirely by in-memory data.\n * Useful for testing or for static datasets that do not require server persistence.\n *\n * @category Data\n */\nexport class MemoryStore extends AbstractStore {\n\n readonly model: Model;\n readonly proxy: MemoryProxy = new MemoryProxy();\n\n /**\n * Constructs a MemoryStore with the given model and an optional initial data array.\n *\n * @param modelOrOptions - The Model that defines the record schema, or a {@link MemoryStoreOptions} bag.\n * @param data - Optional. The initial data records to load into the store. Ignored when the first argument is a {@link MemoryStoreOptions} bag.\n */\n constructor(modelOrOptions: Model | MemoryStoreOptions, data: any[] = []) {\n super();\n\n if (modelOrOptions instanceof Model) {\n this.model = modelOrOptions;\n this.proxy.setData(data);\n } else {\n this.model = modelOrOptions.model;\n this.proxy.setData(modelOrOptions.data ?? []);\n\n this.applyOptions(modelOrOptions);\n }\n }\n}\n"],"mappings":"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"}