@powersync/service-module-postgres 0.0.0-dev-20251111093449 → 0.0.0-dev-20251120150014
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.
- package/CHANGELOG.md +29 -5
- package/dist/api/PostgresRouteAPIAdapter.js +1 -2
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/module/PostgresModule.d.ts +0 -1
- package/dist/module/PostgresModule.js +4 -9
- package/dist/module/PostgresModule.js.map +1 -1
- package/dist/replication/ConnectionManagerFactory.d.ts +3 -5
- package/dist/replication/ConnectionManagerFactory.js +11 -9
- package/dist/replication/ConnectionManagerFactory.js.map +1 -1
- package/dist/replication/PgManager.d.ts +6 -4
- package/dist/replication/PgManager.js +17 -7
- package/dist/replication/PgManager.js.map +1 -1
- package/dist/replication/PostgresErrorRateLimiter.js +6 -1
- package/dist/replication/PostgresErrorRateLimiter.js.map +1 -1
- package/dist/replication/WalStream.js +5 -1
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/WalStreamReplicationJob.d.ts +1 -2
- package/dist/replication/WalStreamReplicationJob.js +47 -70
- package/dist/replication/WalStreamReplicationJob.js.map +1 -1
- package/dist/types/resolver.d.ts +9 -3
- package/dist/types/resolver.js +26 -5
- package/dist/types/resolver.js.map +1 -1
- package/dist/types/types.d.ts +1 -4
- package/dist/types/types.js.map +1 -1
- package/package.json +11 -11
- package/src/api/PostgresRouteAPIAdapter.ts +1 -1
- package/src/module/PostgresModule.ts +4 -9
- package/src/replication/ConnectionManagerFactory.ts +14 -13
- package/src/replication/PgManager.ts +22 -11
- package/src/replication/PostgresErrorRateLimiter.ts +5 -1
- package/src/replication/WalStream.ts +5 -1
- package/src/replication/WalStreamReplicationJob.ts +48 -68
- package/src/types/resolver.ts +27 -6
- package/src/types/types.ts +1 -5
- package/test/src/pg_test.test.ts +2 -2
- package/test/src/slow_tests.test.ts +2 -2
- package/test/src/wal_stream.test.ts +14 -3
- package/test/src/wal_stream_utils.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,19 +12,13 @@ export interface WalStreamReplicationJobOptions extends replication.AbstractRepl
|
|
|
12
12
|
|
|
13
13
|
export class WalStreamReplicationJob extends replication.AbstractReplicationJob {
|
|
14
14
|
private connectionFactory: ConnectionManagerFactory;
|
|
15
|
-
private
|
|
15
|
+
private connectionManager: PgManager | null = null;
|
|
16
16
|
private lastStream: WalStream | null = null;
|
|
17
17
|
|
|
18
18
|
constructor(options: WalStreamReplicationJobOptions) {
|
|
19
19
|
super(options);
|
|
20
20
|
this.logger = logger.child({ prefix: `[${this.slotName}] ` });
|
|
21
21
|
this.connectionFactory = options.connectionFactory;
|
|
22
|
-
this.connectionManager = this.connectionFactory.create({
|
|
23
|
-
// Pool connections are only used intermittently.
|
|
24
|
-
idleTimeout: 30_000,
|
|
25
|
-
maxSize: 2,
|
|
26
|
-
applicationName: getApplicationName()
|
|
27
|
-
});
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
/**
|
|
@@ -40,10 +34,12 @@ export class WalStreamReplicationJob extends replication.AbstractReplicationJob
|
|
|
40
34
|
* **This may be a bug in pgwire or how we're using it.
|
|
41
35
|
*/
|
|
42
36
|
async keepAlive() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
if (this.connectionManager) {
|
|
38
|
+
try {
|
|
39
|
+
await sendKeepAlive(this.connectionManager.pool);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
this.logger.warn(`KeepAlive failed, unable to post to WAL`, e);
|
|
42
|
+
}
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
|
|
@@ -53,35 +49,58 @@ export class WalStreamReplicationJob extends replication.AbstractReplicationJob
|
|
|
53
49
|
|
|
54
50
|
async replicate() {
|
|
55
51
|
try {
|
|
56
|
-
await this.
|
|
52
|
+
await this.replicateOnce();
|
|
57
53
|
} catch (e) {
|
|
58
54
|
// Fatal exception
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
|
|
56
|
+
if (!this.isStopped) {
|
|
57
|
+
// Ignore aborted errors
|
|
58
|
+
|
|
59
|
+
this.logger.error(`Replication error`, e);
|
|
60
|
+
if (e.cause != null) {
|
|
61
|
+
// Example:
|
|
62
|
+
// PgError.conn_ended: Unable to do postgres query on ended connection
|
|
63
|
+
// at PgConnection.stream (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:315:13)
|
|
64
|
+
// at stream.next (<anonymous>)
|
|
65
|
+
// at PgResult.fromStream (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:1174:22)
|
|
66
|
+
// at PgConnection.query (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:311:21)
|
|
67
|
+
// at WalStream.startInitialReplication (file:///.../powersync/powersync-service/lib/replication/WalStream.js:266:22)
|
|
68
|
+
// ...
|
|
69
|
+
// cause: TypeError: match is not iterable
|
|
70
|
+
// at timestamptzToSqlite (file:///.../powersync/packages/jpgwire/dist/util.js:140:50)
|
|
71
|
+
// at PgType.decode (file:///.../powersync/packages/jpgwire/dist/pgwire_types.js:25:24)
|
|
72
|
+
// at PgConnection._recvDataRow (file:///.../powersync/packages/jpgwire/dist/util.js:88:22)
|
|
73
|
+
// at PgConnection._recvMessages (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:656:30)
|
|
74
|
+
// at PgConnection._ioloopAttempt (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:563:20)
|
|
75
|
+
// at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
|
76
|
+
// at async PgConnection._ioloop (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:517:14),
|
|
77
|
+
// [Symbol(pg.ErrorCode)]: 'conn_ended',
|
|
78
|
+
// [Symbol(pg.ErrorResponse)]: undefined
|
|
79
|
+
// }
|
|
80
|
+
// Without this additional log, the cause would not be visible in the logs.
|
|
81
|
+
this.logger.error(`cause`, e.cause);
|
|
62
82
|
}
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
// Report the error if relevant, before retrying
|
|
84
|
+
container.reporter.captureException(e, {
|
|
85
|
+
metadata: {
|
|
86
|
+
replication_slot: this.slotName
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// This sets the retry delay
|
|
90
|
+
this.rateLimiter.reportError(e);
|
|
91
|
+
}
|
|
65
92
|
|
|
66
93
|
if (e instanceof MissingReplicationSlotError) {
|
|
67
94
|
// This stops replication on this slot and restarts with a new slot
|
|
68
95
|
await this.options.storage.factory.restartReplication(this.storage.group_id);
|
|
69
96
|
}
|
|
97
|
+
|
|
98
|
+
// No need to rethrow - the error is already logged, and retry behavior is the same on error
|
|
70
99
|
} finally {
|
|
71
100
|
this.abortController.abort();
|
|
72
101
|
}
|
|
73
102
|
}
|
|
74
103
|
|
|
75
|
-
async replicateLoop() {
|
|
76
|
-
while (!this.isStopped) {
|
|
77
|
-
await this.replicateOnce();
|
|
78
|
-
|
|
79
|
-
if (!this.isStopped) {
|
|
80
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
104
|
async replicateOnce() {
|
|
86
105
|
// New connections on every iteration (every error with retry),
|
|
87
106
|
// otherwise we risk repeating errors related to the connection,
|
|
@@ -92,6 +111,7 @@ export class WalStreamReplicationJob extends replication.AbstractReplicationJob
|
|
|
92
111
|
maxSize: 2,
|
|
93
112
|
applicationName: getApplicationName()
|
|
94
113
|
});
|
|
114
|
+
this.connectionManager = connectionManager;
|
|
95
115
|
try {
|
|
96
116
|
await this.rateLimiter?.waitUntilAllowed({ signal: this.abortController.signal });
|
|
97
117
|
if (this.isStopped) {
|
|
@@ -106,48 +126,8 @@ export class WalStreamReplicationJob extends replication.AbstractReplicationJob
|
|
|
106
126
|
});
|
|
107
127
|
this.lastStream = stream;
|
|
108
128
|
await stream.replicate();
|
|
109
|
-
} catch (e) {
|
|
110
|
-
if (this.isStopped && e instanceof ReplicationAbortedError) {
|
|
111
|
-
// Ignore aborted errors
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
this.logger.error(`Replication error`, e);
|
|
115
|
-
if (e.cause != null) {
|
|
116
|
-
// Example:
|
|
117
|
-
// PgError.conn_ended: Unable to do postgres query on ended connection
|
|
118
|
-
// at PgConnection.stream (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:315:13)
|
|
119
|
-
// at stream.next (<anonymous>)
|
|
120
|
-
// at PgResult.fromStream (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:1174:22)
|
|
121
|
-
// at PgConnection.query (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:311:21)
|
|
122
|
-
// at WalStream.startInitialReplication (file:///.../powersync/powersync-service/lib/replication/WalStream.js:266:22)
|
|
123
|
-
// ...
|
|
124
|
-
// cause: TypeError: match is not iterable
|
|
125
|
-
// at timestamptzToSqlite (file:///.../powersync/packages/jpgwire/dist/util.js:140:50)
|
|
126
|
-
// at PgType.decode (file:///.../powersync/packages/jpgwire/dist/pgwire_types.js:25:24)
|
|
127
|
-
// at PgConnection._recvDataRow (file:///.../powersync/packages/jpgwire/dist/util.js:88:22)
|
|
128
|
-
// at PgConnection._recvMessages (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:656:30)
|
|
129
|
-
// at PgConnection._ioloopAttempt (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:563:20)
|
|
130
|
-
// at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
|
131
|
-
// at async PgConnection._ioloop (file:///.../powersync/node_modules/.pnpm/github.com+kagis+pgwire@f1cb95f9a0f42a612bb5a6b67bb2eb793fc5fc87/node_modules/pgwire/mod.js:517:14),
|
|
132
|
-
// [Symbol(pg.ErrorCode)]: 'conn_ended',
|
|
133
|
-
// [Symbol(pg.ErrorResponse)]: undefined
|
|
134
|
-
// }
|
|
135
|
-
// Without this additional log, the cause would not be visible in the logs.
|
|
136
|
-
this.logger.error(`cause`, e.cause);
|
|
137
|
-
}
|
|
138
|
-
if (e instanceof MissingReplicationSlotError) {
|
|
139
|
-
throw e;
|
|
140
|
-
} else {
|
|
141
|
-
// Report the error if relevant, before retrying
|
|
142
|
-
container.reporter.captureException(e, {
|
|
143
|
-
metadata: {
|
|
144
|
-
replication_slot: this.slotName
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
// This sets the retry delay
|
|
148
|
-
this.rateLimiter?.reportError(e);
|
|
149
|
-
}
|
|
150
129
|
} finally {
|
|
130
|
+
this.connectionManager = null;
|
|
151
131
|
await connectionManager.end();
|
|
152
132
|
}
|
|
153
133
|
}
|
package/src/types/resolver.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { DatabaseInputRow, SqliteInputRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
2
1
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
3
|
-
import {
|
|
2
|
+
import { DatabaseInputRow, SqliteInputRow, toSyncRulesRow } from '@powersync/service-sync-rules';
|
|
4
3
|
import semver from 'semver';
|
|
5
4
|
import { getServerVersion } from '../utils/postgres_version.js';
|
|
5
|
+
import { CustomTypeRegistry } from './registry.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Resolves descriptions used to decode values for custom postgres types.
|
|
@@ -11,11 +11,9 @@ import { getServerVersion } from '../utils/postgres_version.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export class PostgresTypeResolver {
|
|
13
13
|
private cachedVersion: semver.SemVer | null = null;
|
|
14
|
+
readonly registry: CustomTypeRegistry;
|
|
14
15
|
|
|
15
|
-
constructor(
|
|
16
|
-
readonly registry: CustomTypeRegistry,
|
|
17
|
-
private readonly pool: pgwire.PgClient
|
|
18
|
-
) {
|
|
16
|
+
constructor(private readonly pool: pgwire.PgClient) {
|
|
19
17
|
this.registry = new CustomTypeRegistry();
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -188,6 +186,11 @@ WHERE a.attnum > 0
|
|
|
188
186
|
return toSyncRulesRow(record);
|
|
189
187
|
}
|
|
190
188
|
|
|
189
|
+
constructRowRecord(columnMap: Record<string, number>, tupleRaw: Record<string, any>): SqliteInputRow {
|
|
190
|
+
const record = this.decodeTupleForTable(columnMap, tupleRaw);
|
|
191
|
+
return toSyncRulesRow(record);
|
|
192
|
+
}
|
|
193
|
+
|
|
191
194
|
/**
|
|
192
195
|
* We need a high level of control over how values are decoded, to make sure there is no loss
|
|
193
196
|
* of precision in the process.
|
|
@@ -206,5 +209,23 @@ WHERE a.attnum > 0
|
|
|
206
209
|
return result;
|
|
207
210
|
}
|
|
208
211
|
|
|
212
|
+
/**
|
|
213
|
+
* We need a high level of control over how values are decoded, to make sure there is no loss
|
|
214
|
+
* of precision in the process.
|
|
215
|
+
*/
|
|
216
|
+
private decodeTupleForTable(columnMap: Record<string, number>, tupleRaw: Record<string, any>): DatabaseInputRow {
|
|
217
|
+
let result: Record<string, any> = {};
|
|
218
|
+
for (let columnName in tupleRaw) {
|
|
219
|
+
const rawval = tupleRaw[columnName];
|
|
220
|
+
const typeOid = columnMap[columnName];
|
|
221
|
+
if (typeof rawval == 'string' && typeOid) {
|
|
222
|
+
result[columnName] = this.registry.decodeDatabaseValue(rawval, typeOid);
|
|
223
|
+
} else {
|
|
224
|
+
result[columnName] = rawval;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
209
230
|
private static minVersionForMultirange: semver.SemVer = semver.parse('14.0.0')!;
|
|
210
231
|
}
|
package/src/types/types.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
2
|
import * as service_types from '@powersync/service-types';
|
|
3
3
|
import * as t from 'ts-codec';
|
|
4
|
-
import { CustomTypeRegistry } from './registry.js';
|
|
5
4
|
|
|
6
5
|
// Maintain backwards compatibility by exporting these
|
|
7
6
|
export const validatePort = lib_postgres.validatePort;
|
|
@@ -25,10 +24,7 @@ export type PostgresConnectionConfig = t.Decoded<typeof PostgresConnectionConfig
|
|
|
25
24
|
/**
|
|
26
25
|
* Resolved version of {@link PostgresConnectionConfig}
|
|
27
26
|
*/
|
|
28
|
-
export type ResolvedConnectionConfig = PostgresConnectionConfig &
|
|
29
|
-
NormalizedPostgresConnectionConfig & {
|
|
30
|
-
typeRegistry: CustomTypeRegistry;
|
|
31
|
-
};
|
|
27
|
+
export type ResolvedConnectionConfig = PostgresConnectionConfig & NormalizedPostgresConnectionConfig;
|
|
32
28
|
|
|
33
29
|
export function isPostgresConfig(
|
|
34
30
|
config: service_types.configFile.DataSourceConfig
|
package/test/src/pg_test.test.ts
CHANGED
|
@@ -551,7 +551,7 @@ INSERT INTO test_data(id, time, timestamp, timestamptz) VALUES (1, '17:42:01.12'
|
|
|
551
551
|
test('test replication - multiranges', async () => {
|
|
552
552
|
const db = await connectPgPool();
|
|
553
553
|
|
|
554
|
-
if (!(await new PostgresTypeResolver(
|
|
554
|
+
if (!(await new PostgresTypeResolver(db).supportsMultiRanges())) {
|
|
555
555
|
// This test requires Postgres 14 or later.
|
|
556
556
|
return;
|
|
557
557
|
}
|
|
@@ -620,7 +620,7 @@ INSERT INTO test_data(id, time, timestamp, timestamptz) VALUES (1, '17:42:01.12'
|
|
|
620
620
|
* Return all the inserts from the first transaction in the replication stream.
|
|
621
621
|
*/
|
|
622
622
|
async function getReplicationTx(db: pgwire.PgClient, replicationStream: pgwire.ReplicationStream) {
|
|
623
|
-
const typeCache = new PostgresTypeResolver(
|
|
623
|
+
const typeCache = new PostgresTypeResolver(db);
|
|
624
624
|
await typeCache.fetchTypesForSchema();
|
|
625
625
|
|
|
626
626
|
let transformed: SqliteInputRow[] = [];
|
|
@@ -69,7 +69,7 @@ function defineSlowTests(factory: storage.TestStorageFactory) {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
async function testRepeatedReplication(testOptions: { compact: boolean; maxBatchSize: number; numBatches: number }) {
|
|
72
|
-
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {
|
|
72
|
+
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
73
73
|
const replicationConnection = await connections.replicationConnection();
|
|
74
74
|
const pool = connections.pool;
|
|
75
75
|
await clearTestDb(pool);
|
|
@@ -330,7 +330,7 @@ bucket_definitions:
|
|
|
330
330
|
await pool.query(`SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE active = FALSE`);
|
|
331
331
|
i += 1;
|
|
332
332
|
|
|
333
|
-
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {
|
|
333
|
+
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
334
334
|
const replicationConnection = await connections.replicationConnection();
|
|
335
335
|
|
|
336
336
|
abortController = new AbortController();
|
|
@@ -529,13 +529,24 @@ config:
|
|
|
529
529
|
const { pool } = context;
|
|
530
530
|
await pool.query(`DROP TABLE IF EXISTS test_data`);
|
|
531
531
|
await pool.query(`CREATE TYPE composite AS (foo bool, bar int4);`);
|
|
532
|
-
await pool.query(`CREATE TABLE test_data(id text primary key, description composite);`);
|
|
532
|
+
await pool.query(`CREATE TABLE test_data(id text primary key, description composite, ts timestamptz);`);
|
|
533
|
+
|
|
534
|
+
// Covered by initial replication
|
|
535
|
+
await pool.query(
|
|
536
|
+
`INSERT INTO test_data(id, description, ts) VALUES ('t1', ROW(TRUE, 1)::composite, '2025-11-17T09:11:00Z')`
|
|
537
|
+
);
|
|
533
538
|
|
|
534
539
|
await context.initializeReplication();
|
|
535
|
-
|
|
540
|
+
// Covered by streaming replication
|
|
541
|
+
await pool.query(
|
|
542
|
+
`INSERT INTO test_data(id, description, ts) VALUES ('t2', ROW(TRUE, 2)::composite, '2025-11-17T09:12:00Z')`
|
|
543
|
+
);
|
|
536
544
|
|
|
537
545
|
const data = await context.getBucketData('1#stream|0[]');
|
|
538
|
-
expect(data).toMatchObject([
|
|
546
|
+
expect(data).toMatchObject([
|
|
547
|
+
putOp('test_data', { id: 't1', description: '{"foo":1,"bar":1}', ts: '2025-11-17T09:11:00.000000Z' }),
|
|
548
|
+
putOp('test_data', { id: 't2', description: '{"foo":1,"bar":2}', ts: '2025-11-17T09:12:00.000000Z' })
|
|
549
|
+
]);
|
|
539
550
|
});
|
|
540
551
|
|
|
541
552
|
test('custom types in primary key', async () => {
|
|
@@ -33,7 +33,7 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
33
33
|
options?: { doNotClear?: boolean; walStreamOptions?: Partial<WalStreamOptions> }
|
|
34
34
|
) {
|
|
35
35
|
const f = await factory({ doNotClear: options?.doNotClear });
|
|
36
|
-
const connectionManager = new PgManager(TEST_CONNECTION_OPTIONS, {
|
|
36
|
+
const connectionManager = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
37
37
|
|
|
38
38
|
if (!options?.doNotClear) {
|
|
39
39
|
await clearTestDb(connectionManager.pool);
|