Skip to content

Commit 62a313a

Browse files
authored
fix(composables): repair frozen size getters and drop the ES2025 Set dependency (#333)
1 parent 904e953 commit 62a313a

7 files changed

Lines changed: 78 additions & 6 deletions

File tree

packages/0/PHILOSOPHY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ import { createRegistry } from '../createRegistry'
151151

152152
**Statement.** Composable extension happens by `{ ...parent, newProperty }`. Never redefine a property the parent already exposed. [intent:101, intent:142, intent:147]
153153

154-
**Why.** Spread guarantees the child remains substitutable for the parent in types. The 27 registry-based composables all compose this way; consumers who hold a reference to the parent type can upgrade to the child type without refactoring call sites.
154+
**Why.** Spread guarantees the child remains substitutable for the parent in types. The 27 registry-based composables all compose this way; consumers who hold a reference to the parent type can upgrade to the child type without refactoring call sites. Spread copies accessors by value — a parent's `get size ()` arrives as a data property frozen at spread time — so any accessor the child surface needs must be re-declared after the spread (`get size () { return model.size }`).
155155

156156
**Canonical example.** `packages/0/src/composables/createSelection/index.ts:296-309` — spreads `createModel`, adds `multiple`, `toggle`, `apply`, `mandate`.
157157

packages/0/src/composables/createProgress/index.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ describe('createProgress', () => {
7474
const ticket = progress.register()
7575
expect(ticket.id).toBeDefined()
7676
})
77+
78+
it('should track size for segments registered after creation', () => {
79+
const progress = setup()
80+
progress.register()
81+
progress.register()
82+
progress.register()
83+
expect(progress.size).toBe(3)
84+
})
7785
})
7886

7987
describe('total', () => {

packages/0/src/composables/createProgress/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ export function createProgress (options: ProgressOptions = {}): ProgressContext
175175
apply,
176176
min,
177177
max,
178-
} as ProgressContext
178+
get size () {
179+
return model.size
180+
},
181+
} satisfies ProgressContext as ProgressContext
179182
}
180183

181184
/**

packages/0/src/composables/createSelection/index.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,49 @@ describe('createSelection', () => {
10211021
// Mandatory must keep at least one selected, like in non-multiple mode
10221022
expect(selection.selectedIds.size).toBe(1)
10231023
})
1024+
1025+
it('should apply in multiple mode without Set.prototype.difference', () => {
1026+
const original = Set.prototype.difference
1027+
1028+
// @ts-expect-error removing the ES2025 method to pin the documented ES2016 floor
1029+
delete Set.prototype.difference
1030+
1031+
try {
1032+
const selection = createSelection({ multiple: true })
1033+
selection.onboard([
1034+
{ id: 'a', value: 'A' },
1035+
{ id: 'b', value: 'B' },
1036+
{ id: 'c', value: 'C' },
1037+
])
1038+
selection.select('a')
1039+
selection.select('b')
1040+
1041+
selection.apply(['B', 'C'])
1042+
1043+
expect(selection.selectedIds.has('a')).toBe(false)
1044+
expect(selection.selectedIds.has('b')).toBe(true)
1045+
expect(selection.selectedIds.has('c')).toBe(true)
1046+
expect(selection.selectedIds.size).toBe(2)
1047+
} finally {
1048+
Set.prototype.difference = original
1049+
}
1050+
})
1051+
1052+
it('should keep the mandatory survivor when apply swaps the last selected id', () => {
1053+
const selection = createSelection({ multiple: true, mandatory: true })
1054+
selection.onboard([
1055+
{ id: 'a', value: 'A' },
1056+
{ id: 'b', value: 'B' },
1057+
])
1058+
selection.select('a')
1059+
1060+
selection.apply(['B'])
1061+
1062+
// Removals run before additions, so the mandatory guard keeps 'a'
1063+
expect(selection.selectedIds.has('a')).toBe(true)
1064+
expect(selection.selectedIds.has('b')).toBe(true)
1065+
expect(selection.selectedIds.size).toBe(2)
1066+
})
10241067
})
10251068

10261069
describe('custom ticket types', () => {

packages/0/src/composables/createSelection/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,11 @@ export function createSelection<
253253
}
254254

255255
if (isMultiple) {
256-
for (const id of currentIds.difference(targetIds)) {
257-
unselect(id)
256+
for (const id of currentIds) {
257+
if (!targetIds.has(id)) unselect(id)
258258
}
259-
for (const id of targetIds.difference(currentIds)) {
260-
model.selectedIds.add(id)
259+
for (const id of targetIds) {
260+
if (!currentIds.has(id)) model.selectedIds.add(id)
261261
}
262262
} else {
263263
const next = targetIds.values().next().value

packages/0/src/composables/usePermissions/index.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ describe('createPermissions', () => {
197197
const missing = permissions.get('admin.delete.users')
198198
expect(missing).toBeUndefined()
199199
})
200+
201+
it('should track size for tokens registered after creation', () => {
202+
const permissions = createPermissions({
203+
permissions: {
204+
admin: [
205+
['read', 'users'],
206+
],
207+
},
208+
})
209+
210+
permissions.register({ id: 'editor.edit.posts', value: true })
211+
permissions.register({ id: 'viewer.read.posts', value: true })
212+
213+
expect(permissions.size).toBe(3)
214+
})
200215
})
201216

202217
describe('v0PermissionsAdapter', () => {

packages/0/src/composables/usePermissions/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export function createPermissions (_options: PermissionOptions = {}): Permission
113113
return {
114114
...tokens,
115115
can,
116+
get size () {
117+
return tokens.size
118+
},
116119
} as PermissionContext
117120
}
118121

0 commit comments

Comments
 (0)