akanjs 3.0.0-beta.22 → 3.0.0-beta.23

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.
@@ -107,7 +107,12 @@ export class WsClient {
107
107
  #connect() {
108
108
  if (this.#destroyed) return;
109
109
 
110
- this.#ws = new WebSocket(this.url);
110
+ try {
111
+ this.#ws = new WebSocket(this.url);
112
+ } catch (error) {
113
+ this.logger.error(`Cannot open a WebSocket to ${this.url}: ${error instanceof Error ? error.message : error}`);
114
+ return;
115
+ }
111
116
  this.#ws.binaryType = "arraybuffer";
112
117
  this.#ws.onopen = (e) => {
113
118
  this.#reconnectAttempts = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-beta.22",
3
+ "version": "3.0.0-beta.23",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -5,6 +5,7 @@ import {
5
5
  type Cls,
6
6
  ENDPOINT_META,
7
7
  FIELD_META,
8
+ Float,
8
9
  getEnv,
9
10
  ID,
10
11
  INTERNAL_META,
@@ -14,7 +15,7 @@ import {
14
15
  SLICE_META,
15
16
  } from "akanjs/base";
16
17
  import { capitalize, cookieHeaderHasAuthToken, Logger } from "akanjs/common";
17
- import { deserialize, resolvePageLimit, resolvePageSkip, serialize } from "akanjs/constant";
18
+ import { type ConstantField, deserialize, resolvePageLimit, resolvePageSkip, serialize } from "akanjs/constant";
18
19
  import { baseDocumentColumns, documentQueryHelper, getFilterSortByKey, type QueryFieldMap } from "akanjs/document";
19
20
  import {
20
21
  type AkanJob,
@@ -475,9 +476,11 @@ export class SignalResolver {
475
476
  }) {}
476
477
  return SliceEndpoint;
477
478
  }
478
-
479
+
480
+ static readonly #clientOrderableTypes = new Set<unknown>([Int, Float, String, ID, Boolean, Date]);
479
481
  static #assertLiveSort(refName: string, key: string, sliceInfo: SliceInfo) {
480
- const lightFields = (sliceInfo.light as unknown as { [FIELD_META]?: Record<string, unknown> })[FIELD_META] ?? {};
482
+ const lightFields =
483
+ (sliceInfo.light as unknown as { [FIELD_META]?: Record<string, ConstantField> })[FIELD_META] ?? {};
481
484
  for (const sortKey of sliceInfo.liveOption?.sort ?? []) {
482
485
  const sort = getFilterSortByKey(sliceInfo.filter, sortKey);
483
486
  if (!sort)
@@ -489,9 +492,12 @@ export class SignalResolver {
489
492
  `Live slice "${refName}.${key}" declares sort "${sortKey}" on "${path}", which is not in ` +
490
493
  `Light${capitalize(refName)}. A subscriber cannot order by a field it never receives.`,
491
494
  );
495
+ const { modelRef, isArray, isMap } = lightFields[path].getProps();
496
+ if (!isArray && !isMap && SignalResolver.#clientOrderableTypes.has(modelRef)) continue;
492
497
  SignalResolver.logger.warn(
493
- `Live slice "${refName}.${key}" sorts by "${path}", which is stored inside the document rather than in a ` +
494
- `column. SQLite and Postgres order those differently, so a client placing a row may disagree with the server.`,
498
+ `Live slice "${refName}.${key}" sorts "${sortKey}" by "${path}", which is not an Int, Float, String, ID, ` +
499
+ `Boolean or Date field, so a client placing a new row may disagree with the server. Sort by one of ` +
500
+ `those, or leave "${sortKey}" out of .live({ sort }) so the list refetches when a row arrives.`,
495
501
  );
496
502
  }
497
503
  }
@@ -37,9 +37,9 @@ export class QueryCompiler {
37
37
  }
38
38
 
39
39
  orderBy(sort: Record<string, 1 | -1> = { createdAt: -1 }) {
40
- return Object.entries(sort)
41
- .map(([path, direction]) => this.dialect.orderTerm(this.fieldExpr(path), direction))
42
- .join(", ");
40
+ const terms = Object.entries(sort);
41
+ if (!terms.some(([path]) => path === "id")) terms.push(["id", terms.at(-1)?.[1] ?? -1]);
42
+ return terms.map(([path, direction]) => this.dialect.orderTerm(this.fieldExpr(path), direction)).join(", ");
43
43
  }
44
44
 
45
45
  fieldExpr(path: string) {
@@ -30,31 +30,53 @@ export const livePlacementIndex = ({
30
30
  const sort = sorts?.[sortKey];
31
31
  if (!sort || !Object.keys(sort).length) return null;
32
32
  const paths = Object.entries(sort);
33
- if (paths.some(([path]) => comparableOf(row[path]) === null)) return null;
34
- const index = list.findIndex((item) => compareRows(row, item, paths) < 0);
33
+ if (paths.some(([path]) => comparableOf(row[path]) === undefined)) return null;
34
+ let orderable = true;
35
+ const index = list.findIndex((item) => {
36
+ const order = compareRows(row, item, paths);
37
+ orderable = order !== null;
38
+ return order === null || order < 0;
39
+ });
40
+ if (!orderable) return null;
35
41
  if (index !== -1) return index;
36
42
 
37
43
  if (cumulative) return hasMore ? null : list.length;
38
44
  return list.length < limit ? list.length : null;
39
45
  };
40
46
 
41
- const compareRows = (left: LiveSortableRow, right: LiveSortableRow, paths: [string, 1 | -1][]): number => {
47
+ const compareRows = (left: LiveSortableRow, right: LiveSortableRow, paths: [string, 1 | -1][]): number | null => {
42
48
  for (const [path, direction] of paths) {
43
- const a = comparableOf(left[path]);
44
- const b = comparableOf(right[path]);
45
- if (a === null || b === null) continue;
46
- if (a === b) continue;
47
- return (a < b ? -1 : 1) * direction;
49
+ const order = compareValues(comparableOf(left[path]), comparableOf(right[path]));
50
+ if (order !== 0) return order === null ? null : order * direction;
48
51
  }
49
- return 0;
52
+ const order = compareValues(comparableOf(left.id), comparableOf(right.id));
53
+ return order === null ? null : order * (paths.at(-1)?.[1] ?? -1);
50
54
  };
51
55
 
52
- const comparableOf = (value: unknown): number | string | null => {
53
- if (value === null || value === undefined) return null;
56
+ const compareValues = (a: Comparable | undefined, b: Comparable | undefined): number | null => {
57
+ if (a === undefined || b === undefined) return null;
58
+ if (a === b) return 0;
59
+ if (a === null) return -1;
60
+ if (b === null) return 1;
61
+ if (typeof a === "string" && typeof b === "string") return compareText(a, b);
62
+ if (typeof a === "number" && typeof b === "number") return a < b ? -1 : 1;
63
+ return null;
64
+ };
65
+
66
+ const compareText = (a: string, b: string) => {
67
+ let idx = 0;
68
+ while (idx < a.length && idx < b.length && a[idx] === b[idx]) idx += 1;
69
+ return (a.codePointAt(idx) ?? -1) < (b.codePointAt(idx) ?? -1) ? -1 : 1;
70
+ };
71
+
72
+ type Comparable = number | string | null;
73
+
74
+ const comparableOf = (value: unknown): Comparable | undefined => {
75
+ if (value === null) return null;
54
76
  if (typeof value === "number" || typeof value === "string") return value;
55
77
  if (typeof value === "boolean") return value ? 1 : 0;
56
78
  if (value instanceof Date) return value.getTime();
57
- const valued: unknown = (value as { valueOf?: () => unknown }).valueOf?.();
79
+ const valued: unknown = (value as { valueOf?: () => unknown } | undefined)?.valueOf?.();
58
80
  if (typeof valued === "number" || typeof valued === "string") return valued;
59
- return null;
81
+ return undefined;
60
82
  };