@meshline/expo 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +94 -0
- package/android/build.gradle +20 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/org/meshline/expo/MeshlineRelaySocketModule.kt +82 -0
- package/compatibility/apply-android-fix.mjs +46 -0
- package/compatibility/expo+57.0.25.patch +13 -0
- package/compatibility/expo-modules-core+57.0.19.patch +27 -0
- package/dist/database-path.d.ts +7 -0
- package/dist/database-path.d.ts.map +1 -0
- package/dist/database-path.js +22 -0
- package/dist/database-path.js.map +1 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +64 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/socket.d.ts +23 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/socket.js +143 -0
- package/dist/socket.js.map +1 -0
- package/dist/sqlite-store.d.ts +27 -0
- package/dist/sqlite-store.d.ts.map +1 -0
- package/dist/sqlite-store.js +246 -0
- package/dist/sqlite-store.js.map +1 -0
- package/expo-module.config.json +5 -0
- package/ios/MeshlineExpo.podspec +19 -0
- package/ios/MeshlineRelaySocketModule.swift +160 -0
- package/package.json +56 -0
- package/src/database-path.ts +17 -0
- package/src/http.ts +44 -0
- package/src/index.ts +47 -0
- package/src/socket.ts +93 -0
- package/src/sqlite-store.ts +187 -0
package/dist/socket.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { encodeUtf8, webEndpoint } from '@meshline/sdk';
|
|
2
|
+
let nextId = 0;
|
|
3
|
+
/** The native implementation owns TLS, cookie/redirect policy and actual RFC close codes. */
|
|
4
|
+
export function nativeSocketFactory(module) {
|
|
5
|
+
return endpoint => {
|
|
6
|
+
const normalized = webEndpoint(endpoint, 'wss:').href;
|
|
7
|
+
if (nextId === Number.MAX_SAFE_INTEGER)
|
|
8
|
+
throw new Error('Native socket identifiers are exhausted.');
|
|
9
|
+
const id = ++nextId;
|
|
10
|
+
let state = 0;
|
|
11
|
+
let connected = false;
|
|
12
|
+
let timer;
|
|
13
|
+
const listeners = new Map();
|
|
14
|
+
function emit(event, value) {
|
|
15
|
+
for (const listener of [...listeners.get(event) ?? []])
|
|
16
|
+
listener(value);
|
|
17
|
+
}
|
|
18
|
+
function finish(code, reason) {
|
|
19
|
+
if (state === 3)
|
|
20
|
+
return;
|
|
21
|
+
state = 3;
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
subscription.remove();
|
|
24
|
+
try {
|
|
25
|
+
emit('close', { code, reason });
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
listeners.clear();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function fail(error) {
|
|
32
|
+
if (state === 3)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
try {
|
|
39
|
+
if (connected)
|
|
40
|
+
module.cancel(id);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
finish(1006, 'Native WebSocket failed.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const subscription = module.addListener('socket', event => {
|
|
48
|
+
if (event.id !== id || state === 3)
|
|
49
|
+
return;
|
|
50
|
+
switch (event.type) {
|
|
51
|
+
case 'open':
|
|
52
|
+
if (state === 0) {
|
|
53
|
+
state = 1;
|
|
54
|
+
emit('open', undefined);
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case 'text':
|
|
58
|
+
if (state === 1)
|
|
59
|
+
emit('message', event.text);
|
|
60
|
+
break;
|
|
61
|
+
case 'binary':
|
|
62
|
+
if (state === 1)
|
|
63
|
+
emit('message', new Uint8Array());
|
|
64
|
+
break;
|
|
65
|
+
case 'error':
|
|
66
|
+
fail(new Error(event.message ?? 'Native WebSocket failed.'));
|
|
67
|
+
break;
|
|
68
|
+
case 'close':
|
|
69
|
+
finish(event.code ?? 1006, event.reason ?? '');
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// RpcConnection installs all event handlers before native callbacks can fire.
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
if (state !== 0)
|
|
76
|
+
return;
|
|
77
|
+
connected = true;
|
|
78
|
+
try {
|
|
79
|
+
module.connect(id, normalized);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
fail(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const socket = {
|
|
86
|
+
get readyState() { return state; },
|
|
87
|
+
get bufferedAmount() { return connected && state !== 3 ? module.bufferedAmount(id) : 0; },
|
|
88
|
+
send(text) {
|
|
89
|
+
if (state !== 1)
|
|
90
|
+
throw new Error('Native WebSocket is not open.');
|
|
91
|
+
// Reject malformed UTF-16 before a native bridge can replace it.
|
|
92
|
+
encodeUtf8(text);
|
|
93
|
+
module.send(id, text);
|
|
94
|
+
},
|
|
95
|
+
close(code, reason) {
|
|
96
|
+
if (!Number.isInteger(code) || !([1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014].includes(code) || code >= 3000 && code <= 4999))
|
|
97
|
+
throw new RangeError('Invalid WebSocket close code.');
|
|
98
|
+
if (encodeUtf8(reason).length > 123)
|
|
99
|
+
throw new RangeError('WebSocket close reason exceeds 123 UTF-8 bytes.');
|
|
100
|
+
if (state >= 2)
|
|
101
|
+
return code;
|
|
102
|
+
state = 2;
|
|
103
|
+
if (!connected) {
|
|
104
|
+
queueMicrotask(() => finish(code, reason));
|
|
105
|
+
return code;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
module.close(id, code, reason);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
fail(error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
if (state !== 3)
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
try {
|
|
117
|
+
module.cancel(id);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
finish(1006, 'Native WebSocket close handshake timed out.');
|
|
124
|
+
}
|
|
125
|
+
}, 3000);
|
|
126
|
+
return code;
|
|
127
|
+
},
|
|
128
|
+
on(event, listener) {
|
|
129
|
+
if (state === 3)
|
|
130
|
+
return () => { };
|
|
131
|
+
let set = listeners.get(event);
|
|
132
|
+
if (!set) {
|
|
133
|
+
set = new Set();
|
|
134
|
+
listeners.set(event, set);
|
|
135
|
+
}
|
|
136
|
+
set.add(listener);
|
|
137
|
+
return () => { set.delete(listener); };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return socket;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type MeshlineStore, type QueryReader, type RecordQuery, type StorageBinding, type StoredRecord, type StoreMutation, type StoreSnapshot } from '@meshline/sdk';
|
|
2
|
+
/** Private adapter boundary, also exercised against a real SQLite driver in contract tests. */
|
|
3
|
+
export interface SqliteDatabase {
|
|
4
|
+
execAsync(sql: string): Promise<void>;
|
|
5
|
+
runAsync(sql: string, ...parameters: (string | number | null)[]): Promise<unknown>;
|
|
6
|
+
getFirstAsync<T>(sql: string, ...parameters: (string | number | null)[]): Promise<T | null>;
|
|
7
|
+
getAllAsync<T>(sql: string, ...parameters: (string | number | null)[]): Promise<T[]>;
|
|
8
|
+
closeAsync(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface SqliteRuntime {
|
|
11
|
+
exists(): Promise<boolean>;
|
|
12
|
+
/** Every call MUST return a new, private native connection. Never use Expo's shared connection cache. */
|
|
13
|
+
open(): Promise<SqliteDatabase>;
|
|
14
|
+
}
|
|
15
|
+
/** Serialized private writer and independent WAL snapshots, with no transaction spanning application callbacks. */
|
|
16
|
+
export declare class NativeSqliteStore implements MeshlineStore {
|
|
17
|
+
#private;
|
|
18
|
+
readonly runtime: SqliteRuntime;
|
|
19
|
+
constructor(runtime: SqliteRuntime);
|
|
20
|
+
migrate(signal?: AbortSignal): Promise<void>;
|
|
21
|
+
initialize(binding: StorageBinding, signal?: AbortSignal): Promise<void>;
|
|
22
|
+
read(queries: readonly RecordQuery[], signal?: AbortSignal): Promise<StoreSnapshot>;
|
|
23
|
+
commit(expectedVersion: number, mutations: readonly StoreMutation[], signal?: AbortSignal): Promise<number>;
|
|
24
|
+
openQuery(query: RecordQuery, signal?: AbortSignal): Promise<QueryReader<StoredRecord>>;
|
|
25
|
+
dispose(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=sqlite-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../src/sqlite-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAEiD,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAC1G,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EACjF,MAAM,eAAe,CAAC;AAEvB,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC3B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5F,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACrF,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AACD,MAAM,WAAW,aAAa;IAC1B,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3B,yGAAyG;IACzG,IAAI,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;CACnC;AAWD,mHAAmH;AACnH,qBAAa,iBAAkB,YAAW,aAAa;;IAMvC,QAAQ,CAAC,OAAO,EAAE,aAAa;IAA3C,YAAqB,OAAO,EAAE,aAAa,EAAI;IAE/C,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3C;IAED,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBvE;IAED,IAAI,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAWlF;IAED,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,aAAa,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB1G;IAED,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CActF;IAID,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CASvB;CACJ"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { AsyncGate, StateConflictError, bindingJson, canonicalJson, parseJson, prepareCommit, requireBatchCount, requireObject, throwIfAborted, validateRecordQuery, } from '@meshline/sdk';
|
|
2
|
+
function decodeRow(row) { return { collection: row.collection, key: row.key, revision: row.revision, value: requireObject(parseJson(row.value)) }; }
|
|
3
|
+
function selection(query) {
|
|
4
|
+
validateRecordQuery(query);
|
|
5
|
+
let where = 'collection = ?';
|
|
6
|
+
const parameters = [query.collection];
|
|
7
|
+
if (query.key !== undefined) {
|
|
8
|
+
where += ' AND key = ?';
|
|
9
|
+
parameters.push(query.key);
|
|
10
|
+
}
|
|
11
|
+
else if (query.prefix !== undefined) {
|
|
12
|
+
where += ' AND key >= ? AND key < ?';
|
|
13
|
+
parameters.push(query.prefix, query.prefix + '\uffff');
|
|
14
|
+
}
|
|
15
|
+
return { where, parameters, order: query.reverse ? 'DESC' : 'ASC' };
|
|
16
|
+
}
|
|
17
|
+
/** Serialized private writer and independent WAL snapshots, with no transaction spanning application callbacks. */
|
|
18
|
+
export class NativeSqliteStore {
|
|
19
|
+
runtime;
|
|
20
|
+
#gate = new AsyncGate();
|
|
21
|
+
#readers = new Set();
|
|
22
|
+
#database;
|
|
23
|
+
#disposed = false;
|
|
24
|
+
#disposal;
|
|
25
|
+
constructor(runtime) {
|
|
26
|
+
this.runtime = runtime;
|
|
27
|
+
}
|
|
28
|
+
migrate(signal) {
|
|
29
|
+
this.#check();
|
|
30
|
+
return this.#gate.run(async () => {
|
|
31
|
+
this.#check();
|
|
32
|
+
throwIfAborted(signal);
|
|
33
|
+
if (this.#database)
|
|
34
|
+
throw new Error('Migrate before initializing storage.');
|
|
35
|
+
const database = await this.runtime.open();
|
|
36
|
+
try {
|
|
37
|
+
await database.execAsync('PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;');
|
|
38
|
+
await transaction(database, 'IMMEDIATE', async () => {
|
|
39
|
+
const version = (await database.getFirstAsync('PRAGMA user_version')).user_version;
|
|
40
|
+
if (version > 1)
|
|
41
|
+
throw new Error(`Unsupported database schema ${version}.`);
|
|
42
|
+
if (version === 0)
|
|
43
|
+
await database.execAsync(`
|
|
44
|
+
CREATE TABLE meshline_meta (id INTEGER PRIMARY KEY CHECK(id = 1), binding TEXT, version INTEGER NOT NULL CHECK(version >= 0));
|
|
45
|
+
INSERT INTO meshline_meta VALUES (1, NULL, 0);
|
|
46
|
+
CREATE TABLE meshline_records (collection TEXT NOT NULL, key TEXT NOT NULL, revision INTEGER NOT NULL, value TEXT NOT NULL,
|
|
47
|
+
PRIMARY KEY(collection, key)) WITHOUT ROWID;
|
|
48
|
+
PRAGMA user_version = 1;
|
|
49
|
+
`);
|
|
50
|
+
}, signal);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await database.closeAsync();
|
|
54
|
+
}
|
|
55
|
+
}, signal);
|
|
56
|
+
}
|
|
57
|
+
initialize(binding, signal) {
|
|
58
|
+
this.#check();
|
|
59
|
+
const encoded = bindingJson(binding);
|
|
60
|
+
return this.#gate.run(async () => {
|
|
61
|
+
this.#check();
|
|
62
|
+
throwIfAborted(signal);
|
|
63
|
+
if (this.#database) {
|
|
64
|
+
const existing = await this.#database.getFirstAsync('SELECT binding FROM meshline_meta WHERE id = 1');
|
|
65
|
+
if (existing?.binding !== encoded)
|
|
66
|
+
throw new Error('Database is bound to another network or account.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!await this.runtime.exists())
|
|
70
|
+
throw new Error('Database must be explicitly migrated before initialization.');
|
|
71
|
+
const database = await this.runtime.open();
|
|
72
|
+
try {
|
|
73
|
+
const version = (await database.getFirstAsync('PRAGMA user_version')).user_version;
|
|
74
|
+
if (version !== 1)
|
|
75
|
+
throw new Error('Database must be explicitly migrated before initialization.');
|
|
76
|
+
await database.execAsync('PRAGMA busy_timeout = 5000; PRAGMA synchronous = FULL;');
|
|
77
|
+
await transaction(database, 'IMMEDIATE', async () => {
|
|
78
|
+
const current = await database.getFirstAsync('SELECT binding FROM meshline_meta WHERE id = 1');
|
|
79
|
+
if (!current)
|
|
80
|
+
throw new Error('Missing Meshline database metadata.');
|
|
81
|
+
if (current.binding !== null && current.binding !== encoded)
|
|
82
|
+
throw new Error('Database is bound to another network or account.');
|
|
83
|
+
if (current.binding === null)
|
|
84
|
+
await database.runAsync('UPDATE meshline_meta SET binding = ? WHERE id = 1', encoded);
|
|
85
|
+
}, signal);
|
|
86
|
+
this.#database = database;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
await database.closeAsync();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}, signal);
|
|
93
|
+
}
|
|
94
|
+
read(queries, signal) {
|
|
95
|
+
this.#check();
|
|
96
|
+
const selected = queries.map(selection);
|
|
97
|
+
return this.#gate.run(async () => {
|
|
98
|
+
const database = this.#connection();
|
|
99
|
+
return transaction(database, 'DEFERRED', async () => {
|
|
100
|
+
const version = (await database.getFirstAsync('SELECT version FROM meshline_meta WHERE id = 1')).version;
|
|
101
|
+
const sets = [];
|
|
102
|
+
for (const query of selected)
|
|
103
|
+
sets.push((await database.getAllAsync(`SELECT * FROM meshline_records WHERE ${query.where} ORDER BY key ${query.order}`, ...query.parameters)).map(decodeRow));
|
|
104
|
+
return { version, sets };
|
|
105
|
+
}, signal);
|
|
106
|
+
}, signal);
|
|
107
|
+
}
|
|
108
|
+
commit(expectedVersion, mutations, signal) {
|
|
109
|
+
this.#check();
|
|
110
|
+
const changes = prepareCommit(expectedVersion, mutations);
|
|
111
|
+
return this.#gate.run(async () => {
|
|
112
|
+
const database = this.#connection();
|
|
113
|
+
return transaction(database, 'IMMEDIATE', async () => {
|
|
114
|
+
const version = (await database.getFirstAsync('SELECT version FROM meshline_meta WHERE id = 1')).version;
|
|
115
|
+
if (version !== expectedVersion)
|
|
116
|
+
throw new StateConflictError('Database revision changed before commit.');
|
|
117
|
+
const next = expectedVersion + 1;
|
|
118
|
+
for (const change of changes) {
|
|
119
|
+
throwIfAborted(signal);
|
|
120
|
+
if (change.kind === 'put')
|
|
121
|
+
await database.runAsync('INSERT INTO meshline_records VALUES (?, ?, ?, ?) ON CONFLICT(collection, key) DO UPDATE SET revision = excluded.revision, value = excluded.value', change.collection, change.key, next, canonicalJson(change.value));
|
|
122
|
+
else
|
|
123
|
+
await database.runAsync('DELETE FROM meshline_records WHERE collection = ? AND key = ?', change.collection, change.key);
|
|
124
|
+
}
|
|
125
|
+
await database.runAsync('UPDATE meshline_meta SET version = ? WHERE id = 1', next);
|
|
126
|
+
return next;
|
|
127
|
+
}, signal);
|
|
128
|
+
}, signal);
|
|
129
|
+
}
|
|
130
|
+
openQuery(query, signal) {
|
|
131
|
+
this.#check();
|
|
132
|
+
const selected = selection(query);
|
|
133
|
+
return this.#gate.run(async () => {
|
|
134
|
+
this.#connection();
|
|
135
|
+
throwIfAborted(signal);
|
|
136
|
+
const database = await this.runtime.open();
|
|
137
|
+
try {
|
|
138
|
+
await database.execAsync('PRAGMA busy_timeout = 5000; PRAGMA query_only = ON; BEGIN DEFERRED;');
|
|
139
|
+
// Reading a real table pins the WAL snapshot before returning to application code.
|
|
140
|
+
await database.getFirstAsync('SELECT version FROM meshline_meta WHERE id = 1');
|
|
141
|
+
throwIfAborted(signal);
|
|
142
|
+
const reader = new NativeReader(database, selected, () => this.#readers.delete(reader));
|
|
143
|
+
this.#readers.add(reader);
|
|
144
|
+
return reader;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
await database.closeAsync();
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}, signal);
|
|
151
|
+
}
|
|
152
|
+
#check() { if (this.#disposed)
|
|
153
|
+
throw new Error('Store is disposed.'); }
|
|
154
|
+
#connection() { this.#check(); if (!this.#database)
|
|
155
|
+
throw new Error('Store is not initialized.'); return this.#database; }
|
|
156
|
+
dispose() {
|
|
157
|
+
this.#disposed = true;
|
|
158
|
+
return this.#disposal ??= this.#gate.run(async () => {
|
|
159
|
+
const failures = [];
|
|
160
|
+
for (const reader of this.#readers) {
|
|
161
|
+
try {
|
|
162
|
+
await reader.dispose();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
failures.push(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await this.#database?.closeAsync();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
failures.push(error);
|
|
173
|
+
}
|
|
174
|
+
this.#database = undefined;
|
|
175
|
+
if (failures.length)
|
|
176
|
+
throw new AggregateError(failures, 'Failed to dispose native SQLite storage.');
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function transaction(database, mode, action, signal) {
|
|
181
|
+
throwIfAborted(signal);
|
|
182
|
+
await database.execAsync(`BEGIN ${mode}`);
|
|
183
|
+
try {
|
|
184
|
+
const result = await action();
|
|
185
|
+
throwIfAborted(signal);
|
|
186
|
+
await database.execAsync('COMMIT');
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
try {
|
|
191
|
+
await database.execAsync('ROLLBACK');
|
|
192
|
+
}
|
|
193
|
+
catch (rollbackError) {
|
|
194
|
+
throw new AggregateError([error, rollbackError], 'SQLite operation and rollback both failed.');
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
class NativeReader {
|
|
200
|
+
database;
|
|
201
|
+
query;
|
|
202
|
+
released;
|
|
203
|
+
#gate = new AsyncGate();
|
|
204
|
+
#offset = 0;
|
|
205
|
+
#completed = false;
|
|
206
|
+
#disposed = false;
|
|
207
|
+
#disposal;
|
|
208
|
+
constructor(database, query, released) {
|
|
209
|
+
this.database = database;
|
|
210
|
+
this.query = query;
|
|
211
|
+
this.released = released;
|
|
212
|
+
}
|
|
213
|
+
readNext(count, signal) {
|
|
214
|
+
requireBatchCount(count);
|
|
215
|
+
return this.#gate.run(async () => {
|
|
216
|
+
if (this.#disposed)
|
|
217
|
+
throw new Error('Reader is disposed.');
|
|
218
|
+
throwIfAborted(signal);
|
|
219
|
+
if (this.#completed)
|
|
220
|
+
return [];
|
|
221
|
+
const rows = await this.database.getAllAsync(`SELECT * FROM meshline_records WHERE ${this.query.where} ORDER BY key ${this.query.order} LIMIT ? OFFSET ?`, ...this.query.parameters, count, this.#offset);
|
|
222
|
+
const records = rows.map(decodeRow);
|
|
223
|
+
throwIfAborted(signal);
|
|
224
|
+
this.#offset += records.length;
|
|
225
|
+
this.#completed = records.length < count;
|
|
226
|
+
return records;
|
|
227
|
+
}, signal);
|
|
228
|
+
}
|
|
229
|
+
dispose() {
|
|
230
|
+
this.#disposed = true;
|
|
231
|
+
return this.#disposal ??= this.#gate.run(async () => {
|
|
232
|
+
try {
|
|
233
|
+
await this.database.execAsync('ROLLBACK');
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
try {
|
|
237
|
+
await this.database.closeAsync();
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
this.released();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=sqlite-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-store.js","sourceRoot":"","sources":["../src/sqlite-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
3
|
+
|
|
4
|
+
Pod::Spec.new do |s|
|
|
5
|
+
s.name = 'MeshlineExpo'
|
|
6
|
+
s.version = package['version']
|
|
7
|
+
s.summary = package['description']
|
|
8
|
+
s.description = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.author = 'Meshline contributors'
|
|
11
|
+
s.homepage = 'https://meshline.org'
|
|
12
|
+
s.source = { :git => 'https://github.com/meshline-network/sdk.git' }
|
|
13
|
+
s.platforms = { :ios => '16.4' }
|
|
14
|
+
s.swift_version = '5.9'
|
|
15
|
+
s.static_framework = true
|
|
16
|
+
s.dependency 'ExpoModulesCore'
|
|
17
|
+
s.source_files = '**/*.swift'
|
|
18
|
+
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'SWIFT_COMPILATION_MODE' => 'wholemodule' }
|
|
19
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
public final class MeshlineRelaySocketModule: Module {
|
|
5
|
+
private let lock = NSLock()
|
|
6
|
+
private var sockets: [Double: RelaySocketConnection] = [:]
|
|
7
|
+
|
|
8
|
+
public func definition() -> ModuleDefinition {
|
|
9
|
+
Name("MeshlineRelaySocket")
|
|
10
|
+
Events("socket")
|
|
11
|
+
Function("connect") { (id: Double, endpoint: String) in try self.connect(id, endpoint) }
|
|
12
|
+
Function("send") { (id: Double, text: String) in
|
|
13
|
+
guard let socket = self.socket(id) else { throw SocketError("WebSocket is not open.") }
|
|
14
|
+
try socket.send(text)
|
|
15
|
+
}
|
|
16
|
+
Function("close") { (id: Double, code: Int, reason: String) throws -> Void in
|
|
17
|
+
if let socket = self.socket(id) { try socket.close(code, reason) }
|
|
18
|
+
}
|
|
19
|
+
Function("cancel") { (id: Double) -> Void in
|
|
20
|
+
if let socket = self.remove(id) { socket.cancel() }
|
|
21
|
+
}
|
|
22
|
+
Function("bufferedAmount") { (id: Double) -> Double in Double(self.socket(id)?.bufferedAmount ?? 0) }
|
|
23
|
+
OnDestroy {
|
|
24
|
+
self.lock.lock(); let closing = Array(self.sockets.values); self.sockets.removeAll(); self.lock.unlock()
|
|
25
|
+
closing.forEach { $0.cancel() }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private func socket(_ id: Double) -> RelaySocketConnection? {
|
|
30
|
+
lock.lock(); defer { lock.unlock() }; return sockets[id]
|
|
31
|
+
}
|
|
32
|
+
private func remove(_ id: Double) -> RelaySocketConnection? {
|
|
33
|
+
lock.lock(); defer { lock.unlock() }; return sockets.removeValue(forKey: id)
|
|
34
|
+
}
|
|
35
|
+
private func connect(_ id: Double, _ endpoint: String) throws {
|
|
36
|
+
guard let url = URL(string: endpoint), url.scheme == "wss", url.host != nil,
|
|
37
|
+
url.user == nil, url.password == nil, url.query == nil, url.fragment == nil else {
|
|
38
|
+
throw SocketError("Expected a credential-free WSS endpoint.")
|
|
39
|
+
}
|
|
40
|
+
let connection = RelaySocketConnection(url: url) { [weak self] type, fields in
|
|
41
|
+
guard let self, self.socket(id) != nil else { return }
|
|
42
|
+
self.sendEvent("socket", fields.merging(["id": id, "type": type]) { _, value in value })
|
|
43
|
+
if type == "close" { _ = self.remove(id) }
|
|
44
|
+
}
|
|
45
|
+
lock.lock()
|
|
46
|
+
guard sockets[id] == nil else { lock.unlock(); throw SocketError("Duplicate native WebSocket identifier.") }
|
|
47
|
+
sockets[id] = connection; lock.unlock()
|
|
48
|
+
connection.start()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private struct SocketError: LocalizedError {
|
|
53
|
+
let errorDescription: String?
|
|
54
|
+
init(_ message: String) { errorDescription = message }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private final class RelaySocketConnection: NSObject, URLSessionWebSocketDelegate, @unchecked Sendable {
|
|
58
|
+
private let lock = NSRecursiveLock()
|
|
59
|
+
private let url: URL
|
|
60
|
+
private let event: (String, [String: Any]) -> Void
|
|
61
|
+
private var session: URLSession?
|
|
62
|
+
private var task: URLSessionWebSocketTask?
|
|
63
|
+
private var ended = false
|
|
64
|
+
private var queued = 0
|
|
65
|
+
|
|
66
|
+
init(url: URL, event: @escaping (String, [String: Any]) -> Void) { self.url = url; self.event = event }
|
|
67
|
+
var bufferedAmount: Int { lock.lock(); defer { lock.unlock() }; return queued }
|
|
68
|
+
|
|
69
|
+
func start() {
|
|
70
|
+
lock.lock(); defer { lock.unlock() }
|
|
71
|
+
guard !ended else { return }
|
|
72
|
+
// An ephemeral, dedicated session does not share the app's cookies, credentials or cache.
|
|
73
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
74
|
+
configuration.httpCookieStorage = nil
|
|
75
|
+
configuration.httpShouldSetCookies = false
|
|
76
|
+
configuration.httpCookieAcceptPolicy = .never
|
|
77
|
+
configuration.urlCredentialStorage = nil
|
|
78
|
+
configuration.urlCache = nil
|
|
79
|
+
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
80
|
+
configuration.timeoutIntervalForRequest = 60
|
|
81
|
+
let queue = OperationQueue(); queue.maxConcurrentOperationCount = 1
|
|
82
|
+
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: queue)
|
|
83
|
+
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60)
|
|
84
|
+
request.httpShouldHandleCookies = false
|
|
85
|
+
let task = session.webSocketTask(with: request)
|
|
86
|
+
// The RPC layer enforces 1 MiB UTF-8 text; this also bounds native receive buffering.
|
|
87
|
+
task.maximumMessageSize = 1_048_576
|
|
88
|
+
self.session = session; self.task = task; task.resume()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func send(_ text: String) throws {
|
|
92
|
+
lock.lock(); defer { lock.unlock() }
|
|
93
|
+
guard !ended, let task else { throw SocketError("WebSocket is not open.") }
|
|
94
|
+
let size = text.utf8.count; queued += size
|
|
95
|
+
task.send(.string(text)) { [weak self] error in
|
|
96
|
+
guard let self else { return }
|
|
97
|
+
self.lock.lock(); self.queued = max(0, self.queued - size); self.lock.unlock()
|
|
98
|
+
if let error { self.fail(error) }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
func close(_ code: Int, _ reason: String) throws {
|
|
102
|
+
guard let closeCode = URLSessionWebSocketTask.CloseCode(rawValue: code) else { throw SocketError("Invalid WebSocket close code.") }
|
|
103
|
+
lock.lock(); defer { lock.unlock() }
|
|
104
|
+
guard !ended else { return }
|
|
105
|
+
task?.cancel(with: closeCode, reason: Data(reason.utf8))
|
|
106
|
+
}
|
|
107
|
+
func cancel() { finish(1006, "Native WebSocket canceled.") }
|
|
108
|
+
|
|
109
|
+
private func receive() {
|
|
110
|
+
lock.lock(); defer { lock.unlock() }
|
|
111
|
+
guard !ended, let task else { return }
|
|
112
|
+
task.receive { [weak self] result in
|
|
113
|
+
guard let self else { return }
|
|
114
|
+
self.lock.lock(); let live = !self.ended; self.lock.unlock()
|
|
115
|
+
guard live else { return }
|
|
116
|
+
switch result {
|
|
117
|
+
case .success(let message):
|
|
118
|
+
switch message {
|
|
119
|
+
case .string(let text): self.event("text", ["text": text])
|
|
120
|
+
case .data: self.event("binary", [:])
|
|
121
|
+
@unknown default: self.fail(SocketError("Unknown native WebSocket message kind.")); return
|
|
122
|
+
}
|
|
123
|
+
self.receive()
|
|
124
|
+
case .failure(let error): self.fail(error)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
private func fail(_ error: Error) {
|
|
129
|
+
lock.lock(); defer { lock.unlock() }
|
|
130
|
+
guard !ended else { return }
|
|
131
|
+
event("error", ["message": error.localizedDescription]); finish(1006, "Native WebSocket failed.")
|
|
132
|
+
}
|
|
133
|
+
private func finish(_ code: Int, _ reason: String) {
|
|
134
|
+
lock.lock(); defer { lock.unlock() }
|
|
135
|
+
guard !ended else { return }
|
|
136
|
+
ended = true; queued = 0
|
|
137
|
+
task?.cancel(); session?.invalidateAndCancel(); task = nil; session = nil
|
|
138
|
+
event("close", ["code": code, "reason": reason])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
|
|
142
|
+
lock.lock(); let live = !ended; lock.unlock()
|
|
143
|
+
if live { event("open", [:]); receive() }
|
|
144
|
+
}
|
|
145
|
+
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
|
146
|
+
finish(closeCode.rawValue, reason.flatMap { String(data: $0, encoding: .utf8) } ?? "")
|
|
147
|
+
}
|
|
148
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
149
|
+
if let error { fail(error) } else { finish(1006, "WebSocket ended without a close frame.") }
|
|
150
|
+
}
|
|
151
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
|
|
152
|
+
completionHandler(nil)
|
|
153
|
+
}
|
|
154
|
+
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
155
|
+
completionHandler(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust ? .performDefaultHandling : .cancelAuthenticationChallenge, nil)
|
|
156
|
+
}
|
|
157
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
158
|
+
completionHandler(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust ? .performDefaultHandling : .cancelAuthenticationChallenge, nil)
|
|
159
|
+
}
|
|
160
|
+
}
|