@flowerforce/flowerbase-client 0.3.0 → 0.3.1-beta.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/dist/app.d.ts +4 -2
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +58 -32
- package/dist/mongo.d.ts +2 -2
- package/dist/mongo.d.ts.map +1 -1
- package/dist/session.d.ts +9 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +66 -62
- package/dist/storage.d.ts +11 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +44 -0
- package/dist/storage.native.d.ts +11 -0
- package/dist/storage.native.d.ts.map +1 -0
- package/dist/storage.native.js +42 -0
- package/dist/types.d.ts +23 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/user.d.ts +2 -2
- package/dist/user.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/functions.test.ts +1 -1
- package/src/__tests__/mongo.test.ts +4 -4
- package/src/app.ts +139 -68
- package/src/mongo.ts +2 -2
- package/src/session.ts +63 -54
- package/src/storage.native.ts +49 -0
- package/src/storage.ts +57 -0
- package/src/types.ts +31 -5
- package/src/user.ts +2 -2
- package/dist/session.native.d.ts +0 -14
- package/dist/session.native.d.ts.map +0 -1
- package/dist/session.native.js +0 -76
- package/src/session.native.ts +0 -89
|
@@ -39,14 +39,14 @@ describe('flowerbase-client mongo service wrapper', () => {
|
|
|
39
39
|
|
|
40
40
|
expect((global.fetch as jest.Mock).mock.calls).toHaveLength(8)
|
|
41
41
|
const [url, request] = (global.fetch as jest.Mock).mock.calls[3]
|
|
42
|
-
expect(url).toBe('http://localhost:3000/api/client/v2.0/app/my-app/functions/call')
|
|
42
|
+
expect(url).toBe('http://localhost:3000/api/client/v2.0/app/my-app/functions/call?col=todos-findOne')
|
|
43
43
|
expect(request.method).toBe('POST')
|
|
44
44
|
const parsed = JSON.parse(request.body)
|
|
45
45
|
expect(parsed.service).toBe('mongodb-atlas')
|
|
46
46
|
expect(parsed.name).toBe('findOne')
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
it('supports extended CRUD operations
|
|
49
|
+
it('supports extended CRUD operations on mongodb-atlas', async () => {
|
|
50
50
|
global.fetch = jest
|
|
51
51
|
.fn()
|
|
52
52
|
.mockResolvedValueOnce({
|
|
@@ -65,7 +65,7 @@ describe('flowerbase-client mongo service wrapper', () => {
|
|
|
65
65
|
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
66
66
|
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
67
67
|
|
|
68
|
-
const collection = app.currentUser!.mongoClient('
|
|
68
|
+
const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
|
|
69
69
|
|
|
70
70
|
await collection.findOneAndUpdate({ done: false }, { $set: { done: true } })
|
|
71
71
|
await collection.findOneAndReplace({ done: true }, { done: true, title: 'done' })
|
|
@@ -77,7 +77,7 @@ describe('flowerbase-client mongo service wrapper', () => {
|
|
|
77
77
|
|
|
78
78
|
const calls = (global.fetch as jest.Mock).mock.calls
|
|
79
79
|
const lastBody = JSON.parse(calls[calls.length - 1][1].body)
|
|
80
|
-
expect(lastBody.service).toBe('
|
|
80
|
+
expect(lastBody.service).toBe('mongodb-atlas')
|
|
81
81
|
expect(lastBody.name).toBe('deleteMany')
|
|
82
82
|
})
|
|
83
83
|
})
|
package/src/app.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { Credentials } from './credentials'
|
|
1
2
|
import { normalizeFunctionResponse } from './functions'
|
|
2
3
|
import { FlowerbaseHttpError, requestJson, requestStream } from './http'
|
|
3
4
|
import { SessionManager } from './session'
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import {
|
|
6
|
+
AppConfig,
|
|
7
|
+
CredentialsLike,
|
|
8
|
+
FunctionCallPayload,
|
|
9
|
+
MongoDbServiceArguments,
|
|
10
|
+
MongoDbServiceName,
|
|
11
|
+
ProfileData,
|
|
12
|
+
SessionData
|
|
13
|
+
} from './types'
|
|
6
14
|
import { User } from './user'
|
|
7
15
|
|
|
8
16
|
const API_PREFIX = '/api/client/v2.0'
|
|
@@ -43,7 +51,11 @@ export class App {
|
|
|
43
51
|
passwordOrArg?: string,
|
|
44
52
|
...args: unknown[]
|
|
45
53
|
) => Promise<unknown>
|
|
46
|
-
resetPassword: (input: {
|
|
54
|
+
resetPassword: (input: {
|
|
55
|
+
token: string
|
|
56
|
+
tokenId: string
|
|
57
|
+
password: string
|
|
58
|
+
}) => Promise<unknown>
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
constructor(idOrConfig: string | AppConfig) {
|
|
@@ -52,29 +64,8 @@ export class App {
|
|
|
52
64
|
this.baseUrl = (config.baseUrl ?? '').replace(/\/$/, '')
|
|
53
65
|
this.timeout = config.timeout ?? 10000
|
|
54
66
|
this.sessionManager = new SessionManager(this.id)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.sessionsByUserId.set(userId, session)
|
|
58
|
-
}
|
|
59
|
-
this.usersOrder = this.sessionManager.getUsersOrder()
|
|
60
|
-
for (const userId of this.sessionsByUserId.keys()) {
|
|
61
|
-
if (!this.usersOrder.includes(userId)) {
|
|
62
|
-
this.usersOrder.push(userId)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
for (const userId of this.usersOrder) {
|
|
66
|
-
this.getOrCreateUser(userId)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const currentSession = this.sessionManager.get()
|
|
70
|
-
if (currentSession?.userId) {
|
|
71
|
-
this.sessionsByUserId.set(currentSession.userId, currentSession)
|
|
72
|
-
this.getOrCreateUser(currentSession.userId)
|
|
73
|
-
this.touchUser(currentSession.userId)
|
|
74
|
-
this.persistSessionsByUser()
|
|
75
|
-
} else {
|
|
76
|
-
this.setCurrentSessionFromOrder()
|
|
77
|
-
}
|
|
67
|
+
this.restorePersistedUsers()
|
|
68
|
+
this.restoreCurrentSession()
|
|
78
69
|
this.sessionBootstrapPromise = this.bootstrapSessionOnLoad()
|
|
79
70
|
|
|
80
71
|
this.emailPasswordAuth = {
|
|
@@ -144,19 +135,62 @@ export class App {
|
|
|
144
135
|
}
|
|
145
136
|
|
|
146
137
|
const users = Object.fromEntries(
|
|
147
|
-
[...activeUsers, ...loggedOutUsers].map((userId) => [
|
|
138
|
+
[...activeUsers, ...loggedOutUsers].map((userId) => [
|
|
139
|
+
userId,
|
|
140
|
+
this.usersById.get(userId)!
|
|
141
|
+
])
|
|
148
142
|
)
|
|
149
143
|
return users
|
|
150
144
|
}
|
|
151
145
|
|
|
152
146
|
private persistSessionsByUser() {
|
|
153
|
-
this.sessionManager.setSessionsByUser(
|
|
147
|
+
this.sessionManager.setSessionsByUser(
|
|
148
|
+
Object.fromEntries(this.sessionsByUserId.entries())
|
|
149
|
+
)
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
private persistUsersOrder() {
|
|
157
153
|
this.sessionManager.setUsersOrder(this.usersOrder)
|
|
158
154
|
}
|
|
159
155
|
|
|
156
|
+
private restorePersistedUsers() {
|
|
157
|
+
const persistedSessionsByUser = this.sessionManager.getSessionsByUser()
|
|
158
|
+
for (const [userId, session] of Object.entries(persistedSessionsByUser)) {
|
|
159
|
+
this.sessionsByUserId.set(userId, session)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const persistedOrder = this.sessionManager.getUsersOrder()
|
|
163
|
+
const nextUsersOrder: string[] = []
|
|
164
|
+
for (const userId of persistedOrder) {
|
|
165
|
+
if (!nextUsersOrder.includes(userId)) {
|
|
166
|
+
nextUsersOrder.push(userId)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const userId of this.sessionsByUserId.keys()) {
|
|
170
|
+
if (!nextUsersOrder.includes(userId)) {
|
|
171
|
+
nextUsersOrder.push(userId)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.usersOrder = nextUsersOrder
|
|
175
|
+
|
|
176
|
+
for (const userId of this.usersOrder) {
|
|
177
|
+
this.getOrCreateUser(userId)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private restoreCurrentSession() {
|
|
182
|
+
const currentSession = this.sessionManager.get()
|
|
183
|
+
if (currentSession?.userId) {
|
|
184
|
+
this.sessionsByUserId.set(currentSession.userId, currentSession)
|
|
185
|
+
this.getOrCreateUser(currentSession.userId)
|
|
186
|
+
this.touchUser(currentSession.userId)
|
|
187
|
+
this.persistSessionsByUser()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.setCurrentSessionFromOrder()
|
|
192
|
+
}
|
|
193
|
+
|
|
160
194
|
private touchUser(userId: string) {
|
|
161
195
|
this.usersOrder = [userId, ...this.usersOrder.filter((id) => id !== userId)]
|
|
162
196
|
this.persistUsersOrder()
|
|
@@ -225,8 +259,12 @@ export class App {
|
|
|
225
259
|
}
|
|
226
260
|
|
|
227
261
|
private async bootstrapSessionOnLoad(): Promise<void> {
|
|
262
|
+
await this.sessionManager.whenReady()
|
|
263
|
+
this.restorePersistedUsers()
|
|
264
|
+
this.restoreCurrentSession()
|
|
265
|
+
|
|
228
266
|
const session = this.sessionManager.get()
|
|
229
|
-
if (!session ||
|
|
267
|
+
if (!session || !this.sessionManager.hasPersistentStorage()) {
|
|
230
268
|
return
|
|
231
269
|
}
|
|
232
270
|
|
|
@@ -293,17 +331,24 @@ export class App {
|
|
|
293
331
|
}
|
|
294
332
|
|
|
295
333
|
if (credentials.provider === 'custom-function') {
|
|
296
|
-
const result = await this.postProvider<LoginResponse>(
|
|
334
|
+
const result = await this.postProvider<LoginResponse>(
|
|
335
|
+
'/custom-function/login',
|
|
336
|
+
credentials.payload
|
|
337
|
+
)
|
|
297
338
|
return this.setLoggedInUser(result, 'custom-function')
|
|
298
339
|
}
|
|
299
340
|
|
|
300
341
|
if (credentials.provider === 'custom-token') {
|
|
301
|
-
const result = await this.postProvider<LoginResponse>('/custom-token/login', {
|
|
342
|
+
const result = await this.postProvider<LoginResponse>('/custom-token/login', {
|
|
343
|
+
token: credentials.token
|
|
344
|
+
})
|
|
302
345
|
return this.setLoggedInUser(result, 'custom-token')
|
|
303
346
|
}
|
|
304
347
|
|
|
305
348
|
const unsupportedProvider: never = credentials
|
|
306
|
-
throw new Error(
|
|
349
|
+
throw new Error(
|
|
350
|
+
`Unsupported credentials provider: ${JSON.stringify(unsupportedProvider)}`
|
|
351
|
+
)
|
|
307
352
|
}
|
|
308
353
|
|
|
309
354
|
switchUser(nextUser: User) {
|
|
@@ -336,13 +381,14 @@ export class App {
|
|
|
336
381
|
}
|
|
337
382
|
|
|
338
383
|
async deleteUser(user: User) {
|
|
339
|
-
await this.requestWithAccessToken(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
384
|
+
await this.requestWithAccessToken(
|
|
385
|
+
(accessToken) =>
|
|
386
|
+
requestJson({
|
|
387
|
+
url: this.authUrl('/delete'),
|
|
388
|
+
method: 'DELETE',
|
|
389
|
+
bearerToken: accessToken,
|
|
390
|
+
timeout: this.timeout
|
|
391
|
+
}),
|
|
346
392
|
user.id
|
|
347
393
|
)
|
|
348
394
|
await this.removeUser(user)
|
|
@@ -350,7 +396,9 @@ export class App {
|
|
|
350
396
|
|
|
351
397
|
getSessionOrThrow(userId?: string) {
|
|
352
398
|
const targetUserId = userId ?? this.currentUser?.id
|
|
353
|
-
const session = targetUserId
|
|
399
|
+
const session = targetUserId
|
|
400
|
+
? this.sessionsByUserId.get(targetUserId)
|
|
401
|
+
: this.sessionManager.get()
|
|
354
402
|
if (!session) {
|
|
355
403
|
throw new Error('User is not authenticated')
|
|
356
404
|
}
|
|
@@ -381,7 +429,10 @@ export class App {
|
|
|
381
429
|
})
|
|
382
430
|
}
|
|
383
431
|
|
|
384
|
-
private async requestWithAccessToken<T>(
|
|
432
|
+
private async requestWithAccessToken<T>(
|
|
433
|
+
operation: (accessToken: string) => Promise<T>,
|
|
434
|
+
userId?: string
|
|
435
|
+
) {
|
|
385
436
|
const firstSession = this.getSessionOrThrow(userId)
|
|
386
437
|
try {
|
|
387
438
|
return await operation(firstSession.accessToken)
|
|
@@ -402,21 +453,26 @@ export class App {
|
|
|
402
453
|
arguments: args
|
|
403
454
|
}
|
|
404
455
|
|
|
405
|
-
const result = await this.requestWithAccessToken(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
456
|
+
const result = await this.requestWithAccessToken(
|
|
457
|
+
(accessToken) =>
|
|
458
|
+
requestJson<unknown>({
|
|
459
|
+
url: this.functionsUrl(`/call?func=${name}`),
|
|
460
|
+
method: 'POST',
|
|
461
|
+
body: payload,
|
|
462
|
+
bearerToken: accessToken,
|
|
463
|
+
timeout: this.timeout
|
|
464
|
+
}),
|
|
413
465
|
userId
|
|
414
466
|
)
|
|
415
467
|
|
|
416
468
|
return normalizeFunctionResponse(result)
|
|
417
469
|
}
|
|
418
470
|
|
|
419
|
-
async callFunctionStreaming(
|
|
471
|
+
async callFunctionStreaming(
|
|
472
|
+
name: string,
|
|
473
|
+
args: unknown[],
|
|
474
|
+
userId?: string
|
|
475
|
+
): Promise<AsyncIterable<Uint8Array>> {
|
|
420
476
|
await this.ensureSessionBootstrapped()
|
|
421
477
|
const payload: FunctionCallPayload = {
|
|
422
478
|
name,
|
|
@@ -443,7 +499,11 @@ export class App {
|
|
|
443
499
|
timeout
|
|
444
500
|
})
|
|
445
501
|
} catch (error) {
|
|
446
|
-
if (
|
|
502
|
+
if (
|
|
503
|
+
!didRefresh &&
|
|
504
|
+
error instanceof FlowerbaseHttpError &&
|
|
505
|
+
error.status === 401
|
|
506
|
+
) {
|
|
447
507
|
await refreshSession()
|
|
448
508
|
didRefresh = true
|
|
449
509
|
continue
|
|
@@ -457,7 +517,11 @@ export class App {
|
|
|
457
517
|
}
|
|
458
518
|
return
|
|
459
519
|
} catch (error) {
|
|
460
|
-
if (
|
|
520
|
+
if (
|
|
521
|
+
!didRefresh &&
|
|
522
|
+
error instanceof FlowerbaseHttpError &&
|
|
523
|
+
error.status === 401
|
|
524
|
+
) {
|
|
461
525
|
await refreshSession()
|
|
462
526
|
didRefresh = true
|
|
463
527
|
continue
|
|
@@ -469,7 +533,12 @@ export class App {
|
|
|
469
533
|
}
|
|
470
534
|
}
|
|
471
535
|
|
|
472
|
-
async callService(
|
|
536
|
+
async callService(
|
|
537
|
+
name: string,
|
|
538
|
+
args: MongoDbServiceArguments,
|
|
539
|
+
service: MongoDbServiceName = 'mongodb-atlas',
|
|
540
|
+
userId?: string
|
|
541
|
+
) {
|
|
473
542
|
await this.ensureSessionBootstrapped()
|
|
474
543
|
const payload: FunctionCallPayload = {
|
|
475
544
|
name,
|
|
@@ -477,27 +546,29 @@ export class App {
|
|
|
477
546
|
arguments: args
|
|
478
547
|
}
|
|
479
548
|
|
|
480
|
-
return this.requestWithAccessToken(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
549
|
+
return this.requestWithAccessToken(
|
|
550
|
+
(accessToken) =>
|
|
551
|
+
requestJson<unknown>({
|
|
552
|
+
url: this.functionsUrl(`/call?col=${args[0].collection}-${name}`),
|
|
553
|
+
method: 'POST',
|
|
554
|
+
body: payload,
|
|
555
|
+
bearerToken: accessToken,
|
|
556
|
+
timeout: this.timeout
|
|
557
|
+
}),
|
|
488
558
|
userId
|
|
489
559
|
)
|
|
490
560
|
}
|
|
491
561
|
|
|
492
562
|
async getProfile(userId?: string): Promise<ProfileData> {
|
|
493
563
|
await this.ensureSessionBootstrapped()
|
|
494
|
-
const profile = await this.requestWithAccessToken(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
564
|
+
const profile = await this.requestWithAccessToken(
|
|
565
|
+
(accessToken) =>
|
|
566
|
+
requestJson<ProfileData>({
|
|
567
|
+
url: this.authUrl('/profile'),
|
|
568
|
+
method: 'GET',
|
|
569
|
+
bearerToken: accessToken,
|
|
570
|
+
timeout: this.timeout
|
|
571
|
+
}),
|
|
501
572
|
userId
|
|
502
573
|
)
|
|
503
574
|
const session = this.getSessionOrThrow(userId)
|
package/src/mongo.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { App } from './app'
|
|
2
2
|
import { EJSON } from './bson'
|
|
3
|
-
import { CollectionLike, MongoClientLike } from './types'
|
|
3
|
+
import { CollectionLike, MongoClientLike, MongoDbServiceName } from './types'
|
|
4
4
|
import { createWatchIterator } from './watch'
|
|
5
5
|
|
|
6
6
|
const serialize = (value: unknown) => EJSON.serialize(value, { relaxed: false })
|
|
@@ -20,7 +20,7 @@ const mapResult = (value: unknown) => {
|
|
|
20
20
|
return deserialize(value)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const createMongoClient = (app: App, serviceName:
|
|
23
|
+
export const createMongoClient = (app: App, serviceName: MongoDbServiceName, userId: string): MongoClientLike => ({
|
|
24
24
|
db: (database: string) => ({
|
|
25
25
|
collection: (collection: string): CollectionLike => {
|
|
26
26
|
const callService = async (name: string, args: unknown[]) => {
|
package/src/session.ts
CHANGED
|
@@ -1,24 +1,48 @@
|
|
|
1
|
+
import { createStorage } from './storage'
|
|
1
2
|
import { SessionData } from './types'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
+
const parseSession = (raw: string | null): SessionData | null => {
|
|
5
|
+
if (!raw) return null
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(raw) as SessionData
|
|
9
|
+
} catch {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parseUsersOrder = (raw: string | null) => {
|
|
15
|
+
if (!raw) return []
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(raw)
|
|
19
|
+
if (!Array.isArray(parsed)) return []
|
|
20
|
+
return parsed.filter((item): item is string => typeof item === 'string')
|
|
21
|
+
} catch {
|
|
22
|
+
return []
|
|
12
23
|
}
|
|
24
|
+
}
|
|
13
25
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
const parseSessionsByUser = (raw: string | null) => {
|
|
27
|
+
if (!raw) return {} as Record<string, SessionData>
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw) as Record<string, SessionData>
|
|
31
|
+
const normalized: Record<string, SessionData> = {}
|
|
32
|
+
for (const [userId, session] of Object.entries(parsed)) {
|
|
33
|
+
if (
|
|
34
|
+
session &&
|
|
35
|
+
typeof session === 'object' &&
|
|
36
|
+
typeof session.accessToken === 'string' &&
|
|
37
|
+
typeof session.refreshToken === 'string' &&
|
|
38
|
+
typeof session.userId === 'string'
|
|
39
|
+
) {
|
|
40
|
+
normalized[userId] = session
|
|
41
|
+
}
|
|
21
42
|
}
|
|
43
|
+
return normalized
|
|
44
|
+
} catch {
|
|
45
|
+
return {} as Record<string, SessionData>
|
|
22
46
|
}
|
|
23
47
|
}
|
|
24
48
|
|
|
@@ -26,25 +50,35 @@ export class SessionManager {
|
|
|
26
50
|
private readonly key: string
|
|
27
51
|
private readonly usersKey: string
|
|
28
52
|
private readonly sessionsKey: string
|
|
29
|
-
private readonly storage =
|
|
53
|
+
private readonly storage = createStorage()
|
|
54
|
+
private readonly hydrationPromise: Promise<void>
|
|
30
55
|
private session: SessionData | null = null
|
|
56
|
+
private usersOrder: string[] = []
|
|
57
|
+
private sessionsByUser: Record<string, SessionData> = {}
|
|
31
58
|
|
|
32
59
|
constructor(appId: string) {
|
|
33
60
|
this.key = `flowerbase:${appId}:session`
|
|
34
61
|
this.usersKey = `flowerbase:${appId}:users`
|
|
35
62
|
this.sessionsKey = `flowerbase:${appId}:sessions`
|
|
36
|
-
this.session = this.
|
|
63
|
+
this.session = parseSession(this.storage.getItem(this.key))
|
|
64
|
+
this.usersOrder = parseUsersOrder(this.storage.getItem(this.usersKey))
|
|
65
|
+
this.sessionsByUser = parseSessionsByUser(this.storage.getItem(this.sessionsKey))
|
|
66
|
+
this.hydrationPromise = this.hydrate()
|
|
37
67
|
}
|
|
38
68
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
69
|
+
private async hydrate() {
|
|
70
|
+
const hydrated = await this.storage.hydrate([this.key, this.usersKey, this.sessionsKey])
|
|
71
|
+
this.session = parseSession(hydrated[this.key] ?? null)
|
|
72
|
+
this.usersOrder = parseUsersOrder(hydrated[this.usersKey] ?? null)
|
|
73
|
+
this.sessionsByUser = parseSessionsByUser(hydrated[this.sessionsKey] ?? null)
|
|
74
|
+
}
|
|
42
75
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
whenReady() {
|
|
77
|
+
return this.hydrationPromise
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
hasPersistentStorage() {
|
|
81
|
+
return this.storage.isPersistent
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
get() {
|
|
@@ -62,18 +96,11 @@ export class SessionManager {
|
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
getUsersOrder() {
|
|
65
|
-
|
|
66
|
-
if (!raw) return []
|
|
67
|
-
try {
|
|
68
|
-
const parsed = JSON.parse(raw)
|
|
69
|
-
if (!Array.isArray(parsed)) return []
|
|
70
|
-
return parsed.filter((item): item is string => typeof item === 'string')
|
|
71
|
-
} catch {
|
|
72
|
-
return []
|
|
73
|
-
}
|
|
99
|
+
return [...this.usersOrder]
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
setUsersOrder(order: string[]) {
|
|
103
|
+
this.usersOrder = [...order]
|
|
77
104
|
if (order.length === 0) {
|
|
78
105
|
this.storage.removeItem(this.usersKey)
|
|
79
106
|
return
|
|
@@ -82,29 +109,11 @@ export class SessionManager {
|
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
getSessionsByUser() {
|
|
85
|
-
|
|
86
|
-
if (!raw) return {} as Record<string, SessionData>
|
|
87
|
-
try {
|
|
88
|
-
const parsed = JSON.parse(raw) as Record<string, SessionData>
|
|
89
|
-
const normalized: Record<string, SessionData> = {}
|
|
90
|
-
for (const [userId, session] of Object.entries(parsed)) {
|
|
91
|
-
if (
|
|
92
|
-
session &&
|
|
93
|
-
typeof session === 'object' &&
|
|
94
|
-
typeof session.accessToken === 'string' &&
|
|
95
|
-
typeof session.refreshToken === 'string' &&
|
|
96
|
-
typeof session.userId === 'string'
|
|
97
|
-
) {
|
|
98
|
-
normalized[userId] = session
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return normalized
|
|
102
|
-
} catch {
|
|
103
|
-
return {} as Record<string, SessionData>
|
|
104
|
-
}
|
|
112
|
+
return { ...this.sessionsByUser }
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
setSessionsByUser(sessionsByUser: Record<string, SessionData>) {
|
|
116
|
+
this.sessionsByUser = { ...sessionsByUser }
|
|
108
117
|
if (Object.keys(sessionsByUser).length === 0) {
|
|
109
118
|
this.storage.removeItem(this.sessionsKey)
|
|
110
119
|
return
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
2
|
+
|
|
3
|
+
type StorageSnapshot = Record<string, string | null>
|
|
4
|
+
|
|
5
|
+
export type PersistedStorage = {
|
|
6
|
+
isPersistent: boolean
|
|
7
|
+
getItem: (key: string) => string | null
|
|
8
|
+
setItem: (key: string, value: string) => void
|
|
9
|
+
removeItem: (key: string) => void
|
|
10
|
+
hydrate: (keys: string[]) => Promise<StorageSnapshot>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const memoryStore = new Map<string, string>()
|
|
14
|
+
|
|
15
|
+
const getSnapshot = (keys: string[]): StorageSnapshot =>
|
|
16
|
+
Object.fromEntries(keys.map((key) => [key, memoryStore.get(key) ?? null]))
|
|
17
|
+
|
|
18
|
+
export const createStorage = (): PersistedStorage => ({
|
|
19
|
+
isPersistent: true,
|
|
20
|
+
getItem: (key) => memoryStore.get(key) ?? null,
|
|
21
|
+
setItem: (key, value) => {
|
|
22
|
+
memoryStore.set(key, value)
|
|
23
|
+
void AsyncStorage.setItem(key, value).catch(() => {
|
|
24
|
+
// Ignore write failures and keep the in-memory cache alive.
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
removeItem: (key) => {
|
|
28
|
+
memoryStore.delete(key)
|
|
29
|
+
void AsyncStorage.removeItem(key).catch(() => {
|
|
30
|
+
// Ignore delete failures and keep the in-memory cache alive.
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
async hydrate(keys) {
|
|
34
|
+
try {
|
|
35
|
+
const entries = await AsyncStorage.multiGet(keys)
|
|
36
|
+
for (const [key, value] of entries) {
|
|
37
|
+
if (value === null) {
|
|
38
|
+
memoryStore.delete(key)
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
memoryStore.set(key, value)
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore storage read failures and keep the in-memory cache alive.
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return getSnapshot(keys)
|
|
48
|
+
}
|
|
49
|
+
})
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type StorageSnapshot = Record<string, string | null>
|
|
2
|
+
|
|
3
|
+
export type PersistedStorage = {
|
|
4
|
+
isPersistent: boolean
|
|
5
|
+
getItem: (key: string) => string | null
|
|
6
|
+
setItem: (key: string, value: string) => void
|
|
7
|
+
removeItem: (key: string) => void
|
|
8
|
+
hydrate: (keys: string[]) => Promise<StorageSnapshot>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const memoryStore = new Map<string, string>()
|
|
12
|
+
|
|
13
|
+
const getStorage = () => {
|
|
14
|
+
const browserStorage = globalThis.localStorage
|
|
15
|
+
if (
|
|
16
|
+
browserStorage &&
|
|
17
|
+
typeof browserStorage.getItem === 'function' &&
|
|
18
|
+
typeof browserStorage.setItem === 'function' &&
|
|
19
|
+
typeof browserStorage.removeItem === 'function'
|
|
20
|
+
) {
|
|
21
|
+
return {
|
|
22
|
+
getItem: (key: string) => browserStorage.getItem(key),
|
|
23
|
+
setItem: (key: string, value: string) => browserStorage.setItem(key, value),
|
|
24
|
+
removeItem: (key: string) => browserStorage.removeItem(key)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
getItem: (key: string) => memoryStore.get(key) ?? null,
|
|
30
|
+
setItem: (key: string, value: string) => {
|
|
31
|
+
memoryStore.set(key, value)
|
|
32
|
+
},
|
|
33
|
+
removeItem: (key: string) => {
|
|
34
|
+
memoryStore.delete(key)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const createStorage = (): PersistedStorage => {
|
|
40
|
+
const browserStorage = globalThis.localStorage
|
|
41
|
+
const isPersistent =
|
|
42
|
+
!!browserStorage &&
|
|
43
|
+
typeof browserStorage.getItem === 'function' &&
|
|
44
|
+
typeof browserStorage.setItem === 'function' &&
|
|
45
|
+
typeof browserStorage.removeItem === 'function'
|
|
46
|
+
const storage = getStorage()
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
isPersistent,
|
|
50
|
+
getItem: storage.getItem,
|
|
51
|
+
setItem: storage.setItem,
|
|
52
|
+
removeItem: storage.removeItem,
|
|
53
|
+
async hydrate(keys) {
|
|
54
|
+
return Object.fromEntries(keys.map((key) => [key, storage.getItem(key)]))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|