@flowerforce/flowerbase-client 0.1.1-beta.2

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 (63) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/LICENSE +3 -0
  3. package/README.md +198 -0
  4. package/dist/app.d.ts +40 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +186 -0
  7. package/dist/bson.d.ts +8 -0
  8. package/dist/bson.d.ts.map +1 -0
  9. package/dist/bson.js +10 -0
  10. package/dist/credentials.d.ts +7 -0
  11. package/dist/credentials.d.ts.map +1 -0
  12. package/dist/credentials.js +24 -0
  13. package/dist/functions.d.ts +3 -0
  14. package/dist/functions.d.ts.map +1 -0
  15. package/dist/functions.js +30 -0
  16. package/dist/http.d.ts +15 -0
  17. package/dist/http.d.ts.map +1 -0
  18. package/dist/http.js +74 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +14 -0
  22. package/dist/mongo.d.ts +4 -0
  23. package/dist/mongo.d.ts.map +1 -0
  24. package/dist/mongo.js +61 -0
  25. package/dist/session.d.ts +12 -0
  26. package/dist/session.d.ts.map +1 -0
  27. package/dist/session.js +53 -0
  28. package/dist/session.native.d.ts +14 -0
  29. package/dist/session.native.d.ts.map +1 -0
  30. package/dist/session.native.js +81 -0
  31. package/dist/types.d.ts +73 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +2 -0
  34. package/dist/user.d.ts +17 -0
  35. package/dist/user.d.ts.map +1 -0
  36. package/dist/user.js +30 -0
  37. package/dist/watch.d.ts +3 -0
  38. package/dist/watch.d.ts.map +1 -0
  39. package/dist/watch.js +138 -0
  40. package/jest.config.ts +13 -0
  41. package/package.json +30 -0
  42. package/project.json +11 -0
  43. package/rollup.config.js +17 -0
  44. package/src/__tests__/auth.test.ts +164 -0
  45. package/src/__tests__/compat.test.ts +12 -0
  46. package/src/__tests__/functions.test.ts +76 -0
  47. package/src/__tests__/mongo.test.ts +48 -0
  48. package/src/__tests__/session.test.ts +103 -0
  49. package/src/__tests__/watch.test.ts +138 -0
  50. package/src/app.ts +235 -0
  51. package/src/bson.ts +6 -0
  52. package/src/credentials.ts +24 -0
  53. package/src/functions.ts +32 -0
  54. package/src/http.ts +92 -0
  55. package/src/index.ts +14 -0
  56. package/src/mongo.ts +63 -0
  57. package/src/session.native.ts +98 -0
  58. package/src/session.ts +59 -0
  59. package/src/types.ts +84 -0
  60. package/src/user.ts +39 -0
  61. package/src/watch.ts +150 -0
  62. package/tsconfig.json +34 -0
  63. package/tsconfig.spec.json +13 -0
@@ -0,0 +1,164 @@
1
+ import { App } from '../app'
2
+ import { Credentials } from '../credentials'
3
+
4
+ describe('flowerbase-client auth', () => {
5
+ const originalFetch = global.fetch
6
+ const originalLocalStorage = (globalThis as typeof globalThis & { localStorage?: unknown }).localStorage
7
+
8
+ afterEach(() => {
9
+ global.fetch = originalFetch
10
+ if (typeof originalLocalStorage === 'undefined') {
11
+ Reflect.deleteProperty(globalThis, 'localStorage')
12
+ } else {
13
+ Object.defineProperty(globalThis, 'localStorage', {
14
+ configurable: true,
15
+ value: originalLocalStorage
16
+ })
17
+ }
18
+ })
19
+
20
+ it('logs in with email/password', async () => {
21
+ global.fetch = jest
22
+ .fn()
23
+ .mockResolvedValueOnce({
24
+ ok: true,
25
+ text: async () => JSON.stringify({
26
+ access_token: 'access',
27
+ refresh_token: 'refresh',
28
+ user_id: 'user-1'
29
+ })
30
+ })
31
+ .mockResolvedValueOnce({
32
+ ok: true,
33
+ text: async () => JSON.stringify({ access_token: 'access-from-session' })
34
+ }) as unknown as typeof fetch
35
+
36
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
37
+ const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
38
+
39
+ expect(user.id).toBe('user-1')
40
+ expect(app.currentUser?.id).toBe('user-1')
41
+ expect(global.fetch).toHaveBeenCalledWith(
42
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/login',
43
+ expect.objectContaining({ method: 'POST' })
44
+ )
45
+ expect(global.fetch).toHaveBeenCalledWith(
46
+ 'http://localhost:3000/api/client/v2.0/auth/session',
47
+ expect.objectContaining({
48
+ method: 'POST',
49
+ headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
50
+ })
51
+ )
52
+ })
53
+
54
+ it('logs in with anonymous provider', async () => {
55
+ global.fetch = jest.fn().mockResolvedValue({
56
+ ok: true,
57
+ text: async () => JSON.stringify({
58
+ access_token: 'access',
59
+ refresh_token: 'refresh',
60
+ user_id: 'anon-1'
61
+ })
62
+ }) as unknown as typeof fetch
63
+
64
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
65
+ const user = await app.logIn(Credentials.anonymous())
66
+
67
+ expect(user.id).toBe('anon-1')
68
+ expect(global.fetch).toHaveBeenCalledWith(
69
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/anon-user/login',
70
+ expect.objectContaining({ method: 'POST' })
71
+ )
72
+ })
73
+
74
+ it('logs in with custom function provider', async () => {
75
+ global.fetch = jest.fn().mockResolvedValue({
76
+ ok: true,
77
+ text: async () => JSON.stringify({
78
+ access_token: 'access',
79
+ refresh_token: 'refresh',
80
+ user_id: 'custom-1'
81
+ })
82
+ }) as unknown as typeof fetch
83
+
84
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
85
+ const user = await app.logIn(Credentials.function({ token: 'abc' }))
86
+
87
+ expect(user.id).toBe('custom-1')
88
+ expect(global.fetch).toHaveBeenCalledWith(
89
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/custom-function/login',
90
+ expect.objectContaining({ method: 'POST' })
91
+ )
92
+ })
93
+
94
+ it('supports register and reset endpoints', async () => {
95
+ global.fetch = jest.fn().mockResolvedValue({
96
+ ok: true,
97
+ text: async () => JSON.stringify({ status: 'ok' })
98
+ }) as unknown as typeof fetch
99
+
100
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
101
+
102
+ await app.emailPasswordAuth.registerUser({ email: 'john@doe.com', password: 'secret123' })
103
+ await app.emailPasswordAuth.sendResetPasswordEmail('john@doe.com')
104
+ await app.emailPasswordAuth.callResetPasswordFunction('john@doe.com', 'new-secret', 'extra')
105
+ await app.emailPasswordAuth.resetPassword({ token: 't1', tokenId: 't2', password: 'new-secret' })
106
+
107
+ expect(global.fetch).toHaveBeenNthCalledWith(
108
+ 1,
109
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/register',
110
+ expect.objectContaining({ method: 'POST' })
111
+ )
112
+ expect(global.fetch).toHaveBeenNthCalledWith(
113
+ 2,
114
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/send',
115
+ expect.objectContaining({ method: 'POST' })
116
+ )
117
+ expect(global.fetch).toHaveBeenNthCalledWith(
118
+ 3,
119
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/call',
120
+ expect.objectContaining({ method: 'POST' })
121
+ )
122
+ expect(global.fetch).toHaveBeenNthCalledWith(
123
+ 4,
124
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset',
125
+ expect.objectContaining({ method: 'POST' })
126
+ )
127
+ })
128
+
129
+ it('calls session endpoint on app load when browser session exists', async () => {
130
+ const storage = new Map<string, string>()
131
+ storage.set(
132
+ 'flowerbase:my-app:session',
133
+ JSON.stringify({ accessToken: 'old-access', refreshToken: 'refresh', userId: 'user-1' })
134
+ )
135
+ Object.defineProperty(globalThis, 'localStorage', {
136
+ configurable: true,
137
+ value: {
138
+ getItem: (key: string) => storage.get(key) ?? null,
139
+ setItem: (key: string, value: string) => {
140
+ storage.set(key, value)
141
+ },
142
+ removeItem: (key: string) => {
143
+ storage.delete(key)
144
+ }
145
+ }
146
+ })
147
+
148
+ global.fetch = jest.fn().mockResolvedValue({
149
+ ok: true,
150
+ text: async () => JSON.stringify({ access_token: 'fresh-access' })
151
+ }) as unknown as typeof fetch
152
+
153
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
154
+ await app.getProfile().catch(() => undefined)
155
+
156
+ expect(global.fetch).toHaveBeenCalledWith(
157
+ 'http://localhost:3000/api/client/v2.0/auth/session',
158
+ expect.objectContaining({
159
+ method: 'POST',
160
+ headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
161
+ })
162
+ )
163
+ })
164
+ })
@@ -0,0 +1,12 @@
1
+ import * as Flowerbase from '../index'
2
+
3
+ describe('flowerbase-client compatibility surface', () => {
4
+ it('exposes Realm-like symbols', () => {
5
+ expect(typeof Flowerbase.App).toBe('function')
6
+ expect(typeof Flowerbase.Credentials.emailPassword).toBe('function')
7
+ expect(typeof Flowerbase.Credentials.anonymous).toBe('function')
8
+ expect(typeof Flowerbase.Credentials.function).toBe('function')
9
+ expect(typeof Flowerbase.BSON.ObjectId).toBe('function')
10
+ expect(Flowerbase.ObjectID).toBe(Flowerbase.ObjectId)
11
+ })
12
+ })
@@ -0,0 +1,76 @@
1
+ import { App } from '../app'
2
+ import { Credentials } from '../credentials'
3
+
4
+ describe('flowerbase-client functions', () => {
5
+ const originalFetch = global.fetch
6
+
7
+ afterEach(() => {
8
+ global.fetch = originalFetch
9
+ })
10
+
11
+ it('calls dynamic function proxies', async () => {
12
+ global.fetch = jest
13
+ .fn()
14
+ .mockResolvedValueOnce({
15
+ ok: true,
16
+ text: async () => JSON.stringify({
17
+ access_token: 'access',
18
+ refresh_token: 'refresh',
19
+ user_id: 'user-1'
20
+ })
21
+ })
22
+ .mockResolvedValueOnce({
23
+ ok: true,
24
+ text: async () => JSON.stringify({ access_token: 'access' })
25
+ })
26
+ .mockResolvedValueOnce({
27
+ ok: true,
28
+ text: async () => JSON.stringify({ result: 42 })
29
+ }) as unknown as typeof fetch
30
+
31
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
32
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
33
+
34
+ const result = await app.currentUser!.functions.sum(40, 2)
35
+ expect(result).toEqual({ result: 42 })
36
+
37
+ expect(global.fetch).toHaveBeenNthCalledWith(
38
+ 3,
39
+ 'http://localhost:3000/api/client/v2.0/app/my-app/functions/call',
40
+ expect.objectContaining({
41
+ method: 'POST',
42
+ headers: expect.objectContaining({ Authorization: 'Bearer access' })
43
+ })
44
+ )
45
+ })
46
+
47
+ it('throws function execution errors', async () => {
48
+ global.fetch = jest
49
+ .fn()
50
+ .mockResolvedValueOnce({
51
+ ok: true,
52
+ text: async () => JSON.stringify({
53
+ access_token: 'access',
54
+ refresh_token: 'refresh',
55
+ user_id: 'user-1'
56
+ })
57
+ })
58
+ .mockResolvedValueOnce({
59
+ ok: true,
60
+ text: async () => JSON.stringify({ access_token: 'access' })
61
+ })
62
+ .mockResolvedValueOnce({
63
+ ok: false,
64
+ status: 400,
65
+ text: async () => JSON.stringify({
66
+ error: '{"message":"boom","name":"Error"}',
67
+ error_code: 'FunctionExecutionError'
68
+ })
69
+ }) as unknown as typeof fetch
70
+
71
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
72
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
73
+
74
+ await expect(app.currentUser!.functions.explode()).rejects.toThrow('boom')
75
+ })
76
+ })
@@ -0,0 +1,48 @@
1
+ import { ObjectId } from 'bson'
2
+ import { App } from '../app'
3
+ import { Credentials } from '../credentials'
4
+
5
+ describe('flowerbase-client mongo service wrapper', () => {
6
+ const originalFetch = global.fetch
7
+
8
+ afterEach(() => {
9
+ global.fetch = originalFetch
10
+ })
11
+
12
+ it('maps CRUD calls to mongodb-atlas service payload', async () => {
13
+ global.fetch = jest
14
+ .fn()
15
+ .mockResolvedValueOnce({
16
+ ok: true,
17
+ text: async () => JSON.stringify({
18
+ access_token: 'access',
19
+ refresh_token: 'refresh',
20
+ user_id: 'user-1'
21
+ })
22
+ })
23
+ .mockResolvedValue({
24
+ ok: true,
25
+ text: async () => JSON.stringify({ ok: true })
26
+ }) as unknown as typeof fetch
27
+
28
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
29
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
30
+
31
+ const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
32
+
33
+ await collection.find({ done: false })
34
+ await collection.findOne({ done: false })
35
+ await collection.insertOne({ title: 'new task' })
36
+ await collection.updateOne({ _id: new ObjectId('507f1f77bcf86cd799439011') }, { $set: { done: true } })
37
+ await collection.updateMany({ done: false }, { $set: { done: true } })
38
+ await collection.deleteOne({ done: true })
39
+
40
+ expect((global.fetch as jest.Mock).mock.calls).toHaveLength(8)
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')
43
+ expect(request.method).toBe('POST')
44
+ const parsed = JSON.parse(request.body)
45
+ expect(parsed.service).toBe('mongodb-atlas')
46
+ expect(parsed.name).toBe('findOne')
47
+ })
48
+ })
@@ -0,0 +1,103 @@
1
+ import { App } from '../app'
2
+ import { Credentials } from '../credentials'
3
+
4
+ describe('flowerbase-client session', () => {
5
+ const originalFetch = global.fetch
6
+
7
+ afterEach(() => {
8
+ global.fetch = originalFetch
9
+ })
10
+
11
+ it('refreshes access token', async () => {
12
+ global.fetch = jest
13
+ .fn()
14
+ .mockResolvedValueOnce({
15
+ ok: true,
16
+ text: async () => JSON.stringify({
17
+ access_token: 'access',
18
+ refresh_token: 'refresh',
19
+ user_id: 'user-1'
20
+ })
21
+ })
22
+ .mockResolvedValueOnce({
23
+ ok: true,
24
+ text: async () => JSON.stringify({ access_token: 'access' })
25
+ })
26
+ .mockResolvedValueOnce({
27
+ ok: true,
28
+ text: async () => JSON.stringify({ access_token: 'access-2' })
29
+ }) as unknown as typeof fetch
30
+
31
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
32
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
33
+
34
+ const token = await app.currentUser!.refreshAccessToken()
35
+ expect(token).toBe('access-2')
36
+ expect(global.fetch).toHaveBeenLastCalledWith(
37
+ 'http://localhost:3000/api/client/v2.0/auth/session',
38
+ expect.objectContaining({
39
+ method: 'POST',
40
+ headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
41
+ })
42
+ )
43
+ })
44
+
45
+ it('clears session when refresh fails', async () => {
46
+ global.fetch = jest
47
+ .fn()
48
+ .mockResolvedValueOnce({
49
+ ok: true,
50
+ text: async () => JSON.stringify({
51
+ access_token: 'access',
52
+ refresh_token: 'refresh',
53
+ user_id: 'user-1'
54
+ })
55
+ })
56
+ .mockResolvedValueOnce({
57
+ ok: true,
58
+ text: async () => JSON.stringify({ access_token: 'access' })
59
+ })
60
+ .mockResolvedValueOnce({
61
+ ok: false,
62
+ status: 401,
63
+ text: async () => JSON.stringify({ message: 'Invalid refresh token provided' })
64
+ }) as unknown as typeof fetch
65
+
66
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
67
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
68
+
69
+ await expect(app.currentUser!.refreshAccessToken()).rejects.toThrow('Invalid refresh token provided')
70
+ expect(app.currentUser).toBeNull()
71
+ })
72
+
73
+ it('revokes session on logout', async () => {
74
+ global.fetch = jest
75
+ .fn()
76
+ .mockResolvedValueOnce({
77
+ ok: true,
78
+ text: async () => JSON.stringify({
79
+ access_token: 'access',
80
+ refresh_token: 'refresh',
81
+ user_id: 'user-1'
82
+ })
83
+ })
84
+ .mockResolvedValueOnce({
85
+ ok: true,
86
+ text: async () => JSON.stringify({ access_token: 'access' })
87
+ })
88
+ .mockResolvedValueOnce({
89
+ ok: true,
90
+ text: async () => JSON.stringify({ status: 'ok' })
91
+ }) as unknown as typeof fetch
92
+
93
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
94
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
95
+ await app.currentUser!.logOut()
96
+
97
+ expect(global.fetch).toHaveBeenLastCalledWith(
98
+ 'http://localhost:3000/api/client/v2.0/auth/session',
99
+ expect.objectContaining({ method: 'DELETE' })
100
+ )
101
+ expect(app.currentUser).toBeNull()
102
+ })
103
+ })
@@ -0,0 +1,138 @@
1
+ import { App } from '../app'
2
+ import { Credentials } from '../credentials'
3
+
4
+ const streamFromLines = (lines: string[]) => {
5
+ const encoded = lines.map((line) => `${line}\n`).join('')
6
+ const bytes = new TextEncoder().encode(encoded)
7
+
8
+ return new ReadableStream<Uint8Array>({
9
+ start(controller) {
10
+ controller.enqueue(bytes)
11
+ controller.close()
12
+ }
13
+ })
14
+ }
15
+
16
+ describe('flowerbase-client watch', () => {
17
+ const originalFetch = global.fetch
18
+
19
+ afterEach(() => {
20
+ jest.useRealTimers()
21
+ global.fetch = originalFetch
22
+ })
23
+
24
+ it('receives SSE events through watch iterator', async () => {
25
+ global.fetch = jest
26
+ .fn()
27
+ .mockResolvedValueOnce({
28
+ ok: true,
29
+ text: async () => JSON.stringify({
30
+ access_token: 'access',
31
+ refresh_token: 'refresh',
32
+ user_id: 'user-1'
33
+ })
34
+ })
35
+ .mockResolvedValueOnce({
36
+ ok: true,
37
+ text: async () => JSON.stringify({ access_token: 'access' })
38
+ })
39
+ .mockResolvedValueOnce({
40
+ ok: true,
41
+ body: streamFromLines(['data: {"operationType":"insert","fullDocument":{"title":"A"}}', ''])
42
+ }) as unknown as typeof fetch
43
+
44
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
45
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
46
+
47
+ const iterator = app.currentUser!
48
+ .mongoClient('mongodb-atlas')
49
+ .db('testdb')
50
+ .collection('todos')
51
+ .watch()
52
+
53
+ const first = await iterator.next()
54
+ expect(first.done).toBe(false)
55
+ expect(first.value).toEqual({ operationType: 'insert', fullDocument: { title: 'A' } })
56
+
57
+ iterator.close()
58
+
59
+ const [url, request] = (global.fetch as jest.Mock).mock.calls[2]
60
+ expect(url).toContain('/functions/call?baas_request=')
61
+ expect(request.headers.Authorization).toBe('Bearer access')
62
+ })
63
+
64
+ it('closes iterator on return', async () => {
65
+ global.fetch = jest
66
+ .fn()
67
+ .mockResolvedValueOnce({
68
+ ok: true,
69
+ text: async () => JSON.stringify({
70
+ access_token: 'access',
71
+ refresh_token: 'refresh',
72
+ user_id: 'user-1'
73
+ })
74
+ })
75
+ .mockResolvedValueOnce({
76
+ ok: true,
77
+ text: async () => JSON.stringify({ access_token: 'access' })
78
+ })
79
+ .mockResolvedValue({
80
+ ok: true,
81
+ body: streamFromLines([])
82
+ }) as unknown as typeof fetch
83
+
84
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
85
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
86
+
87
+ const iterator = app.currentUser!
88
+ .mongoClient('mongodb-atlas')
89
+ .db('testdb')
90
+ .collection('todos')
91
+ .watch()
92
+
93
+ iterator.close()
94
+ const result = await iterator.next()
95
+ expect(result.done).toBe(true)
96
+ })
97
+
98
+ it('reconnects with backoff after network errors', async () => {
99
+ jest.useFakeTimers()
100
+ global.fetch = jest
101
+ .fn()
102
+ .mockResolvedValueOnce({
103
+ ok: true,
104
+ text: async () => JSON.stringify({
105
+ access_token: 'access',
106
+ refresh_token: 'refresh',
107
+ user_id: 'user-1'
108
+ })
109
+ })
110
+ .mockResolvedValueOnce({
111
+ ok: true,
112
+ text: async () => JSON.stringify({ access_token: 'access' })
113
+ })
114
+ .mockRejectedValueOnce(new Error('network'))
115
+ .mockResolvedValueOnce({
116
+ ok: true,
117
+ body: streamFromLines(['data: {"operationType":"update"}', ''])
118
+ }) as unknown as typeof fetch
119
+
120
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
121
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
122
+
123
+ const iterator = app.currentUser!
124
+ .mongoClient('mongodb-atlas')
125
+ .db('testdb')
126
+ .collection('todos')
127
+ .watch()
128
+
129
+ await jest.advanceTimersByTimeAsync(250)
130
+ const result = await iterator.next()
131
+
132
+ expect(result.done).toBe(false)
133
+ expect(result.value).toEqual({ operationType: 'update' })
134
+ expect((global.fetch as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(4)
135
+
136
+ iterator.close()
137
+ })
138
+ })