Skip to content

Commit fd9c9fa

Browse files
Merge pull request #255 from DevResults/nathan/contain-effect-to-data-layer
Keep effects contained to the data layer
2 parents c011749 + 5657e04 commit fd9c9fa

42 files changed

Lines changed: 335 additions & 258 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/context/Database/Collection.ts

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
/* eslint-disable @typescript-eslint/member-ordering */
12
import type { ChangeFn } from "@automerge/automerge"
23
import { NO_OP } from "lib/constants"
3-
import { $, E, type Types } from "lib/Effect"
4+
import { E, type Types } from "lib/Effect"
45
import type { Root, RootEncoded } from "schema/Root"
56
import type { KeyNotFoundError } from "./Errors"
67
import { NonUniqueIndex, UniqueIndex } from "./Indexes"
@@ -67,7 +68,7 @@ export abstract class Collection<
6768

6869
constructor(
6970
/** The initial items to populate the collection with */
70-
items: Item[] | Record<string, Item>,
71+
items: Item[] | Record<string, Item> = [],
7172

7273
/**
7374
* The automerge-repo change function returned by useDoc<Root>.
@@ -79,13 +80,71 @@ export abstract class Collection<
7980
this.index = new UniqueIndex(items, "id" as StringKeyOf<Item>)
8081
}
8182

83+
// NON-EFFECTFUL (SYNC) API FOR USE BY APPLICATION
84+
8285
/** Returns all items in the collection. */
8386
all() {
84-
return this.index.all()
87+
return E.runSync(this.effect_all())
8588
}
8689

8790
/** Finds an item by its ID. */
8891
find(id: string) {
92+
return E.runSync(this.effect_find(id))
93+
}
94+
95+
/**
96+
* Finds an item or items using an index.
97+
* - If the index is unique (first overload), returns a single item, and throws if none is found.
98+
* - If the index is non-unique (second overload), returns an array zero or more of items.
99+
*
100+
* @example `contactCollection.findBy("userName", "alice")` // returns one contact
101+
* @example `contactCollection.findBy("company", "DevResults")` // returns an array of contacts
102+
*/
103+
findBy(...args: Parameters<typeof this.effect_findBy>) {
104+
return E.runSync(this.effect_findBy(...args))
105+
}
106+
107+
/** Adds an item to the collection */
108+
add(arg: Item | Item[]) {
109+
return E.runSync(this.effect_add(arg))
110+
}
111+
112+
/** Updates an item given an object containing its ID and one or more updated properties. */
113+
update(item: { id: IdOf<Item> } & Partial<Item>) {
114+
E.runSync(this.effect_update(item))
115+
}
116+
117+
/** Removes an item from a collection, given its ID. */
118+
destroy(id: IdOf<Item>) {
119+
E.runSync(this.effect_destroy(id))
120+
}
121+
122+
/** Removes all items from the collection. */
123+
destroyAll() {
124+
E.runSync(this.effect_destroyAll())
125+
}
126+
127+
// EFFECTFUL API FOR USE WITHIN DATA LAYER
128+
129+
public effect = {
130+
all: this.effect_all.bind(this),
131+
find: this.effect_find.bind(this),
132+
findBy: this.effect_findBy.bind(this),
133+
add: this.effect_add.bind(this),
134+
update: this.effect_update.bind(this),
135+
destroy: this.effect_destroy.bind(this),
136+
destroyAll: this.effect_destroyAll.bind(this),
137+
}
138+
139+
// PRIVATE
140+
141+
/** Returns all items in the collection. Returns an effect. */
142+
private effect_all() {
143+
return this.index.all()
144+
}
145+
146+
/** Finds an item by its ID. Returns an effect. */
147+
private effect_find(id: string) {
89148
return this.index.find(id)
90149
}
91150

@@ -96,24 +155,26 @@ export abstract class Collection<
96155
*
97156
* @example `contactCollection.findBy("userName", "alice")` // returns one contact
98157
* @example `contactCollection.findBy("company", "DevResults")` // returns an array of contacts
158+
*
159+
* Returns an effect.
99160
*/
100-
findBy<IndexName extends UniqueIndexName<this>>(
161+
private effect_findBy<IndexName extends UniqueIndexName<this>>(
101162
indexName: IndexName,
102163
value: any,
103164
): E.Effect<never, KeyNotFoundError> | E.Effect<Item>
104165

105-
findBy<IndexName extends NonUniqueIndexName<this>>(
166+
private effect_findBy<IndexName extends NonUniqueIndexName<this>>(
106167
indexName: IndexName,
107168
value: any,
108169
): E.Effect<Item[]>
109170

110-
findBy<IndexName extends AnyIndexName<this>>(indexName: IndexName, value: any) {
171+
private effect_findBy<IndexName extends AnyIndexName<this>>(indexName: IndexName, value: any) {
111172
const index = this.getIndex(indexName)
112173
return index.find(String(value))
113174
}
114175

115-
/** Adds an item to the collection */
116-
add(arg: Item | Item[]) {
176+
/** Adds an item to the collection. Returns an effect. */
177+
private effect_add(arg: Item | Item[]) {
117178
const items = Array.isArray(arg) ? arg : [arg]
118179

119180
return E.gen(this, function* () {
@@ -134,10 +195,10 @@ export abstract class Collection<
134195
})
135196
}
136197

137-
/** Updates an item given an object containing its ID and one or more updated properties. */
138-
update(item: { id: IdOf<Item> } & Partial<Item>) {
198+
/** Updates an item given an object containing its ID and one or more updated properties. Returns an effect. */
199+
private effect_update(item: { id: IdOf<Item> } & Partial<Item>) {
139200
return E.gen(this, function* () {
140-
const prevValue = yield* this.find(item.id)
201+
const prevValue = yield* this.effect_find(item.id)
141202
const newValue: Item = { ...prevValue, ...item }
142203

143204
// update the item in all indexes
@@ -155,10 +216,10 @@ export abstract class Collection<
155216
})
156217
}
157218

158-
/** Removes an item from a collection, given its ID. */
159-
destroy(id: IdOf<Item>) {
219+
/** Removes an item from a collection, given its ID. Returns an effect. */
220+
private effect_destroy(id: IdOf<Item>) {
160221
return E.gen(this, function* () {
161-
const item = yield* this.find(id)
222+
const item = yield* this.effect_find(id)
162223

163224
// remove the item from all indexes
164225
for (const index of this.allIndexes) {
@@ -173,8 +234,8 @@ export abstract class Collection<
173234
})
174235
}
175236

176-
/** Removes all items from the collection. */
177-
destroyAll() {
237+
/** Removes all items from the collection. Returns an effect. */
238+
private effect_destroyAll() {
178239
return E.gen(this, function* () {
179240
// remove all items from all indexes
180241
for (const index of this.allIndexes) {
@@ -188,8 +249,6 @@ export abstract class Collection<
188249
})
189250
}
190251

191-
// PRIVATE
192-
193252
/** Returns an array containing all indexes in the collection */
194253
private get allIndexes() {
195254
return [
@@ -207,7 +266,7 @@ export abstract class Collection<
207266

208267
// create the index on demand
209268
if (!(name in indexes)) {
210-
const items = $(this.all())
269+
const items = E.runSync(this.effect_all())
211270
const index =
212271
unique ?
213272
new UniqueIndex(items, key, name) //

app/context/Database/test/Collection.benchmark.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { LocalDate } from "@js-joda/core"
22
import { clients } from "data/clients"
33
import { contacts } from "data/contacts"
44
import { projects } from "data/projects"
5-
import { $ } from "lib/Effect"
65
import { generateTimeEntries } from "lib/generateTimeEntries"
76
import { TimeEntryCollection } from "schema/TimeEntryCollection"
87
import { bench, describe } from "vitest"
@@ -17,7 +16,7 @@ const addEntries = (weekCount: number) => {
1716
})
1817

1918
const collection = new TimeEntryCollection([])
20-
$(collection.add(entries))
19+
collection.add(entries)
2120
}
2221

2322
describe("collection.add()", () => {

app/context/Database/test/Collection.test.ts

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LocalDate } from "@js-joda/core"
22
import { contacts } from "data/contacts"
3-
import { $, E, Either } from "lib/Effect"
3+
import { E, Either } from "lib/Effect"
44
import { generateDones } from "lib/generateDones"
55
import { Contact } from "schema/Contact"
66
import { DoneEntry } from "schema/DoneEntry"
@@ -56,40 +56,40 @@ describe("Collection", () => {
5656
describe("constructor", () => {
5757
it("instantiates the collection", () => {
5858
const { collection } = setup()
59-
expect($(collection.all())).toHaveLength(14)
59+
expect(collection.all()).toHaveLength(14)
6060
})
6161
})
6262

6363
describe("find", () => {
6464
it("finds an item by ID", () => {
6565
const { dones, collection } = setup()
6666
const { id } = dones[0]
67-
const item = $(collection.find(id))
67+
const item = collection.find(id)
6868
expect(item).toEqual(dones[0])
6969
})
7070

7171
it("fails if an ID is not found", () => {
7272
const { collection } = setup()
7373

74-
const result = collection.find("pizza").pipe(E.either, E.runSync)
74+
const result = collection.effect.find("pizza").pipe(E.either, E.runSync)
7575
expect(Either.isLeft(result)).toBe(true)
7676
})
7777

7878
it("finds an item by a non-unique indexed property", () => {
7979
const { collection } = setup()
80-
const items = $(collection.findBy("contactId", alice.id))
80+
const items = collection.findBy("contactId", alice.id)
8181
expect(items).toHaveLength(7)
8282
})
8383

8484
it("finds an item using an accessor function", () => {
8585
const { collection } = setup()
86-
const items = $(collection.findBy("week", "2024-11-03"))
86+
const items = collection.findBy("week", "2024-11-03")
8787
expect(items).toHaveLength(8)
8888
})
8989

9090
it("finds an item by year", () => {
9191
const { collection } = setup()
92-
const items = $(collection.findBy("year", 2024))
92+
const items = collection.findBy("year", 2024)
9393
expect(items).toHaveLength(14)
9494
})
9595
})
@@ -102,16 +102,16 @@ describe("Collection", () => {
102102
content: "new",
103103
date: LocalDate.parse("2024-11-04"),
104104
})
105-
const result = $(collection.add(newDone))
105+
const result = collection.add(newDone)
106106
expect(result).toBe(newDone)
107-
expect($(collection.all())).toHaveLength(15)
108-
expect($(collection.findBy("contactId", alice.id))).toHaveLength(8)
109-
expect($(collection.findBy("week", "2024-11-03"))).toHaveLength(9)
107+
expect(collection.all()).toHaveLength(15)
108+
expect(collection.findBy("contactId", alice.id)).toHaveLength(8)
109+
expect(collection.findBy("week", "2024-11-03")).toHaveLength(9)
110110
})
111111

112112
it("adds multiple items to the collection", () => {
113113
const { collection } = setup()
114-
expect($(collection.all())).toHaveLength(14)
114+
expect(collection.all()).toHaveLength(14)
115115

116116
// generate a bunch of dones
117117
const dones = generateDones({
@@ -121,59 +121,59 @@ describe("Collection", () => {
121121
enthusiasm: 0.1,
122122
contacts,
123123
})
124-
const result = $(collection.add(dones))
124+
const result = collection.add(dones)
125125
expect(result).toBe(dones)
126-
expect($(collection.all())).toHaveLength(224)
126+
expect(collection.all()).toHaveLength(224)
127127
})
128128
})
129129

130130
describe("update", () => {
131131
it("updates a string value", () => {
132132
const { collection } = setup()
133-
const dones = $(collection.all())
133+
const dones = collection.all()
134134
const item = dones[0]
135-
$(collection.update({ ...item, content: "updated" }))
136-
const updated = $(collection.find(item.id))
135+
collection.update({ ...item, content: "updated" })
136+
const updated = collection.find(item.id)
137137
expect(updated.content).toBe("updated")
138138
})
139139

140140
it("updates a date value", () => {
141141
const { collection } = setup()
142-
const dones = $(collection.all())
142+
const dones = collection.all()
143143
const item = dones[0]
144-
$(collection.update({ ...item, date: LocalDate.parse("2024-11-05") }))
144+
collection.update({ ...item, date: LocalDate.parse("2024-11-05") })
145145

146-
const updated = $(collection.find(item.id))
146+
const updated = collection.find(item.id)
147147
expect(updated.date.toString()).toBe("2024-11-05")
148148
})
149149

150150
it("updates an array value", () => {
151151
const { collection } = setup()
152-
const dones = $(collection.all())
152+
const dones = collection.all()
153153
const item = dones[0]
154-
$(collection.update({ ...item, likes: [bob] }))
154+
collection.update({ ...item, likes: [bob] })
155155

156-
const updated = $(collection.find(item.id))
156+
const updated = collection.find(item.id)
157157
expect(updated.likes).toEqual([bob])
158158
})
159159
})
160160

161161
describe("destroy", () => {
162162
it("removes an item from the collection", () => {
163163
const { collection } = setup()
164-
const dones = $(collection.all())
164+
const dones = collection.all()
165165
const item = dones[0]
166-
$(collection.destroy(item.id))
166+
collection.destroy(item.id)
167167

168-
expect($(collection.all())).toHaveLength(13)
168+
expect(collection.all()).toHaveLength(13)
169169
})
170170
})
171171

172172
describe("destroyAll", () => {
173173
it("removes all items from the collection", () => {
174174
const { collection } = setup()
175-
$(collection.destroyAll())
176-
expect($(collection.all())).toHaveLength(0)
175+
collection.destroyAll()
176+
expect(collection.all()).toHaveLength(0)
177177
})
178178
})
179179
})

0 commit comments

Comments
 (0)