@basictech/react 0.2.0-beta.2 → 0.2.0-beta.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basictech/react",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.4",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,9 +17,13 @@
17
17
  "author": "",
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
- "@repo/sync": "0.1.0-beta.0",
20
+ "ajv": "^8.17.1",
21
+ "dexie": "^4.0.8",
22
+ "dexie-observable": "^4.0.1-beta.13",
21
23
  "dexie-react-hooks": "^1.1.7",
22
- "jwt-decode": "^4.0.0"
24
+ "dexie-syncable": "^4.0.1-beta.13",
25
+ "jwt-decode": "^4.0.0",
26
+ "uuid": "^10.0.0"
23
27
  },
24
28
  "devDependencies": {
25
29
  "@repo/typescript-config": "*",
package/readme.md CHANGED
@@ -17,9 +17,31 @@ In your root component or App.tsx, wrap your application with the `BasicProvider
17
17
  ```typescript
18
18
  import { BasicProvider } from '@basictech/react';
19
19
 
20
+ const schema = {
21
+ tables: {
22
+ todos: {
23
+ fields: {
24
+ id: {
25
+ type: "string",
26
+ primary: true
27
+ },
28
+ title: {
29
+ type: "string",
30
+ indexed: true
31
+ },
32
+ completed: {
33
+ type: "boolean",
34
+ indexed: true
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+
20
42
  function App() {
21
43
  return (
22
- <BasicProvider project_id="YOUR_PROJECT_ID">
44
+ <BasicProvider project_id="YOUR_PROJECT_ID" schema={schema}>
23
45
  {/* Your app components */}
24
46
  </BasicProvider>
25
47
  );
@@ -3,23 +3,61 @@
3
3
  import React, { createContext, useContext, useEffect, useState, useRef } from 'react'
4
4
  import { jwtDecode } from 'jwt-decode'
5
5
 
6
+ import { BasicSync } from './sync'
7
+ import { get, add, update, deleteRecord } from './db'
6
8
 
9
+ import { validator, log } from './config'
10
+
11
+ /*
12
+ schema todo:
13
+ field types
14
+ array types
15
+ relations
16
+ */
17
+
18
+
19
+ const example = {
20
+ project_id: '123',
21
+ version: 0,
22
+ tables: {
23
+ example: {
24
+ name: 'example',
25
+ type: 'collection',
26
+ fields: {
27
+ id: {
28
+ type: 'uuid',
29
+ primary: true,
30
+ },
31
+ value: {
32
+ type: 'string',
33
+ indexed: true,
34
+ },
35
+ }
36
+ },
37
+ example2: {
38
+ name: 'example2',
39
+ type: 'collection',
40
+ fields: {
41
+ id: { type: 'string', primary: true },
42
+ id: { type: 'string', primary: true },
43
+ }
44
+ }
45
+ }
46
+ }
7
47
 
8
- import { BasicSync } from '@repo/sync'
9
- import { get, add, update, deleteRecord } from './db'
10
48
 
11
49
  type BasicSyncType = {
12
50
  basic_schema: any;
13
51
  connect: (options: { access_token: string }) => void;
14
52
  debugeroo: () => void;
15
53
  collection: (name: string) => {
16
- ref: {
17
- toArray: () => Promise<any[]>;
18
- count: () => Promise<number>;
19
- };
54
+ ref: {
55
+ toArray: () => Promise<any[]>;
56
+ count: () => Promise<number>;
57
+ };
20
58
  };
21
59
  [key: string]: any; // For other potential methods and properties
22
- };
60
+ };
23
61
 
24
62
 
25
63
  enum DBStatus {
@@ -56,7 +94,7 @@ export const BasicContext = createContext<{
56
94
  signin: () => void,
57
95
  getToken: () => Promise<string>,
58
96
  getSignInLink: () => string,
59
- db: any,
97
+ db: any,
60
98
  dbStatus: DBStatus
61
99
  }>({
62
100
  unicorn: "🦄",
@@ -103,6 +141,12 @@ function getSyncStatus(statusCode: number): string {
103
141
  }
104
142
  }
105
143
 
144
+ type ErrorObject = {
145
+ code: string;
146
+ title: string;
147
+ message: string;
148
+ }
149
+
106
150
  export function BasicProvider({ children, project_id, schema }: { children: React.ReactNode, project_id: string, schema: any }) {
107
151
  const [isLoaded, setIsLoaded] = useState(false)
108
152
  const [isSignedIn, setIsSignedIn] = useState(false)
@@ -110,30 +154,55 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
110
154
  const [authCode, setAuthCode] = useState<string | null>(null)
111
155
  const [user, setUser] = useState<User>({})
112
156
 
113
- const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
114
-
157
+ const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.LOADING)
115
158
 
116
159
  const syncRef = useRef<BasicSync | null>(null);
117
160
 
161
+ const [error, setError] = useState<ErrorObject | null>(null)
162
+
118
163
  useEffect(() => {
119
- if (!syncRef.current) {
120
- syncRef.current = new BasicSync('basicdb', { schema: schema });
164
+ function initDb() {
165
+ // console.log('S', validator(example))
166
+ if (!validator(example)) {
167
+ console.error('Basic Schema is invalid!', validator.errors)
168
+ console.group('Schema Errors')
169
+ let errorMessage = ''
170
+ validator.errors.forEach((error, index) => {
171
+ console.log('error', error)
172
+ console.log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`)
173
+ errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
174
+ })
175
+ console.groupEnd('Schema Errors')
176
+ setError({
177
+ code: 'schema_invalid',
178
+ title: 'Basic Schema is invalid!',
179
+ message: errorMessage
180
+ })
181
+ return null
182
+ }
121
183
 
122
- // console.log('db is open', syncRef.current.isOpen())
123
- // syncRef.current.open()
124
- // .then(() => {
125
- // console.log("is open now:", syncRef.current.isOpen())
126
- // })
127
184
 
128
- syncRef.current.handleStatusChange((status: number, url: string) => {
129
- setDbStatus(getSyncStatus(status))
130
- })
185
+ if (!syncRef.current) {
186
+ syncRef.current = new BasicSync('basicdb', { schema: schema });
131
187
 
132
- syncRef.current.syncable.getStatus().then((status) => {
133
- console.log('sync status', getSyncStatus(status))
134
- })
188
+ // console.log('db is open', syncRef.current.isOpen())
189
+ // syncRef.current.open()
190
+ // .then(() => {
191
+ // console.log("is open now:", syncRef.current.isOpen())
192
+ // })
193
+
194
+ syncRef.current.handleStatusChange((status: number, url: string) => {
195
+ setDbStatus(getSyncStatus(status))
196
+ })
197
+
198
+ syncRef.current.syncable.getStatus().then((status) => {
199
+ console.log('sync status', getSyncStatus(status))
200
+ })
201
+ }
135
202
 
136
203
  }
204
+
205
+ initDb()
137
206
  }, []);
138
207
 
139
208
 
@@ -153,7 +222,7 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
153
222
  }
154
223
 
155
224
  useEffect(() => {
156
- if (token) {
225
+ if (token && syncRef.current) {
157
226
  connectToDb()
158
227
  }
159
228
  }, [token])
@@ -163,8 +232,7 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
163
232
 
164
233
  const randomState = Math.random().toString(36).substring(7);
165
234
 
166
- // let baseUrl = "https://api.basic.tech/auth/authorize"
167
- let baseUrl = "http://localhost:3003/auth/authorize"
235
+ let baseUrl = "https://api.basic.tech/auth/authorize"
168
236
  baseUrl += `?client_id=${project_id}`
169
237
  baseUrl += `&redirect_uri=${encodeURIComponent(window.location.href)}`
170
238
  baseUrl += `&response_type=code`
@@ -366,11 +434,39 @@ export function BasicProvider({ children, project_id, schema }: { children: Reac
366
434
  db: syncRef.current,
367
435
  dbStatus
368
436
  }}>
437
+ {error && <ErrorDisplay error={error} />}
369
438
  {syncRef.current ? children : null}
370
439
  </BasicContext.Provider>
371
440
  )
372
441
  }
373
442
 
443
+ function ErrorDisplay({ error }: { error: ErrorObject }) {
444
+ return <div style={{
445
+ position: 'absolute',
446
+ top: 20,
447
+ left: 20,
448
+ color: 'black',
449
+ backgroundColor: '#f8d7da',
450
+ border: '1px solid #f5c6cb',
451
+ borderRadius: '4px',
452
+ padding: '20px',
453
+ maxWidth: '400px',
454
+ margin: '20px auto',
455
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
456
+ fontFamily: 'monospace',
457
+ }}>
458
+ <h3 style={{fontSize: '0.8rem', opacity: 0.8}}>code: {error.code}</h3>
459
+ <h1 style={{fontSize: '1.2rem', lineHeight: '1.5'}}>{error.title}</h1>
460
+ <p>{error.message}</p>
461
+ </div>
462
+ }
463
+
464
+ /*
465
+ possible errors:
466
+ - projectid missing / invalid
467
+ - schema missing / invalid
468
+ */
469
+
374
470
  export function useBasic() {
375
471
  return useContext(BasicContext);
376
472
  }
package/src/config.ts ADDED
@@ -0,0 +1,73 @@
1
+ import Ajv from 'ajv'
2
+
3
+ export const SERVER_URL = "https://api.basic.tech"
4
+ // export const WS_URL = `${SERVER_URL}/ws`
5
+
6
+ export const log = (...args: any[]) => {
7
+ if (process.env.NODE_ENV === 'development') {
8
+ console.log(...args)
9
+ }
10
+ }
11
+
12
+
13
+ const basicJsonSchema = {
14
+ "$schema": "http://json-schema.org/draft-07/schema#",
15
+ "type": "object",
16
+ "properties": {
17
+ "project_id": {
18
+ "type": "string"
19
+ },
20
+ "namespace": {
21
+ "type": "string",
22
+ },
23
+ "version": {
24
+ "type": "integer",
25
+ "minimum": 0
26
+ },
27
+ "tables": {
28
+ "type": "object",
29
+ "patternProperties": {
30
+ "^[a-zA-Z0-9_]+$": {
31
+ "type": "object",
32
+ "properties": {
33
+ "name": {
34
+ "type": "string"
35
+ },
36
+ "type": {
37
+ "type": "string",
38
+ "enum": ["collection"]
39
+ },
40
+ "fields": {
41
+ "type": "object",
42
+ "patternProperties": {
43
+ "^[a-zA-Z0-9_]+$": {
44
+ "type": "object",
45
+ "properties": {
46
+ "type": {
47
+ "type": "string"
48
+ },
49
+ "primary": {
50
+ "type": "boolean"
51
+ },
52
+ "indexed": {
53
+ "type": "boolean"
54
+ }
55
+ },
56
+ "required": ["type"]
57
+ }
58
+ },
59
+ "additionalProperties": true
60
+ }
61
+ },
62
+ "required": ["fields"]
63
+ }
64
+ },
65
+ "additionalProperties": true
66
+ }
67
+ },
68
+ "required": ["project_id", "version", "tables"]
69
+ }
70
+
71
+
72
+ const ajv = new Ajv()
73
+ export const validator = ajv.compile(basicJsonSchema)
@@ -0,0 +1,195 @@
1
+ "use client"
2
+
3
+ import { v7 as uuidv7 } from 'uuid';
4
+ import { Dexie, PromiseExtended } from 'dexie';
5
+ // if (typeof window !== 'undefined') {
6
+ // import('dexie-observable');
7
+ // }
8
+ import 'dexie-syncable';
9
+ import 'dexie-observable';
10
+
11
+ import { syncProtocol } from './syncProtocol'
12
+ import { SERVER_URL } from '../config'
13
+ syncProtocol()
14
+
15
+
16
+ // const DexieSyncStatus = {
17
+ // "-1": "ERROR",
18
+ // "0": "OFFLINE",
19
+ // "1": "CONNECTING",
20
+ // "2": "ONLINE",
21
+ // "3": "SYNCING",
22
+ // "4": "ERROR_WILL_RETRY"
23
+ // }
24
+
25
+
26
+
27
+
28
+
29
+ export class BasicSync extends Dexie {
30
+ basic_schema: any
31
+
32
+ constructor(name: string, options: any) {
33
+ super(name, options);
34
+
35
+ // --- INIT SCHEMA --- //
36
+
37
+ //todo: handle versions?
38
+
39
+ // TODO: validate schema
40
+ this.basic_schema = options.schema
41
+ this.version(1).stores(this._convertSchemaToDxSchema(this.basic_schema))
42
+
43
+ this.version(2).stores({})
44
+ // this.verssion
45
+
46
+
47
+ // create an alias for toArray
48
+ // @ts-ignore
49
+ this.Collection.prototype.get = this.Collection.prototype.toArray
50
+
51
+
52
+ // --- SYNC --- //
53
+
54
+ // this.syncable.on("statusChanged", (status, url) => {
55
+ // console.log("statusChanged", status, url)
56
+ // })
57
+
58
+ }
59
+
60
+ async connect({ access_token }: { access_token: string }) {
61
+ // const WS_URL = "ws://localhost:3003/ws"
62
+ const WS_URL = `${SERVER_URL}/ws`
63
+
64
+
65
+ // Update sync nodes
66
+ await this.updateSyncNodes();
67
+
68
+ // Proceed with the WebSocket connection
69
+
70
+ console.log('Starting connection...')
71
+ return this.syncable.connect("websocket", WS_URL, { authToken: access_token });
72
+ }
73
+
74
+ private async updateSyncNodes() {
75
+ try {
76
+ const syncNodes = await this.table('_syncNodes').toArray();
77
+ const localSyncNodes = syncNodes.filter(node => node.type === 'local');
78
+ console.log('Local sync nodes:', localSyncNodes);
79
+
80
+ if (localSyncNodes.length > 1) {
81
+
82
+
83
+ const largestNodeId = Math.max(...localSyncNodes.map(node => node.id));
84
+ // Check if the largest node is already the master
85
+ const largestNode = localSyncNodes.find(node => node.id === largestNodeId);
86
+ if (largestNode && largestNode.isMaster === 1) {
87
+ console.log('Largest node is already the master. No changes needed.');
88
+ return; // Exit the function early as no changes are needed
89
+ }
90
+
91
+
92
+ console.log('Largest node id:', largestNodeId);
93
+ console.error('HEISENBUG: More than one local sync node found.')
94
+
95
+ for (const node of localSyncNodes) {
96
+ console.log(`Local sync node keys:`, node.id, node.isMaster);
97
+ await this.table('_syncNodes').update(node.id, { isMaster: node.id === largestNodeId ? 1 : 0 });
98
+
99
+ console.log(`HEISENBUG: Setting ${node.id} to ${node.id === largestNodeId ? 'master' : '0'}`);
100
+ }
101
+
102
+ // Add a 1 second delay before returning // i dont think this helps?
103
+ await new Promise(resolve => setTimeout(resolve, 2000));
104
+
105
+ }
106
+
107
+ console.log('Sync nodes updated');
108
+ } catch (error) {
109
+ console.error('Error updating _syncNodes table:', error);
110
+ }
111
+ }
112
+
113
+ handleStatusChange(fn: any) {
114
+ this.syncable.on("statusChanged", fn)
115
+ }
116
+
117
+
118
+ _convertSchemaToDxSchema(schema: any) {
119
+ const stores = Object.entries(schema.tables).map(([key, table]: any) => {
120
+
121
+ const indexedFields = Object.entries(table.fields).filter(([key, field]: any) => field.indexed).map(([key, field]: any) => `,${key}`).join('')
122
+ return {
123
+ [key]: 'id' + indexedFields
124
+ }
125
+ })
126
+
127
+ return Object.assign({}, ...stores)
128
+ }
129
+
130
+ debugeroo() {
131
+ // console.log("debugeroo", this.syncable)
132
+
133
+ // this.syncable.list().then(x => console.log(x))
134
+
135
+ // this.syncable
136
+ return this.syncable
137
+ }
138
+
139
+
140
+ collection(name: string) {
141
+ // TODO: check against schema
142
+
143
+ return {
144
+
145
+ /**
146
+ * Returns the underlying Dexie table
147
+ * @type {Dexie.Table}
148
+ */
149
+ ref: this.table(name),
150
+
151
+ // --- WRITE ---- //
152
+ add: (data: any) => {
153
+ console.log("Adding data to", name, data)
154
+ return this.table(name).add({
155
+ id: uuidv7(),
156
+ ...data
157
+ })
158
+ },
159
+
160
+ put: (data: any) => {
161
+ return this.table(name).put({
162
+ id: uuidv7(),
163
+ ...data
164
+ })
165
+ },
166
+
167
+ update: (id: string, data: any) => {
168
+ return this.table(name).update(id, data)
169
+ },
170
+
171
+ delete: (id: string) => {
172
+ return this.table(name).delete(id)
173
+ },
174
+
175
+
176
+ // --- READ ---- //
177
+
178
+ get: (id: string) => {
179
+ return this.table(name).get(id)
180
+ },
181
+
182
+ getAll: () => {
183
+ return this.table(name).toArray()
184
+ },
185
+
186
+ // --- QUERY ---- //
187
+ // TODO: lots to do here. simplifing creating querie, filtering/ordering/limit, and execute
188
+
189
+ query: () => this.table(name),
190
+
191
+ filter: (fn: any) => this.table(name).filter(fn).toArray(),
192
+
193
+ }
194
+ }
195
+ }