Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
- [ ] make computeds run all the time by updating getter
- [ ] make relationshipMeta always return a descriptive value, and make field accessor never throw NotLoaded

Note from LM: The issue I have run into is that if the field accessor doesn't throw, there is no way for the indexer to wait for the card to be fully loaded (currently we are catching NotLoaded errors when attempting to render a card for indexing)

ef4: makes sense. we can make the relationship loading process register itself somehow. It's not unlike test waiters or fastboot deferred.
  • Loading branch information
lukemelia committed Jun 11, 2024
1 parent 8e1fe92 commit 1c9e54d
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 152 deletions.
98 changes: 54 additions & 44 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,13 @@ function getter<CardT extends BaseDefConstructor>(
cardTracking.get(instance);

if (field.computeVia) {
let value = deserialized.get(field.name);
if (isStaleValue(value)) {
value = value.staleValue;
} else if (!deserialized.has(field.name)) {
value = field.computeVia.bind(instance)();
deserialized.set(field.name, value);
let result = field.computeVia.bind(instance)() as unknown as
| undefined
| BaseInstanceType<CardT>;
if (result !== undefined) {
return result;
}
return value;
return field.emptyValue(instance);
} else {
if (deserialized.has(field.name)) {
return deserialized.get(field.name);
Expand Down Expand Up @@ -787,11 +786,23 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
cardTracking.get(instance);
let maybeNotLoaded = deserialized.get(this.name);
if (isNotLoadedValue(maybeNotLoaded)) {
throw new NotLoaded(instance, maybeNotLoaded.reference, this.name);
return this.emptyValue(instance) as unknown as BaseInstanceType<CardT>;
// throw new NotLoaded(instance, maybeNotLoaded.reference, this.name);
}
return getter(instance, this);
}

getRelationshipMeta(instance: CardDef) {
let deserialized = getDataBucket(instance);
let maybeNotLoaded = deserialized.get(this.name);

if (isNotLoadedValue(maybeNotLoaded)) {
return { type: 'not-loaded', reference: maybeNotLoaded.reference };
} else {
return { type: 'loaded', card: maybeNotLoaded ?? null };
}
}

queryableValue(instance: any, stack: CardDef[]): any {
if (primitive in this.card) {
throw new Error(
Expand Down Expand Up @@ -1099,22 +1110,40 @@ class LinksToMany<FieldT extends CardDefConstructor>
}

getter(instance: CardDef): BaseInstanceType<FieldT> {
// let deserialized = getDataBucket(instance);
// cardTracking.get(instance);
// let maybeNotLoaded = deserialized.get(this.name);
// if (maybeNotLoaded) {
// let notLoadedRefs: string[] = [];
// for (let entry of maybeNotLoaded) {
// if (isNotLoadedValue(entry)) {
// notLoadedRefs = [...notLoadedRefs, entry.reference];
// }
// }
// if (notLoadedRefs.length > 0) {
// throw new NotLoaded(instance, notLoadedRefs, this.name);
// }
// }

return getter(instance, this);
}

getRelationshipMeta(instance: CardDef) {
let deserialized = getDataBucket(instance);
cardTracking.get(instance);
let maybeNotLoaded = deserialized.get(this.name);
if (maybeNotLoaded) {
let notLoadedRefs: string[] = [];
for (let entry of maybeNotLoaded) {
if (isNotLoadedValue(entry)) {
notLoadedRefs = [...notLoadedRefs, entry.reference];
}
}
if (notLoadedRefs.length > 0) {
throw new NotLoaded(instance, notLoadedRefs, this.name);
}
}

return getter(instance, this);
if (!Array.isArray(maybeNotLoaded)) {
throw new Error(
`expected ${this.name} to be an array but was ${typeof maybeNotLoaded}`,
);
}
return maybeNotLoaded.map((rel) => {
if (isNotLoadedValue(rel)) {
return { type: 'not-loaded', reference: rel.reference };
} else {
return { type: 'loaded', card: rel ?? null };
}
});
}

queryableValue(instances: any[] | null, stack: CardDef[]): any[] | null {
Expand Down Expand Up @@ -2318,27 +2347,7 @@ export function relationshipMeta(
if (!(field.fieldType === 'linksTo' || field.fieldType === 'linksToMany')) {
return undefined;
}
let related = getter(instance, field) as CardDef; // only compound cards can be linksTo fields
if (field.fieldType === 'linksToMany') {
if (!Array.isArray(related)) {
throw new Error(
`expected ${fieldName} to be an array but was ${typeof related}`,
);
}
return related.map((rel) => {
if (isNotLoadedValue(rel)) {
return { type: 'not-loaded', reference: rel.reference };
} else {
return { type: 'loaded', card: rel ?? null };
}
});
}

if (isNotLoadedValue(related)) {
return { type: 'not-loaded', reference: related.reference };
} else {
return { type: 'loaded', card: related ?? null };
}
return field.getRelationshipMeta(instance);
}

function serializedGet<CardT extends BaseDefConstructor>(
Expand Down Expand Up @@ -2855,7 +2864,7 @@ function makeDescriptor<
}
}
notifySubscribers(this, field.name, value);
logger.log(recompute(this));
cardTracking.set(this, true);
};
}
if (field.description) {
Expand Down Expand Up @@ -3092,7 +3101,8 @@ export function getFields(
if (
opts?.usedFieldsOnly &&
!usedFields.includes(maybeFieldName) &&
!maybeField.isUsed
!maybeField.isUsed &&
!maybeField.computeVia
) {
return [];
}
Expand Down
91 changes: 41 additions & 50 deletions packages/base/room.gts
Original file line number Diff line number Diff line change
Expand Up @@ -330,12 +330,6 @@ interface RoomState {
created?: number;
}

// in addition to acting as a cache, this also ensures we have
// triple equal equivalence for the interior cards of RoomField
const eventCache = initSharedState(
'eventCache',
() => new WeakMap<RoomField, Map<string, MatrixEvent>>(),
);
const messageCache = initSharedState(
'messageCache',
() => new WeakMap<RoomField, Map<string, MessageField>>(),
Expand All @@ -353,39 +347,34 @@ const fragmentCache = initSharedState(
() => new WeakMap<RoomField, Map<string, CardFragmentContent>>(),
);

function newEvents(events: () => MatrixEvent[]): () => MatrixEvent[] {
let seen = new Set<string>();
return function () {
return events().filter((e) => {
if (seen.has(e.event_id)) {
return false;
}
seen.add(e.event_id);
return true;
});
};
}

export class RoomField extends FieldDef {
static displayName = 'Room';

// the only writeable field for this card should be the "events" field.
// All other fields should derive from the "events" field.
@field events = containsMany(MatrixEventField);

// This works well for synchronous computeds only
@field newEvents = containsMany(MatrixEventField, {
computeVia: function (this: RoomField) {
let cache = eventCache.get(this);
if (!cache) {
cache = new Map();
eventCache.set(this, cache);
}
let newEvents = new Map<string, MatrixEvent>();
for (let event of this.events) {
if (cache.has(event.event_id)) {
continue;
}
cache.set(event.event_id, event);
newEvents.set(event.event_id, event);
}
return [...newEvents.values()];
},
});

@field roomId = contains(StringField, {
computeVia: function (this: RoomField) {
return this.events.length > 0 ? this.events[0].room_id : undefined;
},
});

private nameEvents = newEvents(() => this.events);

@field name = contains(StringField, {
computeVia: function (this: RoomField) {
let roomState = roomStateCache.get(this);
Expand All @@ -394,9 +383,7 @@ export class RoomField extends FieldDef {
roomStateCache.set(this, roomState);
}

// Read from this.events instead of this.newEvents to avoid a race condition bug where
// newEvents never returns the m.room.name while the event is present in events
let events = this.events
let events = this.nameEvents()
.filter((e) => e.type === 'm.room.name')
.sort(
(a, b) => a.origin_server_ts - b.origin_server_ts,
Expand All @@ -409,6 +396,8 @@ export class RoomField extends FieldDef {
},
});

private creatorEvents = newEvents(() => this.events);

@field creator = contains(RoomMemberField, {
computeVia: function (this: RoomField) {
let roomState = roomStateCache.get(this);
Expand All @@ -420,9 +409,9 @@ export class RoomField extends FieldDef {
if (creator) {
return creator;
}
let event = this.newEvents.find((e) => e.type === 'm.room.create') as
| RoomCreateEvent
| undefined;
let event = this.creatorEvents().find(
(e) => e.type === 'm.room.create',
) as RoomCreateEvent | undefined;
if (event) {
roomState.creator = upsertRoomMember({
room: this,
Expand All @@ -433,6 +422,8 @@ export class RoomField extends FieldDef {
},
});

private createdEvents = newEvents(() => this.events);

@field created = contains(DateTimeField, {
computeVia: function (this: RoomField) {
let roomState = roomStateCache.get(this);
Expand All @@ -444,9 +435,9 @@ export class RoomField extends FieldDef {
if (created != null) {
return new Date(created);
}
let event = this.newEvents.find((e) => e.type === 'm.room.create') as
| RoomCreateEvent
| undefined;
let event = this.createdEvents().find(
(e) => e.type === 'm.room.create',
) as RoomCreateEvent | undefined;
if (event) {
roomState.created = event.origin_server_ts;
}
Expand All @@ -456,19 +447,23 @@ export class RoomField extends FieldDef {
},
});

private roomMemberEvents = newEvents(() => this.events);

@field roomMembers = containsMany(RoomMemberField, {
computeVia: function (this: RoomField) {
console.log('computing roomMembers');
let roomMembers = roomMemberCache.get(this);
if (!roomMembers) {
roomMembers = new Map();
roomMemberCache.set(this, roomMembers);
}

for (let event of this.newEvents) {
for (let event of this.roomMemberEvents()) {
if (event.type !== 'm.room.member') {
continue;
}
let userId = event.state_key;
console.log('upsertRoomMember');
upsertRoomMember({
room: this,
userId,
Expand All @@ -482,6 +477,8 @@ export class RoomField extends FieldDef {
},
});

private messageEvents = newEvents(() => this.events);

@field messages = containsMany(MessageField, {
// since we are rendering this card without the isolated renderer, we cannot use
// the rendering mechanism to test if a field is used or not, so we explicitely
Expand All @@ -502,8 +499,7 @@ export class RoomField extends FieldDef {
messageCache.set(this, cache);
}
let index = cache.size;
let newMessages = new Map<string, MessageField>();
for (let event of this.events) {
for (let event of this.messageEvents()) {
if (event.type !== 'm.room.message') {
continue;
}
Expand Down Expand Up @@ -611,26 +607,17 @@ export class RoomField extends FieldDef {
if (messageField) {
// if the message is a replacement for other messages,
// use `created` from the oldest one.
if (newMessages.has(event_id)) {
messageField.created = newMessages.get(event_id)!.created;
if (cache.has(event_id)) {
messageField.created = cache.get(event_id)!.created;
}
newMessages.set(
cache.set(
(event.content as CardMessageContent).clientGeneratedId ?? event_id,
messageField,
);
index++;
}
}

// update the cache with the new messages
for (let [id, message] of newMessages) {
// The `id` can either be an `eventId` or `clientGeneratedId`.
// For messages sent by the user, we prefer to use `clientGeneratedId`
// because `eventId` can change in certain scenarios,
// such as when resending a failed message or updating its status from sending to sent.
cache.set(id, message);
}

// this sort should hopefully be very optimized since events will
// be close to chronological order
return [...cache.values()].sort(
Expand All @@ -641,12 +628,16 @@ export class RoomField extends FieldDef {

@field joinedMembers = containsMany(RoomMemberField, {
computeVia: function (this: RoomField) {
console.log('computing joinedMembers');
console.log('this.roomMembers', this.roomMembers);
return this.roomMembers.filter((m) => m.membership === 'join');
},
});

@field invitedMembers = containsMany(RoomMemberField, {
computeVia: function (this: RoomField) {
console.log('computing invitedMembers');
console.log('this.roomMembers', this.roomMembers);
return this.roomMembers.filter((m) => m.membership === 'invite');
},
});
Expand Down
10 changes: 9 additions & 1 deletion packages/host/app/components/ai-assistant/panel.gts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@ export default class AiAssistantPanel extends Component<Signature> {
@cached
private get aiSessionRooms() {
let rooms: RoomField[] = [];
for (let resource of this.roomResources.values()) {
let roomResources = this.roomResources.values();
console.log('roomResources', roomResources);
for (let resource of roomResources) {
if (!resource.room) {
continue;
}
Expand All @@ -385,12 +387,18 @@ export default class AiAssistantPanel extends Component<Signature> {
// rooms don't immediately have a created date
room.created = new Date();
}
console.log('this.matrixService.userId', this.matrixService.userId);
console.log('room.invitedMembers', room.invitedMembers);
console.log('room.joinedMembers', room.joinedMembers);
if (
(room.invitedMembers.find((m) => aiBotUserId === m.userId) ||
room.joinedMembers.find((m) => aiBotUserId === m.userId)) &&
room.joinedMembers.find((m) => this.matrixService.userId === m.userId)
) {
console.log('pushing room to result set', room);
rooms.push(room);
} else {
console.log('excluding room from result set', room);
}
}
// sort in reverse chronological order of last activity
Expand Down
Loading

0 comments on commit 1c9e54d

Please sign in to comment.