# Buggy code
def compute_state_delta(
    self, room_id, batch, sync_config, since_token, now_token, full_state
):
    """Works out the difference in state between the start of the timeline
    and the previous sync.

    Args:
        room_id(str):
        batch(synapse.handlers.sync.TimelineBatch): The timeline batch for
            the room that will be sent to the user.
        sync_config(synapse.handlers.sync.SyncConfig):
        since_token(str|None): Token of the end of the previous batch. May
            be None.
        now_token(str): Token of the end of the current batch.
        full_state(bool): Whether to force returning the full state.

    Returns:
         A deferred dict of (type, state_key) -> Event
    """
    # TODO(mjark) Check if the state events were received by the server
    # after the previous sync, since we need to include those state
    # updates even if they occured logically before the previous event.
    # TODO(mjark) Check for new redactions in the state events.

    with Measure(self.clock, "compute_state_delta"):
        members_to_fetch = None

        lazy_load_members = sync_config.filter_collection.lazy_load_members()
        include_redundant_members = (
            sync_config.filter_collection.include_redundant_members()
        )

        if lazy_load_members:
            # We only request state for the members needed to display the
            # timeline:

            members_to_fetch = set(
                event.sender  # FIXME: we also care about invite targets etc.
                for event in batch.events
            )

            if full_state:
                # always make sure we LL ourselves so we know we're in the room
                # (if we are) to fix https://github.com/vector-im/riot-web/issues/7209
                # We only need apply this on full state syncs given we disabled
                # LL for incr syncs in #3840.
                members_to_fetch.add(sync_config.user.to_string())

            state_filter = StateFilter.from_lazy_load_member_list(members_to_fetch)
        else:
            state_filter = StateFilter.all()

        timeline_state = {
            (event.type, event.state_key): event.event_id
            for event in batch.events
            if event.is_state()
        }

        if full_state:
            if batch:
                current_state_ids = yield self.store.get_state_ids_for_event(
                    batch.events[-1].event_id, state_filter=state_filter
                )

                state_ids = yield self.store.get_state_ids_for_event(
                    batch.events[0].event_id, state_filter=state_filter
                )

            else:
                current_state_ids = yield self.get_state_at(
                    room_id, stream_position=now_token, state_filter=state_filter
                )

                state_ids = current_state_ids

            state_ids = _calculate_state(
                timeline_contains=timeline_state,
                timeline_start=state_ids,
                previous={},
                current=current_state_ids,
                lazy_load_members=lazy_load_members,
            )
        elif batch.limited:
            state_at_timeline_start = yield self.store.get_state_ids_for_event(
                batch.events[0].event_id, state_filter=state_filter
            )

            # for now, we disable LL for gappy syncs - see
            # https://github.com/vector-im/riot-web/issues/7211#issuecomment-419976346
            # N.B. this slows down incr syncs as we are now processing way
            # more state in the server than if we were LLing.
            #
            # We still have to filter timeline_start to LL entries (above) in order
            # for _calculate_state's LL logic to work, as we have to include LL
            # members for timeline senders in case they weren't loaded in the initial
            # sync.  We do this by (counterintuitively) by filtering timeline_start
            # members to just be ones which were timeline senders, which then ensures
            # all of the rest get included in the state block (if we need to know
            # about them).
            state_filter = StateFilter.all()

            state_at_previous_sync = yield self.get_state_at(
                room_id, stream_position=since_token, state_filter=state_filter
            )

            current_state_ids = yield self.store.get_state_ids_for_event(
                batch.events[-1].event_id, state_filter=state_filter
            )

            state_ids = _calculate_state(
                timeline_contains=timeline_state,
                timeline_start=state_at_timeline_start,
                previous=state_at_previous_sync,
                current=current_state_ids,
                # we have to include LL members in case LL initial sync missed them
                lazy_load_members=lazy_load_members,
            )
        else:
            state_ids = {}
            if lazy_load_members:
                if members_to_fetch and batch.events:
                    # We're returning an incremental sync, with no
                    # "gap" since the previous sync, so normally there would be
                    # no state to return.
                    # But we're lazy-loading, so the client might need some more
                    # member events to understand the events in this timeline.
                    # So we fish out all the member events corresponding to the
                    # timeline here, and then dedupe any redundant ones below.

                    state_ids = yield self.store.get_state_ids_for_event(
                        batch.events[0].event_id,
                        # we only want members!
                        state_filter=StateFilter.from_types(
                            (EventTypes.Member, member) for member in members_to_fetch
                        ),
                    )

        if lazy_load_members and not include_redundant_members:
            cache_key = (sync_config.user.to_string(), sync_config.device_id)
            cache = self.get_lazy_loaded_members_cache(cache_key)

            # if it's a new sync sequence, then assume the client has had
            # amnesia and doesn't want any recent lazy-loaded members
            # de-duplicated.
            if since_token is None:
                logger.debug("clearing LruCache for %r", cache_key)
                cache.clear()
            else:
                # only send members which aren't in our LruCache (either
                # because they're new to this client or have been pushed out
                # of the cache)
                logger.debug("filtering state from %r...", state_ids)
                state_ids = {
                    t: event_id
                    for t, event_id in iteritems(state_ids)
                    if cache.get(t[1]) != event_id
                }
                logger.debug("...to %r", state_ids)

            # add any member IDs we are about to send into our LruCache
            for t, event_id in itertools.chain(
                state_ids.items(), timeline_state.items()
            ):
                if t[0] == EventTypes.Member:
                    cache.set(t[1], event_id)

    state = {}
    if state_ids:
        state = yield self.store.get_events(list(state_ids.values()))

    return {
        (e.type, e.state_key): e
        for e in sync_config.filter_collection.filter_room_state(list(state.values()))
    }


# Traceback (IndexError)
2019-06-09 19:03:46,101 - synapse.handlers.sync - 909 - INFO - GET-835 - Calculating sync response for @uelen:riot.firechicken.net between StreamToken(room_key='s707461', presence_key='26561430', typing_key='2874', receipt_key='634533', account_data_key='24909', push_rules_key='56', to_device_key='95', device_list_key='55442', groups_key='1') and StreamToken(room_key='s709258', presence_key=26599051, typing_key=57, receipt_key=636004, account_data_key=24914, push_rules_key=56, to_device_key=95, device_list_key=55442, groups_key=1)
2019-06-09 19:03:46,150 - synapse.metrics - 372 - INFO -  - Collecting gc 0
2019-06-09 19:03:46,437 - synapse.metrics - 372 - INFO -  - Collecting gc 0
2019-06-09 19:03:46,446 - synapse.handlers.sync - 1810 - INFO - GET-835 - Incremental gappy sync of !IFlHsKQHpcFgnpwgFn:matrix.org for user @uelen:riot.firechicken.net with 0 state events
2019-06-09 19:03:46,490 - synapse.handlers.sync - 1810 - INFO - GET-835 - Incremental gappy sync of !MbRaSiMIRhhxDtJENL:maunium.net for user @uelen:riot.firechicken.net with 2 state events
2019-06-09 19:03:46,493 - synapse.metrics - 372 - INFO -  - Collecting gc 0
2019-06-09 19:03:46,500 - synapse.handlers.sync - 1810 - INFO - GET-835 - Incremental gappy sync of !iNmaIQExDMeqdITdHH:matrix.org for user @uelen:riot.firechicken.net with 2 state events
2019-06-09 19:03:46,505 - synapse.handlers.sync - 1810 - INFO - GET-835 - Incremental gappy sync of !tvlbVxTSVUgJbmelND:matrix.org for user @uelen:riot.firechicken.net with 3 state events
2019-06-09 19:03:46,553 - synapse.http.server - 112 - ERROR - GET-835 - Failed handle request via 'SyncRestServlet': <SynapseRequest at 0x7f955618ce10 method='GET' uri='/_matrix/client/r0/sync?filter=0&timeout=0&since=s707461_26561430_2874_634533_24909_56_95_55442_1' clientproto='HTTP/1.0' site=8083>
Traceback (most recent call last):
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
    result = g.send(result)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/storage/_base.py", line 527, in runWithConnection
    defer.returnValue(result)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1362, in returnValue
    raise _DefGen_Return(val)
twisted.internet.defer._DefGen_Return: [_EventDictReturn(event_id='$15600950391010258hrwAd:matrix.org', topological_ordering=None, stream_ordering=708773), _EventDictReturn(event_id='$15600950171010117CdspK:matrix.org', topological_ordering=None, stream_ordering=708772), _EventDictReturn(event_id='$15600949401009508lTpzl:matrix.org', topological_ordering=None, stream_ordering=708771), _EventDictReturn(event_id='$15600949261009398foetQ:matrix.org', topological_ordering=None, stream_ordering=708770), _EventDictReturn(event_id='$15600949091009307PONGY:matrix.org', topological_ordering=None, stream_ordering=708769), _EventDictReturn(event_id='$15600948911009164JkHeG:matrix.org', topological_ordering=None, stream_ordering=708768), _EventDictReturn(event_id='$15600948821009108qkrIj:matrix.org', topological_ordering=None, stream_ordering=708767), _EventDictReturn(event_id='$15600948591008929UoHxd:matrix.org', topological_ordering=None, stream_ordering=708766), _EventDictReturn(event_id='$15600948501008732joyAz:matrix.org', topological_ordering=None, stream_ordering=708765), _EventDictReturn(event_id='$15600948321008586DDtRZ:matrix.org', topological_ordering=None, stream_ordering=708764), _EventDictReturn(event_id='$15600948311008562BFdsx:matrix.org', topological_ordering=None, stream_ordering=708763), _EventDictReturn(event_id='$15600948311008555lylfW:matrix.org', topological_ordering=None, stream_ordering=708762), _EventDictReturn(event_id='$15600948051008364bsBFK:matrix.org', topological_ordering=None, stream_ordering=708761), _EventDictReturn(event_id='$15600947551008018jCiVq:matrix.org', topological_ordering=None, stream_ordering=708760), _EventDictReturn(event_id='$15600947491007978QDPLH:matrix.org', topological_ordering=None, stream_ordering=708759), _EventDictReturn(event_id='$15600947131007663SiNSd:matrix.org', topological_ordering=None, stream_ordering=708758), _EventDictReturn(event_id='$15600946391007033aolro:matrix.org', topological_ordering=None, stream_ordering=708755), _EventDictReturn(event_id='$15600946091006766ZzkSV:matrix.org', topological_ordering=None, stream_ordering=708754), _EventDictReturn(event_id='$15600945021005812lLBZU:matrix.org', topological_ordering=None, stream_ordering=708752), _EventDictReturn(event_id='$15600945001005804zAlqr:matrix.org', topological_ordering=None, stream_ordering=708751), _EventDictReturn(event_id='$15600945001005796CUSFf:matrix.org', topological_ordering=None, stream_ordering=708750)]

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
    result = g.send(result)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/storage/_base.py", line 487, in runInteraction
    defer.returnValue(result)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1362, in returnValue
    raise _DefGen_Return(val)
twisted.internet.defer._DefGen_Return: [_EventDictReturn(event_id='$15600950391010258hrwAd:matrix.org', topological_ordering=None, stream_ordering=708773), _EventDictReturn(event_id='$15600950171010117CdspK:matrix.org', topological_ordering=None, stream_ordering=708772), _EventDictReturn(event_id='$15600949401009508lTpzl:matrix.org', topological_ordering=None, stream_ordering=708771), _EventDictReturn(event_id='$15600949261009398foetQ:matrix.org', topological_ordering=None, stream_ordering=708770), _EventDictReturn(event_id='$15600949091009307PONGY:matrix.org', topological_ordering=None, stream_ordering=708769), _EventDictReturn(event_id='$15600948911009164JkHeG:matrix.org', topological_ordering=None, stream_ordering=708768), _EventDictReturn(event_id='$15600948821009108qkrIj:matrix.org', topological_ordering=None, stream_ordering=708767), _EventDictReturn(event_id='$15600948591008929UoHxd:matrix.org', topological_ordering=None, stream_ordering=708766), _EventDictReturn(event_id='$15600948501008732joyAz:matrix.org', topological_ordering=None, stream_ordering=708765), _EventDictReturn(event_id='$15600948321008586DDtRZ:matrix.org', topological_ordering=None, stream_ordering=708764), _EventDictReturn(event_id='$15600948311008562BFdsx:matrix.org', topological_ordering=None, stream_ordering=708763), _EventDictReturn(event_id='$15600948311008555lylfW:matrix.org', topological_ordering=None, stream_ordering=708762), _EventDictReturn(event_id='$15600948051008364bsBFK:matrix.org', topological_ordering=None, stream_ordering=708761), _EventDictReturn(event_id='$15600947551008018jCiVq:matrix.org', topological_ordering=None, stream_ordering=708760), _EventDictReturn(event_id='$15600947491007978QDPLH:matrix.org', topological_ordering=None, stream_ordering=708759), _EventDictReturn(event_id='$15600947131007663SiNSd:matrix.org', topological_ordering=None, stream_ordering=708758), _EventDictReturn(event_id='$15600946391007033aolro:matrix.org', topological_ordering=None, stream_ordering=708755), _EventDictReturn(event_id='$15600946091006766ZzkSV:matrix.org', topological_ordering=None, stream_ordering=708754), _EventDictReturn(event_id='$15600945021005812lLBZU:matrix.org', topological_ordering=None, stream_ordering=708752), _EventDictReturn(event_id='$15600945001005804zAlqr:matrix.org', topological_ordering=None, stream_ordering=708751), _EventDictReturn(event_id='$15600945001005796CUSFf:matrix.org', topological_ordering=None, stream_ordering=708750)]

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
    result = g.send(result)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/storage/stream.py", line 414, in get_room_events_stream_for_room
    defer.returnValue((ret, key))
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1362, in returnValue
    raise _DefGen_Return(val)
twisted.internet.defer._DefGen_Return: ([<FrozenEvent event_id='$15600945001005796CUSFf:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600945001005804zAlqr:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600945021005812lLBZU:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600946091006766ZzkSV:matrix.org', type='m.room.member', state_key='@freenode_heatsink:matrix.org'>, <FrozenEvent event_id='$15600946391007033aolro:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600947131007663SiNSd:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600947491007978QDPLH:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600947551008018jCiVq:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948051008364bsBFK:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948311008555lylfW:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948311008562BFdsx:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948321008586DDtRZ:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948501008732joyAz:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948591008929UoHxd:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948821009108qkrIj:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600948911009164JkHeG:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600949091009307PONGY:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600949261009398foetQ:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600949401009508lTpzl:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600950171010117CdspK:matrix.org', type='m.room.message', state_key='None'>, <FrozenEvent event_id='$15600950391010258hrwAd:matrix.org', type='m.room.message', state_key='None'>], 's708750')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
    result = g.send(result)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/handlers/sync.py", line 472, in _load_filtered_recents
    limited=limited or newly_joined_room
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1362, in returnValue
    raise _DefGen_Return(val)
twisted.internet.defer._DefGen_Return: TimelineBatch(prev_batch=StreamToken(room_key='s708889', presence_key=26599051, typing_key=57, receipt_key=636004, account_data_key=24914, push_rules_key=56, to_device_key=95, device_list_key=55442, groups_key=1), events=[], limited=True)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/http/server.py", line 81, in wrapped_request_handler
    yield h(self, request)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/http/server.py", line 316, in _async_render
    callback_return = yield callback(request, **kwargs)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/rest/client/v2_alpha/sync.py", line 167, in on_GET
    full_state=full_state
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/handlers/sync.py", line 242, in wait_for_sync_for_user
    sync_config, since_token, timeout, full_state,
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/handlers/sync.py", line 264, in _wait_for_sync_for_user
    sync_config, since_token, full_state=full_state,
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/handlers/sync.py", line 935, in generate_sync_result
    sync_result_builder, account_data_by_room
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/handlers/sync.py", line 1316, in _generate_sync_entry_for_rooms
    yield concurrently_execute(handle_room_entries, room_entries, 10)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/util/async_helpers.py", line 149, in _concurrently_execute_inner
    yield func(next(it))
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/handlers/sync.py", line 1752, in _generate_room_entry
    full_state=full_state
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
    result = g.send(result)
  File "/home/synapse/synapse/env3/lib/python3.6/site-packages/synapse/handlers/sync.py", line 775, in compute_state_delta
    batch.events[0].event_id, state_filter=state_filter,
IndexError: list index out of range
2019-06-09 19:03:46,556 - synapse.access.http.8083 - 302 - INFO - GET-835 - 127.0.0.1 - 8083 - {@uelen:riot.firechicken.net} Processed request: 0.457sec/0.001sec (0.185sec, 0.004sec) (0.186sec/0.941sec/32) 55B 500 "GET /_matrix/client/r0/sync?filter=0&timeout=0&since=s707461_26561430_2874_634533_24909_56_95_55442_1 HTTP/1.0" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36" [1 dbevts]


# Fixed code
def compute_state_delta(
    self, room_id, batch, sync_config, since_token, now_token, full_state
):
    """Works out the difference in state between the start of the timeline
    and the previous sync.

    Args:
        room_id(str):
        batch(synapse.handlers.sync.TimelineBatch): The timeline batch for
            the room that will be sent to the user.
        sync_config(synapse.handlers.sync.SyncConfig):
        since_token(str|None): Token of the end of the previous batch. May
            be None.
        now_token(str): Token of the end of the current batch.
        full_state(bool): Whether to force returning the full state.

    Returns:
         A deferred dict of (type, state_key) -> Event
    """
    # TODO(mjark) Check if the state events were received by the server
    # after the previous sync, since we need to include those state
    # updates even if they occured logically before the previous event.
    # TODO(mjark) Check for new redactions in the state events.

    with Measure(self.clock, "compute_state_delta"):
        members_to_fetch = None

        lazy_load_members = sync_config.filter_collection.lazy_load_members()
        include_redundant_members = (
            sync_config.filter_collection.include_redundant_members()
        )

        if lazy_load_members:
            # We only request state for the members needed to display the
            # timeline:

            members_to_fetch = set(
                event.sender  # FIXME: we also care about invite targets etc.
                for event in batch.events
            )

            if full_state:
                # always make sure we LL ourselves so we know we're in the room
                # (if we are) to fix https://github.com/vector-im/riot-web/issues/7209
                # We only need apply this on full state syncs given we disabled
                # LL for incr syncs in #3840.
                members_to_fetch.add(sync_config.user.to_string())

            state_filter = StateFilter.from_lazy_load_member_list(members_to_fetch)
        else:
            state_filter = StateFilter.all()

        timeline_state = {
            (event.type, event.state_key): event.event_id
            for event in batch.events
            if event.is_state()
        }

        if full_state:
            if batch:
                current_state_ids = yield self.store.get_state_ids_for_event(
                    batch.events[-1].event_id, state_filter=state_filter
                )

                state_ids = yield self.store.get_state_ids_for_event(
                    batch.events[0].event_id, state_filter=state_filter
                )

            else:
                current_state_ids = yield self.get_state_at(
                    room_id, stream_position=now_token, state_filter=state_filter
                )

                state_ids = current_state_ids

            state_ids = _calculate_state(
                timeline_contains=timeline_state,
                timeline_start=state_ids,
                previous={},
                current=current_state_ids,
                lazy_load_members=lazy_load_members,
            )
        elif batch.limited:
            if batch:
                state_at_timeline_start = yield self.store.get_state_ids_for_event(
                    batch.events[0].event_id, state_filter=state_filter
                )
            else:
                # Its not clear how we get here, but empirically we do
                # (#5407). Logging has been added elsewhere to try and
                # figure out where this state comes from.
                state_at_timeline_start = yield self.get_state_at(
                    room_id, stream_position=now_token, state_filter=state_filter
                )

            # for now, we disable LL for gappy syncs - see
            # https://github.com/vector-im/riot-web/issues/7211#issuecomment-419976346
            # N.B. this slows down incr syncs as we are now processing way
            # more state in the server than if we were LLing.
            #
            # We still have to filter timeline_start to LL entries (above) in order
            # for _calculate_state's LL logic to work, as we have to include LL
            # members for timeline senders in case they weren't loaded in the initial
            # sync.  We do this by (counterintuitively) by filtering timeline_start
            # members to just be ones which were timeline senders, which then ensures
            # all of the rest get included in the state block (if we need to know
            # about them).
            state_filter = StateFilter.all()

            state_at_previous_sync = yield self.get_state_at(
                room_id, stream_position=since_token, state_filter=state_filter
            )

            if batch:
                current_state_ids = yield self.store.get_state_ids_for_event(
                    batch.events[-1].event_id, state_filter=state_filter
                )
            else:
                # Its not clear how we get here, but empirically we do
                # (#5407). Logging has been added elsewhere to try and
                # figure out where this state comes from.
                current_state_ids = yield self.get_state_at(
                    room_id, stream_position=now_token, state_filter=state_filter
                )

            state_ids = _calculate_state(
                timeline_contains=timeline_state,
                timeline_start=state_at_timeline_start,
                previous=state_at_previous_sync,
                current=current_state_ids,
                # we have to include LL members in case LL initial sync missed them
                lazy_load_members=lazy_load_members,
            )
        else:
            state_ids = {}
            if lazy_load_members:
                if members_to_fetch and batch.events:
                    # We're returning an incremental sync, with no
                    # "gap" since the previous sync, so normally there would be
                    # no state to return.
                    # But we're lazy-loading, so the client might need some more
                    # member events to understand the events in this timeline.
                    # So we fish out all the member events corresponding to the
                    # timeline here, and then dedupe any redundant ones below.

                    state_ids = yield self.store.get_state_ids_for_event(
                        batch.events[0].event_id,
                        # we only want members!
                        state_filter=StateFilter.from_types(
                            (EventTypes.Member, member) for member in members_to_fetch
                        ),
                    )

        if lazy_load_members and not include_redundant_members:
            cache_key = (sync_config.user.to_string(), sync_config.device_id)
            cache = self.get_lazy_loaded_members_cache(cache_key)

            # if it's a new sync sequence, then assume the client has had
            # amnesia and doesn't want any recent lazy-loaded members
            # de-duplicated.
            if since_token is None:
                logger.debug("clearing LruCache for %r", cache_key)
                cache.clear()
            else:
                # only send members which aren't in our LruCache (either
                # because they're new to this client or have been pushed out
                # of the cache)
                logger.debug("filtering state from %r...", state_ids)
                state_ids = {
                    t: event_id
                    for t, event_id in iteritems(state_ids)
                    if cache.get(t[1]) != event_id
                }
                logger.debug("...to %r", state_ids)

            # add any member IDs we are about to send into our LruCache
            for t, event_id in itertools.chain(
                state_ids.items(), timeline_state.items()
            ):
                if t[0] == EventTypes.Member:
                    cache.set(t[1], event_id)

    state = {}
    if state_ids:
        state = yield self.store.get_events(list(state_ids.values()))

    return {
        (e.type, e.state_key): e
        for e in sync_config.filter_collection.filter_room_state(list(state.values()))
    }