Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/bindx-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export { ComponentBrand } from './brand/ComponentBrand.js'
export type { AnyBrand } from './brand/ComponentBrand.js'

// Utils
export { generateHasManyAlias } from './utils/aliasGenerator.js'
export { canHasManyParamsExcludeMembers, generateHasManyAlias } from './utils/aliasGenerator.js'
export type { IsPlainObject } from './utils/fieldShape.js'

// Query Builder (static qb module)
Expand Down
13 changes: 13 additions & 0 deletions packages/bindx-client/src/utils/aliasGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,16 @@ export function generateHasManyAlias(fieldName: string, params?: HasManyParams):

return `${fieldName}_${hashStr}`
}

/**
* Whether a has-many selected with {@link params} can be missing members of the
* relation — i.e. whether the server may have left rows out of this read.
*
* `orderBy` and `totalCount` only shape how the rows come back, so a view that
* carries nothing else still contains every member even though it is read under a
* generated alias.
*/
export function canHasManyParamsExcludeMembers(params?: HasManyParams): boolean {
if (!params) return false
return params.filter !== undefined || params.limit !== undefined || params.offset !== undefined
}
1 change: 0 additions & 1 deletion packages/bindx/src/core/ActionDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,6 @@ export class ActionDispatcher {
action.fieldName,
action.itemKey,
action.removalType,
action.alias,
)
break

Expand Down
4 changes: 1 addition & 3 deletions packages/bindx/src/core/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ export interface RemoveFromListAction {
readonly fieldName: string
readonly itemKey: string
readonly removalType: 'disconnect' | 'delete'
readonly alias?: string
}

/**
Expand Down Expand Up @@ -581,9 +580,8 @@ export function removeFromList(
fieldName: string,
itemKey: string,
removalType: 'disconnect' | 'delete',
alias?: string,
): RemoveFromListAction {
return { type: 'REMOVE_FROM_LIST', entityType, entityId, fieldName, itemKey, removalType, alias }
return { type: 'REMOVE_FROM_LIST', entityType, entityId, fieldName, itemKey, removalType }
}

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/bindx/src/handles/EntityHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../core/actions.js'
import { FIELD_REF_META, type FieldRefMeta, type FieldAccessor, type HasOneAccessor, type HasManyAccessor, type EntityFieldsAccessor, type Unsubscribe, type EntityAccessor } from './types.js'
import { deepEqual } from '../utils/deepEqual.js'
import { canHasManyParamsExcludeMembers } from '../utils/aliasGenerator.js'
import { createClientError, type ErrorInput, type FieldError } from '../errors/types.js'
import type {
EventTypeMap,
Expand Down Expand Up @@ -511,6 +512,16 @@ export class EntityHandle<T extends object = object, TSelected = T> extends Enti
// Has-many relation - return HasManyListHandle.
// Thread the selected alias so the handle reads data stored under the
// auto-generated alias (e.g. `tags_<hash>`) for params-bearing relations.
// The alias is a hash of the params, so the store cannot tell a filtered
// view from one that only orders — say so here, where the params are.
if (fieldMeta && fieldMeta.alias !== schemaFieldName) {
this.store.declareHasManyViewMembership(
this.entityType,
schemaFieldName,
fieldMeta.alias,
canHasManyParamsExcludeMembers(fieldMeta.hasManyParams) ? 'partial' : 'total',
)
}
return this.hasMany(schemaFieldName, fieldMeta?.alias, nestedSelection)
}

Expand Down
29 changes: 13 additions & 16 deletions packages/bindx/src/handles/HasManyListHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,23 +406,18 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt

/**
* Checks if the list is dirty (items added/removed/moved/connected/disconnected).
*
* Pending writes belong to the relation, not to this view, so a sibling view's
* unsaved change reports here too — persisting either view sends the whole
* relation, and a view claiming to be clean while pushing its sibling's writes
* would be incoherent. Only the manual ordering is this view's own.
*/
get isDirty(): boolean {
this.materializeEmbeddedItems()

const state = this.store.getHasMany(
this.entityType,
this.entityId,
this.fieldName,
this.alias,
)

if (!state) return false

return (
state.plannedRemovals.size > 0 ||
state.plannedAdditions.size > 0 ||
state.orderedIds !== null
this.store.hasPendingHasManyWrites(this.entityType, this.entityId, this.fieldName)
|| this.store.hasExplicitHasManyOrder(this.entityType, this.entityId, this.fieldName, this.alias)
)
}

Expand Down Expand Up @@ -452,7 +447,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
disconnect(itemId: string): void {
this.materializeEmbeddedItems()
this.dispatcher.dispatch(
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, 'disconnect', this.alias),
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, 'disconnect'),
)
}

Expand All @@ -464,7 +459,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
delete(itemId: string): void {
this.materializeEmbeddedItems()
this.dispatcher.dispatch(
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, 'delete', this.alias),
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, 'delete'),
)
}

Expand Down Expand Up @@ -503,7 +498,7 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
remove(itemId: string): void {
this.materializeEmbeddedItems()
this.dispatcher.dispatch(
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, this.resolveRemovalType(), this.alias),
removeFromList(this.entityType, this.entityId, this.fieldName, itemId, this.resolveRemovalType()),
)
}

Expand Down Expand Up @@ -541,14 +536,16 @@ export class HasManyListHandle<TEntity extends object = object, TSelected = TEnt
/**
* Resets the has-many relation to server state.
* Clears all planned connections and removals.
*
* Relation-level, like {@link isDirty}: planned removals carry no view, so a
* per-view reset could not know which of them to drop.
*/
reset(): void {
this.materializeEmbeddedItems()
this.store.resetHasMany(
this.entityType,
this.entityId,
this.fieldName,
this.alias,
)
}

Expand Down
6 changes: 6 additions & 0 deletions packages/bindx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ export type {
export type {
StoredRelationState,
StoredHasManyState,
HasManyView,
HasManyViewMembership,
PlannedHasManyAddition,
HasManyAdditionKind,
HasManyRelationProjection,
HasManyViewProjection,
EntityMeta,
} from './store/SnapshotStore.js'

Expand Down
Loading
Loading