@powersync/op-sqlite 0.0.0-dev-20241014164640 → 0.0.0-dev-20241107150304

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 (53) hide show
  1. package/README.md +26 -1
  2. package/lib/commonjs/db/OPSqliteAdapter.js +67 -47
  3. package/lib/commonjs/db/OPSqliteAdapter.js.map +1 -1
  4. package/lib/commonjs/db/OPSqliteDBOpenFactory.js +2 -2
  5. package/lib/commonjs/db/SqliteOptions.js +2 -1
  6. package/lib/commonjs/db/SqliteOptions.js.map +1 -1
  7. package/lib/commonjs/index.js +1 -1
  8. package/lib/module/db/OPSqliteAdapter.js +67 -47
  9. package/lib/module/db/OPSqliteAdapter.js.map +1 -1
  10. package/lib/module/db/OPSqliteDBOpenFactory.js +2 -2
  11. package/lib/module/db/OPSqliteDBOpenFactory.js.map +1 -1
  12. package/lib/module/db/SqliteOptions.js +2 -1
  13. package/lib/module/db/SqliteOptions.js.map +1 -1
  14. package/lib/module/index.js +1 -1
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/typescript/commonjs/src/NativePowerSyncOpSqlite.d.ts.map +1 -0
  17. package/lib/typescript/commonjs/src/db/OPSQLiteConnection.d.ts.map +1 -0
  18. package/lib/typescript/{src → commonjs/src}/db/OPSqliteAdapter.d.ts +8 -2
  19. package/lib/typescript/commonjs/src/db/OPSqliteAdapter.d.ts.map +1 -0
  20. package/lib/typescript/commonjs/src/db/OPSqliteDBOpenFactory.d.ts.map +1 -0
  21. package/lib/typescript/{src → commonjs/src}/db/SqliteOptions.d.ts +5 -0
  22. package/lib/typescript/commonjs/src/db/SqliteOptions.d.ts.map +1 -0
  23. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  24. package/lib/typescript/commonjs/tsconfig.build.tsbuildinfo +1 -0
  25. package/lib/typescript/module/src/NativePowerSyncOpSqlite.d.ts +7 -0
  26. package/lib/typescript/module/src/NativePowerSyncOpSqlite.d.ts.map +1 -0
  27. package/lib/typescript/module/src/db/OPSQLiteConnection.d.ts +17 -0
  28. package/lib/typescript/module/src/db/OPSQLiteConnection.d.ts.map +1 -0
  29. package/lib/typescript/module/src/db/OPSqliteAdapter.d.ts +43 -0
  30. package/lib/typescript/module/src/db/OPSqliteAdapter.d.ts.map +1 -0
  31. package/lib/typescript/module/src/db/OPSqliteDBOpenFactory.d.ts +12 -0
  32. package/lib/typescript/module/src/db/OPSqliteDBOpenFactory.d.ts.map +1 -0
  33. package/lib/typescript/module/src/db/SqliteOptions.d.ts +44 -0
  34. package/lib/typescript/module/src/db/SqliteOptions.d.ts.map +1 -0
  35. package/lib/typescript/module/src/index.d.ts +3 -0
  36. package/lib/typescript/module/src/index.d.ts.map +1 -0
  37. package/lib/typescript/module/tsconfig.build.tsbuildinfo +1 -0
  38. package/package.json +32 -9
  39. package/powersync-op-sqlite.podspec +1 -1
  40. package/src/db/OPSqliteAdapter.ts +67 -52
  41. package/src/db/SqliteOptions.ts +9 -2
  42. package/lib/typescript/src/NativePowerSyncOpSqlite.d.ts.map +0 -1
  43. package/lib/typescript/src/db/OPSQLiteConnection.d.ts.map +0 -1
  44. package/lib/typescript/src/db/OPSqliteAdapter.d.ts.map +0 -1
  45. package/lib/typescript/src/db/OPSqliteDBOpenFactory.d.ts.map +0 -1
  46. package/lib/typescript/src/db/SqliteOptions.d.ts.map +0 -1
  47. package/lib/typescript/src/index.d.ts.map +0 -1
  48. /package/lib/{commonjs → typescript/commonjs}/package.json +0 -0
  49. /package/lib/typescript/{src → commonjs/src}/NativePowerSyncOpSqlite.d.ts +0 -0
  50. /package/lib/typescript/{src → commonjs/src}/db/OPSQLiteConnection.d.ts +0 -0
  51. /package/lib/typescript/{src → commonjs/src}/db/OPSqliteDBOpenFactory.d.ts +0 -0
  52. /package/lib/typescript/{src → commonjs/src}/index.d.ts +0 -0
  53. /package/lib/{module → typescript/module}/package.json +0 -0
@@ -11,7 +11,7 @@ import { ANDROID_DATABASE_PATH, IOS_LIBRARY_PATH, open, type DB } from '@op-engi
11
11
  import Lock from 'async-lock';
12
12
  import { OPSQLiteConnection } from './OPSQLiteConnection';
13
13
  import { NativeModules, Platform } from 'react-native';
14
- import { DEFAULT_SQLITE_OPTIONS, SqliteOptions } from './SqliteOptions';
14
+ import { SqliteOptions } from './SqliteOptions';
15
15
 
16
16
  /**
17
17
  * Adapter for React Native Quick SQLite
@@ -35,10 +35,12 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
35
35
 
36
36
  protected initialized: Promise<void>;
37
37
 
38
- protected readConnections: OPSQLiteConnection[] | null;
38
+ protected readConnections: Array<{ busy: boolean; connection: OPSQLiteConnection }> | null;
39
39
 
40
40
  protected writeConnection: OPSQLiteConnection | null;
41
41
 
42
+ private readQueue: Array<() => void> = [];
43
+
42
44
  constructor(protected options: OPSQLiteAdapterOptions) {
43
45
  super();
44
46
  this.name = this.options.name;
@@ -50,15 +52,10 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
50
52
  }
51
53
 
52
54
  protected async init() {
53
- const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous } = this.options.sqliteOptions;
54
- // const { dbFilename, dbLocation } = this.options;
55
+ const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous, encryptionKey } = this.options.sqliteOptions;
55
56
  const dbFilename = this.options.name;
56
- //This is needed because an undefined dbLocation will cause the open function to fail
57
- const location = this.getDbLocation(this.options.dbLocation);
58
- const DB: DB = open({
59
- name: dbFilename,
60
- location: location
61
- });
57
+
58
+ this.writeConnection = await this.openConnection(dbFilename);
62
59
 
63
60
  const statements: string[] = [
64
61
  `PRAGMA busy_timeout = ${lockTimeoutMs}`,
@@ -70,7 +67,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
70
67
  for (const statement of statements) {
71
68
  for (let tries = 0; tries < 30; tries++) {
72
69
  try {
73
- await DB.execute(statement);
70
+ await this.writeConnection!.execute(statement);
74
71
  break;
75
72
  } catch (e: any) {
76
73
  if (e instanceof Error && e.message.includes('database is locked') && tries < 29) {
@@ -82,34 +79,24 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
82
79
  }
83
80
  }
84
81
 
85
- this.loadExtension(DB);
86
-
87
- await DB.execute('SELECT powersync_init()');
82
+ // Changes should only occur in the write connection
83
+ this.writeConnection!.registerListener({
84
+ tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
85
+ });
88
86
 
89
87
  this.readConnections = [];
90
88
  for (let i = 0; i < READ_CONNECTIONS; i++) {
91
89
  // Workaround to create read-only connections
92
90
  let dbName = './'.repeat(i + 1) + dbFilename;
93
- const conn = await this.openConnection(location, dbName);
91
+ const conn = await this.openConnection(dbName);
94
92
  await conn.execute('PRAGMA query_only = true');
95
- this.readConnections.push(conn);
93
+ this.readConnections.push({ busy: false, connection: conn });
96
94
  }
97
-
98
- this.writeConnection = new OPSQLiteConnection({
99
- baseDB: DB
100
- });
101
-
102
- // Changes should only occur in the write connection
103
- this.writeConnection!.registerListener({
104
- tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
105
- });
106
95
  }
107
96
 
108
- protected async openConnection(dbLocation: string, filenameOverride?: string): Promise<OPSQLiteConnection> {
109
- const DB: DB = open({
110
- name: filenameOverride ?? this.options.name,
111
- location: dbLocation
112
- });
97
+ protected async openConnection(filenameOverride?: string): Promise<OPSQLiteConnection> {
98
+ const dbFilename = filenameOverride ?? this.options.name;
99
+ const DB: DB = this.openDatabase(dbFilename, this.options.sqliteOptions.encryptionKey);
113
100
 
114
101
  //Load extension for all connections
115
102
  this.loadExtension(DB);
@@ -129,6 +116,24 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
129
116
  }
130
117
  }
131
118
 
119
+ private openDatabase(dbFilename: string, encryptionKey?: string): DB {
120
+ //This is needed because an undefined/null dbLocation will cause the open function to fail
121
+ const location = this.getDbLocation(this.options.dbLocation);
122
+ //Simarlily if the encryption key is undefined/null when using SQLCipher it will cause the open function to fail
123
+ if (encryptionKey) {
124
+ return open({
125
+ name: dbFilename,
126
+ location: location,
127
+ encryptionKey: encryptionKey
128
+ });
129
+ } else {
130
+ return open({
131
+ name: dbFilename,
132
+ location: location
133
+ });
134
+ }
135
+ }
136
+
132
137
  private loadExtension(DB: DB) {
133
138
  if (Platform.OS === 'ios') {
134
139
  const bundlePath: string = NativeModules.PowerSyncOpSqlite.getBundlePath();
@@ -142,36 +147,46 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
142
147
  close() {
143
148
  this.initialized.then(() => {
144
149
  this.writeConnection!.close();
145
- this.readConnections!.forEach((c) => c.close());
150
+ this.readConnections!.forEach((c) => c.connection.close());
146
151
  });
147
152
  }
148
153
 
149
154
  async readLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
150
155
  await this.initialized;
151
- // TODO: Use async queues to handle multiple read connections
152
- const sortedConnections = this.readConnections!.map((connection, index) => ({
153
- lockKey: `${LockType.READ}-${index}`,
154
- connection
155
- })).sort((a, b) => {
156
- const aBusy = this.locks.isBusy(a.lockKey);
157
- const bBusy = this.locks.isBusy(b.lockKey);
158
- // Sort by ones which are not busy
159
- return aBusy > bBusy ? 1 : 0;
156
+ return new Promise(async (resolve, reject) => {
157
+ const execute = async () => {
158
+ // Find an available connection that is not busy
159
+ const availableConnection = this.readConnections!.find((conn) => !conn.busy);
160
+
161
+ // If we have an available connection, use it
162
+ if (availableConnection) {
163
+ availableConnection.busy = true;
164
+ try {
165
+ resolve(await fn(availableConnection.connection));
166
+ } catch (error) {
167
+ reject(error);
168
+ } finally {
169
+ availableConnection.busy = false;
170
+ // After query execution, process any queued tasks
171
+ this.processQueue();
172
+ }
173
+ } else {
174
+ // If no available connections, add to the queue
175
+ this.readQueue.push(execute);
176
+ }
177
+ };
178
+
179
+ execute();
160
180
  });
181
+ }
161
182
 
162
- return new Promise(async (resolve, reject) => {
163
- try {
164
- await this.locks.acquire(
165
- sortedConnections[0].lockKey,
166
- async () => {
167
- resolve(await fn(sortedConnections[0].connection));
168
- },
169
- { timeout: options?.timeoutMs }
170
- );
171
- } catch (ex) {
172
- reject(ex);
183
+ private async processQueue(): Promise<void> {
184
+ if (this.readQueue.length > 0) {
185
+ const next = this.readQueue.shift();
186
+ if (next) {
187
+ next();
173
188
  }
174
- });
189
+ }
175
190
  }
176
191
 
177
192
  async writeLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
@@ -23,6 +23,12 @@ export interface SqliteOptions {
23
23
  * Set to null or zero to fail immediately when the database is locked.
24
24
  */
25
25
  lockTimeoutMs?: number;
26
+
27
+ /**
28
+ * Encryption key for the database.
29
+ * If set, the database will be encrypted using SQLCipher.
30
+ */
31
+ encryptionKey?: string;
26
32
  }
27
33
 
28
34
  // SQLite journal mode. Set on the primary connection.
@@ -36,14 +42,14 @@ enum SqliteJournalMode {
36
42
  truncate = 'TRUNCATE',
37
43
  persist = 'PERSIST',
38
44
  memory = 'MEMORY',
39
- off = 'OFF',
45
+ off = 'OFF'
40
46
  }
41
47
 
42
48
  // SQLite file commit mode.
43
49
  enum SqliteSynchronous {
44
50
  normal = 'NORMAL',
45
51
  full = 'FULL',
46
- off = 'OFF',
52
+ off = 'OFF'
47
53
  }
48
54
 
49
55
  export const DEFAULT_SQLITE_OPTIONS: Required<SqliteOptions> = {
@@ -51,4 +57,5 @@ export const DEFAULT_SQLITE_OPTIONS: Required<SqliteOptions> = {
51
57
  synchronous: SqliteSynchronous.normal,
52
58
  journalSizeLimit: 6 * 1024 * 1024,
53
59
  lockTimeoutMs: 30000,
60
+ encryptionKey: null
54
61
  };
@@ -1 +0,0 @@
1
- {"version":3,"file":"NativePowerSyncOpSqlite.d.ts","sourceRoot":"","sources":["../../../src/NativePowerSyncOpSqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,aAAa,IAAI,MAAM,CAAC;CACzB;;AAED,wBAA2E"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"OPSQLiteConnection.d.ts","sourceRoot":"","sources":["../../../../src/db/OPSQLiteConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAiB,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAiB,MAAM,mBAAmB,CAAC;AAEhG,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,EAAE,CAAC;CACZ,CAAC;AAEF,qBAAa,kBAAmB,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IAEzD,SAAS,CAAC,OAAO,EAAE,yBAAyB;IADxD,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC;gBACK,OAAO,EAAE,yBAAyB;IA4BxD,KAAK;IAIC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IAa5D,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,GAAG,EAAE,EAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IAUvE,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAKxD,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAKlE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;CAO1D"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"OPSqliteAdapter.d.ts","sourceRoot":"","sources":["../../../../src/db/OPSqliteAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,aAAa,EACb,WAAW,EAEX,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,IAAI,MAAM,YAAY,CAAC;AAC9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAA0B,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,CAAC;AASF,qBAAa,iBAAkB,SAAQ,YAAY,CAAC,iBAAiB,CAAE,YAAW,SAAS;IAU7E,SAAS,CAAC,OAAO,EAAE,sBAAsB;IATrD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC;IAEtB,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAErC,SAAS,CAAC,eAAe,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC;IAEvD,SAAS,CAAC,eAAe,EAAE,kBAAkB,GAAG,IAAI,CAAC;gBAE/B,OAAO,EAAE,sBAAsB;cAUrC,IAAI;cAwDJ,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAgB1G,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAUrB,KAAK;IAOC,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC;IA4B5F,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC;IAkBnG,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC;IAI5F,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC;IAI7F,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAIxD,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAIlE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;IAInD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE;IAI/B,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,GAAG,EAAE,EAAO,GAAG,OAAO,CAAC,WAAW,CAAC;cAI7D,mBAAmB,CAAC,CAAC,EACnC,UAAU,EAAE,kBAAkB,EAC9B,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC;CAiCd"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"OPSqliteDBOpenFactory.d.ts","sourceRoot":"","sources":["../../../../src/db/OPSqliteDBOpenFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9E,OAAO,EAA0B,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAExE,MAAM,WAAW,0BAA2B,SAAQ,cAAc;IAChE,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AACD,qBAAa,mBAAoB,YAAW,cAAc;IAG5C,SAAS,CAAC,OAAO,EAAE,0BAA0B;IAFzD,OAAO,CAAC,aAAa,CAA0B;gBAEzB,OAAO,EAAE,0BAA0B;IAOzD,MAAM,IAAI,SAAS;CAOpB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"SqliteOptions.d.ts","sourceRoot":"","sources":["../../../../src/db/SqliteOptions.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAEhC;;;OAGG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAEhC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAKD,aAAK,iBAAiB;IAGpB,GAAG,QAAQ;IACX,MAAM,WAAW;IACjB,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,GAAG,QAAQ;CACZ;AAGD,aAAK,iBAAiB;IACpB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,GAAG,QAAQ;CACZ;AAED,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,aAAa,CAK1D,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAyBA,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC"}
File without changes
File without changes