@sqlrooms/deck 0.29.0-rc.2 → 0.29.0-rc.4

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 (123) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +69 -0
  3. package/dist/DeckJsonMap.d.ts +4 -0
  4. package/dist/DeckJsonMap.d.ts.map +1 -0
  5. package/dist/{DeckMap.js → DeckJsonMap.js} +57 -44
  6. package/dist/DeckJsonMap.js.map +1 -0
  7. package/dist/DeckJsonMapSpec.d.ts +7617 -0
  8. package/dist/DeckJsonMapSpec.d.ts.map +1 -0
  9. package/dist/DeckJsonMapSpec.js +81 -0
  10. package/dist/DeckJsonMapSpec.js.map +1 -0
  11. package/dist/DeckMapConfigPopoverEditor.d.ts +8 -0
  12. package/dist/DeckMapConfigPopoverEditor.d.ts.map +1 -0
  13. package/dist/DeckMapConfigPopoverEditor.js +43 -0
  14. package/dist/DeckMapConfigPopoverEditor.js.map +1 -0
  15. package/dist/createDeckJsonSpecFromDatasets.d.ts +11 -0
  16. package/dist/createDeckJsonSpecFromDatasets.d.ts.map +1 -0
  17. package/dist/createDeckJsonSpecFromDatasets.js +85 -0
  18. package/dist/createDeckJsonSpecFromDatasets.js.map +1 -0
  19. package/dist/dashboard.d.ts +4 -0
  20. package/dist/dashboard.d.ts.map +1 -0
  21. package/dist/dashboard.js +465 -0
  22. package/dist/dashboard.js.map +1 -0
  23. package/dist/dashboardConfig.d.ts +51 -0
  24. package/dist/dashboardConfig.d.ts.map +1 -0
  25. package/dist/dashboardConfig.js +54 -0
  26. package/dist/dashboardConfig.js.map +1 -0
  27. package/dist/datasets/PreparedDatasetStore.d.ts +120 -0
  28. package/dist/datasets/PreparedDatasetStore.d.ts.map +1 -0
  29. package/dist/datasets/PreparedDatasetStore.js +262 -0
  30. package/dist/datasets/PreparedDatasetStore.js.map +1 -0
  31. package/dist/datasets/helpers.d.ts +57 -0
  32. package/dist/datasets/helpers.d.ts.map +1 -0
  33. package/dist/datasets/helpers.js +146 -0
  34. package/dist/datasets/helpers.js.map +1 -0
  35. package/dist/datasets/normalizeDatasets.d.ts +8 -2
  36. package/dist/datasets/normalizeDatasets.d.ts.map +1 -1
  37. package/dist/datasets/normalizeDatasets.js +33 -48
  38. package/dist/datasets/normalizeDatasets.js.map +1 -1
  39. package/dist/datasets/tableAdapter.d.ts +11 -0
  40. package/dist/datasets/tableAdapter.d.ts.map +1 -0
  41. package/dist/datasets/tableAdapter.js +11 -0
  42. package/dist/datasets/tableAdapter.js.map +1 -0
  43. package/dist/datasets/types.d.ts +40 -0
  44. package/dist/datasets/types.d.ts.map +1 -0
  45. package/dist/datasets/types.js +2 -0
  46. package/dist/datasets/types.js.map +1 -0
  47. package/dist/datasets/usePreparedDatasetStates.d.ts +16 -0
  48. package/dist/datasets/usePreparedDatasetStates.d.ts.map +1 -0
  49. package/dist/datasets/usePreparedDatasetStates.js +59 -0
  50. package/dist/datasets/usePreparedDatasetStates.js.map +1 -0
  51. package/dist/index.d.ts +10 -3
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +7 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/json/colorScaleFunction.d.ts +14 -0
  56. package/dist/json/colorScaleFunction.d.ts.map +1 -0
  57. package/dist/json/colorScaleFunction.js +34 -0
  58. package/dist/json/colorScaleFunction.js.map +1 -0
  59. package/dist/json/compileColorScale.d.ts +4 -27
  60. package/dist/json/compileColorScale.d.ts.map +1 -1
  61. package/dist/json/compileColorScale.js +16 -450
  62. package/dist/json/compileColorScale.js.map +1 -1
  63. package/dist/json/compileGeoArrowAccessor.d.ts +7 -0
  64. package/dist/json/compileGeoArrowAccessor.d.ts.map +1 -1
  65. package/dist/json/compileGeoArrowAccessor.js +68 -2
  66. package/dist/json/compileGeoArrowAccessor.js.map +1 -1
  67. package/dist/json/createDeckJsonConfiguration.d.ts.map +1 -1
  68. package/dist/json/createDeckJsonConfiguration.js +86 -33
  69. package/dist/json/createDeckJsonConfiguration.js.map +1 -1
  70. package/dist/json/defaultClasses.d.ts +12 -6
  71. package/dist/json/defaultClasses.d.ts.map +1 -1
  72. package/dist/json/defaultClasses.js +7 -1
  73. package/dist/json/defaultClasses.js.map +1 -1
  74. package/dist/json/extractColorScaleLegends.d.ts +1 -1
  75. package/dist/json/extractColorScaleLegends.d.ts.map +1 -1
  76. package/dist/json/extractColorScaleLegends.js +8 -6
  77. package/dist/json/extractColorScaleLegends.js.map +1 -1
  78. package/dist/json/layerCompatibility.d.ts +9 -3
  79. package/dist/json/layerCompatibility.d.ts.map +1 -1
  80. package/dist/json/layerCompatibility.js +135 -11
  81. package/dist/json/layerCompatibility.js.map +1 -1
  82. package/dist/json/layerConfig.d.ts +7 -3
  83. package/dist/json/layerConfig.d.ts.map +1 -1
  84. package/dist/json/layerConfig.js +19 -8
  85. package/dist/json/layerConfig.js.map +1 -1
  86. package/dist/prepare/detectGeometryColumn.d.ts.map +1 -1
  87. package/dist/prepare/detectGeometryColumn.js +1 -1
  88. package/dist/prepare/detectGeometryColumn.js.map +1 -1
  89. package/dist/prepare/geoarrow.d.ts.map +1 -1
  90. package/dist/prepare/geoarrow.js.map +1 -1
  91. package/dist/prepare/geometryDecoder.d.ts +1 -2
  92. package/dist/prepare/geometryDecoder.d.ts.map +1 -1
  93. package/dist/prepare/geometryDecoder.js.map +1 -1
  94. package/dist/prepare/prepareDeckDataset.d.ts +46 -0
  95. package/dist/prepare/prepareDeckDataset.d.ts.map +1 -1
  96. package/dist/prepare/prepareDeckDataset.js +46 -0
  97. package/dist/prepare/prepareDeckDataset.js.map +1 -1
  98. package/dist/prepare/toGeoJsonBinary.d.ts.map +1 -1
  99. package/dist/prepare/toGeoJsonBinary.js +3 -2
  100. package/dist/prepare/toGeoJsonBinary.js.map +1 -1
  101. package/dist/prepare/wkbDecoder.d.ts.map +1 -1
  102. package/dist/prepare/wkbDecoder.js +36 -9
  103. package/dist/prepare/wkbDecoder.js.map +1 -1
  104. package/dist/types.d.ts +31 -92
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/types.js +6 -1
  107. package/dist/types.js.map +1 -1
  108. package/dist/useDeckLayersReadyRedraw.d.ts +13 -0
  109. package/dist/useDeckLayersReadyRedraw.d.ts.map +1 -0
  110. package/dist/useDeckLayersReadyRedraw.js +35 -0
  111. package/dist/useDeckLayersReadyRedraw.js.map +1 -0
  112. package/package.json +35 -29
  113. package/dist/ColorScaleLegend.d.ts +0 -8
  114. package/dist/ColorScaleLegend.d.ts.map +0 -1
  115. package/dist/ColorScaleLegend.js +0 -11
  116. package/dist/ColorScaleLegend.js.map +0 -1
  117. package/dist/DeckMap.d.ts +0 -4
  118. package/dist/DeckMap.d.ts.map +0 -1
  119. package/dist/DeckMap.js.map +0 -1
  120. package/dist/datasets/usePreparedDeckDatasets.d.ts +0 -3
  121. package/dist/datasets/usePreparedDeckDatasets.d.ts.map +0 -1
  122. package/dist/datasets/usePreparedDeckDatasets.js +0 -61
  123. package/dist/datasets/usePreparedDeckDatasets.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboardConfig.js","sourceRoot":"","sources":["../src/dashboardConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,GACN,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAC,QAAQ,EAAC,MAAM,oBAAoB,CAAC;AAI5C,MAAM,CAAC,MAAM,6BAA6B,GAAG,eAAe,CAAC;AAiD7D,MAAM,UAAU,mBAAmB,CACjC,MAA+B;IAE/B,IACE,CAAC,MAAM,CAAC,IAAI;QACZ,CAAC,MAAM,CAAC,QAAQ;QAChB,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;QACnC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC9B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,MAAqC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,iCAAiC,CAC/C,OAAiD;IAEjD,MAAM,EAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAC,GAAG,OAAO,CAAC;IAC3C,OAAO;QACL,EAAE,EAAE,QAAQ,EAAE;QACd,IAAI,EAAE,6BAA6B;QACnC,KAAK,EAAE,KAAK,IAAI,KAAK;QACrB,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAC,MAAM,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3B,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAA4B;KACtE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oCAAoC,CAAC,OAIpD;IACC,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC;IAC9C,IAAI,aAAa,EAAE,QAAQ,IAAI,aAAa,EAAE,SAAS,EAAE,CAAC;QACxD,OAAO,aAAa,CAAC;IACvB,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;QACtE,OAAO,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC,SAAS,CAAC,aAAa;QACpC,CAAC,CAAC,EAAC,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,aAAa,EAAC;QAC9C,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,kCAAkC,CAChD,MAAsC,EACtC,MAAe;IAEf,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ;QAC3B,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;YACT,uBAAuB,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC;SAC1D,CAAC;QACJ,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAEvC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,MAAe,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,8BAA8B,CAC5C,SAAsC,EACtC,aAGC;IAED,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC;QAC/D,SAAS;QACT;YACE,UAAU,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE,UAAU;YAChD,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,oBAAoB,EAAE,OAAO,CAAC,oBAAoB;SACnD;KACF,CAAC,CACH,CAAC;AACJ,CAAC","sourcesContent":["import {\n type MosaicDashboardEntryType,\n type MosaicDashboardPanelConfigType,\n type MosaicDashboardPanelSourceType,\n Query,\n} from '@sqlrooms/mosaic';\nimport {createId} from '@paralleldrive/cuid2';\nimport {verbatim} from '@uwdata/mosaic-sql';\nimport type {Table as ArrowTable} from 'apache-arrow';\nimport type {DeckJsonMapProps, DeckSqlDatasetInput} from './types';\n\nexport const DECK_MAP_DASHBOARD_PANEL_TYPE = 'deck-json-map';\n\nexport type DeckMapDashboardDatasetConfig = Omit<\n DeckSqlDatasetInput,\n 'sqlQuery'\n> & {\n source?: MosaicDashboardPanelSourceType;\n};\n\nexport type DeckMapDashboardInteractionConfig = {\n type: 'point-radius-brush';\n dataset: string;\n longitudeColumn: string;\n latitudeColumn: string;\n radiusMeters?: number;\n event?: 'hover' | 'click';\n};\n\nexport type DeckMapDashboardFitToDataConfig = {\n dataset: string;\n longitudeColumn: string;\n latitudeColumn: string;\n padding?: number;\n maxZoom?: number;\n};\n\nexport type DeckMapDashboardPanelConfig = {\n spec: DeckJsonMapProps['spec'];\n datasets: Record<string, DeckMapDashboardDatasetConfig>;\n mapStyle?: string;\n mapProps?: Record<string, unknown>;\n showLegends?: boolean;\n interaction?: DeckMapDashboardInteractionConfig;\n fitToData?: DeckMapDashboardFitToDataConfig;\n};\n\nexport type CreateDeckMapDashboardPanelConfigOptions =\n DeckMapDashboardPanelConfig & {\n title?: string;\n source?: MosaicDashboardPanelSourceType;\n };\n\nexport type DeckMapDashboardDatasetClientState = {\n arrowTable?: ArrowTable;\n isLoading: boolean;\n error?: Error;\n client: unknown;\n};\n\nexport function asDeckJsonMapConfig(\n config: Record<string, unknown>,\n): DeckMapDashboardPanelConfig | null {\n if (\n !config.spec ||\n !config.datasets ||\n typeof config.datasets !== 'object' ||\n Array.isArray(config.datasets)\n ) {\n return null;\n }\n\n return config as DeckMapDashboardPanelConfig;\n}\n\nexport function createDeckMapDashboardPanelConfig(\n options: CreateDeckMapDashboardPanelConfigOptions,\n): MosaicDashboardPanelConfigType {\n const {title, source, ...config} = options;\n return {\n id: createId(),\n type: DECK_MAP_DASHBOARD_PANEL_TYPE,\n title: title ?? 'Map',\n ...(source ? {source} : {}),\n config: JSON.parse(JSON.stringify(config)) as Record<string, unknown>,\n };\n}\n\nexport function resolveDeckMapDashboardDatasetSource(options: {\n dashboard: MosaicDashboardEntryType;\n panel: MosaicDashboardPanelConfigType;\n dataset?: DeckMapDashboardDatasetConfig;\n}): MosaicDashboardPanelSourceType | undefined {\n const datasetSource = options.dataset?.source;\n if (datasetSource?.sqlQuery || datasetSource?.tableName) {\n return datasetSource;\n }\n if (options.panel.source?.sqlQuery || options.panel.source?.tableName) {\n return options.panel.source;\n }\n return options.dashboard.selectedTable\n ? {tableName: options.dashboard.selectedTable}\n : undefined;\n}\n\nexport function createDeckMapDashboardDatasetQuery(\n source: MosaicDashboardPanelSourceType,\n filter: unknown,\n) {\n const query = source.sqlQuery\n ? Query.from({\n __dashboard_map_dataset: verbatim(`(${source.sqlQuery})`),\n })\n : Query.from(source.tableName ?? '');\n\n return query.select('*').where(filter as never);\n}\n\nexport function createDeckMapDashboardDatasets(\n mapConfig: DeckMapDashboardPanelConfig,\n datasetStates: Record<\n string,\n Pick<DeckMapDashboardDatasetClientState, 'arrowTable'>\n >,\n): DeckJsonMapProps['datasets'] {\n return Object.fromEntries(\n Object.entries(mapConfig.datasets).map(([datasetId, dataset]) => [\n datasetId,\n {\n arrowTable: datasetStates[datasetId]?.arrowTable,\n geometryColumn: dataset.geometryColumn,\n geometryEncodingHint: dataset.geometryEncodingHint,\n },\n ]),\n );\n}\n"]}
@@ -0,0 +1,120 @@
1
+ import type { QueryHandle } from '@sqlrooms/duckdb';
2
+ import { type DeckDatasetInput, type PreparedDeckDatasetState } from '../types';
3
+ import type { PreparedDatasetCacheEntry, PreparedDatasetStoreOptions } from './types';
4
+ /**
5
+ * Internal store state for the prepared-dataset cache.
6
+ *
7
+ * `DeckJsonMap` never talks to this shape directly; `usePreparedDatasetStates`
8
+ * uses it as the backing state machine for preparation, reuse, and eviction.
9
+ */
10
+ type PreparedDatasetStoreState = {
11
+ /**
12
+ * Cache keys currently referenced by each hook consumer.
13
+ *
14
+ * This lets the store update consumer memberships incrementally when a map's
15
+ * dataset registry changes, instead of resetting all cache state.
16
+ */
17
+ consumerKeys: Record<string, string[]>;
18
+ /**
19
+ * Prepared entries keyed by resolved dataset identity.
20
+ *
21
+ * Keys are based on query/table identity plus geometry options, not dataset
22
+ * id, so the same prepared result can be reused across multiple maps or
23
+ * differently named datasets that point at the same underlying data.
24
+ */
25
+ entries: Record<string, PreparedDatasetCacheEntry>;
26
+ /**
27
+ * Ensure a cache entry exists for the given dataset key.
28
+ *
29
+ * If an entry already exists, this is a no-op other than touching its LRU
30
+ * metadata. Otherwise the store creates a `loading` entry, resolves the
31
+ * source table through DuckDB if needed, runs `prepareDeckDataset(...)`, and
32
+ * stores the resulting `ready` or `error` state.
33
+ */
34
+ ensureEntry: (options: {
35
+ cacheKey: string;
36
+ datasetId: string;
37
+ executeSql: (query: string, version?: number) => Promise<QueryHandle | null>;
38
+ input: DeckDatasetInput;
39
+ }) => void;
40
+ /**
41
+ * Remove all subscriptions for one hook consumer.
42
+ *
43
+ * This is called when a `DeckJsonMap` instance unmounts so the store can
44
+ * release the consumer's references and allow now-unreferenced entries to be
45
+ * evicted later.
46
+ */
47
+ removeConsumer: (consumerId: string) => void;
48
+ /**
49
+ * Reconcile the set of cache keys referenced by one hook consumer.
50
+ *
51
+ * This adds the consumer to newly needed entries, removes it from entries no
52
+ * longer needed, and preserves existing entries in place so unrelated dataset
53
+ * states do not get reset during prop changes.
54
+ */
55
+ syncConsumer: (consumerId: string, keys: string[]) => void;
56
+ /**
57
+ * Reconcile and ensure all prepared entries needed by one dataset registry.
58
+ *
59
+ * This is the store-level orchestration entrypoint used by
60
+ * `usePreparedDatasetStates`. It computes cache keys for the current dataset
61
+ * registry, syncs consumer memberships, and eagerly ensures any resolvable
62
+ * entries exist.
63
+ */
64
+ syncDatasetsForConsumer: (options: {
65
+ consumerId: string;
66
+ datasets: Record<string, DeckDatasetInput>;
67
+ executeSql: (query: string, version?: number) => Promise<QueryHandle | null>;
68
+ sqlSourceIdentity: object;
69
+ }) => void;
70
+ };
71
+ /**
72
+ * Create a feature-local cache store for prepared deck datasets.
73
+ *
74
+ * The store is intentionally scoped to deck's preparation layer. It reuses the
75
+ * expensive result of `prepareDeckDataset(...)` across consumers while leaving
76
+ * raw query/result caching to the upstream data provider:
77
+ *
78
+ * - Mosaic handles cached query results through its coordinator/query manager
79
+ * - DuckDB-backed SQL datasets still use the DuckDB slice execution path
80
+ *
81
+ * This keeps `@sqlrooms/deck` independent from either provider while still
82
+ * avoiding repeated geometry decoding and GeoArrow/GeoJSON shaping work.
83
+ */
84
+ export declare function createPreparedDatasetStore(options?: PreparedDatasetStoreOptions): import("zustand").StoreApi<PreparedDatasetStoreState>;
85
+ /**
86
+ * Resolve a shared prepared-store entry back into the dataset-state shape
87
+ * consumed by `DeckJsonMap`.
88
+ *
89
+ * Prepared entries are shared by cache key, while `datasetId` stays a
90
+ * view-level label used by layer binding and user-facing errors. When a shared
91
+ * prepared entry is reused for a different dataset id, this helper remaps the
92
+ * prepared payload so downstream callers still see the dataset id they asked
93
+ * for.
94
+ */
95
+ export declare function resolvePreparedDeckDatasetState(options: {
96
+ datasetId: string;
97
+ entry?: PreparedDatasetCacheEntry;
98
+ }): PreparedDeckDatasetState;
99
+ /**
100
+ * Resolve the full `datasetId -> state` map for a dataset registry.
101
+ *
102
+ * This mirrors the lookup logic used by `DeckJsonMap`: unresolved Arrow
103
+ * datasets remain `loading`, while resolvable datasets reuse shared prepared
104
+ * entries keyed by data identity rather than by dataset id.
105
+ */
106
+ export declare function resolvePreparedDeckDatasetStates(options: {
107
+ datasets: Record<string, DeckDatasetInput>;
108
+ entries: Record<string, PreparedDatasetCacheEntry>;
109
+ sqlSourceIdentity: object;
110
+ }): Record<string, PreparedDeckDatasetState>;
111
+ /**
112
+ * Module-global prepared dataset cache shared by all `DeckJsonMap` instances.
113
+ *
114
+ * The cache stores only deck-specific prepared payloads, not raw query
115
+ * results, and applies a small internal LRU policy so settled entries do not
116
+ * grow without bound.
117
+ */
118
+ export declare const preparedDatasetStore: import("zustand").StoreApi<PreparedDatasetStoreState>;
119
+ export {};
120
+ //# sourceMappingURL=PreparedDatasetStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PreparedDatasetStore.d.ts","sourceRoot":"","sources":["../../src/datasets/PreparedDatasetStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAGlD,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC9B,MAAM,UAAU,CAAC;AAUlB,OAAO,KAAK,EACV,yBAAyB,EACzB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB;;;;;GAKG;AACH,KAAK,yBAAyB,GAAG;IAC/B;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;IACnD;;;;;;;OAOG;IACH,WAAW,EAAE,CAAC,OAAO,EAAE;QACrB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,CACV,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,KACb,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;QACjC,KAAK,EAAE,gBAAgB,CAAC;KACzB,KAAK,IAAI,CAAC;IACX;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC3D;;;;;;;OAOG;IACH,uBAAuB,EAAE,CAAC,OAAO,EAAE;QACjC,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;QAC3C,UAAU,EAAE,CACV,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,KACb,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;QACjC,iBAAiB,EAAE,MAAM,CAAC;KAC3B,KAAK,IAAI,CAAC;CACZ,CAAC;AAwBF;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,2BAAgC,yDAwO1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,yBAAyB,CAAC;CACnC,GAAG,wBAAwB,CAqB3B;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,OAAO,EAAE;IACxD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;IACnD,iBAAiB,EAAE,MAAM,CAAC;CAC3B,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAc3C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,uDAA+B,CAAC"}
@@ -0,0 +1,262 @@
1
+ import { createStore } from 'zustand/vanilla';
2
+ import { prepareDeckDataset } from '../prepare/prepareDeckDataset';
3
+ import { isSqlDatasetInput, } from '../types';
4
+ import { DEFAULT_MAX_PREPARED_DATASET_ENTRIES, cloneEntryWithConsumers, evictLruEntries, nextAccessTimestamp, resolvePreparedDatasetCacheKey, touchEntry, } from './helpers';
5
+ import { resolveArrowTable } from './normalizeDatasets';
6
+ function resolvePreparedDatasetDescriptors(options) {
7
+ const { datasets, sqlSourceIdentity } = options;
8
+ return Object.entries(datasets).map(([datasetId, input]) => ({
9
+ datasetId,
10
+ input,
11
+ cacheKey: resolvePreparedDatasetCacheKey({
12
+ input,
13
+ sqlSourceIdentity,
14
+ }),
15
+ }));
16
+ }
17
+ /**
18
+ * Create a feature-local cache store for prepared deck datasets.
19
+ *
20
+ * The store is intentionally scoped to deck's preparation layer. It reuses the
21
+ * expensive result of `prepareDeckDataset(...)` across consumers while leaving
22
+ * raw query/result caching to the upstream data provider:
23
+ *
24
+ * - Mosaic handles cached query results through its coordinator/query manager
25
+ * - DuckDB-backed SQL datasets still use the DuckDB slice execution path
26
+ *
27
+ * This keeps `@sqlrooms/deck` independent from either provider while still
28
+ * avoiding repeated geometry decoding and GeoArrow/GeoJSON shaping work.
29
+ */
30
+ export function createPreparedDatasetStore(options = {}) {
31
+ const { maxEntries = DEFAULT_MAX_PREPARED_DATASET_ENTRIES, prepareDataset = prepareDeckDataset, } = options;
32
+ return createStore((set, get) => ({
33
+ consumerKeys: {},
34
+ entries: {},
35
+ ensureEntry({ cacheKey, datasetId, executeSql, input }) {
36
+ const existing = get().entries[cacheKey];
37
+ if (existing) {
38
+ if (existing.status !== 'loading') {
39
+ set((state) => ({
40
+ ...state,
41
+ entries: {
42
+ ...state.entries,
43
+ [cacheKey]: touchEntry(existing),
44
+ },
45
+ }));
46
+ }
47
+ return;
48
+ }
49
+ const promise = Promise.resolve().then(async () => {
50
+ try {
51
+ let table = resolveArrowTable(input);
52
+ if (!table && isSqlDatasetInput(input)) {
53
+ const queryHandle = await executeSql(input.sqlQuery);
54
+ if (!queryHandle) {
55
+ throw new Error(`Query for dataset "${datasetId}" was cancelled.`);
56
+ }
57
+ table = await queryHandle;
58
+ }
59
+ if (!table) {
60
+ return;
61
+ }
62
+ const prepared = prepareDataset({
63
+ datasetId,
64
+ table,
65
+ geometryColumn: input.geometryColumn,
66
+ geometryEncodingHint: input.geometryEncodingHint,
67
+ });
68
+ set((state) => ({
69
+ ...state,
70
+ entries: evictLruEntries({
71
+ ...state.entries,
72
+ [cacheKey]: {
73
+ status: 'ready',
74
+ prepared,
75
+ consumers: state.entries[cacheKey]?.consumers ?? new Set(),
76
+ lastAccessedAt: nextAccessTimestamp(),
77
+ },
78
+ }, maxEntries),
79
+ }));
80
+ }
81
+ catch (error) {
82
+ set((state) => ({
83
+ ...state,
84
+ entries: evictLruEntries({
85
+ ...state.entries,
86
+ [cacheKey]: {
87
+ status: 'error',
88
+ error: error instanceof Error ? error : new Error(String(error)),
89
+ consumers: state.entries[cacheKey]?.consumers ?? new Set(),
90
+ lastAccessedAt: nextAccessTimestamp(),
91
+ },
92
+ }, maxEntries),
93
+ }));
94
+ }
95
+ });
96
+ set((state) => ({
97
+ ...state,
98
+ entries: {
99
+ ...state.entries,
100
+ [cacheKey]: {
101
+ status: 'loading',
102
+ promise,
103
+ consumers: state.entries[cacheKey]?.consumers ?? new Set(),
104
+ lastAccessedAt: nextAccessTimestamp(),
105
+ },
106
+ },
107
+ }));
108
+ },
109
+ syncConsumer(consumerId, keys) {
110
+ const previousKeys = new Set(get().consumerKeys[consumerId] ?? []);
111
+ const nextKeys = new Set(keys);
112
+ set((state) => {
113
+ let didChangeEntries = false;
114
+ let nextEntries = state.entries;
115
+ const updateEntry = (cacheKey, updater) => {
116
+ const currentEntry = nextEntries[cacheKey];
117
+ if (!currentEntry) {
118
+ return;
119
+ }
120
+ if (!didChangeEntries) {
121
+ nextEntries = { ...nextEntries };
122
+ didChangeEntries = true;
123
+ }
124
+ nextEntries[cacheKey] = updater(currentEntry);
125
+ };
126
+ for (const cacheKey of previousKeys) {
127
+ if (nextKeys.has(cacheKey)) {
128
+ continue;
129
+ }
130
+ updateEntry(cacheKey, (entry) => {
131
+ const consumers = new Set(entry.consumers);
132
+ consumers.delete(consumerId);
133
+ return cloneEntryWithConsumers(entry, consumers);
134
+ });
135
+ }
136
+ for (const cacheKey of nextKeys) {
137
+ updateEntry(cacheKey, (entry) => {
138
+ const consumers = new Set(entry.consumers);
139
+ consumers.add(consumerId);
140
+ return cloneEntryWithConsumers(entry, consumers);
141
+ });
142
+ }
143
+ return {
144
+ ...state,
145
+ consumerKeys: {
146
+ ...state.consumerKeys,
147
+ [consumerId]: [...nextKeys],
148
+ },
149
+ entries: didChangeEntries
150
+ ? evictLruEntries(nextEntries, maxEntries)
151
+ : state.entries,
152
+ };
153
+ });
154
+ },
155
+ removeConsumer(consumerId) {
156
+ const previousKeys = get().consumerKeys[consumerId];
157
+ if (!previousKeys) {
158
+ return;
159
+ }
160
+ set((state) => {
161
+ let didChangeEntries = false;
162
+ let nextEntries = state.entries;
163
+ for (const cacheKey of previousKeys) {
164
+ const entry = nextEntries[cacheKey];
165
+ if (!entry) {
166
+ continue;
167
+ }
168
+ const consumers = new Set(entry.consumers);
169
+ consumers.delete(consumerId);
170
+ if (!didChangeEntries) {
171
+ nextEntries = { ...nextEntries };
172
+ didChangeEntries = true;
173
+ }
174
+ nextEntries[cacheKey] = cloneEntryWithConsumers(entry, consumers);
175
+ }
176
+ const { [consumerId]: _removedConsumer, ...nextConsumerKeys } = state.consumerKeys;
177
+ return {
178
+ ...state,
179
+ consumerKeys: nextConsumerKeys,
180
+ entries: didChangeEntries
181
+ ? evictLruEntries(nextEntries, maxEntries)
182
+ : state.entries,
183
+ };
184
+ });
185
+ },
186
+ syncDatasetsForConsumer({ consumerId, datasets, executeSql, sqlSourceIdentity, }) {
187
+ const descriptors = resolvePreparedDatasetDescriptors({
188
+ datasets,
189
+ sqlSourceIdentity,
190
+ });
191
+ const cacheKeys = descriptors
192
+ .map((descriptor) => descriptor.cacheKey)
193
+ .filter((cacheKey) => Boolean(cacheKey));
194
+ for (const descriptor of descriptors) {
195
+ if (!descriptor.cacheKey) {
196
+ continue;
197
+ }
198
+ get().ensureEntry({
199
+ cacheKey: descriptor.cacheKey,
200
+ datasetId: descriptor.datasetId,
201
+ executeSql,
202
+ input: descriptor.input,
203
+ });
204
+ }
205
+ get().syncConsumer(consumerId, cacheKeys);
206
+ },
207
+ }));
208
+ }
209
+ /**
210
+ * Resolve a shared prepared-store entry back into the dataset-state shape
211
+ * consumed by `DeckJsonMap`.
212
+ *
213
+ * Prepared entries are shared by cache key, while `datasetId` stays a
214
+ * view-level label used by layer binding and user-facing errors. When a shared
215
+ * prepared entry is reused for a different dataset id, this helper remaps the
216
+ * prepared payload so downstream callers still see the dataset id they asked
217
+ * for.
218
+ */
219
+ export function resolvePreparedDeckDatasetState(options) {
220
+ const { datasetId, entry } = options;
221
+ if (!entry || entry.status === 'loading') {
222
+ return { status: 'loading' };
223
+ }
224
+ if (entry.status === 'error') {
225
+ return { status: 'error', error: entry.error };
226
+ }
227
+ return {
228
+ status: 'ready',
229
+ prepared: entry.prepared.datasetId === datasetId
230
+ ? entry.prepared
231
+ : {
232
+ ...entry.prepared,
233
+ datasetId,
234
+ },
235
+ };
236
+ }
237
+ /**
238
+ * Resolve the full `datasetId -> state` map for a dataset registry.
239
+ *
240
+ * This mirrors the lookup logic used by `DeckJsonMap`: unresolved Arrow
241
+ * datasets remain `loading`, while resolvable datasets reuse shared prepared
242
+ * entries keyed by data identity rather than by dataset id.
243
+ */
244
+ export function resolvePreparedDeckDatasetStates(options) {
245
+ const { datasets, entries, sqlSourceIdentity } = options;
246
+ return Object.fromEntries(resolvePreparedDatasetDescriptors({ datasets, sqlSourceIdentity }).map(({ datasetId, cacheKey }) => [
247
+ datasetId,
248
+ resolvePreparedDeckDatasetState({
249
+ datasetId,
250
+ entry: cacheKey ? entries[cacheKey] : undefined,
251
+ }),
252
+ ]));
253
+ }
254
+ /**
255
+ * Module-global prepared dataset cache shared by all `DeckJsonMap` instances.
256
+ *
257
+ * The cache stores only deck-specific prepared payloads, not raw query
258
+ * results, and applies a small internal LRU policy so settled entries do not
259
+ * grow without bound.
260
+ */
261
+ export const preparedDatasetStore = createPreparedDatasetStore();
262
+ //# sourceMappingURL=PreparedDatasetStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PreparedDatasetStore.js","sourceRoot":"","sources":["../../src/datasets/PreparedDatasetStore.ts"],"names":[],"mappings":"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","sourcesContent":["import type {QueryHandle} from '@sqlrooms/duckdb';\nimport {createStore} from 'zustand/vanilla';\nimport {prepareDeckDataset} from '../prepare/prepareDeckDataset';\nimport {\n isSqlDatasetInput,\n type DeckDatasetInput,\n type PreparedDeckDatasetState,\n} from '../types';\nimport {\n DEFAULT_MAX_PREPARED_DATASET_ENTRIES,\n cloneEntryWithConsumers,\n evictLruEntries,\n nextAccessTimestamp,\n resolvePreparedDatasetCacheKey,\n touchEntry,\n} from './helpers';\nimport {resolveArrowTable} from './normalizeDatasets';\nimport type {\n PreparedDatasetCacheEntry,\n PreparedDatasetStoreOptions,\n} from './types';\n\n/**\n * Internal store state for the prepared-dataset cache.\n *\n * `DeckJsonMap` never talks to this shape directly; `usePreparedDatasetStates`\n * uses it as the backing state machine for preparation, reuse, and eviction.\n */\ntype PreparedDatasetStoreState = {\n /**\n * Cache keys currently referenced by each hook consumer.\n *\n * This lets the store update consumer memberships incrementally when a map's\n * dataset registry changes, instead of resetting all cache state.\n */\n consumerKeys: Record<string, string[]>;\n /**\n * Prepared entries keyed by resolved dataset identity.\n *\n * Keys are based on query/table identity plus geometry options, not dataset\n * id, so the same prepared result can be reused across multiple maps or\n * differently named datasets that point at the same underlying data.\n */\n entries: Record<string, PreparedDatasetCacheEntry>;\n /**\n * Ensure a cache entry exists for the given dataset key.\n *\n * If an entry already exists, this is a no-op other than touching its LRU\n * metadata. Otherwise the store creates a `loading` entry, resolves the\n * source table through DuckDB if needed, runs `prepareDeckDataset(...)`, and\n * stores the resulting `ready` or `error` state.\n */\n ensureEntry: (options: {\n cacheKey: string;\n datasetId: string;\n executeSql: (\n query: string,\n version?: number,\n ) => Promise<QueryHandle | null>;\n input: DeckDatasetInput;\n }) => void;\n /**\n * Remove all subscriptions for one hook consumer.\n *\n * This is called when a `DeckJsonMap` instance unmounts so the store can\n * release the consumer's references and allow now-unreferenced entries to be\n * evicted later.\n */\n removeConsumer: (consumerId: string) => void;\n /**\n * Reconcile the set of cache keys referenced by one hook consumer.\n *\n * This adds the consumer to newly needed entries, removes it from entries no\n * longer needed, and preserves existing entries in place so unrelated dataset\n * states do not get reset during prop changes.\n */\n syncConsumer: (consumerId: string, keys: string[]) => void;\n /**\n * Reconcile and ensure all prepared entries needed by one dataset registry.\n *\n * This is the store-level orchestration entrypoint used by\n * `usePreparedDatasetStates`. It computes cache keys for the current dataset\n * registry, syncs consumer memberships, and eagerly ensures any resolvable\n * entries exist.\n */\n syncDatasetsForConsumer: (options: {\n consumerId: string;\n datasets: Record<string, DeckDatasetInput>;\n executeSql: (\n query: string,\n version?: number,\n ) => Promise<QueryHandle | null>;\n sqlSourceIdentity: object;\n }) => void;\n};\n\ntype PreparedDatasetDescriptor = {\n datasetId: string;\n input: DeckDatasetInput;\n cacheKey: string | undefined;\n};\n\nfunction resolvePreparedDatasetDescriptors(options: {\n datasets: Record<string, DeckDatasetInput>;\n sqlSourceIdentity: object;\n}): PreparedDatasetDescriptor[] {\n const {datasets, sqlSourceIdentity} = options;\n\n return Object.entries(datasets).map(([datasetId, input]) => ({\n datasetId,\n input,\n cacheKey: resolvePreparedDatasetCacheKey({\n input,\n sqlSourceIdentity,\n }),\n }));\n}\n\n/**\n * Create a feature-local cache store for prepared deck datasets.\n *\n * The store is intentionally scoped to deck's preparation layer. It reuses the\n * expensive result of `prepareDeckDataset(...)` across consumers while leaving\n * raw query/result caching to the upstream data provider:\n *\n * - Mosaic handles cached query results through its coordinator/query manager\n * - DuckDB-backed SQL datasets still use the DuckDB slice execution path\n *\n * This keeps `@sqlrooms/deck` independent from either provider while still\n * avoiding repeated geometry decoding and GeoArrow/GeoJSON shaping work.\n */\nexport function createPreparedDatasetStore(\n options: PreparedDatasetStoreOptions = {},\n) {\n const {\n maxEntries = DEFAULT_MAX_PREPARED_DATASET_ENTRIES,\n prepareDataset = prepareDeckDataset,\n } = options;\n\n return createStore<PreparedDatasetStoreState>((set, get) => ({\n consumerKeys: {},\n entries: {},\n\n ensureEntry({cacheKey, datasetId, executeSql, input}) {\n const existing = get().entries[cacheKey];\n if (existing) {\n if (existing.status !== 'loading') {\n set((state) => ({\n ...state,\n entries: {\n ...state.entries,\n [cacheKey]: touchEntry(existing),\n },\n }));\n }\n return;\n }\n\n const promise = Promise.resolve().then(async () => {\n try {\n let table = resolveArrowTable(input);\n if (!table && isSqlDatasetInput(input)) {\n const queryHandle = await executeSql(input.sqlQuery);\n if (!queryHandle) {\n throw new Error(\n `Query for dataset \"${datasetId}\" was cancelled.`,\n );\n }\n\n table = await queryHandle;\n }\n\n if (!table) {\n return;\n }\n\n const prepared = prepareDataset({\n datasetId,\n table,\n geometryColumn: input.geometryColumn,\n geometryEncodingHint: input.geometryEncodingHint,\n });\n\n set((state) => ({\n ...state,\n entries: evictLruEntries(\n {\n ...state.entries,\n [cacheKey]: {\n status: 'ready',\n prepared,\n consumers:\n state.entries[cacheKey]?.consumers ?? new Set<string>(),\n lastAccessedAt: nextAccessTimestamp(),\n },\n },\n maxEntries,\n ),\n }));\n } catch (error) {\n set((state) => ({\n ...state,\n entries: evictLruEntries(\n {\n ...state.entries,\n [cacheKey]: {\n status: 'error',\n error:\n error instanceof Error ? error : new Error(String(error)),\n consumers:\n state.entries[cacheKey]?.consumers ?? new Set<string>(),\n lastAccessedAt: nextAccessTimestamp(),\n },\n },\n maxEntries,\n ),\n }));\n }\n });\n\n set((state) => ({\n ...state,\n entries: {\n ...state.entries,\n [cacheKey]: {\n status: 'loading',\n promise,\n consumers: state.entries[cacheKey]?.consumers ?? new Set<string>(),\n lastAccessedAt: nextAccessTimestamp(),\n },\n },\n }));\n },\n\n syncConsumer(consumerId, keys) {\n const previousKeys = new Set(get().consumerKeys[consumerId] ?? []);\n const nextKeys = new Set(keys);\n\n set((state) => {\n let didChangeEntries = false;\n let nextEntries = state.entries;\n\n const updateEntry = (\n cacheKey: string,\n updater: (\n entry: PreparedDatasetCacheEntry,\n ) => PreparedDatasetCacheEntry,\n ) => {\n const currentEntry = nextEntries[cacheKey];\n if (!currentEntry) {\n return;\n }\n\n if (!didChangeEntries) {\n nextEntries = {...nextEntries};\n didChangeEntries = true;\n }\n\n nextEntries[cacheKey] = updater(currentEntry);\n };\n\n for (const cacheKey of previousKeys) {\n if (nextKeys.has(cacheKey)) {\n continue;\n }\n\n updateEntry(cacheKey, (entry) => {\n const consumers = new Set(entry.consumers);\n consumers.delete(consumerId);\n return cloneEntryWithConsumers(entry, consumers);\n });\n }\n\n for (const cacheKey of nextKeys) {\n updateEntry(cacheKey, (entry) => {\n const consumers = new Set(entry.consumers);\n consumers.add(consumerId);\n return cloneEntryWithConsumers(entry, consumers);\n });\n }\n\n return {\n ...state,\n consumerKeys: {\n ...state.consumerKeys,\n [consumerId]: [...nextKeys],\n },\n entries: didChangeEntries\n ? evictLruEntries(nextEntries, maxEntries)\n : state.entries,\n };\n });\n },\n\n removeConsumer(consumerId) {\n const previousKeys = get().consumerKeys[consumerId];\n if (!previousKeys) {\n return;\n }\n\n set((state) => {\n let didChangeEntries = false;\n let nextEntries = state.entries;\n\n for (const cacheKey of previousKeys) {\n const entry = nextEntries[cacheKey];\n if (!entry) {\n continue;\n }\n\n const consumers = new Set(entry.consumers);\n consumers.delete(consumerId);\n\n if (!didChangeEntries) {\n nextEntries = {...nextEntries};\n didChangeEntries = true;\n }\n\n nextEntries[cacheKey] = cloneEntryWithConsumers(entry, consumers);\n }\n\n const {[consumerId]: _removedConsumer, ...nextConsumerKeys} =\n state.consumerKeys;\n\n return {\n ...state,\n consumerKeys: nextConsumerKeys,\n entries: didChangeEntries\n ? evictLruEntries(nextEntries, maxEntries)\n : state.entries,\n };\n });\n },\n\n syncDatasetsForConsumer({\n consumerId,\n datasets,\n executeSql,\n sqlSourceIdentity,\n }) {\n const descriptors = resolvePreparedDatasetDescriptors({\n datasets,\n sqlSourceIdentity,\n });\n const cacheKeys = descriptors\n .map((descriptor) => descriptor.cacheKey)\n .filter((cacheKey): cacheKey is string => Boolean(cacheKey));\n\n for (const descriptor of descriptors) {\n if (!descriptor.cacheKey) {\n continue;\n }\n\n get().ensureEntry({\n cacheKey: descriptor.cacheKey,\n datasetId: descriptor.datasetId,\n executeSql,\n input: descriptor.input,\n });\n }\n\n get().syncConsumer(consumerId, cacheKeys);\n },\n }));\n}\n\n/**\n * Resolve a shared prepared-store entry back into the dataset-state shape\n * consumed by `DeckJsonMap`.\n *\n * Prepared entries are shared by cache key, while `datasetId` stays a\n * view-level label used by layer binding and user-facing errors. When a shared\n * prepared entry is reused for a different dataset id, this helper remaps the\n * prepared payload so downstream callers still see the dataset id they asked\n * for.\n */\nexport function resolvePreparedDeckDatasetState(options: {\n datasetId: string;\n entry?: PreparedDatasetCacheEntry;\n}): PreparedDeckDatasetState {\n const {datasetId, entry} = options;\n\n if (!entry || entry.status === 'loading') {\n return {status: 'loading'};\n }\n\n if (entry.status === 'error') {\n return {status: 'error', error: entry.error};\n }\n\n return {\n status: 'ready',\n prepared:\n entry.prepared.datasetId === datasetId\n ? entry.prepared\n : {\n ...entry.prepared,\n datasetId,\n },\n };\n}\n\n/**\n * Resolve the full `datasetId -> state` map for a dataset registry.\n *\n * This mirrors the lookup logic used by `DeckJsonMap`: unresolved Arrow\n * datasets remain `loading`, while resolvable datasets reuse shared prepared\n * entries keyed by data identity rather than by dataset id.\n */\nexport function resolvePreparedDeckDatasetStates(options: {\n datasets: Record<string, DeckDatasetInput>;\n entries: Record<string, PreparedDatasetCacheEntry>;\n sqlSourceIdentity: object;\n}): Record<string, PreparedDeckDatasetState> {\n const {datasets, entries, sqlSourceIdentity} = options;\n\n return Object.fromEntries(\n resolvePreparedDatasetDescriptors({datasets, sqlSourceIdentity}).map(\n ({datasetId, cacheKey}) => [\n datasetId,\n resolvePreparedDeckDatasetState({\n datasetId,\n entry: cacheKey ? entries[cacheKey] : undefined,\n }),\n ],\n ),\n );\n}\n\n/**\n * Module-global prepared dataset cache shared by all `DeckJsonMap` instances.\n *\n * The cache stores only deck-specific prepared payloads, not raw query\n * results, and applies a small internal LRU policy so settled entries do not\n * grow without bound.\n */\nexport const preparedDatasetStore = createPreparedDatasetStore();\n"]}
@@ -0,0 +1,57 @@
1
+ import type * as arrow from 'apache-arrow';
2
+ import { type DeckDatasetInput } from '../types';
3
+ import type { PreparedDatasetCacheEntry } from './types';
4
+ export declare const DEFAULT_MAX_PREPARED_DATASET_ENTRIES = 20;
5
+ /** Return a monotonic access timestamp used by the prepared-dataset LRU. */
6
+ export declare function nextAccessTimestamp(): number;
7
+ /**
8
+ * Assign a stable cache identity to an Arrow table object.
9
+ *
10
+ * Table identities are stored in a `WeakMap` so this cache does not extend the
11
+ * lifetime of tables once they are no longer referenced elsewhere.
12
+ */
13
+ export declare function getTableIdentity(table: arrow.Table): string;
14
+ /**
15
+ * Assign a stable cache identity to the current SQL source object.
16
+ *
17
+ * For SQL datasets we include the upstream DuckDB connector identity in the
18
+ * cache key so two rooms using the same SQL text do not accidentally share
19
+ * prepared results across different database instances.
20
+ */
21
+ export declare function getSqlSourceIdentity(sqlSourceIdentity: object): string;
22
+ export declare function buildGeometryKey(input: DeckDatasetInput): string;
23
+ /**
24
+ * Build the canonical cache key for one dataset input.
25
+ *
26
+ * The key intentionally ignores the user-facing dataset id and instead uses
27
+ * the underlying data identity:
28
+ *
29
+ * - SQL datasets: DuckDB connector identity + SQL text + geometry options
30
+ * - Arrow datasets: table object identity + geometry options
31
+ *
32
+ * Unresolved Arrow inputs (`arrowTable: undefined`) return `undefined`, which
33
+ * signals that the dataset should remain in `loading` until a table exists.
34
+ */
35
+ export declare function resolvePreparedDatasetCacheKey(options: {
36
+ input: DeckDatasetInput;
37
+ sqlSourceIdentity?: object;
38
+ }): string | undefined;
39
+ /**
40
+ * Clone a cache entry while replacing its consumer set and refreshing the
41
+ * access timestamp.
42
+ *
43
+ * This keeps the discriminated union shape intact while centralizing the
44
+ * bookkeeping needed by consumer reconciliation and LRU tracking.
45
+ */
46
+ export declare function cloneEntryWithConsumers(entry: PreparedDatasetCacheEntry, consumers: Set<string>): PreparedDatasetCacheEntry;
47
+ /** Mark an entry as recently used without changing its lifecycle payload. */
48
+ export declare function touchEntry(entry: PreparedDatasetCacheEntry): PreparedDatasetCacheEntry;
49
+ /**
50
+ * Evict least-recently-used settled entries when the cache exceeds capacity.
51
+ *
52
+ * Only `ready` and `error` entries participate in eviction. `loading` entries
53
+ * are preserved so in-flight work is not discarded, and referenced entries are
54
+ * preserved so active maps never lose their prepared data mid-render.
55
+ */
56
+ export declare function evictLruEntries(entries: Record<string, PreparedDatasetCacheEntry>, maxEntries: number): Record<string, PreparedDatasetCacheEntry>;
57
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/datasets/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAoB,KAAK,gBAAgB,EAAC,MAAM,UAAU,CAAC;AAElE,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,SAAS,CAAC;AAEvD,eAAO,MAAM,oCAAoC,KAAK,CAAC;AASvD,4EAA4E;AAC5E,wBAAgB,mBAAmB,IAAI,MAAM,CAG5C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAU3D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAUtE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAEhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE;IACtD,KAAK,EAAE,gBAAgB,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,GAAG,MAAM,GAAG,SAAS,CA0BrB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,yBAAyB,EAChC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,yBAAyB,CA2B3B;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CACxB,KAAK,EAAE,yBAAyB,GAC/B,yBAAyB,CAE3B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,EAClD,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CA8B3C"}
@@ -0,0 +1,146 @@
1
+ import { isSqlDatasetInput } from '../types';
2
+ import { resolveArrowTable } from './normalizeDatasets';
3
+ export const DEFAULT_MAX_PREPARED_DATASET_ENTRIES = 20;
4
+ let nextDatasetAccess = 0;
5
+ let nextTableIdentity = 0;
6
+ let nextSqlSourceIdentity = 0;
7
+ const tableIdentities = new WeakMap();
8
+ const sqlSourceIdentities = new WeakMap();
9
+ /** Return a monotonic access timestamp used by the prepared-dataset LRU. */
10
+ export function nextAccessTimestamp() {
11
+ nextDatasetAccess += 1;
12
+ return nextDatasetAccess;
13
+ }
14
+ /**
15
+ * Assign a stable cache identity to an Arrow table object.
16
+ *
17
+ * Table identities are stored in a `WeakMap` so this cache does not extend the
18
+ * lifetime of tables once they are no longer referenced elsewhere.
19
+ */
20
+ export function getTableIdentity(table) {
21
+ const cached = tableIdentities.get(table);
22
+ if (cached) {
23
+ return cached;
24
+ }
25
+ nextTableIdentity += 1;
26
+ const identity = `table:${nextTableIdentity}`;
27
+ tableIdentities.set(table, identity);
28
+ return identity;
29
+ }
30
+ /**
31
+ * Assign a stable cache identity to the current SQL source object.
32
+ *
33
+ * For SQL datasets we include the upstream DuckDB connector identity in the
34
+ * cache key so two rooms using the same SQL text do not accidentally share
35
+ * prepared results across different database instances.
36
+ */
37
+ export function getSqlSourceIdentity(sqlSourceIdentity) {
38
+ const cached = sqlSourceIdentities.get(sqlSourceIdentity);
39
+ if (cached) {
40
+ return cached;
41
+ }
42
+ nextSqlSourceIdentity += 1;
43
+ const identity = `sql-source:${nextSqlSourceIdentity}`;
44
+ sqlSourceIdentities.set(sqlSourceIdentity, identity);
45
+ return identity;
46
+ }
47
+ export function buildGeometryKey(input) {
48
+ return `${input.geometryColumn ?? ''}\u0001${input.geometryEncodingHint ?? ''}`;
49
+ }
50
+ /**
51
+ * Build the canonical cache key for one dataset input.
52
+ *
53
+ * The key intentionally ignores the user-facing dataset id and instead uses
54
+ * the underlying data identity:
55
+ *
56
+ * - SQL datasets: DuckDB connector identity + SQL text + geometry options
57
+ * - Arrow datasets: table object identity + geometry options
58
+ *
59
+ * Unresolved Arrow inputs (`arrowTable: undefined`) return `undefined`, which
60
+ * signals that the dataset should remain in `loading` until a table exists.
61
+ */
62
+ export function resolvePreparedDatasetCacheKey(options) {
63
+ const { input, sqlSourceIdentity } = options;
64
+ if (isSqlDatasetInput(input)) {
65
+ if (!sqlSourceIdentity) {
66
+ throw new Error('SQL dataset cache keys require a sqlSourceIdentity object.');
67
+ }
68
+ return [
69
+ 'sql',
70
+ getSqlSourceIdentity(sqlSourceIdentity),
71
+ input.sqlQuery,
72
+ buildGeometryKey(input),
73
+ ].join('\u0001');
74
+ }
75
+ const table = resolveArrowTable(input);
76
+ if (!table) {
77
+ return undefined;
78
+ }
79
+ return ['arrow', getTableIdentity(table), buildGeometryKey(input)].join('\u0001');
80
+ }
81
+ /**
82
+ * Clone a cache entry while replacing its consumer set and refreshing the
83
+ * access timestamp.
84
+ *
85
+ * This keeps the discriminated union shape intact while centralizing the
86
+ * bookkeeping needed by consumer reconciliation and LRU tracking.
87
+ */
88
+ export function cloneEntryWithConsumers(entry, consumers) {
89
+ const lastAccessedAt = nextAccessTimestamp();
90
+ if (entry.status === 'loading') {
91
+ return {
92
+ status: 'loading',
93
+ promise: entry.promise,
94
+ consumers,
95
+ lastAccessedAt,
96
+ };
97
+ }
98
+ if (entry.status === 'ready') {
99
+ return {
100
+ status: 'ready',
101
+ prepared: entry.prepared,
102
+ consumers,
103
+ lastAccessedAt,
104
+ };
105
+ }
106
+ return {
107
+ status: 'error',
108
+ error: entry.error,
109
+ consumers,
110
+ lastAccessedAt,
111
+ };
112
+ }
113
+ /** Mark an entry as recently used without changing its lifecycle payload. */
114
+ export function touchEntry(entry) {
115
+ return cloneEntryWithConsumers(entry, new Set(entry.consumers));
116
+ }
117
+ /**
118
+ * Evict least-recently-used settled entries when the cache exceeds capacity.
119
+ *
120
+ * Only `ready` and `error` entries participate in eviction. `loading` entries
121
+ * are preserved so in-flight work is not discarded, and referenced entries are
122
+ * preserved so active maps never lose their prepared data mid-render.
123
+ */
124
+ export function evictLruEntries(entries, maxEntries) {
125
+ const settledEntries = Object.entries(entries).filter(([, entry]) => entry.status !== 'loading');
126
+ if (settledEntries.length <= maxEntries) {
127
+ return entries;
128
+ }
129
+ const evictable = settledEntries
130
+ .filter(([, entry]) => entry.consumers.size === 0)
131
+ .sort(([, left], [, right]) => left.lastAccessedAt - right.lastAccessedAt);
132
+ if (evictable.length === 0) {
133
+ return entries;
134
+ }
135
+ const nextEntries = { ...entries };
136
+ let settledCount = settledEntries.length;
137
+ for (const [cacheKey] of evictable) {
138
+ if (settledCount <= maxEntries) {
139
+ break;
140
+ }
141
+ delete nextEntries[cacheKey];
142
+ settledCount -= 1;
143
+ }
144
+ return nextEntries;
145
+ }
146
+ //# sourceMappingURL=helpers.js.map