mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
Preserve redo log for mutable objects
Replay it during time travel debugging
This commit is contained in:
142
src/runtime/array.js
Normal file
142
src/runtime/array.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import {Multiversion, rollback_if_needed, wrap_methods, mutate} from './multiversion.js'
|
||||
|
||||
function set(prop, value) {
|
||||
this[prop] = value
|
||||
}
|
||||
|
||||
export const defineMultiversionArray = window => {
|
||||
// We declare class in such a weird name to have its displayed name to be
|
||||
// exactly 'Array'
|
||||
window.MultiversionArray = class Array extends window.Array {
|
||||
|
||||
constructor(initial, cxt) {
|
||||
super()
|
||||
this.multiversion = new Multiversion(cxt)
|
||||
this.initial = [...initial]
|
||||
this.redo_log = []
|
||||
this.apply_initial()
|
||||
}
|
||||
|
||||
apply_initial() {
|
||||
super.length = this.initial.length
|
||||
for(let i = 0; i < this.initial.length; i++) {
|
||||
this[i] = this.initial[i]
|
||||
}
|
||||
}
|
||||
|
||||
static get [Symbol.species]() {
|
||||
return globalThis.Array
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
wrap_methods(
|
||||
window.MultiversionArray,
|
||||
|
||||
[
|
||||
'at',
|
||||
'concat',
|
||||
'copyWithin',
|
||||
'entries',
|
||||
'every',
|
||||
'fill',
|
||||
'filter',
|
||||
'find',
|
||||
'findIndex',
|
||||
'findLast',
|
||||
'findLastIndex',
|
||||
'flat',
|
||||
'flatMap',
|
||||
'forEach',
|
||||
'includes',
|
||||
'indexOf',
|
||||
'join',
|
||||
'keys',
|
||||
'lastIndexOf',
|
||||
'map',
|
||||
'pop',
|
||||
'push',
|
||||
'reduce',
|
||||
'reduceRight',
|
||||
'reverse',
|
||||
'shift',
|
||||
'slice',
|
||||
'some',
|
||||
'sort',
|
||||
'splice',
|
||||
'toLocaleString',
|
||||
'toReversed',
|
||||
'toSorted',
|
||||
'toSpliced',
|
||||
'toString',
|
||||
'unshift',
|
||||
'values',
|
||||
'with',
|
||||
Symbol.iterator,
|
||||
],
|
||||
|
||||
[
|
||||
'copyWithin',
|
||||
'fill',
|
||||
'pop',
|
||||
'push',
|
||||
'reverse',
|
||||
'shift',
|
||||
'sort',
|
||||
'splice',
|
||||
'unshift',
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const methods_that_return_self = new Set([
|
||||
'copyWithin',
|
||||
'fill',
|
||||
'reverse',
|
||||
'sort',
|
||||
])
|
||||
|
||||
export function wrap_array(initial, cxt) {
|
||||
const array = new cxt.window.MultiversionArray(initial, cxt)
|
||||
const handler = {
|
||||
get(target, prop, receiver) {
|
||||
rollback_if_needed(target)
|
||||
const result = target[prop]
|
||||
if(
|
||||
typeof(prop) == 'string'
|
||||
&& isNaN(Number(prop))
|
||||
&& typeof(result) == 'function'
|
||||
) {
|
||||
if(methods_that_return_self.has(prop)) {
|
||||
// declare object with key prop for function to have a name
|
||||
return {
|
||||
[prop]() {
|
||||
result.apply(target, arguments)
|
||||
return receiver
|
||||
}
|
||||
}[prop]
|
||||
} else {
|
||||
return {
|
||||
[prop]() {
|
||||
return result.apply(target, arguments)
|
||||
}
|
||||
}[prop]
|
||||
}
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
},
|
||||
|
||||
set(obj, prop, val) {
|
||||
mutate(obj, set, [prop, val])
|
||||
return true
|
||||
},
|
||||
}
|
||||
return new Proxy(array, handler)
|
||||
}
|
||||
|
||||
export function create_array(initial, cxt, index, literals) {
|
||||
const result = wrap_array(initial, cxt)
|
||||
literals.set(index, result)
|
||||
return result
|
||||
}
|
||||
61
src/runtime/let_multiversion.js
Normal file
61
src/runtime/let_multiversion.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import {Multiversion} from './multiversion.js'
|
||||
|
||||
// https://stackoverflow.com/a/29018745
|
||||
function binarySearch(arr, el, compare_fn) {
|
||||
let m = 0;
|
||||
let n = arr.length - 1;
|
||||
while (m <= n) {
|
||||
let k = (n + m) >> 1;
|
||||
let cmp = compare_fn(el, arr[k]);
|
||||
if (cmp > 0) {
|
||||
m = k + 1;
|
||||
} else if(cmp < 0) {
|
||||
n = k - 1;
|
||||
} else {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return ~m;
|
||||
}
|
||||
|
||||
export class LetMultiversion extends Multiversion {
|
||||
constructor(cxt, initial) {
|
||||
super(cxt)
|
||||
this.latest = initial
|
||||
this.versions = [{version_number: cxt.version_counter, value: initial}]
|
||||
}
|
||||
|
||||
rollback_if_needed() {
|
||||
if(this.needs_rollback()) {
|
||||
this.latest = this.get_version(this.cxt.version_counter)
|
||||
}
|
||||
}
|
||||
|
||||
get() {
|
||||
this.rollback_if_needed()
|
||||
return this.latest
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.rollback_if_needed()
|
||||
const version_number = ++this.cxt.version_counter
|
||||
if(this.is_created_during_current_expansion()) {
|
||||
this.versions.push({version_number, value})
|
||||
}
|
||||
this.latest = value
|
||||
}
|
||||
|
||||
get_version(version_number) {
|
||||
if(version_number == null) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
const idx = binarySearch(this.versions, version_number, (id, el) => id - el.version_number)
|
||||
if(idx >= 0) {
|
||||
return this.versions[idx].value
|
||||
} else if(idx == -1) {
|
||||
throw new Error('illegal state')
|
||||
} else {
|
||||
return this.versions[-idx - 2].value
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/runtime/map.js
Normal file
45
src/runtime/map.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import {Multiversion, wrap_methods, rollback_if_needed} from './multiversion.js'
|
||||
|
||||
export const defineMultiversionMap = window => {
|
||||
|
||||
// We declare class in such a weird name to have its displayed name to be
|
||||
// exactly 'Map'
|
||||
window.MultiversionMap = class Map extends window.Map {
|
||||
|
||||
constructor(initial, cxt) {
|
||||
super()
|
||||
this.multiversion = new Multiversion(cxt)
|
||||
this.initial = new globalThis.Map(initial)
|
||||
this.redo_log = []
|
||||
this.apply_initial()
|
||||
}
|
||||
|
||||
apply_initial() {
|
||||
super.clear()
|
||||
for(let [k,v] of this.initial) {
|
||||
super.set(k,v)
|
||||
}
|
||||
}
|
||||
|
||||
get size() {
|
||||
rollback_if_needed(this)
|
||||
return super.size
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
wrap_methods(
|
||||
window.MultiversionMap,
|
||||
|
||||
// all methods
|
||||
[
|
||||
'clear', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', 'values',
|
||||
Symbol.iterator,
|
||||
],
|
||||
|
||||
// mutation methods
|
||||
['set', 'delete', 'clear'],
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,76 +1,91 @@
|
||||
// https://stackoverflow.com/a/29018745
|
||||
function binarySearch(arr, el, compare_fn) {
|
||||
let m = 0;
|
||||
let n = arr.length - 1;
|
||||
while (m <= n) {
|
||||
let k = (n + m) >> 1;
|
||||
let cmp = compare_fn(el, arr[k]);
|
||||
if (cmp > 0) {
|
||||
m = k + 1;
|
||||
} else if(cmp < 0) {
|
||||
n = k - 1;
|
||||
} else {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return ~m;
|
||||
}
|
||||
|
||||
export class Multiversion {
|
||||
constructor(cxt, initial) {
|
||||
constructor(cxt) {
|
||||
this.cxt = cxt
|
||||
this.expand_calltree_node_number = cxt.expand_calltree_node_number
|
||||
this.latest = initial
|
||||
this.versions = [{version_number: cxt.version_counter, value: initial}]
|
||||
this.ct_expansion_id = cxt.ct_expansion_id
|
||||
}
|
||||
|
||||
is_created_during_current_expand() {
|
||||
return this.expand_calltree_node_number == this.cxt.expand_calltree_node_number
|
||||
is_created_during_current_expansion() {
|
||||
return this.ct_expansion_id == this.cxt.ct_expansion_id
|
||||
}
|
||||
|
||||
get() {
|
||||
if(!this.cxt.is_expanding_calltree_node) {
|
||||
return this.latest
|
||||
} else {
|
||||
if(this.is_created_during_current_expand()) {
|
||||
return this.latest
|
||||
} else {
|
||||
const version_number = this.cxt.version_counter
|
||||
return this.get_version(version_number)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_version(version_number) {
|
||||
if(version_number == null) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
const idx = binarySearch(this.versions, version_number, (id, el) => id - el.version_number)
|
||||
if(idx >= 0) {
|
||||
return this.versions[idx].value
|
||||
} else if(idx == -1) {
|
||||
throw new Error('illegal state')
|
||||
} else {
|
||||
return this.versions[-idx - 2].value
|
||||
}
|
||||
}
|
||||
|
||||
set(value) {
|
||||
const version_number = ++this.cxt.version_counter
|
||||
needs_rollback() {
|
||||
if(this.cxt.is_expanding_calltree_node) {
|
||||
if(this.is_created_during_current_expand()) {
|
||||
this.latest = value
|
||||
this.set_version(version_number, value)
|
||||
if(this.is_created_during_current_expansion()) {
|
||||
// do nothing, keep using current version
|
||||
} else {
|
||||
if(this.rollback_expansion_id == this.cxt.ct_expansion_id) {
|
||||
// do nothing, keep using current version
|
||||
// We are in the same expansion rollback was done, keep using current version
|
||||
} else {
|
||||
this.rollback_expansion_id = this.cxt.ct_expansion_id
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(this.rollback_expansion_id != null) {
|
||||
this.rollback_expansion_id = null
|
||||
return true
|
||||
} else {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
this.latest = value
|
||||
this.set_version(version_number, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set_version(version_number, value) {
|
||||
this.versions.push({version_number, value})
|
||||
|
||||
export function rollback_if_needed(object) {
|
||||
if(object.multiversion.needs_rollback()) {
|
||||
// Rollback to initial value
|
||||
object.apply_initial()
|
||||
// Replay redo log
|
||||
for(let i = 0; i < object.redo_log.length; i++) {
|
||||
const log_item = object.redo_log[i]
|
||||
if(log_item.version_number > object.multiversion.cxt.version_counter) {
|
||||
break
|
||||
}
|
||||
log_item.method.apply(object, log_item.args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wrap_readonly_method(clazz, method) {
|
||||
const original = clazz.__proto__.prototype[method]
|
||||
clazz.prototype[method] = {
|
||||
[method](){
|
||||
rollback_if_needed(this)
|
||||
return original.apply(this, arguments)
|
||||
}
|
||||
}[method]
|
||||
}
|
||||
|
||||
export function mutate(object, method, args) {
|
||||
rollback_if_needed(object)
|
||||
const version_number = ++object.multiversion.cxt.version_counter
|
||||
if(object.multiversion.is_created_during_current_expansion()) {
|
||||
object.redo_log.push({
|
||||
method,
|
||||
args,
|
||||
version_number,
|
||||
})
|
||||
}
|
||||
return method.apply(object, args)
|
||||
}
|
||||
|
||||
function wrap_mutating_method(clazz, method) {
|
||||
const original = clazz.__proto__.prototype[method]
|
||||
clazz.prototype[method] = {
|
||||
[method]() {
|
||||
return mutate(this, original, arguments)
|
||||
}
|
||||
}[method]
|
||||
}
|
||||
|
||||
export function wrap_methods(clazz, all_methods, mutating_methods) {
|
||||
for (let method of all_methods) {
|
||||
if(mutating_methods.includes(method)) {
|
||||
wrap_mutating_method(clazz, method)
|
||||
} else {
|
||||
wrap_readonly_method(clazz, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
src/runtime/object.js
Normal file
68
src/runtime/object.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import {Multiversion, rollback_if_needed, wrap_methods, mutate} from './multiversion.js'
|
||||
|
||||
export function create_object(initial, cxt, index, literals) {
|
||||
const multiversion = new Multiversion(cxt)
|
||||
|
||||
let latest = {...initial}
|
||||
const redo_log = []
|
||||
|
||||
function rollback_if_needed() {
|
||||
if(multiversion.needs_rollback()) {
|
||||
latest = {...initial}
|
||||
for(let i = 0; i < redo_log.length; i++) {
|
||||
const log_item = redo_log[i]
|
||||
if(log_item.version_number > multiversion.cxt.version_counter) {
|
||||
break
|
||||
}
|
||||
if(log_item.type == 'set') {
|
||||
latest[log_item.prop] = log_item.value
|
||||
} else if(log_item.type == 'delete') {
|
||||
delete latest[log_item.prop]
|
||||
} else {
|
||||
throw new Error('illegal type')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handler = {
|
||||
get(target, prop, receiver) {
|
||||
rollback_if_needed()
|
||||
return latest[prop]
|
||||
},
|
||||
|
||||
has(target, prop) {
|
||||
rollback_if_needed()
|
||||
return prop in latest
|
||||
},
|
||||
|
||||
set(obj, prop, value) {
|
||||
rollback_if_needed()
|
||||
const version_number = ++multiversion.cxt.version_counter
|
||||
if(multiversion.is_created_during_current_expansion()) {
|
||||
redo_log.push({ type: 'set', prop, value, version_number })
|
||||
}
|
||||
latest[prop] = value
|
||||
return true
|
||||
},
|
||||
|
||||
ownKeys(target) {
|
||||
rollback_if_needed()
|
||||
return Object.keys(latest)
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
rollback_if_needed()
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: latest[prop],
|
||||
};
|
||||
},
|
||||
|
||||
// TODO delete property handler
|
||||
}
|
||||
const result = new Proxy(initial, handler)
|
||||
literals.set(index, result)
|
||||
return result
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import {set_current_context} from './record_io.js'
|
||||
import {Multiversion} from './multiversion.js'
|
||||
|
||||
// Create separate class to check value instanceof LetMultiversion
|
||||
export class LetMultiversion extends Multiversion {}
|
||||
import {LetMultiversion} from './let_multiversion.js'
|
||||
import {defineMultiversionArray, create_array, wrap_array} from './array.js'
|
||||
import {create_object} from './object.js'
|
||||
import {defineMultiversionSet} from './set.js'
|
||||
import {defineMultiversionMap} from './map.js'
|
||||
|
||||
/*
|
||||
Converts generator-returning function to promise-returning function. Allows to
|
||||
@@ -70,6 +71,7 @@ const do_run = function*(module_fns, cxt, io_trace){
|
||||
io_trace_abort_replay,
|
||||
}
|
||||
|
||||
defineMultiversion(cxt.window)
|
||||
apply_promise_patch(cxt)
|
||||
set_current_context(cxt)
|
||||
|
||||
@@ -85,6 +87,7 @@ const do_run = function*(module_fns, cxt, io_trace){
|
||||
id: ++cxt.call_counter,
|
||||
version_number: cxt.version_counter,
|
||||
let_vars: {},
|
||||
literals: new Map(),
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -92,12 +95,15 @@ const do_run = function*(module_fns, cxt, io_trace){
|
||||
const result = fn(
|
||||
cxt,
|
||||
calltree.let_vars,
|
||||
calltree.literals,
|
||||
calltree_node_by_loc.get(module),
|
||||
__trace,
|
||||
__trace_call,
|
||||
__do_await,
|
||||
__save_ct_node_for_path,
|
||||
LetMultiversion,
|
||||
create_array,
|
||||
create_object,
|
||||
)
|
||||
if(result instanceof cxt.window.Promise) {
|
||||
yield cxt.window.Promise.race([replay_aborted_promise, result])
|
||||
@@ -193,10 +199,6 @@ export const set_record_call = cxt => {
|
||||
|
||||
export const do_eval_expand_calltree_node = (cxt, node) => {
|
||||
cxt.is_recording_deferred_calls = false
|
||||
cxt.is_expanding_calltree_node = true
|
||||
cxt.expand_calltree_node_number = cxt.expand_calltree_node_number == null
|
||||
? 0
|
||||
: cxt.expand_calltree_node_number + 1
|
||||
|
||||
// Save call counter and set it to the value it had when executed 'fn' for
|
||||
// the first time
|
||||
@@ -208,29 +210,24 @@ export const do_eval_expand_calltree_node = (cxt, node) => {
|
||||
// as node.id
|
||||
: node.id - 1
|
||||
|
||||
const version_counter = cxt.version_counter
|
||||
|
||||
// Save version_counter
|
||||
cxt.version_counter = node.version_number
|
||||
|
||||
cxt.children = null
|
||||
try {
|
||||
if(node.is_new) {
|
||||
new node.fn(...node.args)
|
||||
} else {
|
||||
node.fn.apply(node.context, node.args)
|
||||
}
|
||||
with_version_number(cxt, node.version_number, () => {
|
||||
if(node.is_new) {
|
||||
new node.fn(...node.args)
|
||||
} else {
|
||||
node.fn.apply(node.context, node.args)
|
||||
}
|
||||
})
|
||||
} catch(e) {
|
||||
// do nothing. Exception was caught and recorded inside '__trace'
|
||||
}
|
||||
|
||||
// Restore call counter
|
||||
cxt.call_counter = call_counter
|
||||
// Restore version_counter
|
||||
cxt.version_counter = version_counter
|
||||
|
||||
|
||||
cxt.is_expanding_calltree_node = false
|
||||
cxt.is_recording_deferred_calls = true
|
||||
const children = cxt.children
|
||||
cxt.children = null
|
||||
@@ -312,6 +309,9 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
|
||||
let_vars = cxt.let_vars = {}
|
||||
}
|
||||
|
||||
// TODO only allocate map if has literals
|
||||
const literals = cxt.literals = new Map()
|
||||
|
||||
let ok, value, error
|
||||
|
||||
const is_toplevel_call_copy = cxt.is_toplevel_call
|
||||
@@ -343,6 +343,7 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
|
||||
version_number,
|
||||
last_version_number: cxt.version_counter,
|
||||
let_vars,
|
||||
literals,
|
||||
ok,
|
||||
value,
|
||||
error,
|
||||
@@ -386,6 +387,47 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
|
||||
return result
|
||||
}
|
||||
|
||||
const defineMultiversion = window => {
|
||||
if(window.defineMultiversionDone) {
|
||||
return
|
||||
}
|
||||
window.defineMultiversionDone = true
|
||||
defineMultiversionArray(window)
|
||||
defineMultiversionSet(window)
|
||||
defineMultiversionMap(window)
|
||||
}
|
||||
|
||||
const wrap_multiversion_value = (value, cxt) => {
|
||||
|
||||
// TODO use a WeakMap value => wrapper ???
|
||||
|
||||
if(value instanceof cxt.window.Set) {
|
||||
if(!(value instanceof cxt.window.MultiversionSet)) {
|
||||
return new cxt.window.MultiversionSet(value, cxt)
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
if(value instanceof cxt.window.Map) {
|
||||
if(!(value instanceof cxt.window.MultiversionMap)) {
|
||||
return new cxt.window.MultiversionMap(value, cxt)
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
if(value instanceof cxt.window.Array) {
|
||||
if(!(value instanceof cxt.window.MultiversionArray)) {
|
||||
return wrap_array(value, cxt)
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
|
||||
if(fn != null && fn.__location != null && !is_new) {
|
||||
// Call will be traced, because tracing code is already embedded inside
|
||||
@@ -431,7 +473,11 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
|
||||
if(value instanceof cxt.window.Promise) {
|
||||
set_record_call(cxt)
|
||||
}
|
||||
|
||||
value = wrap_multiversion_value(value, cxt)
|
||||
|
||||
return value
|
||||
|
||||
} catch(_error) {
|
||||
ok = false
|
||||
error = _error
|
||||
@@ -488,3 +534,28 @@ const __save_ct_node_for_path = (cxt, __calltree_node_by_loc, index, __call_id)
|
||||
set_record_call(cxt)
|
||||
}
|
||||
}
|
||||
|
||||
export const with_version_number = (rt_cxt, version_number, action) => {
|
||||
if(rt_cxt.logs == null) {
|
||||
// check that argument is rt_cxt
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
if(version_number == null) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
if(rt_cxt.is_expanding_calltree_node) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
rt_cxt.is_expanding_calltree_node = true
|
||||
const version_counter_copy = rt_cxt.version_counter
|
||||
rt_cxt.version_counter = version_number
|
||||
rt_cxt.ct_expansion_id = rt_cxt.ct_expansion_id == null
|
||||
? 0
|
||||
: rt_cxt.ct_expansion_id + 1
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
rt_cxt.is_expanding_calltree_node = false
|
||||
rt_cxt.version_counter = version_counter_copy
|
||||
}
|
||||
}
|
||||
|
||||
44
src/runtime/set.js
Normal file
44
src/runtime/set.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import {Multiversion, wrap_methods, rollback_if_needed} from './multiversion.js'
|
||||
|
||||
export const defineMultiversionSet = window => {
|
||||
|
||||
// We declare class in such a weird name to have its displayed name to be
|
||||
// exactly 'Set'
|
||||
window.MultiversionSet = class Set extends window.Set {
|
||||
|
||||
constructor(initial, cxt) {
|
||||
super()
|
||||
this.multiversion = new Multiversion(cxt)
|
||||
this.initial = new globalThis.Set(initial)
|
||||
this.redo_log = []
|
||||
this.apply_initial()
|
||||
}
|
||||
|
||||
apply_initial() {
|
||||
super.clear()
|
||||
for (const item of this.initial) {
|
||||
super.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
get size() {
|
||||
rollback_if_needed(this)
|
||||
return super.size
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
wrap_methods(
|
||||
window.MultiversionSet,
|
||||
|
||||
// all methods
|
||||
[
|
||||
'has', 'add', 'delete', 'clear', 'entries', 'forEach', 'values', 'keys',
|
||||
Symbol.iterator,
|
||||
],
|
||||
|
||||
// mutation methods
|
||||
['add', 'delete', 'clear'],
|
||||
)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user