@logux/server 0.14.0 → 0.16.0

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.
@@ -1,8 +1,9 @@
1
1
  import os from 'node:os'
2
2
  import { stripVTControlCharacters, styleText } from 'node:util'
3
3
 
4
- import { mulberry32, onceXmur3 } from './utils.js'
4
+ import { isBinary, mulberry32, onceXmur3 } from './utils.js'
5
5
 
6
+ const BINARY_MARK = '[binary]'
6
7
  const INDENT = ' '
7
8
  const PADDING = ' '
8
9
  const SEPARATOR = os.EOL + os.EOL
@@ -119,7 +120,9 @@ function formatNodeId(nodeId) {
119
120
  }
120
121
 
121
122
  function formatValue(value) {
122
- if (typeof value === 'string') {
123
+ if (isBinary(value)) {
124
+ return styleText('bold', BINARY_MARK)
125
+ } else if (typeof value === 'string') {
123
126
  return '"' + styleText('bold', value) + '"'
124
127
  } else if (Array.isArray(value)) {
125
128
  return formatArray(value)
@@ -145,10 +148,7 @@ function formatActionId(id) {
145
148
  if (p.length === 1) {
146
149
  return p
147
150
  }
148
- return (
149
- `${styleText('bold', splitAndColorize(4, p[0]))} ` +
150
- `${formatNodeId(p[1])} ${styleText('bold', p[2])}`
151
- )
151
+ return `${styleText('bold', splitAndColorize(4, p[0]))} ${formatNodeId(p[1])}`
152
152
  }
153
153
 
154
154
  function formatParams(params, parent) {
@@ -164,7 +164,12 @@ function formatParams(params, parent) {
164
164
 
165
165
  let start = PADDING + rightPag(`${name}: `, maxName + 2)
166
166
 
167
- if (name === 'Node ID' || (parent === 'Meta' && name === 'server')) {
167
+ if (isBinary(value)) {
168
+ return start + styleText('bold', BINARY_MARK)
169
+ } else if (
170
+ name === 'Node ID' ||
171
+ (parent === 'Meta' && name === 'server')
172
+ ) {
168
173
  return start + formatNodeId(value)
169
174
  } else if (
170
175
  parent === 'Meta' &&
@@ -1,3 +1,17 @@
1
+ export const BINARY = {
2
+ toJSON() {
3
+ return '[binary]'
4
+ }
5
+ }
6
+
7
+ export function isBinary(value) {
8
+ return (
9
+ value === BINARY ||
10
+ ArrayBuffer.isView(value) ||
11
+ value instanceof ArrayBuffer
12
+ )
13
+ }
14
+
1
15
  export function onceXmur3(str) {
2
16
  let h = 1779033703 ^ str.length
3
17
  for (let i = 0; i < str.length; i++) {
package/index.d.ts CHANGED
@@ -16,8 +16,15 @@ export {
16
16
  ServerMeta,
17
17
  wasNot403
18
18
  } from './base-server/index.js'
19
- export { ChannelContext, Context } from './context/index.js'
19
+ export { Context, ConnectContext, ChannelContext } from './context/index.js'
20
20
  export { filterMeta } from './filter-meta/index.js'
21
+ export {
22
+ PostgresDriver,
23
+ PostgresQuery,
24
+ PostgresRows,
25
+ PostgresStore,
26
+ PostgresStoreOptions
27
+ } from './postgres-store/index.js'
21
28
  export {
22
29
  del,
23
30
  get,
package/index.js CHANGED
@@ -8,6 +8,7 @@ export { ALLOWED_META } from './allowed-meta/index.js'
8
8
  export { BaseServer, wasNot403 } from './base-server/index.js'
9
9
  export { Context } from './context/index.js'
10
10
  export { filterMeta } from './filter-meta/index.js'
11
+ export { PostgresStore } from './postgres-store/index.js'
11
12
  export { ResponseError } from './request/index.js'
12
13
  export { Server } from './server/index.js'
13
14
  export { TestClient } from './test-client/index.js'
package/package.json CHANGED
@@ -1,25 +1,22 @@
1
1
  {
2
2
  "name": "@logux/server",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Build own Logux server",
5
5
  "keywords": [
6
- "logux",
7
- "server",
8
- "framework",
9
- "websocket",
10
- "proxy",
6
+ "collaborative",
11
7
  "crdt",
12
- "event sourcing",
13
8
  "distributed systems",
14
- "collaborative"
9
+ "event sourcing",
10
+ "framework",
11
+ "logux",
12
+ "proxy",
13
+ "server",
14
+ "websocket"
15
15
  ],
16
- "author": "Andrey Sitnik <andrey@sitnik.ru>",
17
- "license": "MIT",
18
16
  "homepage": "https://logux.org/",
17
+ "license": "MIT",
18
+ "author": "Andrey Sitnik <andrey@sitnik.es>",
19
19
  "repository": "logux/server",
20
- "engines": {
21
- "node": "^20.0.0 || >=22.0.0"
22
- },
23
20
  "type": "module",
24
21
  "types": "./index.d.ts",
25
22
  "exports": {
@@ -27,14 +24,15 @@
27
24
  "./package.json": "./package.json"
28
25
  },
29
26
  "dependencies": {
30
- "@logux/actions": "^0.5.0",
31
- "@logux/core": "^0.10.0",
32
- "cookie": "^1.0.2",
33
- "fastq": "^1.19.1",
34
- "nanoevents": "^9.1.0",
35
- "nanoid": "^5.1.5",
36
- "tinyglobby": "^0.2.14",
37
- "url-pattern": "^1.0.3",
38
- "ws": "^8.18.3"
27
+ "@logux/actions": "^0.7.2",
28
+ "@logux/core": "^0.11.6",
29
+ "cookie": "^2.0.1",
30
+ "fastq": "^1.20.1",
31
+ "nanoevents": "^10.0.0",
32
+ "nanoid": "^6.0.1",
33
+ "ws": "^8.21.3"
34
+ },
35
+ "engines": {
36
+ "node": "^22.5.0 || ^24.0.0 || >=26.0.0"
39
37
  }
40
38
  }
@@ -0,0 +1,149 @@
1
+ import type { ActionPackerMap } from '@logux/actions'
2
+ import type {
3
+ Action,
4
+ AnyAction,
5
+ Criteria,
6
+ LogPage,
7
+ LogStore,
8
+ Meta
9
+ } from '@logux/core'
10
+
11
+ /**
12
+ * Logux declares these two, but does not export them, so they are taken
13
+ * from the methods not to repeat the definition here.
14
+ */
15
+ type GetOptions = NonNullable<Parameters<LogStore['get']>[0]>
16
+ type LastSynced = Awaited<ReturnType<LogStore['getLastSynced']>>
17
+
18
+ /**
19
+ * Rows of the query. `pg` and PGlite keep them in the result object,
20
+ * `postgres` returns them as an array.
21
+ */
22
+ export type PostgresRows =
23
+ | { rows: Record<string, unknown>[] }
24
+ | Record<string, unknown>[]
25
+
26
+ /**
27
+ * Minimal database interface for the drivers, which the store does not
28
+ * support out of the box.
29
+ *
30
+ * ```js
31
+ * let query = (sql, params) => driver.run(sql, params)
32
+ * query.transaction = body => {
33
+ * return driver.tx(tx => body((sql, params) => tx.run(sql, params)))
34
+ * }
35
+ *
36
+ * let store = new PostgresStore(query)
37
+ * ```
38
+ */
39
+ export interface PostgresQuery {
40
+ (sql: string, params: unknown[]): Promise<PostgresRows>
41
+
42
+ /**
43
+ * Run the callback on a single connection inside a transaction.
44
+ *
45
+ * Without it `init()` can not lock the migrations, so two servers
46
+ * starting at the same moment can apply them twice.
47
+ */
48
+ transaction?: (body: (query: PostgresQuery) => Promise<void>) => Promise<void>
49
+ }
50
+
51
+ /**
52
+ * Database driver, which the store can use without any wrapper:
53
+ * `pg`’s `Pool` or `Client`, PGlite, or `postgres`.
54
+ *
55
+ * The parameters are `never[]`, so that the driver’s own stricter types
56
+ * for them still match this type.
57
+ */
58
+ export type PostgresDriver =
59
+ | {
60
+ begin?(body: (tx: PostgresDriver) => Promise<unknown>): Promise<unknown>
61
+ unsafe(sql: string, params: never[]): Promise<unknown>
62
+ }
63
+ | {
64
+ connect?(): Promise<unknown>
65
+ query(sql: string, params: never[]): Promise<unknown>
66
+ transaction?(
67
+ body: (tx: PostgresDriver) => Promise<unknown>
68
+ ): Promise<unknown>
69
+ }
70
+
71
+ export interface PostgresStoreOptions<Packers = Record<string, never>> {
72
+ /**
73
+ * How many entries to load by a single query in `get()`.
74
+ */
75
+ pageSize?: number
76
+
77
+ /**
78
+ * Packers to keep the binary parts of the action in the `blob` column
79
+ * instead of Base64 inside JSON. The key must be the `type` of the action.
80
+ *
81
+ * `zeroPacker` is always added, so this is only for custom actions.
82
+ */
83
+ packers?: Packers
84
+ }
85
+
86
+ /**
87
+ * Log store, which keeps actions in PostgreSQL.
88
+ *
89
+ * It takes the database of `pg`, `postgres`, or PGlite. For any other driver,
90
+ * pass the function to send the query, see {@link PostgresQuery}.
91
+ *
92
+ * ```js
93
+ * import { PostgresStore, Server } from '@logux/server'
94
+ * import { Pool } from 'pg'
95
+ *
96
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL })
97
+ * const store = new PostgresStore(pool)
98
+ * await store.init()
99
+ *
100
+ * const server = new Server(Server.loadOptions(process, {
101
+ * minSubprotocol: 1,
102
+ * subprotocol: 1,
103
+ * root: import.meta.dirname,
104
+ * store
105
+ * }))
106
+ * ```
107
+ */
108
+ export class PostgresStore<
109
+ Packers extends ActionPackerMap<Packers> = Record<string, never>
110
+ >
111
+ implements LogStore
112
+ {
113
+ constructor(
114
+ db: PostgresDriver | PostgresQuery,
115
+ opts?: PostgresStoreOptions<Packers>
116
+ )
117
+
118
+ add(action: AnyAction, meta: Meta): Promise<false | Meta>
119
+
120
+ addReason(reasons: string[], criteria: Criteria): Promise<void>
121
+
122
+ byId(id: string): Promise<[Action, Meta] | [null, null]>
123
+
124
+ changeMeta(id: string, diff: Partial<Meta>): Promise<boolean>
125
+
126
+ clean(): Promise<void>
127
+
128
+ get(opts?: GetOptions): Promise<LogPage>
129
+
130
+ getLastAdded(): Promise<number>
131
+
132
+ getLastSynced(): Promise<LastSynced>
133
+
134
+ /**
135
+ * Bring the log tables to the latest version, creating them if they were
136
+ * not created by the application’s migration.
137
+ */
138
+ init(): Promise<void>
139
+
140
+ remove(id: string): Promise<[Action, Meta] | false>
141
+
142
+ removeReason(
143
+ reasons: string[],
144
+ criteria: Criteria,
145
+ callback: (action: Action, meta: Meta) => void
146
+ ): Promise<void>
147
+
148
+ setLastSynced(values: Partial<LastSynced>): Promise<void>
149
+ }