docs: async function basicDelta(path, getDirStatFn, options) {
  	const outputLimit = 50;
  	const itemIds = await options.allItemIdsHandler();
  	if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
  
  	const context = basicDeltaContextFromOptions_(options);
  
  	let newContext = {
  		timestamp: context.timestamp,
  		filesAtTimestamp: context.filesAtTimestamp.slice(),
  		statsCache: context.statsCache,
  		statIdsCache: context.statIdsCache,
  		deletedItemsProcessed: context.deletedItemsProcessed,
  	};
  
  	// Stats are cached until all items have been processed (until hasMore is false)
  	if (newContext.statsCache === null) {
  		newContext.statsCache = await getDirStatFn(path);
  		newContext.statsCache.sort(function(a, b) {
  			return a.updated_time - b.updated_time;
  		});
  		newContext.statIdsCache = newContext.statsCache
  			.filter(item => BaseItem.isSystemPath(item.path))
  			.map(item => BaseItem.pathToId(item.path));
  		newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
  	}
  
  	let output = [];
  
  	// Find out which files have been changed since the last time. Note that we keep
  	// both the timestamp of the most recent change, *and* the items that exactly match
  	// this timestamp. This to handle cases where an item is modified while this delta
  	// function is running. For example:
  	// t0: Item 1 is changed
  	// t0: Sync items - run delta function
  	// t0: While delta() is running, modify Item 2
  	// Since item 2 was modified within the same millisecond, it would be skipped in the
  	// next sync if we relied exclusively on a timestamp.
  	for (let i = 0; i < newContext.statsCache.length; i++) {
  		const stat = newContext.statsCache[i];
  
  		if (stat.isDir) continue;
  
  		if (stat.updated_time < context.timestamp) continue;
  
  		// Special case for items that exactly match the timestamp
  		if (stat.updated_time === context.timestamp) {
  			if (context.filesAtTimestamp.indexOf(stat.path) >= 0) continue;
  		}
  
  		if (stat.updated_time > newContext.timestamp) {
  			newContext.timestamp = stat.updated_time;
  			newContext.filesAtTimestamp = [];
  		}
  
  		newContext.filesAtTimestamp.push(stat.path);
  		output.push(stat);
  
  		if (output.length >= outputLimit) break;
  	}
  
  	if (!newContext.deletedItemsProcessed) {
  		// Find out which items have been deleted on the sync target by comparing the items
  		// we have to the items on the target.
  		// Note that when deleted items are processed it might result in the output having
  		// more items than outputLimit. This is acceptable since delete operations are cheap.
  		let deletedItems = [];
  		for (let i = 0; i < itemIds.length; i++) {
  			const itemId = itemIds[i];
  
  			if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) {
  				deletedItems.push({
  					path: BaseItem.systemPath(itemId),
  					isDeleted: true,
  				});
  			}
  		}
  
  		output = output.concat(deletedItems);
  	}
  
  	newContext.deletedItemsProcessed = true;
  
  	const hasMore = output.length >= outputLimit;
  
  	if (!hasMore) {
  		// Clear temporary info from context. It's especially important to remove deletedItemsProcessed
  		// so that they are processed again on the next sync.
  		newContext.statsCache = null;
  		newContext.statIdsCache = null;
  		delete newContext.deletedItemsProcessed;
  	}
  
  	return {
  		hasMore: hasMore,
  		context: newContext,
  		items: output,
  	};
  }