@svgrid/enterprise 3.1.1 → 3.1.2

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 (34) hide show
  1. package/dist/SvSheet.svelte +7 -1
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +10534 -10332
  3. package/dist/designer/assets/{index-DkhD2kIe.js → index-V_cMXdxt.js} +311 -311
  4. package/dist/designer/assets/validate-YxSrBF1H.js +1 -0
  5. package/dist/designer/index.html +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +1 -1
  8. package/dist/node/studio.js +16 -4
  9. package/dist/server/index.d.ts +1 -0
  10. package/dist/server/index.js +1 -0
  11. package/dist/server/server-row-model.d.ts +9 -0
  12. package/dist/server/server-row-model.js +120 -3
  13. package/dist/server/worker-data-source.d.ts +62 -0
  14. package/dist/server/worker-data-source.js +133 -0
  15. package/dist/studio/emit-project.js +8 -3
  16. package/dist/sveltekit/in-memory.d.ts +21 -1
  17. package/dist/sveltekit/in-memory.js +153 -105
  18. package/dist/sveltekit/index.d.ts +1 -1
  19. package/dist/version.d.ts +14 -1
  20. package/dist/version.js +14 -1
  21. package/dist/wc/sv-sheet-element.js +14 -9
  22. package/dist/wc/{validate-BqDthkNr.js → validate-JAuMsk6Z.js} +36 -36
  23. package/dist/worker/columnar.d.ts +42 -0
  24. package/dist/worker/columnar.js +410 -0
  25. package/dist/worker/index.d.ts +12 -0
  26. package/dist/worker/index.js +12 -0
  27. package/dist/worker/infer-schema.d.ts +21 -0
  28. package/dist/worker/infer-schema.js +34 -0
  29. package/dist/worker/protocol.d.ts +73 -0
  30. package/dist/worker/protocol.js +14 -0
  31. package/dist/worker/serve.d.ts +44 -0
  32. package/dist/worker/serve.js +100 -0
  33. package/package.json +7 -3
  34. package/dist/designer/assets/validate-C3B8QKsM.js +0 -1
@@ -2,13 +2,16 @@ import { resolveIdField } from '../schema.js';
2
2
  import { planQuery } from './query-plan.js';
3
3
  import { compilePredicate } from '../expressions/compile.js';
4
4
  import { aggregateRows } from '../sources/aggregate.js';
5
- function asString(v) {
5
+ // asString, matchesPredicate and sortRows are exported for the columnar
6
+ // engine behind the worker data source, which must answer exactly as this
7
+ // reference does. They are not part of the package API.
8
+ export function asString(v) {
6
9
  return v == null ? '' : String(v);
7
10
  }
8
11
  function num(v) {
9
12
  return typeof v === 'number' ? v : Number(v);
10
13
  }
11
- function matchesPredicate(row, p) {
14
+ export function matchesPredicate(row, p) {
12
15
  const cell = row[p.field];
13
16
  switch (p.op) {
14
17
  case 'eq':
@@ -103,28 +106,42 @@ function grandTotalOf(all, plan, expression) {
103
106
  function aggregate(rows, field, fn) {
104
107
  if (fn === 'count')
105
108
  return rows.length;
106
- const nums = [];
109
+ // One pass, no spread: `Math.min(...nums)` throws a RangeError once a
110
+ // group holds more than about 150,000 values (V8's argument limit).
111
+ let count = 0;
112
+ let sum = 0;
113
+ let min = Infinity;
114
+ let max = -Infinity;
107
115
  for (const row of rows) {
108
116
  const n = Number(row[field]);
109
- if (Number.isFinite(n))
110
- nums.push(n);
117
+ if (!Number.isFinite(n))
118
+ continue;
119
+ count += 1;
120
+ sum += n;
121
+ if (n < min)
122
+ min = n;
123
+ if (n > max)
124
+ max = n;
111
125
  }
112
- if (nums.length === 0)
126
+ if (count === 0)
113
127
  return null;
114
128
  switch (fn) {
115
129
  case 'sum':
116
- return nums.reduce((s, n) => s + n, 0);
130
+ return sum;
117
131
  case 'avg':
118
- return nums.reduce((s, n) => s + n, 0) / nums.length;
132
+ return sum / count;
119
133
  case 'min':
120
- return Math.min(...nums);
134
+ return min;
121
135
  case 'max':
122
- return Math.max(...nums);
136
+ return max;
123
137
  default:
124
138
  return null;
125
139
  }
126
140
  }
127
- function sortRows(rows, orderBy) {
141
+ // What `localeCompare` with no arguments does, without building a collator
142
+ // per comparison - which is most of the cost of sorting a large array.
143
+ const collator = new Intl.Collator();
144
+ export function sortRows(rows, orderBy) {
128
145
  if (orderBy.length === 0)
129
146
  return rows;
130
147
  // Stable multi-key sort: decorate with index, compare keys in order, fall back to index.
@@ -138,7 +155,7 @@ function sortRows(rows, orderBy) {
138
155
  if (typeof av === 'number' && typeof bv === 'number')
139
156
  cmp = av - bv;
140
157
  else
141
- cmp = asString(av).localeCompare(asString(bv));
158
+ cmp = collator.compare(asString(av), asString(bv));
142
159
  if (cmp !== 0)
143
160
  return desc ? -cmp : cmp;
144
161
  }
@@ -146,106 +163,137 @@ function sortRows(rows, orderBy) {
146
163
  })
147
164
  .map((d) => d.row);
148
165
  }
149
- export function createInMemoryDataSource(initial, schema) {
166
+ /** Results kept by `cacheResults`: a handful of sort / filter / group states. */
167
+ const RESULT_CACHE_SIZE = 8;
168
+ export function createInMemoryDataSource(initial, schema, options = {}) {
150
169
  let store = [...initial];
151
170
  const idField = resolveIdField(schema);
152
- return {
153
- async getRows(request) {
154
- const plan = planQuery(schema, request);
155
- let filtered = store.filter((r) => matches(r, plan));
156
- // Advanced filter. `planQuery` only admits an expression whose columns
157
- // are all on the schema, so reaching here means it is safe to run. If it
158
- // was rejected, or fails to compile, we must NOT acknowledge it - the
159
- // grid then tells the user the filter did not run rather than showing a
160
- // superset that looks filtered.
161
- let appliedExpression = false;
162
- let expressionPredicate = null;
163
- if (plan.expression) {
164
- const predicate = compilePredicate(plan.expression, {
165
- getValue: (row, columnId) => row[columnId],
166
- rows: filtered,
167
- });
168
- if (predicate) {
169
- filtered = filtered.filter(predicate);
170
- expressionPredicate = predicate;
171
- appliedExpression = true;
172
- }
171
+ // Every write reassigns `store`, so a cache filled from an older array is
172
+ // stale by identity alone.
173
+ const cache = new Map();
174
+ let cacheStore = null;
175
+ function prepare(plan) {
176
+ if (!options.cacheResults)
177
+ return run(plan);
178
+ if (cacheStore !== store) {
179
+ cache.clear();
180
+ cacheStore = store;
181
+ }
182
+ // The range is the only part of a plan that does not change the result.
183
+ const key = JSON.stringify({ ...plan, limit: 0, offset: 0 });
184
+ const hit = cache.get(key);
185
+ if (hit) {
186
+ // Re-insert so the Map's order is least-recently used first.
187
+ cache.delete(key);
188
+ cache.set(key, hit);
189
+ return hit;
190
+ }
191
+ const prepared = run(plan);
192
+ cache.set(key, prepared);
193
+ if (cache.size > RESULT_CACHE_SIZE)
194
+ cache.delete(cache.keys().next().value);
195
+ return prepared;
196
+ }
197
+ function run(plan) {
198
+ let filtered = store.filter((r) => matches(r, plan));
199
+ // Advanced filter. `planQuery` only admits an expression whose columns
200
+ // are all on the schema, so reaching here means it is safe to run. If it
201
+ // was rejected, or fails to compile, we must NOT acknowledge it - the
202
+ // grid then tells the user the filter did not run rather than showing a
203
+ // superset that looks filtered.
204
+ let appliedExpression = false;
205
+ let expressionPredicate = null;
206
+ if (plan.expression) {
207
+ const predicate = compilePredicate(plan.expression, {
208
+ getValue: (row, columnId) => row[columnId],
209
+ rows: filtered,
210
+ });
211
+ if (predicate) {
212
+ filtered = filtered.filter(predicate);
213
+ expressionPredicate = predicate;
214
+ appliedExpression = true;
173
215
  }
174
- // The grand total spans everything the FILTERS admit - the whole
175
- // store minus the filters and the expression, but NOT minus the group
176
- // path, which only scopes this request.
177
- const grandTotal = plan.grandTotal
178
- ? { grandTotal: grandTotalOf(store, plan, expressionPredicate) }
179
- : {};
180
- // Grouped request: return one row per distinct key at this level, each
181
- // carrying the requested aggregates. The chosen path is already in
182
- // `plan.where`, so `filtered` is exactly this group's slice of the data.
183
- // Reference behaviour for what a SQL backend does with `planToSql`'s
184
- // `select` / `groupByText` / `countText`.
185
- if (plan.groupBy) {
186
- const groupField = plan.groupBy;
187
- const buckets = new Map();
188
- for (const row of filtered) {
189
- const key = asString(row[groupField]);
190
- const bucket = buckets.get(key);
191
- if (bucket)
192
- bucket.push(row);
193
- else
194
- buckets.set(key, [row]);
195
- }
196
- // Pivoting: every aggregate is computed once per distinct pivot key
197
- // path inside the group, under a field named from the path, and the
198
- // field list goes back so the grid can build the columns.
199
- const pivotPaths = new Set();
200
- const groups = [...buckets.entries()].map(([key, rows]) => {
201
- const out = { [groupField]: key };
202
- aggregateInto(out, rows, plan, pivotPaths);
203
- // What opening this group would show: distinct keys of the next
204
- // level, or the rows themselves at the innermost group level.
205
- out.childCount = plan.childGroupBy
206
- ? new Set(rows.map((r) => asString(r[plan.childGroupBy]))).size
207
- : rows.length;
208
- return out;
209
- });
210
- // The pivoted fields in a fixed order - key paths in plain string
211
- // order, aggregations as requested under each - so the grid builds
212
- // the same columns whatever order the groups came in. Every group
213
- // row carries every field; a cell with no rows under it is null, the
214
- // way a conditional aggregate answers in SQL.
215
- const pivotFields = [...pivotPaths]
216
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
217
- .flatMap((path) => (plan.aggregations ?? []).map((agg) => `${path}_${agg.field}`));
218
- for (const g of groups) {
219
- for (const f of pivotFields)
220
- if (!(f in g))
221
- g[f] = null;
222
- }
223
- // Sort by the group key unless the request sorted on a column we
224
- // actually produced (the key or an aggregate).
225
- const produced = new Set([
226
- groupField,
227
- ...(plan.pivotBy?.length ? [] : (plan.aggregations ?? []).map((a) => a.field)),
228
- ]);
229
- const groupOrder = plan.orderBy.filter((o) => produced.has(o.field));
230
- const sorted = groupOrder.length
231
- ? sortRows(groups, groupOrder)
232
- : sortRows(groups, [{ field: groupField, desc: false }]);
233
- return {
234
- rows: sorted.slice(plan.offset, plan.offset + plan.limit),
235
- // The count is DISTINCT GROUPS, not underlying rows - the grid sizes
236
- // its scrollbar and paging from this.
237
- rowCount: sorted.length,
238
- appliedExpression,
239
- ...grandTotal,
240
- ...(plan.pivotBy?.length ? { pivotResultFields: pivotFields } : {}),
241
- };
216
+ }
217
+ // The grand total spans everything the FILTERS admit - the whole
218
+ // store minus the filters and the expression, but NOT minus the group
219
+ // path, which only scopes this request.
220
+ const grandTotal = plan.grandTotal
221
+ ? { grandTotal: grandTotalOf(store, plan, expressionPredicate) }
222
+ : {};
223
+ // Grouped request: return one row per distinct key at this level, each
224
+ // carrying the requested aggregates. The chosen path is already in
225
+ // `plan.where`, so `filtered` is exactly this group's slice of the data.
226
+ // Reference behaviour for what a SQL backend does with `planToSql`'s
227
+ // `select` / `groupByText` / `countText`.
228
+ if (plan.groupBy) {
229
+ const groupField = plan.groupBy;
230
+ const buckets = new Map();
231
+ for (const row of filtered) {
232
+ const key = asString(row[groupField]);
233
+ const bucket = buckets.get(key);
234
+ if (bucket)
235
+ bucket.push(row);
236
+ else
237
+ buckets.set(key, [row]);
238
+ }
239
+ // Pivoting: every aggregate is computed once per distinct pivot key
240
+ // path inside the group, under a field named from the path, and the
241
+ // field list goes back so the grid can build the columns.
242
+ const pivotPaths = new Set();
243
+ const groups = [...buckets.entries()].map(([key, rows]) => {
244
+ const out = { [groupField]: key };
245
+ aggregateInto(out, rows, plan, pivotPaths);
246
+ // What opening this group would show: distinct keys of the next
247
+ // level, or the rows themselves at the innermost group level.
248
+ out.childCount = plan.childGroupBy
249
+ ? new Set(rows.map((r) => asString(r[plan.childGroupBy]))).size
250
+ : rows.length;
251
+ return out;
252
+ });
253
+ // The pivoted fields in a fixed order - key paths in plain string
254
+ // order, aggregations as requested under each - so the grid builds
255
+ // the same columns whatever order the groups came in. Every group
256
+ // row carries every field; a cell with no rows under it is null, the
257
+ // way a conditional aggregate answers in SQL.
258
+ const pivotFields = [...pivotPaths]
259
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
260
+ .flatMap((path) => (plan.aggregations ?? []).map((agg) => `${path}_${agg.field}`));
261
+ for (const g of groups) {
262
+ for (const f of pivotFields)
263
+ if (!(f in g))
264
+ g[f] = null;
242
265
  }
243
- const sortedLeaves = sortRows(filtered, plan.orderBy);
266
+ // Sort by the group key unless the request sorted on a column we
267
+ // actually produced (the key or an aggregate).
268
+ const produced = new Set([
269
+ groupField,
270
+ ...(plan.pivotBy?.length ? [] : (plan.aggregations ?? []).map((a) => a.field)),
271
+ ]);
272
+ const groupOrder = plan.orderBy.filter((o) => produced.has(o.field));
273
+ const sorted = groupOrder.length
274
+ ? sortRows(groups, groupOrder)
275
+ : sortRows(groups, [{ field: groupField, desc: false }]);
244
276
  return {
245
- rows: sortedLeaves.slice(plan.offset, plan.offset + plan.limit),
246
- rowCount: sortedLeaves.length,
277
+ rows: sorted,
247
278
  appliedExpression,
248
279
  ...grandTotal,
280
+ ...(plan.pivotBy?.length ? { pivotFields } : {}),
281
+ };
282
+ }
283
+ return { rows: sortRows(filtered, plan.orderBy), appliedExpression, ...grandTotal };
284
+ }
285
+ return {
286
+ async getRows(request) {
287
+ const plan = planQuery(schema, request);
288
+ const prepared = prepare(plan);
289
+ return {
290
+ rows: prepared.rows.slice(plan.offset, plan.offset + plan.limit),
291
+ // On a grouped request the count is DISTINCT GROUPS, not underlying
292
+ // rows - the grid sizes its scrollbar and paging from this.
293
+ rowCount: prepared.rows.length,
294
+ appliedExpression: prepared.appliedExpression,
295
+ ...(prepared.grandTotal !== undefined ? { grandTotal: prepared.grandTotal } : {}),
296
+ ...(prepared.pivotFields ? { pivotResultFields: prepared.pivotFields } : {}),
249
297
  };
250
298
  },
251
299
  async createRow(input) {
@@ -1,5 +1,5 @@
1
1
  export { planQuery, coerce, BUILTIN_AGGREGATORS, type QueryPlan, type PlanPredicate, type PlanOp, type PlanAggregate, type PlanOptions, } from './query-plan.js';
2
- export { createInMemoryDataSource } from './in-memory.js';
2
+ export { createInMemoryDataSource, type InMemoryDataSourceOptions } from './in-memory.js';
3
3
  export { createKitDataSource, createKitHandlers, type KitDataSourceOptions, type KitHandlerOptions, type KitScope, type KitHandlers, type KitMessage, } from './transport.js';
4
4
  export { planToSql, type SqlDialect, type SqlPlan } from './sql.js';
5
5
  export { createSqlDataSource, type SqlExecutor, type SqlDataSourceConfig, } from './sql-source.js';
package/dist/version.d.ts CHANGED
@@ -10,4 +10,17 @@
10
10
  *
11
11
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
12
12
  */
13
- export declare const SVGRID_VERSION = "3.1.1";
13
+ export declare const SVGRID_VERSION = "3.1.2";
14
+ /**
15
+ * The version of `@svgrid/grid`, which releases on its own line: enterprise is
16
+ * on 3.1.x while the grid is on 3.0.x.
17
+ *
18
+ * A scaffolded app depends on BOTH packages, and pinning the grid to this
19
+ * package's version asked npm for `@svgrid/grid@^3.1.1`, which has never been
20
+ * published. Every `svgrid-studio init` therefore produced an app whose very
21
+ * next step, `npm install`, failed with ERESOLVE. Found 2026-10-05 by
22
+ * scaffolding one and running the install.
23
+ *
24
+ * `version.test.ts` asserts this matches packages/grid/package.json.
25
+ */
26
+ export declare const GRID_VERSION = "3.0.8";
package/dist/version.js CHANGED
@@ -10,4 +10,17 @@
10
10
  *
11
11
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
12
12
  */
13
- export const SVGRID_VERSION = '3.1.1';
13
+ export const SVGRID_VERSION = '3.1.2';
14
+ /**
15
+ * The version of `@svgrid/grid`, which releases on its own line: enterprise is
16
+ * on 3.1.x while the grid is on 3.0.x.
17
+ *
18
+ * A scaffolded app depends on BOTH packages, and pinning the grid to this
19
+ * package's version asked npm for `@svgrid/grid@^3.1.1`, which has never been
20
+ * published. Every `svgrid-studio init` therefore produced an app whose very
21
+ * next step, `npm install`, failed with ERESOLVE. Found 2026-10-05 by
22
+ * scaffolding one and running the install.
23
+ *
24
+ * `version.test.ts` asserts this matches packages/grid/package.json.
25
+ */
26
+ export const GRID_VERSION = '3.0.8';